activerecord 5.2.6 → 6.0.5.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 +956 -559
- data/MIT-LICENSE +3 -1
- data/README.rdoc +5 -3
- data/examples/performance.rb +1 -1
- data/lib/active_record/advisory_lock_base.rb +18 -0
- data/lib/active_record/aggregations.rb +4 -3
- data/lib/active_record/association_relation.rb +10 -8
- data/lib/active_record/associations/alias_tracker.rb +0 -1
- data/lib/active_record/associations/association.rb +55 -19
- data/lib/active_record/associations/association_scope.rb +11 -7
- data/lib/active_record/associations/belongs_to_association.rb +36 -42
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +0 -4
- data/lib/active_record/associations/builder/association.rb +14 -18
- data/lib/active_record/associations/builder/belongs_to.rb +19 -52
- data/lib/active_record/associations/builder/collection_association.rb +3 -13
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +17 -40
- data/lib/active_record/associations/builder/has_many.rb +2 -0
- data/lib/active_record/associations/builder/has_one.rb +35 -1
- data/lib/active_record/associations/builder/singular_association.rb +2 -0
- data/lib/active_record/associations/collection_association.rb +19 -23
- data/lib/active_record/associations/collection_proxy.rb +14 -17
- data/lib/active_record/associations/foreign_association.rb +7 -0
- data/lib/active_record/associations/has_many_association.rb +2 -11
- data/lib/active_record/associations/has_many_through_association.rb +14 -14
- data/lib/active_record/associations/has_one_association.rb +28 -30
- data/lib/active_record/associations/has_one_through_association.rb +5 -5
- data/lib/active_record/associations/join_dependency/join_association.rb +16 -10
- data/lib/active_record/associations/join_dependency/join_part.rb +4 -4
- data/lib/active_record/associations/join_dependency.rb +47 -30
- data/lib/active_record/associations/preloader/association.rb +61 -41
- data/lib/active_record/associations/preloader/through_association.rb +48 -39
- data/lib/active_record/associations/preloader.rb +44 -33
- data/lib/active_record/associations/singular_association.rb +2 -16
- data/lib/active_record/associations/through_association.rb +1 -1
- data/lib/active_record/associations.rb +21 -16
- data/lib/active_record/attribute_assignment.rb +7 -11
- data/lib/active_record/attribute_decorators.rb +0 -2
- data/lib/active_record/attribute_methods/before_type_cast.rb +4 -2
- data/lib/active_record/attribute_methods/dirty.rb +111 -40
- data/lib/active_record/attribute_methods/primary_key.rb +15 -24
- data/lib/active_record/attribute_methods/query.rb +2 -3
- data/lib/active_record/attribute_methods/read.rb +15 -54
- data/lib/active_record/attribute_methods/serialization.rb +1 -2
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +1 -3
- data/lib/active_record/attribute_methods/write.rb +17 -25
- data/lib/active_record/attribute_methods.rb +28 -100
- data/lib/active_record/attributes.rb +13 -1
- data/lib/active_record/autosave_association.rb +12 -14
- data/lib/active_record/base.rb +2 -3
- data/lib/active_record/callbacks.rb +6 -21
- data/lib/active_record/coders/yaml_column.rb +13 -2
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +109 -18
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +17 -4
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +102 -124
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +18 -9
- data/lib/active_record/connection_adapters/abstract/quoting.rb +68 -17
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +20 -14
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +105 -72
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +1 -3
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +175 -79
- data/lib/active_record/connection_adapters/abstract/transaction.rb +96 -57
- data/lib/active_record/connection_adapters/abstract_adapter.rb +197 -43
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +149 -217
- data/lib/active_record/connection_adapters/column.rb +17 -13
- data/lib/active_record/connection_adapters/connection_specification.rb +54 -45
- data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +6 -10
- data/lib/active_record/connection_adapters/mysql/column.rb +1 -1
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +70 -14
- data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +0 -1
- data/lib/active_record/connection_adapters/mysql/quoting.rb +44 -7
- data/lib/active_record/connection_adapters/mysql/schema_creation.rb +4 -6
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +40 -32
- data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +14 -6
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +139 -19
- data/lib/active_record/connection_adapters/mysql/type_metadata.rb +6 -10
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +26 -10
- data/lib/active_record/connection_adapters/postgresql/column.rb +17 -31
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +26 -1
- 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/date_time.rb +8 -0
- 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/legacy_point.rb +1 -2
- data/lib/active_record/connection_adapters/postgresql/oid/oid.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/point.rb +1 -2
- 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 +5 -3
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +44 -7
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +14 -3
- 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 +63 -75
- data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +24 -27
- data/lib/active_record/connection_adapters/postgresql/utils.rb +0 -1
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +168 -75
- data/lib/active_record/connection_adapters/schema_cache.rb +37 -14
- data/lib/active_record/connection_adapters/sql_type_metadata.rb +11 -8
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +119 -0
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +42 -7
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +43 -12
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +137 -147
- data/lib/active_record/connection_adapters/statement_pool.rb +0 -1
- data/lib/active_record/connection_handling.rb +139 -26
- data/lib/active_record/core.rb +117 -66
- data/lib/active_record/counter_cache.rb +8 -30
- data/lib/active_record/database_configurations/database_config.rb +37 -0
- data/lib/active_record/database_configurations/hash_config.rb +50 -0
- data/lib/active_record/database_configurations/url_config.rb +78 -0
- data/lib/active_record/database_configurations.rb +233 -0
- data/lib/active_record/dynamic_matchers.rb +3 -4
- data/lib/active_record/enum.rb +44 -7
- data/lib/active_record/errors.rb +15 -7
- data/lib/active_record/explain.rb +1 -2
- data/lib/active_record/fixture_set/model_metadata.rb +33 -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 +144 -474
- data/lib/active_record/gem_version.rb +4 -4
- data/lib/active_record/inheritance.rb +13 -6
- data/lib/active_record/insert_all.rb +179 -0
- data/lib/active_record/integration.rb +68 -16
- data/lib/active_record/internal_metadata.rb +11 -3
- data/lib/active_record/locking/optimistic.rb +14 -7
- data/lib/active_record/locking/pessimistic.rb +3 -3
- data/lib/active_record/log_subscriber.rb +8 -27
- data/lib/active_record/middleware/database_selector/resolver/session.rb +45 -0
- data/lib/active_record/middleware/database_selector/resolver.rb +87 -0
- data/lib/active_record/middleware/database_selector.rb +74 -0
- data/lib/active_record/migration/command_recorder.rb +54 -22
- data/lib/active_record/migration/compatibility.rb +79 -52
- data/lib/active_record/migration/join_table.rb +0 -1
- data/lib/active_record/migration.rb +104 -85
- data/lib/active_record/model_schema.rb +62 -11
- data/lib/active_record/nested_attributes.rb +2 -4
- data/lib/active_record/no_touching.rb +9 -2
- data/lib/active_record/null_relation.rb +0 -1
- data/lib/active_record/persistence.rb +232 -29
- data/lib/active_record/query_cache.rb +11 -4
- data/lib/active_record/querying.rb +33 -21
- data/lib/active_record/railtie.rb +95 -40
- data/lib/active_record/railties/collection_cache_association_loading.rb +34 -0
- data/lib/active_record/railties/controller_runtime.rb +30 -35
- data/lib/active_record/railties/databases.rake +199 -46
- data/lib/active_record/reflection.rb +51 -51
- data/lib/active_record/relation/batches.rb +13 -11
- data/lib/active_record/relation/calculations.rb +55 -49
- data/lib/active_record/relation/delegation.rb +35 -50
- data/lib/active_record/relation/finder_methods.rb +23 -28
- data/lib/active_record/relation/from_clause.rb +4 -0
- data/lib/active_record/relation/merger.rb +12 -17
- data/lib/active_record/relation/predicate_builder/array_handler.rb +5 -4
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +1 -4
- data/lib/active_record/relation/predicate_builder/base_handler.rb +1 -2
- data/lib/active_record/relation/predicate_builder/basic_object_handler.rb +1 -2
- data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -4
- data/lib/active_record/relation/predicate_builder/range_handler.rb +3 -23
- data/lib/active_record/relation/predicate_builder.rb +5 -11
- data/lib/active_record/relation/query_attribute.rb +13 -8
- data/lib/active_record/relation/query_methods.rb +232 -69
- data/lib/active_record/relation/spawn_methods.rb +1 -2
- data/lib/active_record/relation/where_clause.rb +14 -11
- data/lib/active_record/relation/where_clause_factory.rb +1 -2
- data/lib/active_record/relation.rb +326 -81
- data/lib/active_record/result.rb +30 -12
- data/lib/active_record/sanitization.rb +32 -40
- data/lib/active_record/schema.rb +2 -11
- data/lib/active_record/schema_dumper.rb +22 -7
- data/lib/active_record/schema_migration.rb +6 -2
- data/lib/active_record/scoping/default.rb +4 -6
- data/lib/active_record/scoping/named.rb +25 -16
- data/lib/active_record/scoping.rb +8 -9
- data/lib/active_record/statement_cache.rb +30 -3
- data/lib/active_record/store.rb +87 -8
- data/lib/active_record/suppressor.rb +2 -2
- data/lib/active_record/table_metadata.rb +23 -15
- data/lib/active_record/tasks/database_tasks.rb +194 -25
- data/lib/active_record/tasks/mysql_database_tasks.rb +5 -6
- data/lib/active_record/tasks/postgresql_database_tasks.rb +5 -8
- data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -9
- data/lib/active_record/test_databases.rb +23 -0
- data/lib/active_record/test_fixtures.rb +243 -0
- data/lib/active_record/timestamp.rb +39 -26
- data/lib/active_record/touch_later.rb +5 -4
- data/lib/active_record/transactions.rb +64 -73
- 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 +0 -1
- 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 +3 -5
- data/lib/active_record/type_caster/connection.rb +15 -14
- data/lib/active_record/type_caster/map.rb +1 -4
- data/lib/active_record/validations/associated.rb +0 -1
- data/lib/active_record/validations/uniqueness.rb +15 -27
- data/lib/active_record/validations.rb +3 -3
- data/lib/active_record.rb +10 -2
- data/lib/arel/alias_predication.rb +9 -0
- data/lib/arel/attributes/attribute.rb +37 -0
- data/lib/arel/attributes.rb +22 -0
- data/lib/arel/collectors/bind.rb +24 -0
- data/lib/arel/collectors/composite.rb +31 -0
- data/lib/arel/collectors/plain_string.rb +20 -0
- data/lib/arel/collectors/sql_string.rb +20 -0
- data/lib/arel/collectors/substitute_binds.rb +28 -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 +52 -0
- data/lib/arel/nodes/bind_param.rb +36 -0
- data/lib/arel/nodes/case.rb +55 -0
- data/lib/arel/nodes/casted.rb +50 -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 +18 -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 +8 -0
- data/lib/arel/nodes/in.rb +8 -0
- data/lib/arel/nodes/infix_operation.rb +80 -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 +50 -0
- data/lib/arel/nodes/node_expression.rb +13 -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 +16 -0
- data/lib/arel/nodes/string_join.rb +11 -0
- data/lib/arel/nodes/table_alias.rb +27 -0
- data/lib/arel/nodes/terminal.rb +16 -0
- data/lib/arel/nodes/true.rb +16 -0
- data/lib/arel/nodes/unary.rb +45 -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 +68 -0
- data/lib/arel/order_predications.rb +13 -0
- data/lib/arel/predications.rb +256 -0
- data/lib/arel/select_manager.rb +271 -0
- data/lib/arel/table.rb +110 -0
- data/lib/arel/tree_manager.rb +72 -0
- data/lib/arel/update_manager.rb +34 -0
- data/lib/arel/visitors/depth_first.rb +203 -0
- data/lib/arel/visitors/dot.rb +296 -0
- data/lib/arel/visitors/ibm_db.rb +34 -0
- data/lib/arel/visitors/informix.rb +62 -0
- data/lib/arel/visitors/mssql.rb +156 -0
- data/lib/arel/visitors/mysql.rb +83 -0
- data/lib/arel/visitors/oracle.rb +158 -0
- data/lib/arel/visitors/oracle12.rb +65 -0
- data/lib/arel/visitors/postgresql.rb +109 -0
- data/lib/arel/visitors/sqlite.rb +38 -0
- data/lib/arel/visitors/to_sql.rb +888 -0
- data/lib/arel/visitors/visitor.rb +45 -0
- data/lib/arel/visitors/where_sql.rb +22 -0
- data/lib/arel/visitors.rb +20 -0
- data/lib/arel/window_predications.rb +9 -0
- data/lib/arel.rb +62 -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 +2 -5
- data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +1 -1
- data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +4 -2
- data/lib/rails/generators/active_record/migration.rb +14 -2
- data/lib/rails/generators/active_record/model/model_generator.rb +1 -1
- data/lib/rails/generators/active_record/model/templates/model.rb.tt +10 -1
- metadata +116 -29
- data/lib/active_record/collection_cache_key.rb +0 -53
@@ -55,7 +55,11 @@ module ActiveRecord
|
|
55
55
|
if has_attribute?(inheritance_column)
|
56
56
|
subclass = subclass_from_attributes(attributes)
|
57
57
|
|
58
|
-
if subclass.nil? &&
|
58
|
+
if subclass.nil? && scope_attributes = current_scope&.scope_for_create
|
59
|
+
subclass = subclass_from_attributes(scope_attributes)
|
60
|
+
end
|
61
|
+
|
62
|
+
if subclass.nil? && base_class?
|
59
63
|
subclass = subclass_from_attributes(column_defaults)
|
60
64
|
end
|
61
65
|
end
|
@@ -104,6 +108,12 @@ module ActiveRecord
|
|
104
108
|
end
|
105
109
|
end
|
106
110
|
|
111
|
+
# Returns whether the class is a base class.
|
112
|
+
# See #base_class for more information.
|
113
|
+
def base_class?
|
114
|
+
base_class == self
|
115
|
+
end
|
116
|
+
|
107
117
|
# Set this to +true+ if this is an abstract class (see
|
108
118
|
# <tt>abstract_class?</tt>).
|
109
119
|
# If you are using inheritance with Active Record and don't want a class
|
@@ -166,11 +176,10 @@ module ActiveRecord
|
|
166
176
|
end
|
167
177
|
|
168
178
|
protected
|
169
|
-
|
170
179
|
# Returns the class type of the record using the current module as a prefix. So descendants of
|
171
180
|
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
|
172
181
|
def compute_type(type_name)
|
173
|
-
if type_name.start_with?("::"
|
182
|
+
if type_name.start_with?("::")
|
174
183
|
# If the type is prefixed with a scope operator then we assume that
|
175
184
|
# the type_name is an absolute reference.
|
176
185
|
ActiveSupport::Dependencies.constantize(type_name)
|
@@ -198,7 +207,6 @@ module ActiveRecord
|
|
198
207
|
end
|
199
208
|
|
200
209
|
private
|
201
|
-
|
202
210
|
# Called by +instantiate+ to decide which class to use for a new
|
203
211
|
# record instance. For single-table inheritance, we check the record
|
204
212
|
# for a +type+ column and return the corresponding class.
|
@@ -239,7 +247,7 @@ module ActiveRecord
|
|
239
247
|
sti_column = arel_attribute(inheritance_column, table)
|
240
248
|
sti_names = ([self] + descendants).map(&:sti_name)
|
241
249
|
|
242
|
-
|
250
|
+
predicate_builder.build(sti_column, sti_names)
|
243
251
|
end
|
244
252
|
|
245
253
|
# Detect the subclass from the inheritance column of attrs. If the inheritance column value
|
@@ -262,7 +270,6 @@ module ActiveRecord
|
|
262
270
|
end
|
263
271
|
|
264
272
|
private
|
265
|
-
|
266
273
|
def initialize_internals_callback
|
267
274
|
super
|
268
275
|
ensure_proper_type
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class InsertAll # :nodoc:
|
5
|
+
attr_reader :model, :connection, :inserts, :keys
|
6
|
+
attr_reader :on_duplicate, :returning, :unique_by
|
7
|
+
|
8
|
+
def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil)
|
9
|
+
raise ArgumentError, "Empty list of attributes passed" if inserts.blank?
|
10
|
+
|
11
|
+
@model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s).to_set
|
12
|
+
@on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
|
13
|
+
|
14
|
+
@returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil?
|
15
|
+
@returning = false if @returning == []
|
16
|
+
|
17
|
+
@unique_by = find_unique_index_for(unique_by) if unique_by
|
18
|
+
@on_duplicate = :skip if @on_duplicate == :update && updatable_columns.empty?
|
19
|
+
|
20
|
+
ensure_valid_options_for_connection!
|
21
|
+
end
|
22
|
+
|
23
|
+
def execute
|
24
|
+
message = +"#{model} "
|
25
|
+
message << "Bulk " if inserts.many?
|
26
|
+
message << (on_duplicate == :update ? "Upsert" : "Insert")
|
27
|
+
connection.exec_insert_all to_sql, message
|
28
|
+
end
|
29
|
+
|
30
|
+
def updatable_columns
|
31
|
+
keys - readonly_columns - unique_by_columns
|
32
|
+
end
|
33
|
+
|
34
|
+
def primary_keys
|
35
|
+
Array(model.primary_key)
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def skip_duplicates?
|
40
|
+
on_duplicate == :skip
|
41
|
+
end
|
42
|
+
|
43
|
+
def update_duplicates?
|
44
|
+
on_duplicate == :update
|
45
|
+
end
|
46
|
+
|
47
|
+
def map_key_with_value
|
48
|
+
inserts.map do |attributes|
|
49
|
+
attributes = attributes.stringify_keys
|
50
|
+
verify_attributes(attributes)
|
51
|
+
|
52
|
+
keys.map do |key|
|
53
|
+
yield key, attributes[key]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def find_unique_index_for(unique_by)
|
60
|
+
match = Array(unique_by).map(&:to_s)
|
61
|
+
|
62
|
+
if index = unique_indexes.find { |i| match.include?(i.name) || i.columns == match }
|
63
|
+
index
|
64
|
+
else
|
65
|
+
raise ArgumentError, "No unique index found for #{unique_by}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def unique_indexes
|
70
|
+
connection.schema_cache.indexes(model.table_name).select(&:unique)
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def ensure_valid_options_for_connection!
|
75
|
+
if returning && !connection.supports_insert_returning?
|
76
|
+
raise ArgumentError, "#{connection.class} does not support :returning"
|
77
|
+
end
|
78
|
+
|
79
|
+
if skip_duplicates? && !connection.supports_insert_on_duplicate_skip?
|
80
|
+
raise ArgumentError, "#{connection.class} does not support skipping duplicates"
|
81
|
+
end
|
82
|
+
|
83
|
+
if update_duplicates? && !connection.supports_insert_on_duplicate_update?
|
84
|
+
raise ArgumentError, "#{connection.class} does not support upsert"
|
85
|
+
end
|
86
|
+
|
87
|
+
if unique_by && !connection.supports_insert_conflict_target?
|
88
|
+
raise ArgumentError, "#{connection.class} does not support :unique_by"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
def to_sql
|
94
|
+
connection.build_insert_sql(ActiveRecord::InsertAll::Builder.new(self))
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
def readonly_columns
|
99
|
+
primary_keys + model.readonly_attributes.to_a
|
100
|
+
end
|
101
|
+
|
102
|
+
def unique_by_columns
|
103
|
+
Array(unique_by&.columns)
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
def verify_attributes(attributes)
|
108
|
+
if keys != attributes.keys.to_set
|
109
|
+
raise ArgumentError, "All objects being inserted must have the same keys"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Builder # :nodoc:
|
114
|
+
attr_reader :model
|
115
|
+
|
116
|
+
delegate :skip_duplicates?, :update_duplicates?, :keys, to: :insert_all
|
117
|
+
|
118
|
+
def initialize(insert_all)
|
119
|
+
@insert_all, @model, @connection = insert_all, insert_all.model, insert_all.connection
|
120
|
+
end
|
121
|
+
|
122
|
+
def into
|
123
|
+
"INTO #{model.quoted_table_name} (#{columns_list})"
|
124
|
+
end
|
125
|
+
|
126
|
+
def values_list
|
127
|
+
types = extract_types_from_columns_on(model.table_name, keys: keys)
|
128
|
+
|
129
|
+
values_list = insert_all.map_key_with_value do |key, value|
|
130
|
+
connection.with_yaml_fallback(types[key].serialize(value))
|
131
|
+
end
|
132
|
+
|
133
|
+
connection.visitor.compile(Arel::Nodes::ValuesList.new(values_list))
|
134
|
+
end
|
135
|
+
|
136
|
+
def returning
|
137
|
+
format_columns(insert_all.returning) if insert_all.returning
|
138
|
+
end
|
139
|
+
|
140
|
+
def conflict_target
|
141
|
+
if index = insert_all.unique_by
|
142
|
+
sql = +"(#{format_columns(index.columns)})"
|
143
|
+
sql << " WHERE #{index.where}" if index.where
|
144
|
+
sql
|
145
|
+
elsif update_duplicates?
|
146
|
+
"(#{format_columns(insert_all.primary_keys)})"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def updatable_columns
|
151
|
+
quote_columns(insert_all.updatable_columns)
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
attr_reader :connection, :insert_all
|
156
|
+
|
157
|
+
def columns_list
|
158
|
+
format_columns(insert_all.keys)
|
159
|
+
end
|
160
|
+
|
161
|
+
def extract_types_from_columns_on(table_name, keys:)
|
162
|
+
columns = connection.schema_cache.columns_hash(table_name)
|
163
|
+
|
164
|
+
unknown_column = (keys - columns.keys).first
|
165
|
+
raise UnknownAttributeError.new(model.new, unknown_column) if unknown_column
|
166
|
+
|
167
|
+
keys.index_with { |key| model.type_for_attribute(key) }
|
168
|
+
end
|
169
|
+
|
170
|
+
def format_columns(columns)
|
171
|
+
columns.respond_to?(:map) ? quote_columns(columns).join(",") : columns
|
172
|
+
end
|
173
|
+
|
174
|
+
def quote_columns(columns)
|
175
|
+
columns.map(&connection.method(:quote_column_name))
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -20,8 +20,16 @@ module ActiveRecord
|
|
20
20
|
# Indicates whether to use a stable #cache_key method that is accompanied
|
21
21
|
# by a changing version in the #cache_version method.
|
22
22
|
#
|
23
|
-
# This is +
|
23
|
+
# This is +true+, by default on Rails 5.2 and above.
|
24
24
|
class_attribute :cache_versioning, instance_writer: false, default: false
|
25
|
+
|
26
|
+
##
|
27
|
+
# :singleton-method:
|
28
|
+
# Indicates whether to use a stable #cache_key method that is accompanied
|
29
|
+
# by a changing version in the #cache_version method on collections.
|
30
|
+
#
|
31
|
+
# This is +false+, by default until Rails 6.1.
|
32
|
+
class_attribute :collection_cache_versioning, instance_writer: false, default: false
|
25
33
|
end
|
26
34
|
|
27
35
|
# Returns a +String+, which Action Pack uses for constructing a URL to this
|
@@ -60,24 +68,15 @@ module ActiveRecord
|
|
60
68
|
# the cache key will also include a version.
|
61
69
|
#
|
62
70
|
# Product.cache_versioning = false
|
63
|
-
#
|
64
|
-
def cache_key
|
71
|
+
# Product.find(5).cache_key # => "products/5-20071224150000" (updated_at available)
|
72
|
+
def cache_key
|
65
73
|
if new_record?
|
66
74
|
"#{model_name.cache_key}/new"
|
67
75
|
else
|
68
|
-
if cache_version
|
76
|
+
if cache_version
|
69
77
|
"#{model_name.cache_key}/#{id}"
|
70
78
|
else
|
71
|
-
timestamp =
|
72
|
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
73
|
-
Specifying a timestamp name for #cache_key has been deprecated in favor of
|
74
|
-
the explicit #cache_version method that can be overwritten.
|
75
|
-
MSG
|
76
|
-
|
77
|
-
max_updated_column_timestamp(timestamp_names)
|
78
|
-
else
|
79
|
-
max_updated_column_timestamp
|
80
|
-
end
|
79
|
+
timestamp = max_updated_column_timestamp
|
81
80
|
|
82
81
|
if timestamp
|
83
82
|
timestamp = timestamp.utc.to_s(cache_timestamp_format)
|
@@ -96,8 +95,19 @@ module ActiveRecord
|
|
96
95
|
# Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to
|
97
96
|
# +false+ (which it is by default until Rails 6.0).
|
98
97
|
def cache_version
|
99
|
-
|
100
|
-
|
98
|
+
return unless cache_versioning
|
99
|
+
|
100
|
+
if has_attribute?("updated_at")
|
101
|
+
timestamp = updated_at_before_type_cast
|
102
|
+
if can_use_fast_cache_version?(timestamp)
|
103
|
+
raw_timestamp_to_cache_version(timestamp)
|
104
|
+
elsif timestamp = updated_at
|
105
|
+
timestamp.utc.to_s(cache_timestamp_format)
|
106
|
+
end
|
107
|
+
else
|
108
|
+
if self.class.has_attribute?("updated_at")
|
109
|
+
raise ActiveModel::MissingAttributeError, "missing attribute: updated_at"
|
110
|
+
end
|
101
111
|
end
|
102
112
|
end
|
103
113
|
|
@@ -150,6 +160,48 @@ module ActiveRecord
|
|
150
160
|
end
|
151
161
|
end
|
152
162
|
end
|
163
|
+
|
164
|
+
def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
|
165
|
+
collection.send(:compute_cache_key, timestamp_column)
|
166
|
+
end
|
153
167
|
end
|
168
|
+
|
169
|
+
private
|
170
|
+
# Detects if the value before type cast
|
171
|
+
# can be used to generate a cache_version.
|
172
|
+
#
|
173
|
+
# The fast cache version only works with a
|
174
|
+
# string value directly from the database.
|
175
|
+
#
|
176
|
+
# We also must check if the timestamp format has been changed
|
177
|
+
# or if the timezone is not set to UTC then
|
178
|
+
# we cannot apply our transformations correctly.
|
179
|
+
def can_use_fast_cache_version?(timestamp)
|
180
|
+
timestamp.is_a?(String) &&
|
181
|
+
cache_timestamp_format == :usec &&
|
182
|
+
default_timezone == :utc &&
|
183
|
+
!updated_at_came_from_user?
|
184
|
+
end
|
185
|
+
|
186
|
+
# Converts a raw database string to `:usec`
|
187
|
+
# format.
|
188
|
+
#
|
189
|
+
# Example:
|
190
|
+
#
|
191
|
+
# timestamp = "2018-10-15 20:02:15.266505"
|
192
|
+
# raw_timestamp_to_cache_version(timestamp)
|
193
|
+
# # => "20181015200215266505"
|
194
|
+
#
|
195
|
+
# PostgreSQL truncates trailing zeros,
|
196
|
+
# https://github.com/postgres/postgres/commit/3e1beda2cde3495f41290e1ece5d544525810214
|
197
|
+
# to account for this we pad the output with zeros
|
198
|
+
def raw_timestamp_to_cache_version(timestamp)
|
199
|
+
key = timestamp.delete("- :.")
|
200
|
+
if key.length < 20
|
201
|
+
key.ljust(20, "0")
|
202
|
+
else
|
203
|
+
key
|
204
|
+
end
|
205
|
+
end
|
154
206
|
end
|
155
207
|
end
|
@@ -8,16 +8,20 @@ module ActiveRecord
|
|
8
8
|
# as which environment migrations were run in.
|
9
9
|
class InternalMetadata < ActiveRecord::Base # :nodoc:
|
10
10
|
class << self
|
11
|
+
def _internal?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
11
15
|
def primary_key
|
12
16
|
"key"
|
13
17
|
end
|
14
18
|
|
15
19
|
def table_name
|
16
|
-
"#{table_name_prefix}#{
|
20
|
+
"#{table_name_prefix}#{internal_metadata_table_name}#{table_name_suffix}"
|
17
21
|
end
|
18
22
|
|
19
23
|
def []=(key, value)
|
20
|
-
find_or_initialize_by(key: key).
|
24
|
+
find_or_initialize_by(key: key).update!(value: value)
|
21
25
|
end
|
22
26
|
|
23
27
|
def [](key)
|
@@ -34,12 +38,16 @@ module ActiveRecord
|
|
34
38
|
key_options = connection.internal_string_options_for_primary_key
|
35
39
|
|
36
40
|
connection.create_table(table_name, id: false) do |t|
|
37
|
-
t.string :key, key_options
|
41
|
+
t.string :key, **key_options
|
38
42
|
t.string :value
|
39
43
|
t.timestamps
|
40
44
|
end
|
41
45
|
end
|
42
46
|
end
|
47
|
+
|
48
|
+
def drop_table
|
49
|
+
connection.drop_table table_name, if_exists: true
|
50
|
+
end
|
43
51
|
end
|
44
52
|
end
|
45
53
|
end
|
@@ -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")
|
@@ -88,7 +96,7 @@ module ActiveRecord
|
|
88
96
|
|
89
97
|
affected_rows = self.class._update_record(
|
90
98
|
attributes_with_values(attribute_names),
|
91
|
-
|
99
|
+
@primary_key => id_in_database,
|
92
100
|
locking_column => previous_lock_value
|
93
101
|
)
|
94
102
|
|
@@ -111,7 +119,7 @@ module ActiveRecord
|
|
111
119
|
locking_column = self.class.locking_column
|
112
120
|
|
113
121
|
affected_rows = self.class._delete_record(
|
114
|
-
|
122
|
+
@primary_key => id_in_database,
|
115
123
|
locking_column => read_attribute_before_type_cast(locking_column)
|
116
124
|
)
|
117
125
|
|
@@ -157,7 +165,6 @@ module ActiveRecord
|
|
157
165
|
end
|
158
166
|
|
159
167
|
private
|
160
|
-
|
161
168
|
# We need to apply this decorator here, rather than on module inclusion. The closure
|
162
169
|
# created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
|
163
170
|
# sub class being decorated. As such, changes to `lock_optimistically`, or
|
@@ -165,7 +172,7 @@ module ActiveRecord
|
|
165
172
|
def inherited(subclass)
|
166
173
|
subclass.class_eval do
|
167
174
|
is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
|
168
|
-
decorate_matching_attribute_types(is_lock_column,
|
175
|
+
decorate_matching_attribute_types(is_lock_column, "_optimistic_locking") do |type|
|
169
176
|
LockingType.new(type)
|
170
177
|
end
|
171
178
|
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
|
@@ -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
|
@@ -38,7 +40,7 @@ module ActiveRecord
|
|
38
40
|
end
|
39
41
|
|
40
42
|
name = colorize_payload_name(name, payload[:name])
|
41
|
-
sql = color(sql, sql_color(sql), true)
|
43
|
+
sql = color(sql, sql_color(sql), true) if colorize_logging
|
42
44
|
|
43
45
|
debug " #{name} #{sql}#{binds}"
|
44
46
|
end
|
@@ -100,36 +102,15 @@ module ActiveRecord
|
|
100
102
|
end
|
101
103
|
|
102
104
|
def log_query_source
|
103
|
-
|
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
|
105
|
+
source = extract_query_source_location(caller)
|
114
106
|
|
115
|
-
|
116
|
-
|
117
|
-
frame.absolute_path && !ignored_callstack(frame.absolute_path)
|
107
|
+
if source
|
108
|
+
logger.debug(" ↳ #{source}")
|
118
109
|
end
|
119
|
-
|
120
|
-
offending_line = line || callstack.first
|
121
|
-
|
122
|
-
[
|
123
|
-
offending_line.path,
|
124
|
-
offending_line.lineno
|
125
|
-
]
|
126
110
|
end
|
127
111
|
|
128
|
-
|
129
|
-
|
130
|
-
def ignored_callstack(path)
|
131
|
-
path.start_with?(RAILS_GEM_ROOT) ||
|
132
|
-
path.start_with?(RbConfig::CONFIG["rubylibdir"])
|
112
|
+
def extract_query_source_location(locations)
|
113
|
+
backtrace_cleaner.clean(locations.lazy).first
|
133
114
|
end
|
134
115
|
end
|
135
116
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Middleware
|
5
|
+
class DatabaseSelector
|
6
|
+
class Resolver
|
7
|
+
# The session class is used by the DatabaseSelector::Resolver to save
|
8
|
+
# timestamps of the last write in the session.
|
9
|
+
#
|
10
|
+
# The last_write is used to determine whether it's safe to read
|
11
|
+
# from the replica or the request needs to be sent to the primary.
|
12
|
+
class Session # :nodoc:
|
13
|
+
def self.call(request)
|
14
|
+
new(request.session)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Converts time to a timestamp that represents milliseconds since
|
18
|
+
# epoch.
|
19
|
+
def self.convert_time_to_timestamp(time)
|
20
|
+
time.to_i * 1000 + time.usec / 1000
|
21
|
+
end
|
22
|
+
|
23
|
+
# Converts milliseconds since epoch timestamp into a time object.
|
24
|
+
def self.convert_timestamp_to_time(timestamp)
|
25
|
+
timestamp ? Time.at(timestamp / 1000, (timestamp % 1000) * 1000) : Time.at(0)
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(session)
|
29
|
+
@session = session
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :session
|
33
|
+
|
34
|
+
def last_write_timestamp
|
35
|
+
self.class.convert_timestamp_to_time(session[:last_write])
|
36
|
+
end
|
37
|
+
|
38
|
+
def update_last_write_timestamp
|
39
|
+
session[:last_write] = self.class.convert_time_to_timestamp(Time.now)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record/middleware/database_selector/resolver/session"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module Middleware
|
7
|
+
class DatabaseSelector
|
8
|
+
# The Resolver class is used by the DatabaseSelector middleware to
|
9
|
+
# determine which database the request should use.
|
10
|
+
#
|
11
|
+
# To change the behavior of the Resolver class in your application,
|
12
|
+
# create a custom resolver class that inherits from
|
13
|
+
# DatabaseSelector::Resolver and implements the methods that need to
|
14
|
+
# be changed.
|
15
|
+
#
|
16
|
+
# By default the Resolver class will send read traffic to the replica
|
17
|
+
# if it's been 2 seconds since the last write.
|
18
|
+
class Resolver # :nodoc:
|
19
|
+
SEND_TO_REPLICA_DELAY = 2.seconds
|
20
|
+
|
21
|
+
def self.call(context, options = {})
|
22
|
+
new(context, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(context, options = {})
|
26
|
+
@context = context
|
27
|
+
@options = options
|
28
|
+
@delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY
|
29
|
+
@instrumenter = ActiveSupport::Notifications.instrumenter
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :context, :delay, :instrumenter
|
33
|
+
|
34
|
+
def read(&blk)
|
35
|
+
if read_from_primary?
|
36
|
+
read_from_primary(&blk)
|
37
|
+
else
|
38
|
+
read_from_replica(&blk)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def write(&blk)
|
43
|
+
write_to_primary(&blk)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def read_from_primary(&blk)
|
48
|
+
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role, prevent_writes: true) do
|
49
|
+
instrumenter.instrument("database_selector.active_record.read_from_primary") do
|
50
|
+
yield
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def read_from_replica(&blk)
|
56
|
+
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role, prevent_writes: true) do
|
57
|
+
instrumenter.instrument("database_selector.active_record.read_from_replica") do
|
58
|
+
yield
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def write_to_primary(&blk)
|
64
|
+
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role, prevent_writes: false) do
|
65
|
+
instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
|
66
|
+
yield
|
67
|
+
ensure
|
68
|
+
context.update_last_write_timestamp
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def read_from_primary?
|
74
|
+
!time_since_last_write_ok?
|
75
|
+
end
|
76
|
+
|
77
|
+
def send_to_replica_delay
|
78
|
+
delay
|
79
|
+
end
|
80
|
+
|
81
|
+
def time_since_last_write_ok?
|
82
|
+
Time.now - context.last_write_timestamp >= send_to_replica_delay
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|