aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.codeclimate.yml16
-rw-r--r--.gitignore1
-rw-r--r--.rubocop.yml7
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock57
-rw-r--r--README.md2
-rw-r--r--RELEASING_RAILS.md2
-rw-r--r--actioncable/app/assets/javascripts/action_cable.js54
-rw-r--r--actioncable/app/javascript/action_cable/consumer.js23
-rw-r--r--actioncable/app/javascript/action_cable/index.js20
-rw-r--r--actioncable/karma.conf.js6
-rw-r--r--actioncable/lib/action_cable/connection/test_case.rb2
-rw-r--r--actioncable/test/javascript/src/unit/action_cable_test.js8
-rw-r--r--actioncable/test/test_helper.rb2
-rw-r--r--actionmailbox/CHANGELOG.md1
-rw-r--r--actionmailbox/README.md2
-rw-r--r--actionmailbox/app/controllers/action_mailbox/base_controller.rb6
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb54
-rw-r--r--actionmailbox/config/routes.rb1
-rw-r--r--actionmailbox/lib/action_mailbox/engine.rb11
-rw-r--r--actionmailbox/lib/action_mailbox/test_helper.rb12
-rw-r--r--actionmailbox/lib/rails/generators/installer.rb2
-rw-r--r--actionmailbox/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb22
-rw-r--r--actionmailbox/test/dummy/config/environments/production.rb11
-rw-r--r--actionmailbox/test/test_helper.rb2
-rw-r--r--actionmailer/test/abstract_unit.rb2
-rw-r--r--actionpack/CHANGELOG.md28
-rw-r--r--actionpack/lib/action_controller/metal/live.rb2
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb2
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb4
-rw-r--r--actionpack/lib/action_controller/renderer.rb2
-rw-r--r--actionpack/lib/action_dispatch.rb1
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb7
-rw-r--r--actionpack/lib/action_dispatch/journey/routes.rb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/actionable_exceptions.rb39
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb10
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_view.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb13
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb0
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb4
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb19
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb24
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb3
-rw-r--r--actionpack/test/abstract_unit.rb3
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb2
-rw-r--r--actionpack/test/controller/integration_test.rb6
-rw-r--r--actionpack/test/controller/mime/respond_to_test.rb13
-rw-r--r--actionpack/test/controller/new_base/render_file_test.rb28
-rw-r--r--actionpack/test/controller/render_test.rb17
-rw-r--r--actionpack/test/controller/renderer_test.rb6
-rw-r--r--actionpack/test/controller/test_case_test.rb2
-rw-r--r--actionpack/test/dispatch/actionable_exceptions_test.rb80
-rw-r--r--actionpack/test/dispatch/cookies_test.rb4
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb39
-rw-r--r--actionpack/test/dispatch/mount_test.rb9
-rw-r--r--actionpack/test/dispatch/prefix_generation_test.rb10
-rw-r--r--actionpack/test/dispatch/request_test.rb2
-rw-r--r--actionpack/test/dispatch/routing/route_set_test.rb6
-rw-r--r--actionpack/test/dispatch/routing_test.rb59
-rw-r--r--actionpack/test/dispatch/show_exceptions_test.rb6
-rw-r--r--actionpack/test/dispatch/system_testing/screenshot_helper_test.rb8
-rw-r--r--actionpack/test/journey/path/pattern_test.rb31
-rw-r--r--actiontext/lib/templates/installer.rb13
-rw-r--r--actiontext/test/test_helper.rb2
-rw-r--r--actionview/CHANGELOG.md20
-rw-r--r--actionview/lib/action_view.rb3
-rw-r--r--actionview/lib/action_view/cache_expiry.rb49
-rw-r--r--actionview/lib/action_view/digestor.rb6
-rw-r--r--actionview/lib/action_view/file_template.rb33
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb2
-rw-r--r--actionview/lib/action_view/lookup_context.rb5
-rw-r--r--actionview/lib/action_view/path_set.rb15
-rw-r--r--actionview/lib/action_view/railtie.rb2
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb33
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb2
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb12
-rw-r--r--actionview/lib/action_view/template.rb50
-rw-r--r--actionview/lib/action_view/template/error.rb22
-rw-r--r--actionview/lib/action_view/template/handlers.rb6
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubi.rb2
-rw-r--r--actionview/lib/action_view/template/raw_file.rb28
-rw-r--r--actionview/lib/action_view/template/resolver.rb115
-rw-r--r--actionview/lib/action_view/template/sources.rb13
-rw-r--r--actionview/lib/action_view/template/sources/file.rb17
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb8
-rw-r--r--actionview/lib/action_view/unbound_template.rb32
-rw-r--r--actionview/test/abstract_unit.rb2
-rw-r--r--actionview/test/actionpack/abstract/abstract_controller_test.rb10
-rw-r--r--actionview/test/actionpack/abstract/render_test.rb2
-rw-r--r--actionview/test/actionpack/controller/layout_test.rb4
-rw-r--r--actionview/test/actionpack/controller/render_test.rb44
-rw-r--r--actionview/test/fixtures/test/_cached_set.erb1
-rw-r--r--actionview/test/fixtures/test/syntax_error.html.erb4
-rw-r--r--actionview/test/template/fallback_file_system_resolver_test.rb2
-rw-r--r--actionview/test/template/file_system_resolver_test.rb12
-rw-r--r--actionview/test/template/javascript_helper_test.rb2
-rw-r--r--actionview/test/template/log_subscriber_test.rb13
-rw-r--r--actionview/test/template/lookup_context_test.rb8
-rw-r--r--actionview/test/template/optimized_file_system_resolver_test.rb12
-rw-r--r--actionview/test/template/render_test.rb70
-rw-r--r--actionview/test/template/resolver_patterns_test.rb5
-rw-r--r--actionview/test/template/resolver_shared_tests.rb148
-rw-r--r--actionview/test/template/streaming_render_test.rb4
-rw-r--r--actionview/test/template/template_test.rb24
-rw-r--r--actionview/test/template/testing/fixture_resolver_test.rb10
-rw-r--r--actionview/test/ujs/public/test/call-remote.js6
-rw-r--r--actionview/test/ujs/public/test/data-remote.js4
-rw-r--r--actionview/test/ujs/public/test/settings.js2
-rw-r--r--activejob/CHANGELOG.md6
-rw-r--r--activejob/lib/active_job/enqueuing.rb2
-rw-r--r--activejob/lib/active_job/exceptions.rb4
-rw-r--r--activejob/lib/active_job/test_helper.rb4
-rw-r--r--activejob/test/cases/exceptions_test.rb20
-rw-r--r--activejob/test/helper.rb2
-rw-r--r--activemodel/CHANGELOG.md12
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb24
-rw-r--r--activemodel/lib/active_model/attribute_mutation_tracker.rb117
-rw-r--r--activemodel/lib/active_model/attributes.rb2
-rw-r--r--activemodel/lib/active_model/dirty.rb133
-rw-r--r--activemodel/lib/active_model/errors.rb6
-rw-r--r--activemodel/lib/active_model/railtie.rb4
-rw-r--r--activemodel/lib/active_model/secure_password.rb36
-rw-r--r--activemodel/lib/active_model/type/boolean.rb11
-rw-r--r--activemodel/test/cases/attribute_methods_test.rb1
-rw-r--r--activemodel/test/cases/helper.rb2
-rw-r--r--activemodel/test/cases/railtie_test.rb16
-rw-r--r--activemodel/test/cases/secure_password_test.rb14
-rw-r--r--activemodel/test/cases/type/boolean_test.rb14
-rw-r--r--activemodel/test/cases/validations/acceptance_validation_test.rb50
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb124
-rw-r--r--activemodel/test/models/user.rb7
-rw-r--r--activerecord/CHANGELOG.md139
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/associations/association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb28
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb68
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb70
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb5
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb48
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb5
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb11
-rw-r--r--activerecord/lib/active_record/autosave_association.rb8
-rw-r--r--activerecord/lib/active_record/base.rb1
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb53
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb64
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb60
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb47
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb68
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb82
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb106
-rw-r--r--activerecord/lib/active_record/connection_handling.rb27
-rw-r--r--activerecord/lib/active_record/core.rb7
-rw-r--r--activerecord/lib/active_record/database_configurations.rb2
-rw-r--r--activerecord/lib/active_record/insert_all.rb34
-rw-r--r--activerecord/lib/active_record/integration.rb14
-rw-r--r--activerecord/lib/active_record/internal_metadata.rb6
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb3
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/migration.rb41
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb25
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb10
-rw-r--r--activerecord/lib/active_record/persistence.rb41
-rw-r--r--activerecord/lib/active_record/querying.rb4
-rw-r--r--activerecord/lib/active_record/railties/databases.rake64
-rw-r--r--activerecord/lib/active_record/reflection.rb4
-rw-r--r--activerecord/lib/active_record/relation.rb105
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb61
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb5
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb8
-rw-r--r--activerecord/lib/active_record/relation/merger.rb23
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb70
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb5
-rw-r--r--activerecord/lib/active_record/schema_migration.rb2
-rw-r--r--activerecord/lib/active_record/scoping/named.rb2
-rw-r--r--activerecord/lib/active_record/store.rb48
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb20
-rw-r--r--activerecord/lib/active_record/touch_later.rb4
-rw-r--r--activerecord/lib/active_record/transactions.rb76
-rw-r--r--activerecord/lib/arel/nodes/delete_statement.rb9
-rw-r--r--activerecord/lib/arel/nodes/insert_statement.rb9
-rw-r--r--activerecord/lib/arel/nodes/select_core.rb1
-rw-r--r--activerecord/lib/arel/nodes/update_statement.rb9
-rw-r--r--activerecord/lib/arel/select_manager.rb4
-rw-r--r--activerecord/lib/arel/tree_manager.rb5
-rw-r--r--activerecord/lib/arel/visitors/oracle.rb48
-rw-r--r--activerecord/lib/arel/visitors/oracle12.rb48
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb6
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt2
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt6
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt11
-rw-r--r--activerecord/test/cases/adapter_test.rb60
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb28
-rw-r--r--activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb3
-rw-r--r--activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb8
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/sp_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/extension_migration_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb16
-rw-r--r--activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/partitions_test.rb2
-rw-r--r--activerecord/test/cases/ar_schema_test.rb17
-rw-r--r--activerecord/test/cases/arel/delete_manager_test.rb18
-rw-r--r--activerecord/test/cases/arel/nodes/delete_statement_test.rb8
-rw-r--r--activerecord/test/cases/arel/nodes/insert_statement_test.rb8
-rw-r--r--activerecord/test/cases/arel/nodes/update_statement_test.rb8
-rw-r--r--activerecord/test/cases/arel/update_manager_test.rb24
-rw-r--r--activerecord/test/cases/arel/visitors/oracle12_test.rb21
-rw-r--r--activerecord/test/cases/arel/visitors/oracle_test.rb21
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb2
-rw-r--r--activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb2
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb4
-rw-r--r--activerecord/test/cases/associations/eager_test.rb23
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb16
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb34
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb5
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb9
-rw-r--r--activerecord/test/cases/associations/left_outer_join_association_test.rb10
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb9
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb4
-rw-r--r--activerecord/test/cases/base_test.rb17
-rw-r--r--activerecord/test/cases/boolean_test.rb9
-rw-r--r--activerecord/test/cases/calculations_test.rb17
-rw-r--r--activerecord/test/cases/callbacks_test.rb4
-rw-r--r--activerecord/test/cases/collection_cache_key_test.rb34
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb43
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb34
-rw-r--r--activerecord/test/cases/date_time_precision_test.rb2
-rw-r--r--activerecord/test/cases/defaults_test.rb2
-rw-r--r--activerecord/test/cases/helper.rb4
-rw-r--r--activerecord/test/cases/insert_all_test.rb101
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb59
-rw-r--r--activerecord/test/cases/locking_test.rb11
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb6
-rw-r--r--activerecord/test/cases/migration/columns_test.rb2
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb34
-rw-r--r--activerecord/test/cases/migration/compatibility_test.rb33
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb2
-rw-r--r--activerecord/test/cases/migration_test.rb12
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb4
-rw-r--r--activerecord/test/cases/relation/delete_all_test.rb19
-rw-r--r--activerecord/test/cases/relation/update_all_test.rb41
-rw-r--r--activerecord/test/cases/relation_test.rb4
-rw-r--r--activerecord/test/cases/relations_test.rb13
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb28
-rw-r--r--activerecord/test/cases/store_test.rb68
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb56
-rw-r--r--activerecord/test/cases/time_precision_test.rb2
-rw-r--r--activerecord/test/cases/timestamp_test.rb14
-rw-r--r--activerecord/test/cases/touch_later_test.rb2
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb44
-rw-r--r--activerecord/test/cases/transactions_test.rb160
-rw-r--r--activerecord/test/cases/validations_test.rb12
-rw-r--r--activerecord/test/models/post.rb8
-rw-r--r--activerecord/test/models/section.rb6
-rw-r--r--activerecord/test/models/seminar.rb6
-rw-r--r--activerecord/test/models/session.rb6
-rw-r--r--activerecord/test/schema/schema.rb19
-rw-r--r--activestorage/activestorage.gemspec3
-rw-r--r--activestorage/app/controllers/active_storage/disk_controller.rb2
-rw-r--r--activestorage/app/models/active_storage/attachment.rb2
-rw-r--r--activestorage/app/models/active_storage/blob.rb13
-rw-r--r--activestorage/lib/active_storage/analyzer.rb4
-rw-r--r--activestorage/lib/active_storage/downloader.rb29
-rw-r--r--activestorage/lib/active_storage/engine.rb4
-rw-r--r--activestorage/lib/active_storage/previewer.rb8
-rw-r--r--activestorage/lib/active_storage/service.rb4
-rw-r--r--activestorage/test/models/blob_test.rb18
-rw-r--r--activestorage/test/service/disk_service_test.rb2
-rw-r--r--activestorage/test/template/image_tag_test.rb2
-rw-r--r--activestorage/test/test_helper.rb2
-rw-r--r--activesupport/CHANGELOG.md117
-rw-r--r--activesupport/activesupport.gemspec2
-rw-r--r--activesupport/lib/active_support.rb1
-rw-r--r--activesupport/lib/active_support/actionable_error.rb48
-rw-r--r--activesupport/lib/active_support/backtrace_cleaner.rb6
-rw-r--r--activesupport/lib/active_support/cache/redis_cache_store.rb13
-rw-r--r--activesupport/lib/active_support/concern.rb25
-rw-r--r--activesupport/lib/active_support/core_ext/array/access.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/hash/except.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/range/compare_range.rb33
-rw-r--r--activesupport/lib/active_support/core_ext/string/output_safety.rb45
-rw-r--r--activesupport/lib/active_support/dependencies.rb5
-rw-r--r--activesupport/lib/active_support/dependencies/zeitwerk_integration.rb42
-rw-r--r--activesupport/lib/active_support/deprecation/method_wrappers.rb25
-rw-r--r--activesupport/lib/active_support/descendants_tracker.rb65
-rw-r--r--activesupport/lib/active_support/duration.rb1
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb9
-rw-r--r--activesupport/lib/active_support/i18n_railtie.rb3
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb4
-rw-r--r--activesupport/lib/active_support/notifications/instrumenter.rb7
-rw-r--r--activesupport/lib/active_support/security_utils.rb2
-rw-r--r--activesupport/lib/active_support/subscriber.rb61
-rw-r--r--activesupport/lib/active_support/testing/parallelization.rb15
-rw-r--r--activesupport/test/abstract_unit.rb2
-rw-r--r--activesupport/test/actionable_error_test.rb47
-rw-r--r--activesupport/test/cache/stores/redis_cache_store_test.rb6
-rw-r--r--activesupport/test/core_ext/range_ext_test.rb8
-rw-r--r--activesupport/test/deprecation/method_wrappers_test.rb8
-rw-r--r--activesupport/test/descendants_tracker_test_cases.rb9
-rw-r--r--activesupport/test/notifications_test.rb17
-rw-r--r--activesupport/test/safe_buffer_test.rb26
-rw-r--r--activesupport/test/subscriber_test.rb23
-rw-r--r--guides/CHANGELOG.md5
-rw-r--r--guides/rails_guides/markdown/renderer.rb4
-rw-r--r--guides/source/3_1_release_notes.md4
-rw-r--r--guides/source/6_0_release_notes.md287
-rw-r--r--guides/source/action_cable_overview.md17
-rw-r--r--guides/source/action_mailbox_basics.md28
-rw-r--r--guides/source/action_mailer_basics.md2
-rw-r--r--guides/source/active_record_basics.md2
-rw-r--r--guides/source/active_record_callbacks.md27
-rw-r--r--guides/source/active_record_migrations.md3
-rw-r--r--guides/source/active_record_querying.md1
-rw-r--r--guides/source/active_support_instrumentation.md27
-rw-r--r--guides/source/asset_pipeline.md23
-rw-r--r--guides/source/association_basics.md45
-rw-r--r--guides/source/caching_with_rails.md2
-rw-r--r--guides/source/command_line.md31
-rw-r--r--guides/source/configuring.md6
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md8
-rw-r--r--guides/source/debugging_rails_applications.md7
-rw-r--r--guides/source/documents.yaml5
-rw-r--r--guides/source/engines.md88
-rw-r--r--guides/source/form_helpers.md2
-rw-r--r--guides/source/i18n.md2
-rw-r--r--guides/source/layouts_and_rendering.md39
-rw-r--r--guides/source/rails_on_rack.md2
-rw-r--r--guides/source/testing.md6
-rw-r--r--railties/CHANGELOG.md40
-rw-r--r--railties/Rakefile41
-rw-r--r--railties/lib/rails/application/configuration.rb4
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb1
-rw-r--r--railties/lib/rails/application/finisher.rb39
-rw-r--r--railties/lib/rails/command/environment_argument.rb4
-rw-r--r--railties/lib/rails/commands/dbconsole/dbconsole_command.rb26
-rw-r--r--railties/lib/rails/commands/dev/dev_command.rb6
-rw-r--r--railties/lib/rails/commands/notes/notes_command.rb2
-rw-r--r--railties/lib/rails/engine.rb54
-rw-r--r--railties/lib/rails/generators/app_name.rb4
-rw-r--r--railties/lib/rails/generators/database.rb2
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt3
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt8
-rw-r--r--railties/lib/rails/generators/generated_attribute.rb46
-rw-r--r--railties/lib/rails/generators/named_base.rb1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/setup.tt5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/update.tt33
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt15
-rw-r--r--railties/lib/rails/generators/rails/app/templates/ruby-version.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb15
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb8
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt2
-rw-r--r--railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt2
-rw-r--r--railties/lib/rails/mailers_controller.rb7
-rw-r--r--railties/lib/rails/source_annotation_extractor.rb15
-rw-r--r--railties/test/abstract_unit.rb2
-rw-r--r--railties/test/application/asset_debugging_test.rb2
-rw-r--r--railties/test/application/assets_test.rb4
-rw-r--r--railties/test/application/bin_setup_test.rb26
-rw-r--r--railties/test/application/configuration_test.rb55
-rw-r--r--railties/test/application/initializers/frameworks_test.rb4
-rw-r--r--railties/test/application/mailer_previews_test.rb7
-rw-r--r--railties/test/application/middleware/exceptions_test.rb2
-rw-r--r--railties/test/application/middleware_test.rb2
-rw-r--r--railties/test/application/rake/dbs_test.rb20
-rw-r--r--railties/test/application/rake/multi_dbs_test.rb79
-rw-r--r--railties/test/application/rake/routes_test.rb1
-rw-r--r--railties/test/application/zeitwerk_integration_test.rb136
-rw-r--r--railties/test/backtrace_cleaner_test.rb10
-rw-r--r--railties/test/commands/credentials_test.rb8
-rw-r--r--railties/test/commands/dbconsole_test.rb24
-rw-r--r--railties/test/commands/notes_test.rb41
-rw-r--r--railties/test/commands/routes_test.rb43
-rw-r--r--railties/test/generators/actions_test.rb2
-rw-r--r--railties/test/generators/api_app_generator_test.rb1
-rw-r--r--railties/test/generators/app_generator_test.rb148
-rw-r--r--railties/test/generators/db_system_change_generator_test.rb2
-rw-r--r--railties/test/generators/generated_attribute_test.rb29
-rw-r--r--railties/test/generators/migration_generator_test.rb67
-rw-r--r--railties/test/generators/model_generator_test.rb97
-rw-r--r--railties/test/generators/scaffold_controller_generator_test.rb18
-rw-r--r--railties/test/generators/scaffold_generator_test.rb18
-rw-r--r--railties/test/generators/shared_generator_tests.rb5
-rw-r--r--railties/test/isolation/abstract_unit.rb4
-rw-r--r--railties/test/railties/engine_test.rb15
-rw-r--r--tools/test_common.rb17
-rw-r--r--yarn.lock5
422 files changed, 6112 insertions, 2366 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
index 908a988e69..7114a98266 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -1,3 +1,5 @@
+version: "2"
+
checks:
argument-count:
enabled: false
@@ -20,17 +22,9 @@ checks:
identical-code:
enabled: false
-engines:
+plugins:
rubocop:
enabled: true
- channel: rubocop-0-63
-
-ratings:
- paths:
- - "**.rb"
+ channel: rubocop-0-67
-exclude_paths:
- - ci/
- - guides/
- - tasks/
- - tools/
+exclude_patterns: []
diff --git a/.gitignore b/.gitignore
index e127852627..71acd2c638 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ package-lock.json
pkg/
/tmp/
/yarn-error.log
+/test-reports/
diff --git a/.rubocop.yml b/.rubocop.yml
index 54dcaf80f9..0cfe5d5d84 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,3 +1,5 @@
+require: rubocop-performance
+
AllCops:
TargetRubyVersion: 2.5
# RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
@@ -10,7 +12,7 @@ AllCops:
- 'railties/test/fixtures/tmp/**/*'
- 'actionmailbox/test/dummy/**/*'
- 'actiontext/test/dummy/**/*'
- - 'node_modules/**/*'
+ - '**/node_modules/**/*'
Performance:
Exclude:
@@ -116,6 +118,9 @@ Layout/SpaceAroundOperators:
Layout/SpaceBeforeComma:
Enabled: true
+Layout/SpaceBeforeComment:
+ Enabled: true
+
Layout/SpaceBeforeFirstArg:
Enabled: true
diff --git a/Gemfile b/Gemfile
index 53f8512cc5..f3dea80e45 100644
--- a/Gemfile
+++ b/Gemfile
@@ -29,6 +29,7 @@ gem "uglifier", ">= 1.3.0", require: false
gem "json", ">= 2.0.0"
gem "rubocop", ">= 0.47", require: false
+gem "rubocop-performance", require: false
group :doc do
gem "sdoc", "~> 1.0"
@@ -44,7 +45,7 @@ gem "libxml-ruby", platforms: :ruby
gem "connection_pool", require: false
# for railties app_generator_test
-gem "bootsnap", ">= 1.4.0", require: false
+gem "bootsnap", ">= 1.4.2", require: false
# Active Job
group :job do
@@ -102,6 +103,7 @@ instance_eval File.read local_gemfile if File.exist? local_gemfile
group :test do
gem "minitest-bisect"
gem "minitest-retry"
+ gem "minitest-reporters"
platforms :mri do
gem "stackprof"
@@ -118,7 +120,7 @@ platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do
gem "racc", ">=1.4.6", require: false
# Active Record.
- gem "sqlite3", "~> 1.3", ">= 1.3.6"
+ gem "sqlite3", "~> 1.4"
group :db do
gem "pg", ">= 0.18.0"
diff --git a/Gemfile.lock b/Gemfile.lock
index d4c2808b38..784774577e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -63,6 +63,7 @@ PATH
activesupport (= 6.0.0.beta3)
activestorage (6.0.0.beta3)
actionpack (= 6.0.0.beta3)
+ activejob (= 6.0.0.beta3)
activerecord (= 6.0.0.beta3)
marcel (~> 0.3.1)
activesupport (6.0.0.beta3)
@@ -70,7 +71,7 @@ PATH
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
- zeitwerk (~> 1.4, >= 1.4.2)
+ zeitwerk (~> 2.1, >= 2.1.2)
rails (6.0.0.beta3)
actioncable (= 6.0.0.beta3)
actionmailbox (= 6.0.0.beta3)
@@ -107,9 +108,10 @@ GEM
activerecord-jdbcsqlite3-adapter (52.1-java)
activerecord-jdbc-adapter (= 52.1)
jdbc-sqlite3 (~> 3.8, < 3.30)
- addressable (2.5.2)
+ addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
amq-protocol (2.3.0)
+ ansi (1.5.0)
ast (2.4.0)
aws-eventstream (1.0.1)
aws-partitions (1.111.0)
@@ -165,9 +167,9 @@ GEM
childprocess
faraday
selenium-webdriver
- bootsnap (1.4.0)
+ bootsnap (1.4.2)
msgpack (~> 1.0)
- bootsnap (1.4.0-java)
+ bootsnap (1.4.2-java)
msgpack (~> 1.0)
builder (3.2.3)
bunny (2.13.0)
@@ -277,6 +279,7 @@ GEM
image_processing (1.7.1)
mini_magick (~> 4.0)
ruby-vips (>= 2.0.13, < 3)
+ jar-dependencies (0.4.0)
jaro_winkler (1.5.2)
jaro_winkler (1.5.2-java)
jdbc-mysql (5.1.46)
@@ -314,15 +317,20 @@ GEM
minitest-bisect (1.4.0)
minitest-server (~> 1.0)
path_expander (~> 1.0)
+ minitest-reporters (1.3.6)
+ ansi
+ builder
+ minitest (>= 5.0)
+ ruby-progressbar
minitest-retry (0.1.9)
minitest (>= 5.0)
minitest-server (1.0.5)
minitest (~> 5.0)
mono_logger (1.1.0)
- msgpack (1.2.6)
- msgpack (1.2.6-java)
- msgpack (1.2.6-x64-mingw32)
- msgpack (1.2.6-x86-mingw32)
+ msgpack (1.2.9)
+ msgpack (1.2.9-java)
+ msgpack (1.2.9-x64-mingw32)
+ msgpack (1.2.9-x86-mingw32)
multi_json (1.13.1)
multipart-post (2.0.0)
mustache (1.1.0)
@@ -342,14 +350,17 @@ GEM
mini_portile2 (~> 2.4.0)
os (1.0.0)
parallel (1.13.0)
- parser (2.6.0.0)
+ parser (2.6.2.0)
ast (~> 2.4.0)
path_expander (1.0.3)
pg (1.1.3)
pg (1.1.3-x64-mingw32)
pg (1.1.3-x86-mingw32)
- powerpack (0.1.2)
- psych (3.0.3)
+ psych (3.1.0)
+ psych (3.1.0-java)
+ jar-dependencies (>= 0.1.7)
+ psych (3.1.0-x64-mingw32)
+ psych (3.1.0-x86-mingw32)
public_suffix (3.0.3)
puma (3.12.1)
puma (3.12.1-java)
@@ -374,7 +385,7 @@ GEM
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
rainbow (3.0.0)
- rake (12.3.1)
+ rake (12.3.2)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
@@ -400,14 +411,16 @@ GEM
resque (~> 1.26)
rufus-scheduler (~> 3.2)
retriable (3.1.2)
- rubocop (0.63.0)
+ rubocop (0.67.2)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1)
- powerpack (~> 0.1)
+ psych (>= 3.1.0)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
- unicode-display_width (~> 1.4.0)
+ unicode-display_width (>= 1.4.0, < 1.6)
+ rubocop-performance (1.1.0)
+ rubocop (>= 0.67.0)
ruby-progressbar (1.10.0)
ruby-vips (2.0.13)
ffi (~> 1.9)
@@ -464,9 +477,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
- sqlite3 (1.3.13)
- sqlite3 (1.3.13-x64-mingw32)
- sqlite3 (1.3.13-x86-mingw32)
+ sqlite3 (1.4.0)
stackprof (0.2.12)
sucker_punch (2.1.1)
concurrent-ruby (~> 1.0)
@@ -488,7 +499,7 @@ GEM
uber (0.1.0)
uglifier (4.1.19)
execjs (>= 0.3.0, < 3)
- unicode-display_width (1.4.0)
+ unicode-display_width (1.5.0)
useragent (0.16.10)
vegas (0.1.11)
rack (>= 1.0.0)
@@ -517,7 +528,7 @@ GEM
websocket-extensions (0.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (1.4.2)
+ zeitwerk (2.1.2)
PLATFORMS
java
@@ -537,7 +548,7 @@ DEPENDENCIES
benchmark-ips
blade
blade-sauce_labs_plugin
- bootsnap (>= 1.4.0)
+ bootsnap (>= 1.4.2)
byebug
capybara (>= 2.15)
connection_pool
@@ -552,6 +563,7 @@ DEPENDENCIES
libxml-ruby
listen (>= 3.0.5, < 3.2)
minitest-bisect
+ minitest-reporters
minitest-retry
mysql2 (>= 0.4.10)
nokogiri (>= 1.8.1)
@@ -571,6 +583,7 @@ DEPENDENCIES
resque
resque-scheduler
rubocop (>= 0.47)
+ rubocop-performance
sass-rails
sdoc (~> 1.0)
selenium-webdriver (>= 3.5.0, < 3.13.0)
@@ -578,7 +591,7 @@ DEPENDENCIES
sidekiq
sneakers
sprockets-export
- sqlite3 (~> 1.3, >= 1.3.6)
+ sqlite3 (~> 1.4)
stackprof
sucker_punch
turbolinks (~> 5)
diff --git a/README.md b/README.md
index 566136e2a2..98ade9b445 100644
--- a/README.md
+++ b/README.md
@@ -97,7 +97,7 @@ Everyone interacting in Rails and its sub-projects' codebases, issue trackers, c
## Code Status
-[![Build Status](https://travis-ci.org/rails/rails.svg?branch=master)](https://travis-ci.org/rails/rails)
+[![Build Status](https://badge.buildkite.com/ab1152b6a1f6a61d3ea4ec5b3eece8d4c2b830998459c75352.svg?branch=master)](https://buildkite.com/rails/rails)
## License
diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md
index dd2742b403..d2d7a771bc 100644
--- a/RELEASING_RAILS.md
+++ b/RELEASING_RAILS.md
@@ -14,7 +14,7 @@ Today is mostly coordination tasks. Here are the things you must do today:
Do not release with a Red CI. You can find the CI status here:
```
-https://travis-ci.org/rails/rails
+https://buildkite.com/rails/rails
```
### Is Sam Ruby happy? If not, make him happy.
diff --git a/actioncable/app/assets/javascripts/action_cable.js b/actioncable/app/assets/javascripts/action_cable.js
index 4efab2ed46..8349361405 100644
--- a/actioncable/app/assets/javascripts/action_cable.js
+++ b/actioncable/app/assets/javascripts/action_cable.js
@@ -28,6 +28,22 @@
throw new TypeError("Cannot call a class as a function");
}
};
+ var createClass = function() {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+ return function(Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
var now = function now() {
return new Date().getTime();
};
@@ -432,7 +448,7 @@
var Consumer = function() {
function Consumer(url) {
classCallCheck(this, Consumer);
- this.url = url;
+ this._url = url;
this.subscriptions = new Subscriptions(this);
this.connection = new Connection(this);
}
@@ -452,11 +468,31 @@
return this.connection.open();
}
};
+ createClass(Consumer, [ {
+ key: "url",
+ get: function get$$1() {
+ return createWebSocketURL(this._url);
+ }
+ } ]);
return Consumer;
}();
+ function createWebSocketURL(url) {
+ if (typeof url === "function") {
+ url = url();
+ }
+ if (url && !/^wss?:/i.test(url)) {
+ var a = document.createElement("a");
+ a.href = url;
+ a.href = a.href;
+ a.protocol = a.protocol.replace("http", "ws");
+ return a.href;
+ } else {
+ return url;
+ }
+ }
function createConsumer() {
var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path;
- return new Consumer(createWebSocketURL(url));
+ return new Consumer(url);
}
function getConfig(name) {
var element = document.head.querySelector("meta[name='action-cable-" + name + "']");
@@ -464,18 +500,6 @@
return element.getAttribute("content");
}
}
- function createWebSocketURL(url) {
- var webSocketURL = typeof url === "function" ? url() : url;
- if (webSocketURL && !/^wss?:/i.test(webSocketURL)) {
- var a = document.createElement("a");
- a.href = webSocketURL;
- a.href = a.href;
- a.protocol = a.protocol.replace("http", "ws");
- return a.href;
- } else {
- return webSocketURL;
- }
- }
exports.Connection = Connection;
exports.ConnectionMonitor = ConnectionMonitor;
exports.Consumer = Consumer;
@@ -483,10 +507,10 @@
exports.Subscription = Subscription;
exports.Subscriptions = Subscriptions;
exports.adapters = adapters;
+ exports.createWebSocketURL = createWebSocketURL;
exports.logger = logger;
exports.createConsumer = createConsumer;
exports.getConfig = getConfig;
- exports.createWebSocketURL = createWebSocketURL;
Object.defineProperty(exports, "__esModule", {
value: true
});
diff --git a/actioncable/app/javascript/action_cable/consumer.js b/actioncable/app/javascript/action_cable/consumer.js
index e8440f39f5..e2e0dea8b5 100644
--- a/actioncable/app/javascript/action_cable/consumer.js
+++ b/actioncable/app/javascript/action_cable/consumer.js
@@ -29,11 +29,15 @@ import Subscriptions from "./subscriptions"
export default class Consumer {
constructor(url) {
- this.url = url
+ this._url = url
this.subscriptions = new Subscriptions(this)
this.connection = new Connection(this)
}
+ get url() {
+ return createWebSocketURL(this._url)
+ }
+
send(data) {
return this.connection.send(data)
}
@@ -52,3 +56,20 @@ export default class Consumer {
}
}
}
+
+export function createWebSocketURL(url) {
+ if (typeof url === "function") {
+ url = url()
+ }
+
+ if (url && !/^wss?:/i.test(url)) {
+ const a = document.createElement("a")
+ a.href = url
+ // Fix populating Location properties in IE. Otherwise, protocol will be blank.
+ a.href = a.href
+ a.protocol = a.protocol.replace("http", "ws")
+ return a.href
+ } else {
+ return url
+ }
+}
diff --git a/actioncable/app/javascript/action_cable/index.js b/actioncable/app/javascript/action_cable/index.js
index e679745fd7..848b5631d6 100644
--- a/actioncable/app/javascript/action_cable/index.js
+++ b/actioncable/app/javascript/action_cable/index.js
@@ -1,6 +1,6 @@
import Connection from "./connection"
import ConnectionMonitor from "./connection_monitor"
-import Consumer from "./consumer"
+import Consumer, { createWebSocketURL } from "./consumer"
import INTERNAL from "./internal"
import Subscription from "./subscription"
import Subscriptions from "./subscriptions"
@@ -15,11 +15,12 @@ export {
Subscription,
Subscriptions,
adapters,
+ createWebSocketURL,
logger,
}
export function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
- return new Consumer(createWebSocketURL(url))
+ return new Consumer(url)
}
export function getConfig(name) {
@@ -28,18 +29,3 @@ export function getConfig(name) {
return element.getAttribute("content")
}
}
-
-export function createWebSocketURL(url) {
- const webSocketURL = typeof url === "function" ? url() : url
-
- if (webSocketURL && !/^wss?:/i.test(webSocketURL)) {
- const a = document.createElement("a")
- a.href = webSocketURL
- // Fix populating Location properties in IE. Otherwise, protocol will be blank.
- a.href = a.href
- a.protocol = a.protocol.replace("http", "ws")
- return a.href
- } else {
- return webSocketURL
- }
-}
diff --git a/actioncable/karma.conf.js b/actioncable/karma.conf.js
index 845b38d74f..83e9c98af1 100644
--- a/actioncable/karma.conf.js
+++ b/actioncable/karma.conf.js
@@ -52,9 +52,9 @@ if (process.env.CI) {
}
function buildId() {
- const { TRAVIS_BUILD_NUMBER, TRAVIS_BUILD_ID } = process.env
- return TRAVIS_BUILD_NUMBER && TRAVIS_BUILD_ID
- ? `TRAVIS #${TRAVIS_BUILD_NUMBER} (${TRAVIS_BUILD_ID})`
+ const { BUILDKITE_JOB_ID } = process.env
+ return BUILDKITE_JOB_ID
+ ? `Buildkite ${BUILDKITE_JOB_ID}`
: ""
}
}
diff --git a/actioncable/lib/action_cable/connection/test_case.rb b/actioncable/lib/action_cable/connection/test_case.rb
index 8d25a55c8a..f1673fea08 100644
--- a/actioncable/lib/action_cable/connection/test_case.rb
+++ b/actioncable/lib/action_cable/connection/test_case.rb
@@ -176,7 +176,7 @@ module ActionCable
#
# Accepts request path as the first argument and the following request options:
#
- # - params – url parameters (Hash)
+ # - params – URL parameters (Hash)
# - headers – request headers (Hash)
# - session – session data (Hash)
# - env – additional Rack env configuration (Hash)
diff --git a/actioncable/test/javascript/src/unit/action_cable_test.js b/actioncable/test/javascript/src/unit/action_cable_test.js
index c9d34abc6d..c46f9878d2 100644
--- a/actioncable/test/javascript/src/unit/action_cable_test.js
+++ b/actioncable/test/javascript/src/unit/action_cable_test.js
@@ -42,12 +42,16 @@ module("ActionCable", () => {
assert.equal(consumer.url, testURL)
})
- test("uses function to generate URL", assert => {
+ test("dynamically computes URL from function", assert => {
+ let dynamicURL = testURL
const generateURL = () => {
- return testURL
+ return dynamicURL
}
const consumer = ActionCable.createConsumer(generateURL)
assert.equal(consumer.url, testURL)
+
+ dynamicURL = `${testURL}foo`
+ assert.equal(consumer.url, `${testURL}foo`)
})
})
})
diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb
index c924f1e475..033f034b0c 100644
--- a/actioncable/test/test_helper.rb
+++ b/actioncable/test/test_helper.rb
@@ -41,3 +41,5 @@ class ActionCable::TestCase < ActiveSupport::TestCase
end
end
end
+
+require_relative "../../tools/test_common"
diff --git a/actionmailbox/CHANGELOG.md b/actionmailbox/CHANGELOG.md
index f59c052d63..f5fb573090 100644
--- a/actionmailbox/CHANGELOG.md
+++ b/actionmailbox/CHANGELOG.md
@@ -11,6 +11,7 @@
*Pratik Naik*
+
## Rails 6.0.0.beta1 (January 18, 2019) ##
* Added to Rails.
diff --git a/actionmailbox/README.md b/actionmailbox/README.md
index 9a47223d3b..593bd429ae 100644
--- a/actionmailbox/README.md
+++ b/actionmailbox/README.md
@@ -1,6 +1,6 @@
# Action Mailbox
-Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, Postmark, and SendGrid. You can also handle inbound mails directly via the built-in Exim, Postfix, and Qmail ingresses.
+Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Mailgun, Mandrill, Postmark, and SendGrid. You can also handle inbound mails directly via the built-in Exim, Postfix, and Qmail ingresses.
The inbound emails are turned into `InboundEmail` records using Active Record and feature lifecycle tracking, storage of the original email on cloud storage via Active Storage, and responsible data handling with on-by-default incineration.
diff --git a/actionmailbox/app/controllers/action_mailbox/base_controller.rb b/actionmailbox/app/controllers/action_mailbox/base_controller.rb
index f0f1f555e6..80a14355b7 100644
--- a/actionmailbox/app/controllers/action_mailbox/base_controller.rb
+++ b/actionmailbox/app/controllers/action_mailbox/base_controller.rb
@@ -3,14 +3,10 @@
module ActionMailbox
# The base class for all Action Mailbox ingress controllers.
class BaseController < ActionController::Base
- skip_forgery_protection
+ skip_forgery_protection if default_protect_from_forgery
before_action :ensure_configured
- def self.prepare
- # Override in concrete controllers to run code on load.
- end
-
private
def ensure_configured
unless ActionMailbox.ingress == ingress_name
diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb
deleted file mode 100644
index e0a187054e..0000000000
--- a/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-module ActionMailbox
- # Ingests inbound emails from Amazon's Simple Email Service (SES).
- #
- # Requires the full RFC 822 message in the +content+ parameter. Authenticates requests by validating their signatures.
- #
- # Returns:
- #
- # - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
- # - <tt>401 Unauthorized</tt> if the request's signature could not be validated
- # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SES
- # - <tt>422 Unprocessable Entity</tt> if the request is missing the required +content+ parameter
- # - <tt>500 Server Error</tt> if one of the Active Record database, the Active Storage service, or
- # the Active Job backend is misconfigured or unavailable
- #
- # == Usage
- #
- # 1. Install the {aws-sdk-sns}[https://rubygems.org/gems/aws-sdk-sns] gem:
- #
- # # Gemfile
- # gem "aws-sdk-sns", ">= 1.9.0", require: false
- #
- # 2. Tell Action Mailbox to accept emails from SES:
- #
- # # config/environments/production.rb
- # config.action_mailbox.ingress = :amazon
- #
- # 3. {Configure SES}[https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html]
- # to deliver emails to your application via POST requests to +/rails/action_mailbox/amazon/inbound_emails+.
- # If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL
- # <tt>https://example.com/rails/action_mailbox/amazon/inbound_emails</tt>.
- class Ingresses::Amazon::InboundEmailsController < BaseController
- before_action :authenticate
-
- cattr_accessor :verifier
-
- def self.prepare
- self.verifier ||= begin
- require "aws-sdk-sns"
- Aws::SNS::MessageVerifier.new
- end
- end
-
- def create
- ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:content)
- end
-
- private
- def authenticate
- head :unauthorized unless verifier.authentic?(request.body)
- end
- end
-end
diff --git a/actionmailbox/config/routes.rb b/actionmailbox/config/routes.rb
index 517d2835af..1496d6f0b3 100644
--- a/actionmailbox/config/routes.rb
+++ b/actionmailbox/config/routes.rb
@@ -2,7 +2,6 @@
Rails.application.routes.draw do
scope "/rails/action_mailbox", module: "action_mailbox/ingresses" do
- post "/amazon/inbound_emails" => "amazon/inbound_emails#create", as: :rails_amazon_inbound_emails
post "/mandrill/inbound_emails" => "mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails
post "/postmark/inbound_emails" => "postmark/inbound_emails#create", as: :rails_postmark_inbound_emails
post "/relay/inbound_emails" => "relay/inbound_emails#create", as: :rails_relay_inbound_emails
diff --git a/actionmailbox/lib/action_mailbox/engine.rb b/actionmailbox/lib/action_mailbox/engine.rb
index 039f04ac2f..bab3964d93 100644
--- a/actionmailbox/lib/action_mailbox/engine.rb
+++ b/actionmailbox/lib/action_mailbox/engine.rb
@@ -26,16 +26,7 @@ module ActionMailbox
ActionMailbox.incinerate = app.config.action_mailbox.incinerate.nil? ? true : app.config.action_mailbox.incinerate
ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
ActionMailbox.queues = app.config.action_mailbox.queues || {}
- end
- end
-
- initializer "action_mailbox.ingress" do |app|
- config.to_prepare do
- if ActionMailbox.ingress = app.config.action_mailbox.ingress.presence
- if ingress_controller_class = "ActionMailbox::Ingresses::#{ActionMailbox.ingress.to_s.classify}::InboundEmailsController".safe_constantize
- ingress_controller_class.prepare
- end
- end
+ ActionMailbox.ingress = app.config.action_mailbox.ingress
end
end
end
diff --git a/actionmailbox/lib/action_mailbox/test_helper.rb b/actionmailbox/lib/action_mailbox/test_helper.rb
index 0ec9152844..d248fa8734 100644
--- a/actionmailbox/lib/action_mailbox/test_helper.rb
+++ b/actionmailbox/lib/action_mailbox/test_helper.rb
@@ -29,16 +29,16 @@ module ActionMailbox
create_inbound_email_from_fixture(*args).tap(&:route)
end
- # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_mail+
- # and immediately route it to processing.
+ # Create an +InboundEmail+ using the same arguments as +create_inbound_email_from_mail+ and immediately route it to
+ # processing.
def receive_inbound_email_from_mail(**kwargs)
create_inbound_email_from_mail(**kwargs).tap(&:route)
end
- # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_source+
- # and immediately route it to processing.
- def receive_inbound_email_from_source(**kwargs)
- create_inbound_email_from_source(**kwargs).tap(&:route)
+ # Create an +InboundEmail+ using the same arguments as +create_inbound_email_from_source+ and immediately route it
+ # to processing.
+ def receive_inbound_email_from_source(*args)
+ create_inbound_email_from_source(*args).tap(&:route)
end
end
end
diff --git a/actionmailbox/lib/rails/generators/installer.rb b/actionmailbox/lib/rails/generators/installer.rb
index 25cf528ef5..2864ea4e62 100644
--- a/actionmailbox/lib/rails/generators/installer.rb
+++ b/actionmailbox/lib/rails/generators/installer.rb
@@ -5,6 +5,6 @@ copy_file "#{__dir__}/mailbox/templates/application_mailbox.rb", "app/mailboxes/
environment <<~end_of_config, env: "production"
# Prepare the ingress controller used to receive mail
- # config.action_mailbox.ingress = :amazon
+ # config.action_mailbox.ingress = :postfix
end_of_config
diff --git a/actionmailbox/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb
deleted file mode 100644
index e10985553e..0000000000
--- a/actionmailbox/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-
-ActionMailbox::Ingresses::Amazon::InboundEmailsController.verifier =
- Module.new { def self.authentic?(message); true; end }
-
-class ActionMailbox::Ingresses::Amazon::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
- setup { ActionMailbox.ingress = :amazon }
-
- test "receiving an inbound email from Amazon" do
- assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
- post rails_amazon_inbound_emails_url, params: { content: file_fixture("../files/welcome.eml").read }, as: :json
- end
-
- assert_response :no_content
-
- inbound_email = ActionMailbox::InboundEmail.last
- assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
- assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
- end
-end
diff --git a/actionmailbox/test/dummy/config/environments/production.rb b/actionmailbox/test/dummy/config/environments/production.rb
index 1731582220..932858fdb6 100644
--- a/actionmailbox/test/dummy/config/environments/production.rb
+++ b/actionmailbox/test/dummy/config/environments/production.rb
@@ -1,9 +1,4 @@
Rails.application.configure do
- # Prepare the ingress controller used to receive mail
- # config.action_mailbox.ingress = :amazon
-
- # Verifies that versions and hashed value of the package contents in the project's package.json
- config.webpacker.check_yarn_integrity = false
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
@@ -96,4 +91,10 @@ Rails.application.configure do
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
+
+ # Prepare the ingress controller used to receive mail
+ # config.action_mailbox.ingress = :postfix
+
+ # Verifies that versions and hashed value of the package contents in the project's package.json
+ config.webpacker.check_yarn_integrity = false
end
diff --git a/actionmailbox/test/test_helper.rb b/actionmailbox/test/test_helper.rb
index bf93df1029..09f6cc818d 100644
--- a/actionmailbox/test/test_helper.rb
+++ b/actionmailbox/test/test_helper.rb
@@ -54,3 +54,5 @@ class BounceMailer < ActionMailer::Base
end
end
end
+
+require_relative "../../tools/test_common"
diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb
index 448807c144..a2a603834c 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -49,3 +49,5 @@ class ActiveSupport::TestCase
skip message if defined?(JRUBY_VERSION)
end
end
+
+require_relative "../../tools/test_common"
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 2df6f5fc09..4109ae7006 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,27 @@
+* Introduce `ActionDispatch::ActionableExceptions`.
+
+ The `ActionDispatch::ActionableExceptions` middleware dispatches actions
+ from `ActiveSupport::ActionableError` descendants.
+
+ Actionable errors let's you dispatch actions from Rails' error pages.
+
+ *Vipul A M*, *Yao Jie*, *Genadi Samokovarov*
+
+* Raise an `ArgumentError` if a resource custom param contains a colon (`:`).
+
+ After this change it's not possible anymore to configure routes like this:
+
+ ```
+ routes.draw do
+ resources :users, param: 'name/:sneaky'
+ end
+ ```
+
+ Fixes #30467.
+
+ *Josua Schmid*
+
+
## Rails 6.0.0.beta3 (March 11, 2019) ##
* No changes.
@@ -28,7 +52,7 @@
*Rafael Mendonça França*
-* Introduce ActionDispatch::HostAuthorization
+* Introduce `ActionDispatch::HostAuthorization`.
This is a new middleware that guards against DNS rebinding attacks by
explicitly permitting the hosts a request can be made to.
@@ -58,7 +82,7 @@
* Raise an error on root route naming conflicts.
- Raises an ArgumentError when multiple root routes are defined in the
+ Raises an `ArgumentError` when multiple root routes are defined in the
same context instead of assigning nil names to subsequent roots.
*Gannon McGibbon*
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index eb43ff9c63..dd69930e25 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -305,7 +305,7 @@ module ActionController
logger.fatal do
message = +"\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << exception.annotated_source_code.to_s if exception.respond_to?(:annotated_source_code)
message << " " << exception.backtrace.join("\n ")
"#{message}\n\n"
end
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index 09716f7588..e635abec8e 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -93,7 +93,7 @@ module ActionController
end
def model
- super || synchronize { super || self.model = _default_wrap_model }
+ super || self.model = _default_wrap_model
end
def include
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index 815f82a1f2..ae774b01f1 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -4,7 +4,6 @@ require "active_support/core_ext/hash/indifferent_access"
require "active_support/core_ext/array/wrap"
require "active_support/core_ext/string/filters"
require "active_support/core_ext/object/to_query"
-require "active_support/rescuable"
require "action_dispatch/http/upload"
require "rack/test"
require "stringio"
@@ -1092,9 +1091,6 @@ module ActionController
# See ActionController::Parameters.require and ActionController::Parameters.permit
# for more information.
module StrongParameters
- extend ActiveSupport::Concern
- include ActiveSupport::Rescuable
-
# Returns a new ActionController::Parameters object that
# has been instantiated with the <tt>request.parameters</tt>.
def params
diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb
index 8c16308ce7..dadf6d3445 100644
--- a/actionpack/lib/action_controller/renderer.rb
+++ b/actionpack/lib/action_controller/renderer.rb
@@ -116,7 +116,7 @@ module ActionController
RACK_VALUE_TRANSLATION = {
https: ->(v) { v ? "on" : "off" },
- method: ->(v) { v.upcase },
+ method: ->(v) { -v.upcase },
}
def rack_key_for(key)
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 8f39b88d56..6a4ba9af4a 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -53,6 +53,7 @@ module ActionDispatch
autoload :RequestId
autoload :Callbacks
autoload :Cookies
+ autoload :ActionableExceptions
autoload :DebugExceptions
autoload :DebugLocks
autoload :DebugView
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index 962d10d81b..88b3a93211 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -315,7 +315,7 @@ module Mime
include Singleton
def initialize
- super "*/*", :all
+ super "*/*", nil
end
def all?; true; end
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index 697f5b9d8b..dee2980eb1 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -119,7 +119,8 @@ module ActionDispatch
class UnanchoredRegexp < AnchoredRegexp # :nodoc:
def accept(node)
- %r{\A#{visit node}(?:\b|\Z)}
+ path = visit node
+ path == "/" ? %r{\A/} : %r{\A#{path}(?:\b|\Z|/)}
end
end
@@ -136,6 +137,10 @@ module ActionDispatch
Array.new(length - 1) { |i| self[i + 1] }
end
+ def named_captures
+ @names.zip(captures).to_h
+ end
+
def [](x)
idx = @offsets[x - 1] + x
@match[idx]
diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb
index c0377459d5..3ba8361d77 100644
--- a/actionpack/lib/action_dispatch/journey/routes.rb
+++ b/actionpack/lib/action_dispatch/journey/routes.rb
@@ -56,7 +56,6 @@ module ActionDispatch
end
def simulator
- return if ast.nil?
@simulator ||= begin
gtg = GTG::Builder.new(ast).transition_table
GTG::Simulator.new(gtg)
diff --git a/actionpack/lib/action_dispatch/middleware/actionable_exceptions.rb b/actionpack/lib/action_dispatch/middleware/actionable_exceptions.rb
new file mode 100644
index 0000000000..e94cc46603
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/actionable_exceptions.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require "erb"
+require "action_dispatch/http/request"
+require "active_support/actionable_error"
+
+module ActionDispatch
+ class ActionableExceptions # :nodoc:
+ cattr_accessor :endpoint, default: "/rails/actions"
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new(env)
+ return @app.call(env) unless actionable_request?(request)
+
+ ActiveSupport::ActionableError.dispatch(request.params[:error].to_s.safe_constantize, request.params[:action])
+
+ redirect_to request.params[:location]
+ end
+
+ private
+ def actionable_request?(request)
+ request.show_exceptions? && request.post? && request.path == endpoint
+ end
+
+ def redirect_to(location)
+ body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
+
+ [302, {
+ "Content-Type" => "text/html; charset=#{Response.default_charset}",
+ "Content-Length" => body.bytesize.to_s,
+ "Location" => location,
+ }, [body]]
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index 61773d97a2..0b15c94122 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -4,6 +4,8 @@ require "action_dispatch/http/request"
require "action_dispatch/middleware/exception_wrapper"
require "action_dispatch/routing/inspector"
+require "active_support/actionable_error"
+
require "action_view"
require "action_view/base"
@@ -60,7 +62,11 @@ module ActionDispatch
log_error(request, wrapper)
if request.get_header("action_dispatch.show_detailed_exceptions")
- content_type = request.formats.first
+ begin
+ content_type = request.formats.first
+ rescue Mime::Type::InvalidMimeType
+ render_for_api_request(Mime[:text], wrapper)
+ end
if api_request?(content_type)
render_for_api_request(content_type, wrapper)
@@ -142,7 +148,7 @@ module ActionDispatch
message = []
message << " "
message << "#{exception.class} (#{exception.message}):"
- message.concat(exception.annoted_source_code) if exception.respond_to?(:annoted_source_code)
+ message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
message << " "
message.concat(trace)
diff --git a/actionpack/lib/action_dispatch/middleware/debug_view.rb b/actionpack/lib/action_dispatch/middleware/debug_view.rb
index 43c0a84504..a03650254e 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_view.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_view.rb
@@ -52,5 +52,9 @@ module ActionDispatch
super
end
end
+
+ def protect_against_forgery?
+ false
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 1fb3e9db00..0cc56f5013 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -12,6 +12,7 @@ module ActionDispatch
"ActionController::UnknownHttpMethod" => :method_not_allowed,
"ActionController::NotImplemented" => :not_implemented,
"ActionController::UnknownFormat" => :not_acceptable,
+ "Mime::Type::InvalidMimeType" => :not_acceptable,
"ActionController::MissingExactTemplate" => :not_acceptable,
"ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
"ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
index 3feb3a19f3..a88ad40f21 100644
--- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -21,8 +21,12 @@ module ActionDispatch
def call(env)
request = ActionDispatch::Request.new(env)
status = request.path_info[1..-1].to_i
- content_type = request.formats.first
- body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
+ begin
+ content_type = request.formats.first
+ rescue Mime::Type::InvalidMimeType
+ content_type = Mime[:text]
+ end
+ body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
render(status, content_type, body)
end
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb
new file mode 100644
index 0000000000..b6c6d2f50d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb
@@ -0,0 +1,13 @@
+<% actions = ActiveSupport::ActionableError.actions(exception) %>
+
+<% if actions.any? %>
+ <div class="actions">
+ <% actions.each do |action, _| %>
+ <%= button_to action, ActionDispatch::ActionableExceptions.endpoint, params: {
+ error: exception.class.name,
+ action: action,
+ location: request.path
+ } %>
+ <% end %>
+ </div>
+<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
index bde26f46c2..999e84e4d6 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
@@ -8,7 +8,11 @@
</header>
<div id="container">
- <h2><%= h @exception.message %></h2>
+ <h2>
+ <%= h @exception.message %>
+
+ <%= render "rescues/actions", exception: @exception, request: @request %>
+ </h2>
<%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx, error_index: 0 %>
<%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show, error_index: 0 %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
index 39ea25bdfc..0f78e23b7f 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
@@ -117,6 +117,10 @@
background-color: #FFCCCC;
}
+ .button_to {
+ display: inline-block;
+ }
+
.hidden {
display: none;
}
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index da3ade652e..f29f66990d 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -115,9 +115,9 @@ module ActionDispatch
@defaults = defaults
@set = set
- @to = to
- @default_controller = controller
- @default_action = default_action
+ @to = intern(to)
+ @default_controller = intern(controller)
+ @default_action = intern(default_action)
@ast = ast
@anchor = anchor
@via = via
@@ -222,6 +222,10 @@ module ActionDispatch
private :build_path
private
+ def intern(object)
+ object.is_a?(String) ? -object : object
+ end
+
def add_wildcard_options(options, formatted, path_ast)
# Add a constraint for wildcard route to make it non-greedy and match the
# optional format part of the route by default.
@@ -1141,6 +1145,10 @@ module ActionDispatch
attr_reader :controller, :path, :param
def initialize(entities, api_only, shallow, options = {})
+ if options[:param].to_s.include?(":")
+ raise ArgumentError, ":param option can't contain colons"
+ end
+
@name = entities.to_s
@path = (options[:path] || @name).to_s
@controller = (options[:controller] || @name).to_s
@@ -1398,6 +1406,8 @@ module ActionDispatch
# as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
# to be shortened to just <tt>/comments/1234</tt>.
#
+ # Set <tt>shallow: false</tt> on a child resource to ignore a parent's shallow parameter.
+ #
# [:shallow_path]
# Prefixes nested shallow routes with the specified path.
#
@@ -1668,7 +1678,8 @@ module ActionDispatch
return true
end
- if options.delete(:shallow)
+ if options[:shallow]
+ options.delete(:shallow)
shallow do
send(method, resources.pop, options, &block)
end
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 972953d4f3..4a24c35efb 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -317,23 +317,21 @@ module ActionDispatch
#
def define_url_helper(mod, route, name, opts, route_key, url_strategy)
helper = UrlHelper.create(route, opts, route_key, url_strategy)
- mod.module_eval do
- define_method(name) do |*args|
- last = args.last
- options = \
- case last
- when Hash
- args.pop
- when ActionController::Parameters
- args.pop.to_h
- end
- helper.call self, args, options
- end
+ mod.define_method(name) do |*args|
+ last = args.last
+ options = \
+ case last
+ when Hash
+ args.pop
+ when ActionController::Parameters
+ args.pop.to_h
+ end
+ helper.call self, args, options
end
end
end
- # strategy for building urls to send to the client
+ # strategy for building URLs to send to the client
PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
index 79359a0c8b..056ce51a61 100644
--- a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
@@ -39,7 +39,8 @@ module ActionDispatch
private
def image_name
- failed? ? "failures_#{method_name}" : method_name
+ name = method_name[0...225]
+ failed? ? "failures_#{name}" : name
end
def image_path
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index f23151e518..32a0b8efeb 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -96,6 +96,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
middleware.use ActionDispatch::DebugExceptions
+ middleware.use ActionDispatch::ActionableExceptions
middleware.use ActionDispatch::Callbacks
middleware.use ActionDispatch::Cookies
middleware.use ActionDispatch::Flash
@@ -382,3 +383,5 @@ end
class DrivenBySeleniumWithHeadlessFirefox < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_firefox
end
+
+require_relative "../../tools/test_common"
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
index ecb8c37e6b..51286155b9 100644
--- a/actionpack/test/controller/action_pack_assertions_test.rb
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -282,7 +282,7 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
assert_no_match(
/#{request.protocol}#{request.host}\/\/www.rubyonrails.org/,
ex.message,
- "protocol relative url was incorrectly normalized"
+ "protocol relative URL was incorrectly normalized"
)
end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index b5503a9c64..4dddd98f9f 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -808,17 +808,17 @@ class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest
end
end
- test "session uses default url options from routes" do
+ test "session uses default URL options from routes" do
assert_equal "http://foo.com/foo", foos_url
end
- test "current host overrides default url options from routes" do
+ test "current host overrides default URL options from routes" do
get "/foo"
assert_response :success
assert_equal "http://www.example.com/foo", foos_url
end
- test "controller can override default url options from request" do
+ test "controller can override default URL options from request" do
get "/bar"
assert_response :success
assert_equal "http://bar.com/foo", foos_url
diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb
index 21de05b323..2f8f191828 100644
--- a/actionpack/test/controller/mime/respond_to_test.rb
+++ b/actionpack/test/controller/mime/respond_to_test.rb
@@ -158,6 +158,12 @@ class RespondToController < ActionController::Base
end
end
+ def handle_any_with_template
+ respond_to do |type|
+ type.any { render "test/hello_world" }
+ end
+ end
+
def all_types_with_layout
respond_to do |type|
type.html
@@ -572,6 +578,13 @@ class RespondToControllerTest < ActionController::TestCase
assert_equal "HTML", @response.body
end
+ def test_handle_any_with_template
+ @request.accept = "*/*"
+
+ get :handle_any_with_template
+ assert_equal "Hello world!", @response.body
+ end
+
def test_html_type_with_layout
@request.accept = "text/html"
get :all_types_with_layout
diff --git a/actionpack/test/controller/new_base/render_file_test.rb b/actionpack/test/controller/new_base/render_file_test.rb
index de8af029e0..01d0223519 100644
--- a/actionpack/test/controller/new_base/render_file_test.rb
+++ b/actionpack/test/controller/new_base/render_file_test.rb
@@ -17,12 +17,12 @@ module RenderFile
def relative_path
@secret = "in the sauce"
- render file: "../../fixtures/test/render_file_with_ivar"
+ render file: "../actionpack/test/fixtures/test/render_file_with_ivar"
end
def relative_path_with_dot
@secret = "in the sauce"
- render file: "../../fixtures/test/dot.directory/render_file_with_ivar"
+ render file: "../actionpack/test/fixtures/test/dot.directory/render_file_with_ivar"
end
def pathname
@@ -40,32 +40,44 @@ module RenderFile
testing RenderFile::BasicController
test "rendering simple template" do
- get :index
+ assert_deprecated do
+ get :index
+ end
assert_response "Hello world!"
end
test "rendering template with ivar" do
- get :with_instance_variables
+ assert_deprecated do
+ get :with_instance_variables
+ end
assert_response "The secret is in the sauce\n"
end
test "rendering a relative path" do
- get :relative_path
+ assert_deprecated do
+ get :relative_path
+ end
assert_response "The secret is in the sauce\n"
end
test "rendering a relative path with dot" do
- get :relative_path_with_dot
+ assert_deprecated do
+ get :relative_path_with_dot
+ end
assert_response "The secret is in the sauce\n"
end
test "rendering a Pathname" do
- get :pathname
+ assert_deprecated do
+ get :pathname
+ end
assert_response "The secret is in the sauce\n"
end
test "rendering file with locals" do
- get :with_locals
+ assert_deprecated do
+ get :with_locals
+ end
assert_response "The secret is in the sauce\n"
end
end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index 4750093c5c..8bb6617eaa 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -323,11 +323,12 @@ class ExpiresInRenderTest < ActionController::TestCase
end
def test_dynamic_render_with_file
- # This is extremely bad, but should be possible to do.
assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
- response = get :dynamic_render_with_file, params: { id: '../\\../test/abstract_unit.rb' }
- assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)),
- response.body
+ assert_deprecated do
+ assert_raises ActionView::MissingTemplate do
+ get :dynamic_render_with_file, params: { id: '../\\../test/abstract_unit.rb' }
+ end
+ end
end
def test_dynamic_render_with_absolute_path
@@ -351,9 +352,11 @@ class ExpiresInRenderTest < ActionController::TestCase
def test_permitted_dynamic_render_file_hash
assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
- response = get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } }
- assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)),
- response.body
+ assert_deprecated do
+ assert_raises ActionView::MissingTemplate do
+ get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } }
+ end
+ end
end
def test_dynamic_render_file_hash
diff --git a/actionpack/test/controller/renderer_test.rb b/actionpack/test/controller/renderer_test.rb
index ae8330e029..ea79f4de85 100644
--- a/actionpack/test/controller/renderer_test.rb
+++ b/actionpack/test/controller/renderer_test.rb
@@ -40,7 +40,7 @@ class RendererTest < ActiveSupport::TestCase
test "rendering with an instance renderer" do
renderer = ApplicationController.renderer.new
- content = renderer.render file: "test/hello_world"
+ content = assert_deprecated { renderer.render file: "test/hello_world" }
assert_equal "Hello world!", content
end
@@ -115,14 +115,14 @@ class RendererTest < ActiveSupport::TestCase
assert_equal "true", content
end
- test "return valid asset url with defaults" do
+ test "return valid asset URL with defaults" do
renderer = ApplicationController.renderer
content = renderer.render inline: "<%= asset_url 'asset.jpg' %>"
assert_equal "http://example.org/asset.jpg", content
end
- test "return valid asset url when https is true" do
+ test "return valid asset URL when https is true" do
renderer = ApplicationController.renderer.new https: true
content = renderer.render inline: "<%= asset_url 'asset.jpg' %>"
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index d1cd190747..998a495d0d 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -952,7 +952,7 @@ XML
get :create
assert_response :created
- # Redirect url doesn't care that it wasn't a :redirect response.
+ # Redirect URL doesn't care that it wasn't a :redirect response.
assert_equal "/resource", @response.redirect_url
assert_equal @response.redirect_url, redirect_to_url
diff --git a/actionpack/test/dispatch/actionable_exceptions_test.rb b/actionpack/test/dispatch/actionable_exceptions_test.rb
new file mode 100644
index 0000000000..9215a91e9c
--- /dev/null
+++ b/actionpack/test/dispatch/actionable_exceptions_test.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ActionableExceptionsTest < ActionDispatch::IntegrationTest
+ Actions = []
+
+ class ActionError < StandardError
+ include ActiveSupport::ActionableError
+
+ action "Successful action" do
+ Actions << "Action!"
+ end
+
+ action "Failed action" do
+ raise "Inaction!"
+ end
+ end
+
+ Noop = -> env { [200, {}, [""]] }
+
+ setup do
+ @app = ActionDispatch::ActionableExceptions.new(Noop)
+
+ Actions.clear
+ end
+
+ test "dispatches an actionable error" do
+ post ActionDispatch::ActionableExceptions.endpoint, params: {
+ error: ActionError.name,
+ action: "Successful action",
+ location: "/",
+ }
+
+ assert_equal ["Action!"], Actions
+
+ assert_equal 302, response.status
+ assert_equal "/", response.headers["Location"]
+ end
+
+ test "cannot dispatch errors if not allowed" do
+ post ActionDispatch::ActionableExceptions.endpoint, params: {
+ error: ActionError.name,
+ action: "Successful action",
+ location: "/",
+ }, headers: { "action_dispatch.show_exceptions" => false }
+
+ assert_empty Actions
+ end
+
+ test "dispatched action can fail" do
+ assert_raise RuntimeError do
+ post ActionDispatch::ActionableExceptions.endpoint, params: {
+ error: ActionError.name,
+ action: "Failed action",
+ location: "/",
+ }
+ end
+ end
+
+ test "cannot dispatch non-actionable errors" do
+ assert_raise ActiveSupport::ActionableError::NonActionable do
+ post ActionDispatch::ActionableExceptions.endpoint, params: {
+ error: RuntimeError.name,
+ action: "Inexistent action",
+ location: "/",
+ }
+ end
+ end
+
+ test "cannot dispatch Inexistent errors" do
+ assert_raise ActiveSupport::ActionableError::NonActionable do
+ post ActionDispatch::ActionableExceptions.endpoint, params: {
+ error: "",
+ action: "Inexistent action",
+ location: "/",
+ }
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index 2c67bb779f..d129fa717d 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -317,7 +317,7 @@ class CookiesTest < ActionController::TestCase
end
def rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off
- cookies[:favorite] = "Wmg4amgvcVVvWGcwK3c4WjJEbTdRQUgrWXhBdDliUTR0cVNidXpmVTMrc2RjcitwUzVsWWEwZGtuVGtFUjJwNi0tcVhVMTFMOTQ1d0hIVE1FK0pJc05SQT09--8b2a55c375049a50f7a959b9d42b31ef0b2bb594"
+ cookies[:favorite] = "rTG4zs5UufEFAr+ppKwh+MDMymKyAUMOSaWyYa3uUVmD8sMQqyiyQBxgYeAncDHVZIlo4y+kDVSzp66u1/7BNYpnmFe8ES/YT2m8ckNA23jBDmnRZ9CTNfMIRXjFtfxO9YxEOzzhn0ZiA0/zFtr5wkluXtxplOz959Q7MgLOyvTze2h9p8A=--QHOS3rAEGq/HCxXs--xQNra8dk24Idc2qBtpMLpg=="
head :ok
end
@@ -341,7 +341,7 @@ class CookiesTest < ActionController::TestCase
SECRET_KEY_BASE = "b3c631c314c0bbca50c1b2843150fe33"
SIGNED_COOKIE_SALT = "signed cookie"
ENCRYPTED_COOKIE_SALT = "encrypted cookie"
- ENCRYPTED_SIGNED_COOKIE_SALT = "sigend encrypted cookie"
+ ENCRYPTED_SIGNED_COOKIE_SALT = "signed encrypted cookie"
AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "authenticated encrypted cookie"
def setup
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb
index c85476fa38..5ae8a20ae4 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -5,6 +5,18 @@ require "abstract_unit"
class DebugExceptionsTest < ActionDispatch::IntegrationTest
InterceptedErrorInstance = StandardError.new
+ class CustomActionableError < StandardError
+ include ActiveSupport::ActionableError
+
+ action "Action 1" do
+ nil
+ end
+
+ action "Action 2" do
+ nil
+ end
+ end
+
class Boomer
attr_accessor :closed
@@ -58,6 +70,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
raise ActionController::NotImplemented
when "/unprocessable_entity"
raise ActionController::InvalidAuthenticityToken
+ when "/invalid_mimetype"
+ raise Mime::Type::InvalidMimeType
when "/not_found_original_exception"
begin
raise AbstractController::ActionNotFound.new
@@ -90,6 +104,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
method_that_raises
when "/nested_exceptions"
raise_nested_exceptions
+ when %r{/actionable_error}
+ raise CustomActionableError
else
raise "puke!"
end
@@ -178,6 +194,10 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
get "/parameter_missing", headers: { "action_dispatch.show_exceptions" => true }
assert_response 400
assert_match(/ActionController::ParameterMissing/, body)
+
+ get "/invalid_mimetype", headers: { "Accept" => "text/html,*", "action_dispatch.show_exceptions" => true }
+ assert_response 406
+ assert_match(/Mime::Type::InvalidMimeType/, body)
end
test "rescue with text error for xhr request" do
@@ -335,7 +355,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_match %r{NameError}, body
end
- test "named urls missing keys raise 500 level error" do
+ test "named URLs missing keys raise 500 level error" do
@app = DevelopmentApp
get "/missing_keys", headers: { "action_dispatch.show_exceptions" => true }
@@ -583,4 +603,21 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
end
end
end
+
+ test "shows a buttons for every action in an actionable error" do
+ @app = DevelopmentApp
+ Rails.stub :root, Pathname.new(".") do
+ cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc|
+ bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} }
+ end
+
+ get "/actionable_error", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
+
+ # Assert correct error
+ assert_response 500
+
+ assert_select 'input[value="Action 1"]'
+ assert_select 'input[value="Action 2"]'
+ end
+ end
end
diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb
index e42ea89f6f..758cee9930 100644
--- a/actionpack/test/dispatch/mount_test.rb
+++ b/actionpack/test/dispatch/mount_test.rb
@@ -27,6 +27,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
}
mount SprocketsApp, at: "/sprockets"
+ mount SprocketsApp, at: "/star*"
mount SprocketsApp => "/shorthand"
mount SinatraLikeApp, at: "/fakeengine", as: :fake
@@ -58,6 +59,14 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
def test_mounting_at_root_path
get "/omg"
assert_equal " -- /omg", response.body
+
+ get "/~omg"
+ assert_equal " -- /~omg", response.body
+ end
+
+ def test_mounting_at_path_with_non_word_character
+ get "/star*/omg"
+ assert_equal "/star* -- /omg", response.body
end
def test_mounting_sets_script_name
diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb
index 7a7a201b11..63c147cb1b 100644
--- a/actionpack/test/dispatch/prefix_generation_test.rb
+++ b/actionpack/test/dispatch/prefix_generation_test.rb
@@ -151,17 +151,17 @@ module TestGenerationPrefix
include BlogEngine.routes.mounted_helpers
# Inside Engine
- test "[ENGINE] generating engine's url use SCRIPT_NAME from request" do
+ test "[ENGINE] generating engine's URL use SCRIPT_NAME from request" do
get "/pure-awesomeness/blog/posts/1"
assert_equal "/pure-awesomeness/blog/posts/1", response.body
end
- test "[ENGINE] generating application's url never uses SCRIPT_NAME from request" do
+ test "[ENGINE] generating application's URL never uses SCRIPT_NAME from request" do
get "/pure-awesomeness/blog/url_to_application"
assert_equal "/generate", response.body
end
- test "[ENGINE] generating engine's url with polymorphic path" do
+ test "[ENGINE] generating engine's URL with polymorphic path" do
get "/pure-awesomeness/blog/polymorphic_path_for_engine"
assert_equal "/pure-awesomeness/blog/posts/1", response.body
end
@@ -243,7 +243,7 @@ module TestGenerationPrefix
assert_equal "/something/awesome/blog/posts/1", response.body
end
- test "[APP] generating engine's url with polymorphic path" do
+ test "[APP] generating engine's URL with polymorphic path" do
get "/polymorphic_path_for_engine"
assert_equal "/awesome/blog/posts/1", response.body
end
@@ -253,7 +253,7 @@ module TestGenerationPrefix
assert_equal "/posts/1", response.body
end
- test "[APP] generating engine's url with url_for(@post)" do
+ test "[APP] generating engine's URL with url_for(@post)" do
get "/polymorphic_with_url_for"
assert_equal "http://www.example.com/awesome/blog/posts/1", response.body
end
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index 2a4d59affe..eb49396145 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -411,7 +411,7 @@ class RequestPath < BaseRequestTest
assert_equal "/foo?bar", path
end
- test "original_url returns url built using ORIGINAL_FULLPATH" do
+ test "original_url returns URL built using ORIGINAL_FULLPATH" do
request = stub_request("ORIGINAL_FULLPATH" => "/foo?bar",
"HTTP_HOST" => "example.org",
"rack.url_scheme" => "http")
diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb
index e61d47b160..e6a2c35798 100644
--- a/actionpack/test/dispatch/routing/route_set_test.rb
+++ b/actionpack/test/dispatch/routing/route_set_test.rb
@@ -29,7 +29,7 @@ module ActionDispatch
assert_not empty?
end
- test "url helpers are added when route is added" do
+ test "URL helpers are added when route is added" do
draw do
get "foo", to: SimpleApp.new("foo#index")
end
@@ -48,7 +48,7 @@ module ActionDispatch
assert_equal "/bar", url_helpers.bar_path
end
- test "url helpers are updated when route is updated" do
+ test "URL helpers are updated when route is updated" do
draw do
get "bar", to: SimpleApp.new("bar#index"), as: :bar
end
@@ -62,7 +62,7 @@ module ActionDispatch
assert_equal "/baz", url_helpers.bar_path
end
- test "url helpers are removed when route is removed" do
+ test "URL helpers are removed when route is removed" do
draw do
get "foo", to: SimpleApp.new("foo#index")
get "bar", to: SimpleApp.new("bar#index")
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 897d17885e..362488d585 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -2200,6 +2200,37 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal "cards#destroy", @response.body
end
+ def test_shallow_false_inside_nested_shallow_resource
+ draw do
+ resources :blogs, shallow: true do
+ resources :posts do
+ resources :comments, shallow: false
+ resources :tags
+ end
+ end
+ end
+
+ get "/posts/1/comments"
+ assert_equal "comments#index", @response.body
+ assert_equal "/posts/1/comments", post_comments_path("1")
+
+ get "/posts/1/comments/new"
+ assert_equal "comments#new", @response.body
+ assert_equal "/posts/1/comments/new", new_post_comment_path("1")
+
+ get "/posts/1/comments/2"
+ assert_equal "comments#show", @response.body
+ assert_equal "/posts/1/comments/2", post_comment_path("1", "2")
+
+ get "/posts/1/comments/2/edit"
+ assert_equal "comments#edit", @response.body
+ assert_equal "/posts/1/comments/2/edit", edit_post_comment_path("1", "2")
+
+ get "/tags/3"
+ assert_equal "tags#show", @response.body
+ assert_equal "/tags/3", tag_path("3")
+ end
+
def test_shallow_deeply_nested_resources
draw do
resources :blogs do
@@ -3338,13 +3369,23 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal "0c0c0b68-d24b-11e1-a861-001ff3fffe6f", @request.params[:download]
end
- def test_action_from_path_is_not_frozen
+ def test_colon_containing_custom_param
+ ex = assert_raises(ArgumentError) {
+ draw do
+ resources :profiles, param: "username/:is_admin"
+ end
+ }
+
+ assert_match(/:param option can't contain colon/, ex.message)
+ end
+
+ def test_action_from_path_is_frozen
draw do
get "search" => "search"
end
get "/search"
- assert_not_predicate @request.params[:action], :frozen?
+ assert_predicate @request.params[:action], :frozen?
end
def test_multiple_positional_args_with_the_same_name
@@ -4382,7 +4423,7 @@ class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest
include Routes.url_helpers
- test "url helpers do not ignore nil parameters when using non-optimized routes" do
+ test "URL helpers do not ignore nil parameters when using non-optimized routes" do
Routes.stub :optimize_routes_generation?, false do
get "/categories/1"
assert_response :success
@@ -4754,7 +4795,7 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest
include Routes.url_helpers
- test "url helpers raise a 'missing keys' error for a nil param with optimized helpers" do
+ test "URL helpers raise a 'missing keys' error for a nil param with optimized helpers" do
url, missing = { action: "show", controller: "products", id: nil }, [:id]
message = "No route matches #{url.inspect}, missing required keys: #{missing.inspect}"
@@ -4762,7 +4803,7 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest
assert_equal message, error.message
end
- test "url helpers raise a 'constraint failure' error for a nil param with non-optimized helpers" do
+ test "URL helpers raise a 'constraint failure' error for a nil param with non-optimized helpers" do
url, missing = { action: "show", controller: "products", id: nil }, [:id]
message = "No route matches #{url.inspect}, possible unmatched constraints: #{missing.inspect}"
@@ -4770,15 +4811,15 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest
assert_equal message, error.message
end
- test "url helpers raise message with mixed parameters when generation fails" do
+ test "URL helpers raise message with mixed parameters when generation fails" do
url, missing = { action: "show", controller: "products", id: nil, "id" => "url-tested" }, [:id]
message = "No route matches #{url.inspect}, possible unmatched constraints: #{missing.inspect}"
- # Optimized url helper
+ # Optimized URL helper
error = assert_raises(ActionController::UrlGenerationError) { product_path(nil, "id" => "url-tested") }
assert_equal message, error.message
- # Non-optimized url helper
+ # Non-optimized URL helper
error = assert_raises(ActionController::UrlGenerationError, message) { product_path(id: nil, "id" => "url-tested") }
assert_equal message, error.message
end
@@ -4996,7 +5037,7 @@ class FlashRedirectTest < ActionDispatch::IntegrationTest
)
Rotations = ActiveSupport::Messages::RotationConfiguration.new
SIGNED_COOKIE_SALT = "signed cookie"
- ENCRYPTED_SIGNED_COOKIE_SALT = "sigend encrypted cookie"
+ ENCRYPTED_SIGNED_COOKIE_SALT = "signed encrypted cookie"
class KeyGeneratorMiddleware
def initialize(app)
diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb
index f802abc653..6fafa4e426 100644
--- a/actionpack/test/dispatch/show_exceptions_test.rb
+++ b/actionpack/test/dispatch/show_exceptions_test.rb
@@ -9,6 +9,8 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
case req.path
when "/not_found"
raise AbstractController::ActionNotFound
+ when "/invalid_mimetype"
+ raise Mime::Type::InvalidMimeType
when "/bad_params", "/bad_params.json"
begin
raise StandardError.new
@@ -62,6 +64,10 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
get "/unknown_http_method", env: { "action_dispatch.show_exceptions" => true }
assert_response 405
assert_equal "", body
+
+ get "/invalid_mimetype", headers: { "Accept" => "text/html,*", "action_dispatch.show_exceptions" => true }
+ assert_response 406
+ assert_equal "", body
end
test "localize rescue error page" do
diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
index b756b91379..b0b36f9d74 100644
--- a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
+++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
@@ -36,6 +36,14 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
end
end
+ test "image name truncates names over 225 characters" do
+ new_test = DrivenBySeleniumWithChrome.new("x" * 400)
+
+ Rails.stub :root, Pathname.getwd do
+ assert_equal Rails.root.join("tmp/screenshots/#{"x" * 225}.png").to_s, new_test.send(:image_path)
+ end
+ end
+
test "defaults to simple output for the screenshot" do
new_test = DrivenBySeleniumWithChrome.new("x")
assert_equal "simple", new_test.send(:output_type)
diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb
index fcfaba96b0..77c19369b0 100644
--- a/actionpack/test/journey/path/pattern_test.rb
+++ b/actionpack/test/journey/path/pattern_test.rb
@@ -34,17 +34,17 @@ module ActionDispatch
end
{
- "/:controller(/:action)" => %r{\A/(#{x})(?:/([^/.?]+))?(?:\b|\Z)},
- "/:controller/foo" => %r{\A/(#{x})/foo(?:\b|\Z)},
- "/:controller/:action" => %r{\A/(#{x})/([^/.?]+)(?:\b|\Z)},
- "/:controller" => %r{\A/(#{x})(?:\b|\Z)},
- "/:controller(/:action(/:id))" => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?(?:\b|\Z)},
- "/:controller/:action.xml" => %r{\A/(#{x})/([^/.?]+)\.xml(?:\b|\Z)},
- "/:controller.:format" => %r{\A/(#{x})\.([^/.?]+)(?:\b|\Z)},
- "/:controller(.:format)" => %r{\A/(#{x})(?:\.([^/.?]+))?(?:\b|\Z)},
- "/:controller/*foo" => %r{\A/(#{x})/(.+)(?:\b|\Z)},
- "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar(?:\b|\Z)},
- "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))(?:\b|\Z)},
+ "/:controller(/:action)" => %r{\A/(#{x})(?:/([^/.?]+))?(?:\b|\Z|/)},
+ "/:controller/foo" => %r{\A/(#{x})/foo(?:\b|\Z|/)},
+ "/:controller/:action" => %r{\A/(#{x})/([^/.?]+)(?:\b|\Z|/)},
+ "/:controller" => %r{\A/(#{x})(?:\b|\Z|/)},
+ "/:controller(/:action(/:id))" => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?(?:\b|\Z|/)},
+ "/:controller/:action.xml" => %r{\A/(#{x})/([^/.?]+)\.xml(?:\b|\Z|/)},
+ "/:controller.:format" => %r{\A/(#{x})\.([^/.?]+)(?:\b|\Z|/)},
+ "/:controller(.:format)" => %r{\A/(#{x})(?:\.([^/.?]+))?(?:\b|\Z|/)},
+ "/:controller/*foo" => %r{\A/(#{x})/(.+)(?:\b|\Z|/)},
+ "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar(?:\b|\Z|/)},
+ "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))(?:\b|\Z|/)},
}.each do |path, expected|
define_method(:"test_to_non_anchored_regexp_#{Regexp.escape(path)}") do
path = Pattern.build(
@@ -280,6 +280,15 @@ module ActionDispatch
assert_equal "list", match[1]
assert_equal "rss", match[2]
end
+
+ def test_named_captures
+ path = Path::Pattern.from_string "/books(/:action(.:format))"
+
+ uri = "/books/list.rss"
+ match = path =~ uri
+ named_captures = { "action" => "list", "format" => "rss" }
+ assert_equal named_captures, match.named_captures
+ end
end
end
end
diff --git a/actiontext/lib/templates/installer.rb b/actiontext/lib/templates/installer.rb
index a8000eb9fc..a15ada92bb 100644
--- a/actiontext/lib/templates/installer.rb
+++ b/actiontext/lib/templates/installer.rb
@@ -29,4 +29,17 @@ if APPLICATION_PACK_PATH.exist?
append_to_file APPLICATION_PACK_PATH, "\n#{line}"
end
end
+else
+ warn <<~WARNING
+ WARNING: Action Text can't locate your JavaScript bundle to add its package dependencies.
+
+ Add these lines to any bundles:
+
+ require("trix")
+ require("@rails/actiontext")
+
+ Alternatively, install and setup the webpacker gem then rerun `bin/rails action_text:install`
+ to have these dependencies added automatically.
+
+ WARNING
end
diff --git a/actiontext/test/test_helper.rb b/actiontext/test/test_helper.rb
index 196fba8c99..51a207f76a 100644
--- a/actiontext/test/test_helper.rb
+++ b/actiontext/test/test_helper.rb
@@ -31,3 +31,5 @@ class ActiveSupport::TestCase
ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, filename: filename, content_type: content_type, metadata: metadata
end
end
+
+require_relative "../../tools/test_common"
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 43688fc8a7..be67aff543 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,16 @@
+* Only clear ActionView cache in development on file changes
+
+ To speed up development mode, view caches are only cleared when files in
+ the view paths have changed. Applications which have implemented custom
+ `ActionView::Resolver` subclasses may need to add their own cache clearing.
+
+ *John Hawthorn*
+
+* Fix `ActionView::FixtureResolver` so that it handles template variants correctly.
+
+ *Edward Rudd*
+
+
## Rails 6.0.0.beta3 (March 11, 2019) ##
* Only accept formats from registered mime types
@@ -14,18 +27,19 @@
## Rails 6.0.0.beta2 (February 25, 2019) ##
-* ActionView::Template.finalize_compiled_template_methods is deprecated with
+* `ActionView::Template.finalize_compiled_template_methods` is deprecated with
no replacement.
*tenderlove*
-* config.action_view.finalize_compiled_template_methods is deprecated with
+* `config.action_view.finalize_compiled_template_methods` is deprecated with
no replacement.
*tenderlove*
* Ensure unique DOM IDs for collection inputs with float values.
- Fixes #34974
+
+ Fixes #34974.
*Mark Edmondson*
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb
index 8cb4648a67..7f85bf2a5e 100644
--- a/actionview/lib/action_view.rb
+++ b/actionview/lib/action_view.rb
@@ -44,7 +44,7 @@ module ActionView
autoload :Rendering
autoload :RoutingUrlFor
autoload :Template
- autoload :FileTemplate
+ autoload :UnboundTemplate
autoload :ViewPaths
autoload_under "renderer" do
@@ -81,6 +81,7 @@ module ActionView
end
end
+ autoload :CacheExpiry
autoload :TestCase
def self.eager_load!
diff --git a/actionview/lib/action_view/cache_expiry.rb b/actionview/lib/action_view/cache_expiry.rb
new file mode 100644
index 0000000000..820afc093d
--- /dev/null
+++ b/actionview/lib/action_view/cache_expiry.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module ActionView
+ class CacheExpiry
+ class Executor
+ def initialize(watcher:)
+ @cache_expiry = CacheExpiry.new(watcher: watcher)
+ end
+
+ def before(target)
+ @cache_expiry.clear_cache_if_necessary
+ end
+ end
+
+ def initialize(watcher:)
+ @watched_dirs = nil
+ @watcher_class = watcher
+ @watcher = nil
+ end
+
+ def clear_cache_if_necessary
+ watched_dirs = dirs_to_watch
+ if watched_dirs != @watched_dirs
+ @watched_dirs = watched_dirs
+ @watcher = @watcher_class.new([], watched_dirs) do
+ clear_cache
+ end
+ @watcher.execute
+ else
+ @watcher.execute_if_updated
+ end
+ end
+
+ def clear_cache
+ ActionView::LookupContext::DetailsKey.clear
+ end
+
+ private
+
+ def dirs_to_watch
+ fs_paths = all_view_paths.grep(FileSystemResolver)
+ fs_paths.map(&:path).sort.uniq
+ end
+
+ def all_view_paths
+ ActionView::ViewPaths.all_view_paths.flat_map(&:paths)
+ end
+ end
+end
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 9fa8d7eab1..97a6da3634 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -6,12 +6,6 @@ module ActionView
class Digestor
@@digest_mutex = Mutex.new
- module PerExecutionDigestCacheExpiry
- def self.before(target)
- ActionView::LookupContext::DetailsKey.clear
- end
- end
-
class << self
# Supported options:
#
diff --git a/actionview/lib/action_view/file_template.rb b/actionview/lib/action_view/file_template.rb
deleted file mode 100644
index dea02176eb..0000000000
--- a/actionview/lib/action_view/file_template.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require "action_view/template"
-
-module ActionView
- class FileTemplate < Template
- def initialize(filename, handler, details)
- @filename = filename
-
- super(nil, filename, handler, details)
- end
-
- def source
- File.binread @filename
- end
-
- def refresh(_)
- self
- end
-
- # Exceptions are marshalled when using the parallel test runner with DRb, so we need
- # to ensure that references to the template object can be marshalled as well. This means forgoing
- # the marshalling of the compiler mutex and instantiating that again on unmarshalling.
- def marshal_dump # :nodoc:
- [ @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ]
- end
-
- def marshal_load(array) # :nodoc:
- @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array
- @compile_mutex = Mutex.new
- end
- end
-end
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index c0996049f0..5d4ff36425 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -24,7 +24,7 @@ module ActionView
mattr_accessor :default_enforce_utf8, default: true
- # Starts a form tag that points the action to a url configured with <tt>url_for_options</tt> just like
+ # Starts a form tag that points the action to a URL configured with <tt>url_for_options</tt> just like
# ActionController::Base#url_for. The method for the form defaults to POST.
#
# ==== Options
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index d63ada3890..df83dff681 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -553,7 +553,7 @@ module ActionView
url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY)
# We ignore any extra parameters in the request_uri if the
- # submitted url doesn't have any either. This lets the function
+ # submitted URL doesn't have any either. This lets the function
# work with things like ?order=asc
# the behaviour can be disabled with check_parameters: true
request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index fd3d025cbf..b9a8cc129c 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -130,9 +130,8 @@ module ActionView
end
alias :find_template :find
- def find_file(name, prefixes = [], partial = false, keys = [], options = {})
- @view_paths.find_file(*args_for_lookup(name, prefixes, partial, keys, options))
- end
+ alias :find_file :find
+ deprecate :find_file
def find_all(name, prefixes = [], partial = false, keys = [], options = {})
@view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
diff --git a/actionview/lib/action_view/path_set.rb b/actionview/lib/action_view/path_set.rb
index 691b53e2da..d54749d8e3 100644
--- a/actionview/lib/action_view/path_set.rb
+++ b/actionview/lib/action_view/path_set.rb
@@ -48,12 +48,11 @@ module ActionView #:nodoc:
find_all(*args).first || raise(MissingTemplate.new(self, *args))
end
- def find_file(path, prefixes = [], *args)
- _find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args))
- end
+ alias :find_file :find
+ deprecate :find_file
def find_all(path, prefixes = [], *args)
- _find_all path, prefixes, args, false
+ _find_all path, prefixes, args
end
def exists?(path, prefixes, *args)
@@ -71,15 +70,11 @@ module ActionView #:nodoc:
private
- def _find_all(path, prefixes, args, outside_app)
+ def _find_all(path, prefixes, args)
prefixes = [prefixes] if String === prefixes
prefixes.each do |prefix|
paths.each do |resolver|
- if outside_app
- templates = resolver.find_all_anywhere(path, prefix, *args)
- else
- templates = resolver.find_all(path, prefix, *args)
- end
+ templates = resolver.find_all(path, prefix, *args)
return templates unless templates.empty?
end
end
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index a25e1d3d02..8bd526cdf9 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -81,7 +81,7 @@ module ActionView
initializer "action_view.per_request_digest_cache" do |app|
ActiveSupport.on_load(:action_view) do
unless ActionView::Resolver.caching?
- app.executor.to_run ActionView::Digestor::PerExecutionDigestCacheExpiry
+ app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
end
end
end
diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
index ed59033e27..ca692699a6 100644
--- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
@@ -17,13 +17,13 @@ module ActionView
# Result is a hash with the key represents the
# key used for cache lookup and the value is the item
# on which the partial is being rendered
- keyed_collection = collection_by_cache_keys(view, template)
+ keyed_collection, ordered_keys = collection_by_cache_keys(view, template)
# Pull all partials from cache
# Result is a hash, key matches the entry in
# `keyed_collection` where the cache was retrieved and the
# value is the value that was present in the cache
- cached_partials = collection_cache.read_multi(*keyed_collection.keys)
+ cached_partials = collection_cache.read_multi(*keyed_collection.keys)
instrumentation_payload[:cache_hits] = cached_partials.size
# Extract the items for the keys that are not found
@@ -40,11 +40,15 @@ module ActionView
rendered_partials = @collection.empty? ? [] : yield
index = 0
- fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do
+ keyed_partials = fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do
# This block is called once
# for every cache miss while preserving order.
rendered_partials[index].tap { index += 1 }
end
+
+ ordered_keys.map do |key|
+ keyed_partials[key]
+ end
end
def callable_cache_key?
@@ -56,8 +60,10 @@ module ActionView
digest_path = view.digest_path_from_template(template)
- @collection.each_with_object({}) do |item, hash|
- hash[expanded_cache_key(seed.call(item), view, template, digest_path)] = item
+ @collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
+ key = expanded_cache_key(seed.call(item), view, template, digest_path)
+ ordered_keys << key
+ hash[key] = item
end
end
@@ -82,15 +88,16 @@ module ActionView
# If the partial is not already cached it will also be
# written back to the underlying cache store.
def fetch_or_cache_partial(cached_partials, template, order_by:)
- order_by.map do |cache_key|
- if content = cached_partials[cache_key]
- build_rendered_template(content, template)
- else
- yield.tap do |rendered_partial|
- collection_cache.write(cache_key, rendered_partial.body)
- end
+ order_by.each_with_object({}) do |cache_key, hash|
+ hash[cache_key] =
+ if content = cached_partials[cache_key]
+ build_rendered_template(content, template)
+ else
+ yield.tap do |rendered_partial|
+ collection_cache.write(cache_key, rendered_partial.body)
+ end
+ end
end
- end
end
end
end
diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index 279ef3c680..19fa399e2c 100644
--- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -34,7 +34,7 @@ module ActionView
return unless logger
message = +"\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << exception.annotated_source_code.to_s if exception.respond_to?(:annotated_source_code)
message << " " << exception.backtrace.join("\n ")
logger.fatal("#{message}\n\n")
end
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index 83b990b081..4aab3f913f 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -26,7 +26,12 @@ module ActionView
elsif options.key?(:html)
Template::HTML.new(options[:html], formats.first)
elsif options.key?(:file)
- @lookup_context.with_fallbacks.find_file(options[:file], nil, false, keys, @details)
+ if File.exist?(options[:file])
+ Template::RawFile.new(options[:file])
+ else
+ ActiveSupport::Deprecation.warn "render file: should be given the absolute path to a file"
+ @lookup_context.with_fallbacks.find_template(options[:file], nil, false, keys, @details)
+ end
elsif options.key?(:inline)
handler = Template.handler_for_extension(options[:type] || "erb")
format = if handler.respond_to?(:default_format)
@@ -49,14 +54,14 @@ module ActionView
# Renders the given template. A string representing the layout can be
# supplied as well.
def render_template(view, template, layout_name, locals)
- render_with_layout(view, layout_name, template, locals) do |layout|
+ render_with_layout(view, template, layout_name, locals) do |layout|
instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do
template.render(view, locals) { |*name| view._layout_for(*name) }
end
end
end
- def render_with_layout(view, path, template, locals)
+ def render_with_layout(view, template, path, locals)
layout = path && find_layout(path, locals.keys, [formats.first])
content = yield(layout)
@@ -84,6 +89,7 @@ module ActionView
when String
begin
if layout.start_with?("/")
+ ActiveSupport::Deprecation.warn "Rendering layouts from an absolute path is deprecated."
@lookup_context.with_fallbacks.find_template(layout, nil, false, [], details)
else
@lookup_context.find_template(layout, nil, false, [], details)
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 6e3af1536a..b80bf56c1b 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -113,16 +113,18 @@ module ActionView
eager_autoload do
autoload :Error
+ autoload :RawFile
autoload :Handlers
autoload :HTML
autoload :Inline
+ autoload :Sources
autoload :Text
autoload :Types
end
extend Template::Handlers
- attr_reader :source, :identifier, :handler, :original_encoding, :updated_at
+ attr_reader :identifier, :handler, :original_encoding, :updated_at
attr_reader :variable, :format, :variant, :locals, :virtual_path
def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil, updated_at: nil)
@@ -139,7 +141,7 @@ module ActionView
@virtual_path = virtual_path
@variable = if @virtual_path
- base = @virtual_path[-1] == "/" ? "" : File.basename(@virtual_path)
+ base = @virtual_path[-1] == "/" ? "" : ::File.basename(@virtual_path)
base =~ /\A_?(.*?)(?:\.\w+)*\z/
$1.to_sym
end
@@ -163,6 +165,7 @@ module ActionView
deprecate def formats; Array(format); end
deprecate def variants=(_); end
deprecate def variants; [variant]; end
+ deprecate def refresh(_); self; end
# Returns whether the underlying handler supports streaming. If so,
# a streaming buffer *may* be passed when it starts rendering.
@@ -189,25 +192,6 @@ module ActionView
@type ||= Types[format]
end
- # Receives a view object and return a template similar to self by using @virtual_path.
- #
- # This method is useful if you have a template object but it does not contain its source
- # anymore since it was already compiled. In such cases, all you need to do is to call
- # refresh passing in the view object.
- #
- # Notice this method raises an error if the template to be refreshed does not have a
- # virtual path set (true just for inline templates).
- def refresh(view)
- raise "A template needs to have a virtual path in order to be refreshed" unless @virtual_path
- lookup = view.lookup_context
- pieces = @virtual_path.split("/")
- name = pieces.pop
- partial = !!name.sub!(/^_/, "")
- lookup.disable_cache do
- lookup.find_template(name, [ pieces.join("/") ], partial, @locals)
- end
- end
-
def short_identifier
@short_identifier ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "") : identifier
end
@@ -216,6 +200,10 @@ module ActionView
"#<#{self.class.name} #{short_identifier} locals=#{@locals.inspect}>"
end
+ def source
+ @source.to_s
+ end
+
# This method is responsible for properly setting the encoding of the
# source. Until this point, we assume that the source is BINARY data.
# If no additional information is supplied, we assume the encoding is
@@ -297,9 +285,6 @@ module ActionView
compile(mod)
end
- # Just discard the source if we have a virtual path. This
- # means we can get the template back.
- @source = nil if @virtual_path
@compiled = true
end
end
@@ -331,6 +316,7 @@ module ActionView
# Make sure that the resulting String to be eval'd is in the
# encoding of the code
+ original_source = source
source = +<<-end_src
def #{method_name}(local_assigns, output_buffer)
@virtual_path = #{@virtual_path.inspect};#{locals_code};#{code}
@@ -351,7 +337,14 @@ module ActionView
raise WrongEncodingError.new(source, Encoding.default_internal)
end
- mod.module_eval(source, identifier, 0)
+ begin
+ mod.module_eval(source, identifier, 0)
+ rescue SyntaxError
+ # Account for when code in the template is not syntactically valid; e.g. if we're using
+ # ERB and the user writes <%= foo( %>, attempting to call a helper `foo` and interpolate
+ # the result into the template, but missing an end parenthesis.
+ raise SyntaxErrorInTemplate.new(self, original_source)
+ end
end
def handle_render_error(view, e)
@@ -359,12 +352,7 @@ module ActionView
e.sub_template_of(self)
raise e
else
- template = self
- unless template.source
- template = refresh(view)
- template.encode!
- end
- raise Template::Error.new(template)
+ raise Template::Error.new(self)
end
end
diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb
index 4e3c02e05e..d0ea03e228 100644
--- a/actionview/lib/action_view/template/error.rb
+++ b/actionview/lib/action_view/template/error.rb
@@ -109,7 +109,7 @@ module ActionView
end
end
- def annoted_source_code
+ def annotated_source_code
source_extract(4)
end
@@ -138,4 +138,24 @@ module ActionView
end
TemplateError = Template::Error
+
+ class SyntaxErrorInTemplate < TemplateError #:nodoc
+ def initialize(template, offending_code_string)
+ @offending_code_string = offending_code_string
+ super(template)
+ end
+
+ def message
+ <<~MESSAGE
+ Encountered a syntax error while rendering template: check #{@offending_code_string}
+ MESSAGE
+ end
+
+ def annotated_source_code
+ @offending_code_string.split("\n").map.with_index(1) { |line, index|
+ indentation = " " * 4
+ "#{index}:#{indentation}#{line}"
+ }
+ end
+ end
end
diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb
index ddaac7a100..6450513003 100644
--- a/actionview/lib/action_view/template/handlers.rb
+++ b/actionview/lib/action_view/template/handlers.rb
@@ -45,12 +45,12 @@ module ActionView #:nodoc:
unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2
ActiveSupport::Deprecation.warn <<~eowarn
- Single arity template handlers are deprecated. Template handlers must
+ Single arity template handlers are deprecated. Template handlers must
now accept two parameters, the view object and the source for the view object.
Change:
- >> #{handler.class}#call(#{params.map(&:last).join(", ")})
+ >> #{handler}.call(#{params.map(&:last).join(", ")})
To:
- >> #{handler.class}#call(#{params.map(&:last).join(", ")}, source)
+ >> #{handler}.call(#{params.map(&:last).join(", ")}, source)
eowarn
handler = LegacyHandlerWrapper.new(handler)
end
diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb
index 307b852440..e602aa117a 100644
--- a/actionview/lib/action_view/template/handlers/erb/erubi.rb
+++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb
@@ -25,7 +25,7 @@ module ActionView
src = @src
view = Class.new(ActionView::Base) {
include action_view_erb_handler_context._routes.url_helpers
- class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", @filename || "(erubi)", 0)
+ class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", defined?(@filename) ? @filename : "(erubi)", 0)
}.empty
view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
end
diff --git a/actionview/lib/action_view/template/raw_file.rb b/actionview/lib/action_view/template/raw_file.rb
new file mode 100644
index 0000000000..623351305f
--- /dev/null
+++ b/actionview/lib/action_view/template/raw_file.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module ActionView #:nodoc:
+ # = Action View RawFile Template
+ class Template #:nodoc:
+ class RawFile #:nodoc:
+ attr_accessor :type, :format
+
+ def initialize(filename)
+ @filename = filename.to_s
+ extname = ::File.extname(filename).delete(".")
+ @type = Template::Types[extname] || Template::Types[:text]
+ @format = @type.symbol
+ end
+
+ def identifier
+ @filename
+ end
+
+ def render(*args)
+ ::File.read(@filename)
+ end
+
+ def formats; Array(format); end
+ deprecate :formats
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index 1c577348e5..ce53eb046d 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -118,17 +118,12 @@ module ActionView
locals = locals.map(&:to_s).sort!.freeze
cached(key, [name, prefix, partial], details, locals) do
- find_templates(name, prefix, partial, details, false, locals)
+ _find_all(name, prefix, partial, details, key, locals)
end
end
- def find_all_anywhere(name, prefix, partial = false, details = {}, key = nil, locals = [])
- locals = locals.map(&:to_s).sort!.freeze
-
- cached(key, [name, prefix, partial], details, locals) do
- find_templates(name, prefix, partial, details, true, locals)
- end
- end
+ alias :find_all_anywhere :find_all
+ deprecate :find_all_anywhere
def find_all_with_query(query) # :nodoc:
@cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
@@ -136,13 +131,17 @@ module ActionView
private
+ def _find_all(name, prefix, partial, details, key, locals)
+ find_templates(name, prefix, partial, details, locals)
+ end
+
delegate :caching?, to: :class
# This is what child classes implement. No defaults are needed
# because Resolver guarantees that the arguments are present and
# normalized.
- def find_templates(name, prefix, partial, details, outside_app_allowed = false, locals = [])
- raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed = false, locals = []) method"
+ def find_templates(name, prefix, partial, details, locals = [])
+ raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, locals = []) method"
end
# Handles templates caching. If a key is given and caching is on
@@ -168,34 +167,57 @@ module ActionView
DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
def initialize(pattern = nil)
- @pattern = pattern || DEFAULT_PATTERN
+ if pattern
+ ActiveSupport::Deprecation.warn "Specifying a custom path for #{self.class} is deprecated. Implement a custom Resolver subclass instead."
+ @pattern = pattern
+ else
+ @pattern = DEFAULT_PATTERN
+ end
+ @unbound_templates = Concurrent::Map.new
+ super()
+ end
+
+ def clear_cache
+ @unbound_templates.clear
super()
end
private
- def find_templates(name, prefix, partial, details, outside_app_allowed = false, locals)
+ def _find_all(name, prefix, partial, details, key, locals)
path = Path.build(name, prefix, partial)
- query(path, details, details[:formats], outside_app_allowed, locals)
+ query(path, details, details[:formats], locals, cache: !!key)
end
- def query(path, details, formats, outside_app_allowed, locals)
+ def query(path, details, formats, locals, cache:)
template_paths = find_template_paths_from_details(path, details)
- template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
+ template_paths = reject_files_external_to_app(template_paths)
template_paths.map do |template|
- build_template(template, path.virtual, locals)
+ unbound_template =
+ if cache
+ @unbound_templates.compute_if_absent([template, path.virtual]) do
+ build_unbound_template(template, path.virtual)
+ end
+ else
+ build_unbound_template(template, path.virtual)
+ end
+
+ unbound_template.bind_locals(locals)
end
end
- def build_template(template, virtual_path, locals)
+ def build_unbound_template(template, virtual_path)
handler, format, variant = extract_handler_and_format_and_variant(template)
+ source = Template::Sources::File.new(template)
- FileTemplate.new(File.expand_path(template), handler,
+ UnboundTemplate.new(
+ source,
+ template,
+ handler,
virtual_path: virtual_path,
format: format,
variant: variant,
- locals: locals
)
end
@@ -273,45 +295,10 @@ module ActionView
end
end
- # A resolver that loads files from the filesystem. It allows setting your own
- # resolving pattern. Such pattern can be a glob string supported by some variables.
- #
- # ==== Examples
- #
- # Default pattern, loads views the same way as previous versions of rails, eg. when you're
- # looking for <tt>users/new</tt> it will produce query glob: <tt>users/new{.{en},}{.{html,js},}{.{erb,haml},}</tt>
- #
- # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
- #
- # This one allows you to keep files with different formats in separate subdirectories,
- # eg. <tt>users/new.html</tt> will be loaded from <tt>users/html/new.erb</tt> or <tt>users/new.html.erb</tt>,
- # <tt>users/new.js</tt> from <tt>users/js/new.erb</tt> or <tt>users/new.js.erb</tt>, etc.
- #
- # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
- #
- # If you don't specify a pattern then the default will be used.
- #
- # In order to use any of the customized resolvers above in a Rails application, you just need
- # to configure ActionController::Base.view_paths in an initializer, for example:
- #
- # ActionController::Base.view_paths = FileSystemResolver.new(
- # Rails.root.join("app/views"),
- # ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}",
- # )
- #
- # ==== Pattern format and variables
- #
- # Pattern has to be a valid glob string, and it allows you to use the
- # following variables:
- #
- # * <tt>:prefix</tt> - usually the controller path
- # * <tt>:action</tt> - name of the action
- # * <tt>:locale</tt> - possible locale versions
- # * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
- # * <tt>:variants</tt> - possible request variants (for example phone, tablet...)
- # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
- #
+ # A resolver that loads files from the filesystem.
class FileSystemResolver < PathResolver
+ attr_reader :path
+
def initialize(path, pattern = nil)
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
super(pattern)
@@ -331,6 +318,10 @@ module ActionView
# An Optimized resolver for Rails' most common case.
class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
+ def initialize(path)
+ super(path)
+ end
+
private
def find_template_paths_from_details(path, details)
@@ -386,12 +377,18 @@ module ActionView
# The same as FileSystemResolver but does not allow templates to store
# a virtual path since it is invalid for such resolvers.
class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
+ private_class_method :new
+
def self.instances
[new(""), new("/")]
end
- def build_template(template, virtual_path, locals)
- super(template, nil, locals)
+ def build_unbound_template(template, _)
+ super(template, nil)
+ end
+
+ def reject_files_external_to_app(files)
+ files
end
end
end
diff --git a/actionview/lib/action_view/template/sources.rb b/actionview/lib/action_view/template/sources.rb
new file mode 100644
index 0000000000..8b89535741
--- /dev/null
+++ b/actionview/lib/action_view/template/sources.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActionView
+ class Template
+ module Sources
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :File
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/sources/file.rb b/actionview/lib/action_view/template/sources/file.rb
new file mode 100644
index 0000000000..2d3a7dec7f
--- /dev/null
+++ b/actionview/lib/action_view/template/sources/file.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionView
+ class Template
+ module Sources
+ class File
+ def initialize(filename)
+ @filename = filename
+ end
+
+ def to_s
+ ::File.binread @filename
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index a16dc0096e..1bedf44934 100644
--- a/actionview/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -23,10 +23,10 @@ module ActionView #:nodoc:
private
- def query(path, exts, _, _, locals)
+ def query(path, exts, _, locals, cache:)
query = +""
- EXTENSIONS.each_key do |ext|
- query << "(" << exts[ext].map { |e| e && Regexp.escape(".#{e}") }.join("|") << "|)"
+ EXTENSIONS.each do |ext, prefix|
+ query << "(" << exts[ext].map { |e| e && Regexp.escape("#{prefix}#{e}") }.join("|") << "|)"
end
query = /^(#{Regexp.escape(path)})#{query}$/
@@ -47,7 +47,7 @@ module ActionView #:nodoc:
end
class NullResolver < PathResolver
- def query(path, exts, _, _, locals)
+ def query(path, exts, _, locals, cache:)
handler, format, variant = extract_handler_and_format_and_variant(path)
[ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant, locals: locals)]
end
diff --git a/actionview/lib/action_view/unbound_template.rb b/actionview/lib/action_view/unbound_template.rb
new file mode 100644
index 0000000000..db69b6d016
--- /dev/null
+++ b/actionview/lib/action_view/unbound_template.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "concurrent/map"
+
+module ActionView
+ class UnboundTemplate
+ def initialize(source, identifer, handler, options)
+ @source = source
+ @identifer = identifer
+ @handler = handler
+ @options = options
+
+ @templates = Concurrent::Map.new(initial_capacity: 2)
+ end
+
+ def bind_locals(locals)
+ @templates[locals] ||= build_template(locals)
+ end
+
+ private
+
+ def build_template(locals)
+ options = @options.merge(locals: locals)
+ Template.new(
+ @source,
+ @identifer,
+ @handler,
+ options
+ )
+ end
+ end
+end
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
index fe317bf39e..2c1b277b41 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -205,3 +205,5 @@ class ActiveSupport::TestCase
skip message if defined?(JRUBY_VERSION)
end
end
+
+require_relative "../../tools/test_common"
diff --git a/actionview/test/actionpack/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb
index 4d4e2b8ef2..eecc19d413 100644
--- a/actionview/test/actionpack/abstract/abstract_controller_test.rb
+++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb
@@ -229,11 +229,11 @@ module AbstractController
end
class ActionMissingRespondToActionController < AbstractController::Base
- # No actions
- private
- def action_missing(action_name)
- self.response_body = "success"
- end
+ # No actions
+ private
+ def action_missing(action_name)
+ self.response_body = "success"
+ end
end
class RespondToActionController < AbstractController::Base
diff --git a/actionview/test/actionpack/abstract/render_test.rb b/actionview/test/actionpack/abstract/render_test.rb
index d863548a5c..e4e8ac93b2 100644
--- a/actionview/test/actionpack/abstract/render_test.rb
+++ b/actionview/test/actionpack/abstract/render_test.rb
@@ -26,7 +26,7 @@ module AbstractController
end
def file
- render file: "some/file"
+ ActiveSupport::Deprecation.silence { render file: "some/file" }
end
def inline
diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb
index 838b564c5d..f946e4360d 100644
--- a/actionview/test/actionpack/controller/layout_test.rb
+++ b/actionview/test/actionpack/controller/layout_test.rb
@@ -233,7 +233,9 @@ class LayoutSetInResponseTest < ActionController::TestCase
def test_absolute_pathed_layout
@controller = AbsolutePathLayoutController.new
- get :hello
+ assert_deprecated do
+ get :hello
+ end
assert_equal "layout_test.erb hello.erb", @response.body.strip
end
end
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
index 52c3c54d96..c8ce7366d1 100644
--- a/actionview/test/actionpack/controller/render_test.rb
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -872,48 +872,64 @@ class RenderTest < ActionController::TestCase
# :ported:
def test_render_file_with_instance_variables
- get :render_file_with_instance_variables
+ assert_deprecated do
+ get :render_file_with_instance_variables
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
def test_render_file
- get :hello_world_file
+ assert_deprecated do
+ get :hello_world_file
+ end
assert_equal "Hello world!", @response.body
end
# :ported:
def test_render_file_not_using_full_path
- get :render_file_not_using_full_path
+ assert_deprecated do
+ get :render_file_not_using_full_path
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
# :ported:
def test_render_file_not_using_full_path_with_dot_in_path
- get :render_file_not_using_full_path_with_dot_in_path
+ assert_deprecated do
+ get :render_file_not_using_full_path_with_dot_in_path
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
# :ported:
def test_render_file_using_pathname
- get :render_file_using_pathname
+ assert_deprecated do
+ get :render_file_using_pathname
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
# :ported:
def test_render_file_with_locals
- get :render_file_with_locals
+ assert_deprecated do
+ get :render_file_with_locals
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
# :ported:
def test_render_file_as_string_with_locals
- get :render_file_as_string_with_locals
+ assert_deprecated do
+ get :render_file_as_string_with_locals
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
# :assessed:
def test_render_file_from_template
- get :render_file_from_template
+ assert_deprecated do
+ get :render_file_from_template
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
@@ -1133,11 +1149,19 @@ class RenderTest < ActionController::TestCase
end
def test_bad_render_to_string_still_throws_exception
- assert_raise(ActionView::MissingTemplate) { get :render_to_string_with_exception }
+ assert_deprecated do
+ assert_raise(ActionView::MissingTemplate) do
+ get :render_to_string_with_exception
+ end
+ end
end
def test_render_to_string_that_throws_caught_exception_doesnt_break_assigns
- assert_nothing_raised { get :render_to_string_with_caught_exception }
+ assert_deprecated do
+ assert_nothing_raised do
+ get :render_to_string_with_caught_exception
+ end
+ end
assert_equal "i'm before the render", @controller.instance_variable_get(:@before)
assert_equal "i'm after the render", @controller.instance_variable_get(:@after)
end
diff --git a/actionview/test/fixtures/test/_cached_set.erb b/actionview/test/fixtures/test/_cached_set.erb
new file mode 100644
index 0000000000..cd492fc519
--- /dev/null
+++ b/actionview/test/fixtures/test/_cached_set.erb
@@ -0,0 +1 @@
+<%= cached_set.first %> | <%= cached_set.second %> | <%= cached_set.third %> | <%= cached_set.fourth %> | <%= cached_set.fifth %>
diff --git a/actionview/test/fixtures/test/syntax_error.html.erb b/actionview/test/fixtures/test/syntax_error.html.erb
new file mode 100644
index 0000000000..4004a2b187
--- /dev/null
+++ b/actionview/test/fixtures/test/syntax_error.html.erb
@@ -0,0 +1,4 @@
+<%= foo(
+ 1,
+ 2,
+%>
diff --git a/actionview/test/template/fallback_file_system_resolver_test.rb b/actionview/test/template/fallback_file_system_resolver_test.rb
index 304cdb8a03..fa770f3a15 100644
--- a/actionview/test/template/fallback_file_system_resolver_test.rb
+++ b/actionview/test/template/fallback_file_system_resolver_test.rb
@@ -4,7 +4,7 @@ require "abstract_unit"
class FallbackFileSystemResolverTest < ActiveSupport::TestCase
def setup
- @root_resolver = ActionView::FallbackFileSystemResolver.new("/")
+ @root_resolver = ActionView::FallbackFileSystemResolver.send(:new, "/")
end
def test_should_have_no_virtual_path
diff --git a/actionview/test/template/file_system_resolver_test.rb b/actionview/test/template/file_system_resolver_test.rb
new file mode 100644
index 0000000000..aa03fdcb13
--- /dev/null
+++ b/actionview/test/template/file_system_resolver_test.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "template/resolver_shared_tests"
+
+class FileSystemResolverTest < ActiveSupport::TestCase
+ include ResolverSharedTests
+
+ def resolver
+ ActionView::FileSystemResolver.new(tmpdir)
+ end
+end
diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb
index 4c28aeaee1..f974e5ae0c 100644
--- a/actionview/test/template/javascript_helper_test.rb
+++ b/actionview/test/template/javascript_helper_test.rb
@@ -54,7 +54,7 @@ class JavaScriptHelperTest < ActionView::TestCase
assert_equal "foo", output_buffer, "javascript_tag without a block should not concat to output_buffer"
end
- # Setting the :extname option will control what extension (if any) is appended to the url for assets
+ # Setting the :extname option will control what extension (if any) is appended to the URL for assets
def test_javascript_include_tag
assert_dom_equal "<script src='/foo.js'></script>", javascript_include_tag("/foo")
assert_dom_equal "<script src='/foo'></script>", javascript_include_tag("/foo", extname: false)
diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb
index 85735139c1..8b160a7336 100644
--- a/actionview/test/template/log_subscriber_test.rb
+++ b/actionview/test/template/log_subscriber_test.rb
@@ -51,9 +51,20 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
def @view.combined_fragment_cache_key(*); "ahoy `controller` dependency"; end
end
+ def test_render_template_template
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ @view.render(template: "test/hello_world")
+ wait
+
+ assert_equal 2, @logger.logged(:info).size
+ assert_match(/Rendering test\/hello_world\.erb/, @logger.logged(:info).first)
+ assert_match(/Rendered test\/hello_world\.erb/, @logger.logged(:info).last)
+ end
+ end
+
def test_render_file_template
Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
- @view.render(file: "test/hello_world")
+ @view.render(file: "#{FIXTURE_LOAD_PATH}/test/hello_world.erb")
wait
assert_equal 2, @logger.logged(:info).size
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
index 3e357fe1a7..72e1f50fdf 100644
--- a/actionview/test/template/lookup_context_test.rb
+++ b/actionview/test/template/lookup_context_test.rb
@@ -143,16 +143,16 @@ class LookupContextTest < ActiveSupport::TestCase
assert_deprecated do
@lookup_context.with_fallbacks do
assert_equal 3, @lookup_context.view_paths.size
- assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("")
- assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/")
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.instances[0]
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.instances[1]
end
end
@lookup_context = @lookup_context.with_fallbacks
assert_equal 3, @lookup_context.view_paths.size
- assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("")
- assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/")
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.instances[0]
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.instances[1]
end
test "add fallbacks just once in nested fallbacks calls" do
diff --git a/actionview/test/template/optimized_file_system_resolver_test.rb b/actionview/test/template/optimized_file_system_resolver_test.rb
new file mode 100644
index 0000000000..c0c64357ce
--- /dev/null
+++ b/actionview/test/template/optimized_file_system_resolver_test.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "template/resolver_shared_tests"
+
+class OptimizedFileSystemResolverTest < ActiveSupport::TestCase
+ include ResolverSharedTests
+
+ def resolver
+ ActionView::OptimizedFileSystemResolver.new(tmpdir)
+ end
+end
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index f0fed601f8..f0b5d7d95e 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -53,15 +53,20 @@ module RenderTestCases
assert_match(/You invoked render but did not give any of (.+) option\./, e.message)
end
+ def test_render_template
+ assert_equal "Hello world!", @view.render(template: "test/hello_world")
+ end
+
+
def test_render_file
- assert_equal "Hello world!", @view.render(file: "test/hello_world")
+ assert_equal "Hello world!", assert_deprecated { @view.render(file: "test/hello_world") }
end
# Test if :formats, :locale etc. options are passed correctly to the resolvers.
def test_render_file_with_format
- assert_match "<h1>No Comment</h1>", @view.render(file: "comments/empty", formats: [:html])
- assert_match "<error>No Comment</error>", @view.render(file: "comments/empty", formats: [:xml])
- assert_match "<error>No Comment</error>", @view.render(file: "comments/empty", formats: :xml)
+ assert_match "<h1>No Comment</h1>", assert_deprecated { @view.render(file: "comments/empty", formats: [:html]) }
+ assert_match "<error>No Comment</error>", assert_deprecated { @view.render(file: "comments/empty", formats: [:xml]) }
+ assert_match "<error>No Comment</error>", assert_deprecated { @view.render(file: "comments/empty", formats: :xml) }
end
def test_render_template_with_format
@@ -94,8 +99,8 @@ module RenderTestCases
end
def test_render_file_with_locale
- assert_equal "<h1>Kein Kommentar</h1>", @view.render(file: "comments/empty", locale: [:de])
- assert_equal "<h1>Kein Kommentar</h1>", @view.render(file: "comments/empty", locale: :de)
+ assert_equal "<h1>Kein Kommentar</h1>", assert_deprecated { @view.render(file: "comments/empty", locale: [:de]) }
+ assert_equal "<h1>Kein Kommentar</h1>", assert_deprecated { @view.render(file: "comments/empty", locale: :de) }
end
def test_render_template_with_locale
@@ -107,8 +112,8 @@ module RenderTestCases
end
def test_render_file_with_handlers
- assert_equal "<h1>No Comment</h1>\n", @view.render(file: "comments/empty", handlers: [:builder])
- assert_equal "<h1>No Comment</h1>\n", @view.render(file: "comments/empty", handlers: :builder)
+ assert_equal "<h1>No Comment</h1>\n", assert_deprecated { @view.render(file: "comments/empty", handlers: [:builder]) }
+ assert_equal "<h1>No Comment</h1>\n", assert_deprecated { @view.render(file: "comments/empty", handlers: :builder) }
end
def test_render_template_with_handlers
@@ -156,22 +161,27 @@ module RenderTestCases
assert_equal "Elastica", @view.render(template: "/shared")
end
- def test_render_file_with_full_path
+ def test_render_file_with_full_path_no_extension
template_path = File.expand_path("../fixtures/test/hello_world", __dir__)
+ assert_equal "Hello world!", assert_deprecated { @view.render(file: template_path) }
+ end
+
+ def test_render_file_with_full_path
+ template_path = File.expand_path("../fixtures/test/hello_world.erb", __dir__)
assert_equal "Hello world!", @view.render(file: template_path)
end
def test_render_file_with_instance_variables
- assert_equal "The secret is in the sauce\n", @view.render(file: "test/render_file_with_ivar")
+ assert_equal "The secret is in the sauce\n", assert_deprecated { @view.render(file: "test/render_file_with_ivar") }
end
def test_render_file_with_locals
locals = { secret: "in the sauce" }
- assert_equal "The secret is in the sauce\n", @view.render(file: "test/render_file_with_locals", locals: locals)
+ assert_equal "The secret is in the sauce\n", assert_deprecated { @view.render(file: "test/render_file_with_locals", locals: locals) }
end
def test_render_file_not_using_full_path_with_dot_in_path
- assert_equal "The secret is in the sauce\n", @view.render(file: "test/dot.directory/render_file_with_ivar")
+ assert_equal "The secret is in the sauce\n", assert_deprecated { @view.render(file: "test/dot.directory/render_file_with_ivar") }
end
def test_render_partial_from_default
@@ -259,18 +269,24 @@ module RenderTestCases
"and is followed by any combination of letters, numbers and underscores.", e.message
end
+ def test_render_template_with_syntax_error
+ e = assert_raises(ActionView::Template::Error) { @view.render(template: "test/syntax_error") }
+ assert_match %r!syntax!, e.message
+ assert_equal "1: <%= foo(", e.annotated_source_code[0].strip
+ end
+
def test_render_partial_with_errors
e = assert_raises(ActionView::Template::Error) { @view.render(partial: "test/raise") }
assert_match %r!method.*doesnt_exist!, e.message
assert_equal "", e.sub_template_message
assert_equal "1", e.line_number
- assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code[0].strip
+ assert_equal "1: <%= doesnt_exist %>", e.annotated_source_code[0].strip
assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
end
def test_render_error_indentation
e = assert_raises(ActionView::Template::Error) { @view.render(partial: "test/raise_indentation") }
- error_lines = e.annoted_source_code
+ error_lines = e.annotated_source_code
assert_match %r!error\shere!, e.message
assert_equal "11", e.line_number
assert_equal " 9: <p>Ninth paragraph</p>", error_lines.second
@@ -286,11 +302,11 @@ module RenderTestCases
end
def test_render_file_with_errors
- e = assert_raises(ActionView::Template::Error) { @view.render(file: File.expand_path("test/_raise", FIXTURE_LOAD_PATH)) }
+ e = assert_raises(ActionView::Template::Error) { assert_deprecated { @view.render(file: File.expand_path("test/_raise", FIXTURE_LOAD_PATH)) } }
assert_match %r!method.*doesnt_exist!, e.message
assert_equal "", e.sub_template_message
assert_equal "1", e.line_number
- assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code[0].strip
+ assert_equal "1: <%= doesnt_exist %>", e.annotated_source_code[0].strip
assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
end
@@ -795,6 +811,28 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase
end
end
+ test "collection caching with repeated collection" do
+ sets = [
+ [1, 2, 3, 4, 5],
+ [1, 2, 3, 4, 4],
+ [1, 2, 3, 4, 5],
+ [1, 2, 3, 4, 4],
+ [1, 2, 3, 4, 6]
+ ]
+
+ result = @view.render(partial: "test/cached_set", collection: sets, cached: true)
+
+ splited_result = result.split("\n")
+ assert_equal 5, splited_result.count
+ assert_equal [
+ "1 | 2 | 3 | 4 | 5",
+ "1 | 2 | 3 | 4 | 4",
+ "1 | 2 | 3 | 4 | 5",
+ "1 | 2 | 3 | 4 | 4",
+ "1 | 2 | 3 | 4 | 6"
+ ], splited_result
+ end
+
private
def cache_key(*names, virtual_path)
digest = ActionView::Digestor.digest name: virtual_path, format: :html, finder: @view.lookup_context, dependencies: []
diff --git a/actionview/test/template/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb
index 8122de779f..22815c8dbe 100644
--- a/actionview/test/template/resolver_patterns_test.rb
+++ b/actionview/test/template/resolver_patterns_test.rb
@@ -6,7 +6,10 @@ class ResolverPatternsTest < ActiveSupport::TestCase
def setup
path = File.expand_path("../fixtures", __dir__)
pattern = ":prefix/{:formats/,}:action{.:formats,}{+:variants,}{.:handlers,}"
- @resolver = ActionView::FileSystemResolver.new(path, pattern)
+
+ assert_deprecated do
+ @resolver = ActionView::FileSystemResolver.new(path, pattern)
+ end
end
def test_should_return_empty_list_for_unknown_path
diff --git a/actionview/test/template/resolver_shared_tests.rb b/actionview/test/template/resolver_shared_tests.rb
new file mode 100644
index 0000000000..8b47c5bc89
--- /dev/null
+++ b/actionview/test/template/resolver_shared_tests.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+module ResolverSharedTests
+ attr_reader :tmpdir
+
+ def run(*args)
+ capture_exceptions do
+ Dir.mktmpdir(nil, __dir__) { |dir| @tmpdir = dir; super }
+ end
+ end
+
+ def with_file(filename, source = "File at #{filename}")
+ path = File.join(tmpdir, filename)
+ FileUtils.mkdir_p(File.dirname(path))
+ File.write(path, source)
+ end
+
+ def context
+ @context ||= ActionView::LookupContext.new(resolver)
+ end
+
+ def test_can_find_with_no_extensions
+ with_file "test/hello_world", "Hello default!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: [:phone], handlers: [:erb])
+ assert_equal 1, templates.size
+ assert_equal "Hello default!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_nil templates[0].format
+ assert_nil templates[0].variant
+ assert_kind_of ActionView::Template::Handlers::Raw, templates[0].handler
+ end
+
+ def test_can_find_with_just_handler
+ with_file "test/hello_world.erb", "Hello erb!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: [:phone], handlers: [:erb])
+ assert_equal 1, templates.size
+ assert_equal "Hello erb!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_nil templates[0].format
+ assert_nil templates[0].variant
+ assert_kind_of ActionView::Template::Handlers::ERB, templates[0].handler
+ end
+
+ def test_can_find_with_format_and_handler
+ with_file "test/hello_world.text.builder", "Hello plain text!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html, :text], variants: [:phone], handlers: [:erb, :builder])
+ assert_equal 1, templates.size
+ assert_equal "Hello plain text!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_equal :text, templates[0].format
+ assert_nil templates[0].variant
+ assert_kind_of ActionView::Template::Handlers::Builder, templates[0].handler
+ end
+
+ def test_can_find_with_variant_format_and_handler
+ with_file "test/hello_world.html+phone.erb", "Hello plain text!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: [:phone], handlers: [:erb])
+ assert_equal 1, templates.size
+ assert_equal "Hello plain text!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_equal :html, templates[0].format
+ assert_equal "phone", templates[0].variant
+ assert_kind_of ActionView::Template::Handlers::ERB, templates[0].handler
+ end
+
+ def test_can_find_with_any_variant_format_and_handler
+ with_file "test/hello_world.html+phone.erb", "Hello plain text!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [:en], formats: [:html], variants: :any, handlers: [:erb])
+ assert_equal 1, templates.size
+ assert_equal "Hello plain text!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_equal :html, templates[0].format
+ assert_equal "phone", templates[0].variant
+ assert_kind_of ActionView::Template::Handlers::ERB, templates[0].handler
+ end
+
+ def test_doesnt_find_template_with_wrong_details
+ with_file "test/hello_world.html.erb", "Hello plain text!"
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:xml], variants: :any, handlers: [:builder])
+ assert_equal 0, templates.size
+
+ templates = resolver.find_all("hello_world", "test", false, locale: [], formats: [:xml], variants: :any, handlers: [:erb])
+ assert_equal 0, templates.size
+ end
+
+ def test_found_template_is_cached
+ with_file "test/hello_world.html.erb", "Hello HTML!"
+
+ a = context.find("hello_world", "test", false, [], {})
+ b = context.find("hello_world", "test", false, [], {})
+ assert_same a, b
+ end
+
+ def test_different_templates_when_cache_disabled
+ with_file "test/hello_world.html.erb", "Hello HTML!"
+
+ a = context.find("hello_world", "test", false, [], {})
+ b = context.disable_cache { context.find("hello_world", "test", false, [], {}) }
+ c = context.find("hello_world", "test", false, [], {})
+
+ # disable_cache should give us a new object
+ assert_not_same a, b
+
+ # but it should not clear the cache
+ assert_same a, c
+ end
+
+ def test_same_template_from_different_details_is_same_object
+ with_file "test/hello_world.html.erb", "Hello HTML!"
+
+ a = context.find("hello_world", "test", false, [], locale: [:en])
+ b = context.find("hello_world", "test", false, [], locale: [:fr])
+ assert_same a, b
+ end
+
+ def test_templates_with_optional_locale_shares_common_object
+ with_file "test/hello_world.text.erb", "Generic plain text!"
+ with_file "test/hello_world.fr.text.erb", "Texte en Francais!"
+
+ en = context.find_all("hello_world", "test", false, [], locale: [:en])
+ fr = context.find_all("hello_world", "test", false, [], locale: [:fr])
+
+ assert_equal 1, en.size
+ assert_equal 2, fr.size
+
+ assert_equal "Generic plain text!", en[0].source
+ assert_equal "Texte en Francais!", fr[0].source
+ assert_equal "Generic plain text!", fr[1].source
+
+ assert_same en[0], fr[1]
+ end
+
+ def test_virtual_path_is_preserved_with_dot
+ with_file "test/hello_world.html.erb", "Hello html!"
+
+ template = context.find("hello_world.html", "test", false, [], {})
+ assert_equal "test/hello_world.html", template.virtual_path
+
+ template = context.find("hello_world", "test", false, [], {})
+ assert_equal "test/hello_world", template.virtual_path
+ end
+end
diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb
index a5b59a700e..a5e673e71e 100644
--- a/actionview/test/template/streaming_render_test.rb
+++ b/actionview/test/template/streaming_render_test.rb
@@ -47,12 +47,12 @@ class FiberedTest < SetupFiberedBase
end
def test_render_file
- assert_equal "Hello world!", buffered_render(file: "test/hello_world")
+ assert_equal "Hello world!", assert_deprecated { buffered_render(file: "test/hello_world") }
end
def test_render_file_with_locals
locals = { secret: "in the sauce" }
- assert_equal "The secret is in the sauce\n", buffered_render(file: "test/render_file_with_locals", locals: locals)
+ assert_equal "The secret is in the sauce\n", assert_deprecated { buffered_render(file: "test/render_file_with_locals", locals: locals) }
end
def test_render_partial
diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb
index 71fb99115b..049e0bc129 100644
--- a/actionview/test/template/template_test.rb
+++ b/actionview/test/template/template_test.rb
@@ -91,10 +91,10 @@ class TestERBTemplate < ActiveSupport::TestCase
assert_equal "<%= hello %>", render
end
- def test_template_loses_its_source_after_rendering
+ def test_template_does_not_lose_its_source_after_rendering
@template = new_template
render
- assert_nil @template.source
+ assert_equal "<%= hello %>", @template.source
end
def test_template_does_not_lose_its_source_after_rendering_if_it_does_not_have_a_virtual_path
@@ -121,24 +121,10 @@ class TestERBTemplate < ActiveSupport::TestCase
assert_equal "hellopartialhello", render
end
- def test_refresh_with_templates
+ def test_refresh_is_deprecated
@template = new_template("Hello", virtual_path: "test/foo/bar", locals: [:key])
- assert_called_with(@context.lookup_context, :find_template, ["bar", %w(test/foo), false, [:key]], returns: "template") do
- assert_equal "template", @template.refresh(@context)
- end
- end
-
- def test_refresh_with_partials
- @template = new_template("Hello", virtual_path: "test/_foo", locals: [:key])
- assert_called_with(@context.lookup_context, :find_template, ["foo", %w(test), true, [:key]], returns: "partial") do
- assert_equal "partial", @template.refresh(@context)
- end
- end
-
- def test_refresh_raises_an_error_without_virtual_path
- @template = new_template("Hello", virtual_path: nil)
- assert_raise RuntimeError do
- @template.refresh(@context)
+ assert_deprecated do
+ assert_same @template, @template.refresh(@context)
end
end
diff --git a/actionview/test/template/testing/fixture_resolver_test.rb b/actionview/test/template/testing/fixture_resolver_test.rb
index afb6686dac..6d0317e0c9 100644
--- a/actionview/test/template/testing/fixture_resolver_test.rb
+++ b/actionview/test/template/testing/fixture_resolver_test.rb
@@ -17,4 +17,14 @@ class FixtureResolverTest < ActiveSupport::TestCase
assert_equal "arbitrary/path", templates.first.virtual_path
assert_nil templates.first.format
end
+
+ def test_should_match_templates_with_variants
+ resolver = ActionView::FixtureResolver.new("arbitrary/path.html+variant.erb" => "this text")
+ templates = resolver.find_all("path", "arbitrary", false, locale: [], formats: [:html], variants: [:variant], handlers: [:erb])
+ assert_equal 1, templates.size, "expected one template"
+ assert_equal "this text", templates.first.source
+ assert_equal "arbitrary/path", templates.first.virtual_path
+ assert_equal :html, templates.first.format
+ assert_equal "variant", templates.first.variant
+ end
end
diff --git a/actionview/test/ujs/public/test/call-remote.js b/actionview/test/ujs/public/test/call-remote.js
index 0f92007007..fb033491f9 100644
--- a/actionview/test/ujs/public/test/call-remote.js
+++ b/actionview/test/ujs/public/test/call-remote.js
@@ -53,7 +53,7 @@ asyncTest('form default method is GET', 1, function() {
})
})
-asyncTest('form url is picked up from "action"', 1, function() {
+asyncTest('form URL is picked up from "action"', 1, function() {
buildForm({ method: 'post' })
submit(function(e, data, status, xhr) {
@@ -61,7 +61,7 @@ asyncTest('form url is picked up from "action"', 1, function() {
})
})
-asyncTest('form url is read from "action" not "href"', 1, function() {
+asyncTest('form URL is read from "action" not "href"', 1, function() {
buildForm({ method: 'post', href: '/echo2' })
submit(function(e, data, status, xhr) {
@@ -69,7 +69,7 @@ asyncTest('form url is read from "action" not "href"', 1, function() {
})
})
-asyncTest('form url is read from submit button "formaction" if submit is triggered by that button', 1, function() {
+asyncTest('form URL is read from submit button "formaction" if submit is triggered by that button', 1, function() {
var submitButton = $('<input type="submit" formaction="/echo">')
buildForm({ method: 'post', href: '/echo2' })
diff --git a/actionview/test/ujs/public/test/data-remote.js b/actionview/test/ujs/public/test/data-remote.js
index 55d39b0a52..9e41067549 100644
--- a/actionview/test/ujs/public/test/data-remote.js
+++ b/actionview/test/ujs/public/test/data-remote.js
@@ -121,7 +121,7 @@ asyncTest('clicking on a link with both query string in href and data-params', 4
App.assertGetRequest(data)
equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value')
- equal(data.params.data3, 'value3', 'query string in url should be passed to server with right value')
+ equal(data.params.data3, 'value3', 'query string in URL should be passed to server with right value')
})
.bindNative('ajax:complete', function() { start() })
.triggerNative('click')
@@ -135,7 +135,7 @@ asyncTest('clicking on a link with both query string in href and data-params wit
App.assertPostRequest(data)
equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value')
- equal(data.params.data3, 'value3', 'query string in url should be passed to server with right value')
+ equal(data.params.data3, 'value3', 'query string in URL should be passed to server with right value')
})
.bindNative('ajax:complete', function() { start() })
.triggerNative('click')
diff --git a/actionview/test/ujs/public/test/settings.js b/actionview/test/ujs/public/test/settings.js
index 682d044403..ff2b057012 100644
--- a/actionview/test/ujs/public/test/settings.js
+++ b/actionview/test/ujs/public/test/settings.js
@@ -18,7 +18,7 @@ App.assertPostRequest = function(requestEnv) {
}
App.assertRequestPath = function(requestEnv, path) {
- equal(requestEnv['PATH_INFO'], path, 'request should be sent to right url')
+ equal(requestEnv['PATH_INFO'], path, 'request should be sent to right URL')
}
App.getVal = function(el) {
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
index e09ba4b055..c4e21b48ac 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Use individual execution counters when calculating retry delay.
+
+ *Patrik Bóna*
+
* Make job argument assertions with `Time`, `ActiveSupport::TimeWithZone`, and `DateTime` work by dropping microseconds. Microsecond precision is lost during serialization.
*Gannon McGibbon*
@@ -34,7 +38,7 @@
*Edouard Chin*
-* Restore HashWithIndifferentAccess support to ActiveJob::Arguments.deserialize.
+* Restore `HashWithIndifferentAccess` support to `ActiveJob::Arguments.deserialize`.
*Gannon McGibbon*
diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb
index 5609d13f5f..c5ee76a69e 100644
--- a/activejob/lib/active_job/enqueuing.rb
+++ b/activejob/lib/active_job/enqueuing.rb
@@ -67,7 +67,7 @@ module ActiveJob
false
else
ActiveSupport::Deprecation.warn(
- "Rails 6.0 will return false when the enqueuing is aborted. Make sure your code doesn't depend on it" \
+ "Rails 6.1 will return false when the enqueuing is aborted. Make sure your code doesn't depend on it" \
" returning the instance of the job and set `config.active_job.return_false_on_aborted_enqueue = true`" \
" to remove the deprecations."
)
diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb
index 9e00942a1c..35c1476368 100644
--- a/activejob/lib/active_job/exceptions.rb
+++ b/activejob/lib/active_job/exceptions.rb
@@ -54,7 +54,7 @@ module ActiveJob
self.exception_executions[exceptions.to_s] = (exception_executions[exceptions.to_s] || 0) + 1
if exception_executions[exceptions.to_s] < attempts
- retry_job wait: determine_delay(wait), queue: queue, priority: priority, error: error
+ retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: exception_executions[exceptions.to_s]), queue: queue, priority: priority, error: error
else
if block_given?
instrument :retry_stopped, error: error do
@@ -123,7 +123,7 @@ module ActiveJob
end
private
- def determine_delay(seconds_or_duration_or_algorithm)
+ def determine_delay(seconds_or_duration_or_algorithm:, executions:)
case seconds_or_duration_or_algorithm
when :exponentially_longer
(executions**4) + 2
diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb
index e5e2b086bc..463020a332 100644
--- a/activejob/lib/active_job/test_helper.rb
+++ b/activejob/lib/active_job/test_helper.rb
@@ -353,7 +353,7 @@ module ActiveJob
#
#
# The +args+ argument also accepts a proc which will get passed the actual
- # job's arguments. Your proc needs to returns a boolean value determining if
+ # job's arguments. Your proc needs to return a boolean value determining if
# the job's arguments matches your expectation. This is useful to check only
# for a subset of arguments.
#
@@ -426,7 +426,7 @@ module ActiveJob
# end
#
# The +args+ argument also accepts a proc which will get passed the actual
- # job's arguments. Your proc needs to returns a boolean value determining if
+ # job's arguments. Your proc needs to return a boolean value determining if
# the job's arguments matches your expectation. This is useful to check only
# for a subset of arguments.
#
diff --git a/activejob/test/cases/exceptions_test.rb b/activejob/test/cases/exceptions_test.rb
index c88162bf58..840f4d40b5 100644
--- a/activejob/test/cases/exceptions_test.rb
+++ b/activejob/test/cases/exceptions_test.rb
@@ -140,6 +140,26 @@ class ExceptionsTest < ActiveSupport::TestCase
], JobBuffer.values
end
+ test "use individual execution timers when calculating retry delay" do
+ travel_to Time.now
+
+ exceptions_to_raise = %w(ExponentialWaitTenAttemptsError CustomWaitTenAttemptsError ExponentialWaitTenAttemptsError CustomWaitTenAttemptsError)
+
+ RetryJob.perform_later exceptions_to_raise, 5, :log_scheduled_at
+
+ assert_equal [
+ "Raised ExponentialWaitTenAttemptsError for the 1st time",
+ "Next execution scheduled at #{(Time.now + 3.seconds).to_f}",
+ "Raised CustomWaitTenAttemptsError for the 2nd time",
+ "Next execution scheduled at #{(Time.now + 2.seconds).to_f}",
+ "Raised ExponentialWaitTenAttemptsError for the 3rd time",
+ "Next execution scheduled at #{(Time.now + 18.seconds).to_f}",
+ "Raised CustomWaitTenAttemptsError for the 4th time",
+ "Next execution scheduled at #{(Time.now + 4.seconds).to_f}",
+ "Successfully completed job"
+ ], JobBuffer.values
+ end
+
test "successfully retry job throwing one of two retryable exceptions" do
RetryJob.perform_later "SecondRetryableErrorOfTwo", 3
diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb
index 694232d7ef..d400210fef 100644
--- a/activejob/test/helper.rb
+++ b/activejob/test/helper.rb
@@ -16,3 +16,5 @@ else
end
require "active_support/testing/autorun"
+
+require_relative "../../tools/test_common"
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 56fbfacbd7..ad87abfa3a 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,3 +1,9 @@
+* Type cast falsy boolean symbols on boolean attribute as false.
+
+ Fixes #35676.
+
+ *Ryuta Kamizono*
+
* Change how validation error translation strings are fetched: The new behavior
will first try the more specific keys, including doing locale fallback, then try
the less specific ones.
@@ -35,12 +41,12 @@
Before:
Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
- => #<Day id: nil, day: "0001-01-03", created_at: nil, updated_at: nil>
+ # => #<Day id: nil, day: "0001-01-03", created_at: nil, updated_at: nil>
After:
Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
- => #<Day id: nil, day: "0001-01-01", created_at: nil, updated_at: nil>
+ # => #<Day id: nil, day: "0001-01-01", created_at: nil, updated_at: nil>
Fixes #28521.
@@ -130,7 +136,7 @@
*Unathi Chonco*
-* Add `config.active_model.i18n_full_message` in order to control whether
+* Add `config.active_model.i18n_customize_full_message` in order to control whether
the `full_message` error format can be overridden at the attribute or model
level in the locale files. This is `false` by default.
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 5c4670f393..0075cd3c01 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -286,12 +286,12 @@ module ActiveModel
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
- generate_method = "define_method_#{matcher.method_missing_target}"
+ generate_method = "define_method_#{matcher.target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
- define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s
+ define_proxy_call true, generated_attribute_methods, method_name, matcher.target, attr_name.to_s
end
end
end
@@ -355,14 +355,14 @@ module ActiveModel
# Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
# will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
- matchers.map { |method| method.match(method_name) }.compact
+ matchers.map { |matcher| matcher.match(method_name) }.compact
end
end
# Define a method `name` in `mod` that dispatches to `send`
# using the given `extra` args. This falls back on `define_method`
# and `send` if the given names cannot be compiled.
- def define_proxy_call(include_private, mod, name, send, *extra)
+ def define_proxy_call(include_private, mod, name, target, *extra)
defn = if NAME_COMPILABLE_REGEXP.match?(name)
"def #{name}(*args)"
else
@@ -371,34 +371,34 @@ module ActiveModel
extra = (extra.map!(&:inspect) << "*args").join(", ")
- target = if CALL_COMPILABLE_REGEXP.match?(send)
- "#{"self." unless include_private}#{send}(#{extra})"
+ body = if CALL_COMPILABLE_REGEXP.match?(target)
+ "#{"self." unless include_private}#{target}(#{extra})"
else
- "send(:'#{send}', #{extra})"
+ "send(:'#{target}', #{extra})"
end
mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
#{defn}
- #{target}
+ #{body}
end
RUBY
end
class AttributeMethodMatcher #:nodoc:
- attr_reader :prefix, :suffix, :method_missing_target
+ attr_reader :prefix, :suffix, :target
- AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
+ AttributeMethodMatch = Struct.new(:target, :attr_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
- @method_missing_target = "#{@prefix}attribute#{@suffix}"
+ @target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
- AttributeMethodMatch.new(method_missing_target, $1, method_name)
+ AttributeMethodMatch.new(target, $1)
end
end
diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb
index 6abf37bd44..d8cd48a53b 100644
--- a/activemodel/lib/active_model/attribute_mutation_tracker.rb
+++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb
@@ -1,14 +1,15 @@
# frozen_string_literal: true
require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/object/duplicable"
module ActiveModel
class AttributeMutationTracker # :nodoc:
OPTION_NOT_GIVEN = Object.new
- def initialize(attributes)
+ def initialize(attributes, forced_changes = Set.new)
@attributes = attributes
- @forced_changes = Set.new
+ @forced_changes = forced_changes
end
def changed_attribute_names
@@ -18,24 +19,22 @@ module ActiveModel
def changed_values
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
if changed?(attr_name)
- result[attr_name] = attributes[attr_name].original_value
+ result[attr_name] = original_value(attr_name)
end
end
end
def changes
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
- change = change_to_attribute(attr_name)
- if change
+ if change = change_to_attribute(attr_name)
result.merge!(attr_name => change)
end
end
end
def change_to_attribute(attr_name)
- attr_name = attr_name.to_s
if changed?(attr_name)
- [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
+ [original_value(attr_name), fetch_value(attr_name)]
end
end
@@ -44,29 +43,26 @@ module ActiveModel
end
def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
- attr_name = attr_name.to_s
- forced_changes.include?(attr_name) ||
- attributes[attr_name].changed? &&
- (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
- (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
+ attribute_changed?(attr_name) &&
+ (OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
+ (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
end
def changed_in_place?(attr_name)
- attributes[attr_name.to_s].changed_in_place?
+ attributes[attr_name].changed_in_place?
end
def forget_change(attr_name)
- attr_name = attr_name.to_s
attributes[attr_name] = attributes[attr_name].forgetting_assignment
forced_changes.delete(attr_name)
end
def original_value(attr_name)
- attributes[attr_name.to_s].original_value
+ attributes[attr_name].original_value
end
def force_change(attr_name)
- forced_changes << attr_name.to_s
+ forced_changes << attr_name
end
private
@@ -75,45 +71,108 @@ module ActiveModel
def attr_names
attributes.keys
end
+
+ def attribute_changed?(attr_name)
+ forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
+ end
+
+ def fetch_value(attr_name)
+ attributes.fetch_value(attr_name)
+ end
+ end
+
+ class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
+ def initialize(attributes, forced_changes = {})
+ super
+ @finalized_changes = nil
+ end
+
+ def changed_in_place?(attr_name)
+ false
+ end
+
+ def change_to_attribute(attr_name)
+ if finalized_changes&.include?(attr_name)
+ finalized_changes[attr_name].dup
+ else
+ super
+ end
+ end
+
+ def forget_change(attr_name)
+ forced_changes.delete(attr_name)
+ end
+
+ def original_value(attr_name)
+ if changed?(attr_name)
+ forced_changes[attr_name]
+ else
+ fetch_value(attr_name)
+ end
+ end
+
+ def force_change(attr_name)
+ forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
+ end
+
+ def finalize_changes
+ @finalized_changes = changes
+ end
+
+ private
+ attr_reader :finalized_changes
+
+ def attr_names
+ forced_changes.keys
+ end
+
+ def attribute_changed?(attr_name)
+ forced_changes.include?(attr_name)
+ end
+
+ def fetch_value(attr_name)
+ attributes.send(:_read_attribute, attr_name)
+ end
+
+ def clone_value(attr_name)
+ value = fetch_value(attr_name)
+ value.duplicable? ? value.clone : value
+ rescue TypeError, NoMethodError
+ value
+ end
end
class NullMutationTracker # :nodoc:
include Singleton
- def changed_attribute_names(*)
+ def changed_attribute_names
[]
end
- def changed_values(*)
+ def changed_values
{}
end
- def changes(*)
+ def changes
{}
end
def change_to_attribute(attr_name)
end
- def any_changes?(*)
+ def any_changes?
false
end
- def changed?(*)
+ def changed?(attr_name, **)
false
end
- def changed_in_place?(*)
+ def changed_in_place?(attr_name)
false
end
- def forget_change(*)
- end
-
- def original_value(*)
- end
-
- def force_change(*)
+ def original_value(attr_name)
end
end
end
diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb
index c3a446098c..b7b2f35bcc 100644
--- a/activemodel/lib/active_model/attributes.rb
+++ b/activemodel/lib/active_model/attributes.rb
@@ -91,7 +91,7 @@ module ActiveModel
@attributes.fetch_value(name)
end
- # Handle *= for method_missing.
+ # Dispatch target for <tt>*=</tt> attribute methods.
def attribute=(attribute_name, value)
write_attribute(attribute_name, value)
end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 0d9e761b1e..35a587658c 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/hash_with_indifferent_access"
-require "active_support/core_ext/object/duplicable"
require "active_model/attribute_mutation_tracker"
module ActiveModel
@@ -122,9 +120,6 @@ module ActiveModel
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
- OPTION_NOT_GIVEN = Object.new # :nodoc:
- private_constant :OPTION_NOT_GIVEN
-
included do
attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
attribute_method_suffix "_previously_changed?", "_previous_change"
@@ -145,10 +140,9 @@ module ActiveModel
# +mutations_from_database+ to +mutations_before_last_save+ respectively.
def changes_applied
unless defined?(@attributes)
- @previously_changed = changes
+ mutations_from_database.finalize_changes
end
@mutations_before_last_save = mutations_from_database
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
forget_attribute_assignments
@mutations_from_database = nil
end
@@ -159,7 +153,7 @@ module ActiveModel
# person.name = 'bob'
# person.changed? # => true
def changed?
- changed_attributes.present?
+ mutations_from_database.any_changes?
end
# Returns an array with the name of the attributes with unsaved changes.
@@ -168,42 +162,37 @@ module ActiveModel
# person.name = 'bob'
# person.changed # => ["name"]
def changed
- changed_attributes.keys
+ mutations_from_database.changed_attribute_names
end
- # Handles <tt>*_changed?</tt> for +method_missing+.
- def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
- !!changes_include?(attr) &&
- (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
- (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
+ # Dispatch target for <tt>*_changed?</tt> attribute methods.
+ def attribute_changed?(attr_name, **options) # :nodoc:
+ mutations_from_database.changed?(attr_name.to_s, options)
end
- # Handles <tt>*_was</tt> for +method_missing+.
- def attribute_was(attr) # :nodoc:
- attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
+ # Dispatch target for <tt>*_was</tt> attribute methods.
+ def attribute_was(attr_name) # :nodoc:
+ mutations_from_database.original_value(attr_name.to_s)
end
- # Handles <tt>*_previously_changed?</tt> for +method_missing+.
- def attribute_previously_changed?(attr) #:nodoc:
- previous_changes_include?(attr)
+ # Dispatch target for <tt>*_previously_changed?</tt> attribute methods.
+ def attribute_previously_changed?(attr_name) # :nodoc:
+ mutations_before_last_save.changed?(attr_name.to_s)
end
# Restore all previous data of the provided attributes.
- def restore_attributes(attributes = changed)
- attributes.each { |attr| restore_attribute! attr }
+ def restore_attributes(attr_names = changed)
+ attr_names.each { |attr_name| restore_attribute!(attr_name) }
end
# Clears all dirty data: current changes and previous changes.
def clear_changes_information
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@mutations_before_last_save = nil
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
forget_attribute_assignments
@mutations_from_database = nil
end
def clear_attribute_changes(attr_names)
- attributes_changed_by_setter.except!(*attr_names)
attr_names.each do |attr_name|
clear_attribute_change(attr_name)
end
@@ -216,13 +205,7 @@ module ActiveModel
# person.name = 'robert'
# person.changed_attributes # => {"name" => "bob"}
def changed_attributes
- # This should only be set by methods which will call changed_attributes
- # multiple times when it is known that the computed value cannot change.
- if defined?(@cached_changed_attributes)
- @cached_changed_attributes
- else
- attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
- end
+ mutations_from_database.changed_values
end
# Returns a hash of changed attributes indicating their original
@@ -232,9 +215,7 @@ module ActiveModel
# person.name = 'bob'
# person.changes # => { "name" => ["bill", "bob"] }
def changes
- cache_changed_attributes do
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
- end
+ mutations_from_database.changes
end
# Returns a hash of attributes that were changed before the model was saved.
@@ -244,27 +225,23 @@ module ActiveModel
# person.save
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
- @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
- @previously_changed.merge(mutations_before_last_save.changes)
+ mutations_before_last_save.changes
end
def attribute_changed_in_place?(attr_name) # :nodoc:
- mutations_from_database.changed_in_place?(attr_name)
+ mutations_from_database.changed_in_place?(attr_name.to_s)
end
private
def clear_attribute_change(attr_name)
- mutations_from_database.forget_change(attr_name)
+ mutations_from_database.forget_change(attr_name.to_s)
end
def mutations_from_database
- unless defined?(@mutations_from_database)
- @mutations_from_database = nil
- end
@mutations_from_database ||= if defined?(@attributes)
ActiveModel::AttributeMutationTracker.new(@attributes)
else
- NullMutationTracker.instance
+ ActiveModel::ForcedMutationTracker.new(self)
end
end
@@ -276,68 +253,28 @@ module ActiveModel
@mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
end
- def cache_changed_attributes
- @cached_changed_attributes = changed_attributes
- yield
- ensure
- clear_changed_attributes_cache
- end
-
- def clear_changed_attributes_cache
- remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
- end
-
- # Returns +true+ if attr_name is changed, +false+ otherwise.
- def changes_include?(attr_name)
- attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
- end
- alias attribute_changed_by_setter? changes_include?
-
- # Returns +true+ if attr_name were changed before the model was saved,
- # +false+ otherwise.
- def previous_changes_include?(attr_name)
- previous_changes.include?(attr_name)
- end
-
- # Handles <tt>*_change</tt> for +method_missing+.
- def attribute_change(attr)
- [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
+ # Dispatch target for <tt>*_change</tt> attribute methods.
+ def attribute_change(attr_name)
+ mutations_from_database.change_to_attribute(attr_name.to_s)
end
- # Handles <tt>*_previous_change</tt> for +method_missing+.
- def attribute_previous_change(attr)
- previous_changes[attr]
+ # Dispatch target for <tt>*_previous_change</tt> attribute methods.
+ def attribute_previous_change(attr_name)
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
end
- # Handles <tt>*_will_change!</tt> for +method_missing+.
- def attribute_will_change!(attr)
- unless attribute_changed?(attr)
- begin
- value = _read_attribute(attr)
- value = value.duplicable? ? value.clone : value
- rescue TypeError, NoMethodError
- end
-
- set_attribute_was(attr, value)
- end
- mutations_from_database.force_change(attr)
+ # Dispatch target for <tt>*_will_change!</tt> attribute methods.
+ def attribute_will_change!(attr_name)
+ mutations_from_database.force_change(attr_name.to_s)
end
- # Handles <tt>restore_*!</tt> for +method_missing+.
- def restore_attribute!(attr)
- if attribute_changed?(attr)
- __send__("#{attr}=", changed_attributes[attr])
- clear_attribute_changes([attr])
+ # Dispatch target for <tt>restore_*!</tt> attribute methods.
+ def restore_attribute!(attr_name)
+ attr_name = attr_name.to_s
+ if attribute_changed?(attr_name)
+ __send__("#{attr_name}=", attribute_was(attr_name))
+ clear_attribute_change(attr_name)
end
end
-
- def attributes_changed_by_setter
- @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
- end
-
- # Force an attribute to have a particular "before" value
- def set_attribute_was(attr, old_value)
- attributes_changed_by_setter[attr] = old_value
- end
end
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index d7e682d406..3a692a3e64 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -63,9 +63,9 @@ module ActiveModel
MESSAGE_OPTIONS = [:message]
class << self
- attr_accessor :i18n_full_message # :nodoc:
+ attr_accessor :i18n_customize_full_message # :nodoc:
end
- self.i18n_full_message = false
+ self.i18n_customize_full_message = false
attr_reader :messages, :details
@@ -413,7 +413,7 @@ module ActiveModel
return message if attribute == :base
attribute = attribute.to_s
- if self.class.i18n_full_message && @base.class.respond_to?(:i18n_scope)
+ if self.class.i18n_customize_full_message && @base.class.respond_to?(:i18n_scope)
attribute = attribute.remove(/\[\d\]/)
parts = attribute.split(".")
attribute_name = parts.pop
diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb
index 0ed70bd473..eb7901c7e9 100644
--- a/activemodel/lib/active_model/railtie.rb
+++ b/activemodel/lib/active_model/railtie.rb
@@ -13,8 +13,8 @@ module ActiveModel
ActiveModel::SecurePassword.min_cost = Rails.env.test?
end
- initializer "active_model.i18n_full_message" do
- ActiveModel::Errors.i18n_full_message = config.active_model.delete(:i18n_full_message) || false
+ initializer "active_model.i18n_customize_full_message" do
+ ActiveModel::Errors.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
end
end
end
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 51d54f34f3..5f409326bd 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -69,6 +69,27 @@ module ActiveModel
raise
end
+ include InstanceMethodsOnActivation.new(attribute)
+
+ if validations
+ include ActiveModel::Validations
+
+ # This ensures the model has a password by checking whether the password_digest
+ # is present, so that this works with both new and existing records. However,
+ # when there is an error, the message is added to the password attribute instead
+ # so that the error message will make sense to the end-user.
+ validate do |record|
+ record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
+ end
+
+ validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
+ validates_confirmation_of attribute, allow_blank: true
+ end
+ end
+ end
+
+ class InstanceMethodsOnActivation < Module
+ def initialize(attribute)
attr_reader attribute
define_method("#{attribute}=") do |unencrypted_password|
@@ -101,21 +122,6 @@ module ActiveModel
end
alias_method :authenticate, :authenticate_password if attribute == :password
-
- if validations
- include ActiveModel::Validations
-
- # This ensures the model has a password by checking whether the password_digest
- # is present, so that this works with both new and existing records. However,
- # when there is an error, the message is added to the password attribute instead
- # so that the error message will make sense to the end-user.
- validate do |record|
- record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
- end
-
- validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
- validates_confirmation_of attribute, allow_blank: true
- end
end
end
end
diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb
index f6c6efbc87..e64d2c793c 100644
--- a/activemodel/lib/active_model/type/boolean.rb
+++ b/activemodel/lib/active_model/type/boolean.rb
@@ -14,7 +14,16 @@ module ActiveModel
# - Empty strings are coerced to +nil+
# - All other values will be coerced to +true+
class Boolean < Value
- FALSE_VALUES = [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"].to_set
+ FALSE_VALUES = [
+ false, 0,
+ "0", :"0",
+ "f", :f,
+ "F", :F,
+ "false", :false,
+ "FALSE", :FALSE,
+ "off", :off,
+ "OFF", :OFF,
+ ].to_set.freeze
def type # :nodoc:
:boolean
diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb
index ebb6cc542d..4e228032c3 100644
--- a/activemodel/test/cases/attribute_methods_test.rb
+++ b/activemodel/test/cases/attribute_methods_test.rb
@@ -264,6 +264,5 @@ class AttributeMethodsTest < ActiveModel::TestCase
assert_equal "foo", match.attr_name
assert_equal "attribute_test", match.target
- assert_equal "foo_test", match.method_name
end
end
diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb
index 138b1d1bb9..a4cb472ffc 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -25,3 +25,5 @@ class ActiveModel::TestCase < ActiveSupport::TestCase
skip message if defined?(JRUBY_VERSION)
end
end
+
+require_relative "../../../tools/test_common"
diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb
index ab60285e2a..95ee7cace3 100644
--- a/activemodel/test/cases/railtie_test.rb
+++ b/activemodel/test/cases/railtie_test.rb
@@ -32,23 +32,23 @@ class RailtieTest < ActiveModel::TestCase
assert_equal true, ActiveModel::SecurePassword.min_cost
end
- test "i18n full message defaults to false" do
+ test "i18n customize full message defaults to false" do
@app.initialize!
- assert_equal false, ActiveModel::Errors.i18n_full_message
+ assert_equal false, ActiveModel::Errors.i18n_customize_full_message
end
- test "i18n full message can be disabled" do
- @app.config.active_model.i18n_full_message = false
+ test "i18n customize full message can be disabled" do
+ @app.config.active_model.i18n_customize_full_message = false
@app.initialize!
- assert_equal false, ActiveModel::Errors.i18n_full_message
+ assert_equal false, ActiveModel::Errors.i18n_customize_full_message
end
- test "i18n full message can be enabled" do
- @app.config.active_model.i18n_full_message = true
+ test "i18n customize full message can be enabled" do
+ @app.config.active_model.i18n_customize_full_message = true
@app.initialize!
- assert_equal true, ActiveModel::Errors.i18n_full_message
+ assert_equal true, ActiveModel::Errors.i18n_customize_full_message
end
end
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index bbf443290b..0aca714bd2 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -184,6 +184,20 @@ class SecurePasswordTest < ActiveModel::TestCase
assert_nil @existing_user.password_digest
end
+ test "override secure password attribute" do
+ assert_nil @user.password_called
+
+ @user.password = "secret"
+
+ assert_equal "secret", @user.password
+ assert_equal 1, @user.password_called
+
+ @user.password = "terces"
+
+ assert_equal "terces", @user.password
+ assert_equal 2, @user.password_called
+ end
+
test "authenticate" do
@user.password = "secret"
@user.recovery_password = "42password"
diff --git a/activemodel/test/cases/type/boolean_test.rb b/activemodel/test/cases/type/boolean_test.rb
index 2de0f53640..7f8490b2fe 100644
--- a/activemodel/test/cases/type/boolean_test.rb
+++ b/activemodel/test/cases/type/boolean_test.rb
@@ -23,6 +23,13 @@ module ActiveModel
assert type.cast("\u3000\r\n")
assert type.cast("\u0000")
assert type.cast("SOMETHING RANDOM")
+ assert type.cast(:"1")
+ assert type.cast(:t)
+ assert type.cast(:T)
+ assert type.cast(:true)
+ assert type.cast(:TRUE)
+ assert type.cast(:on)
+ assert type.cast(:ON)
# explicitly check for false vs nil
assert_equal false, type.cast(false)
@@ -34,6 +41,13 @@ module ActiveModel
assert_equal false, type.cast("FALSE")
assert_equal false, type.cast("off")
assert_equal false, type.cast("OFF")
+ assert_equal false, type.cast(:"0")
+ assert_equal false, type.cast(:f)
+ assert_equal false, type.cast(:F)
+ assert_equal false, type.cast(:false)
+ assert_equal false, type.cast(:FALSE)
+ assert_equal false, type.cast(:off)
+ assert_equal false, type.cast(:OFF)
end
end
end
diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb
index 7662f996ae..72baf6e7a7 100644
--- a/activemodel/test/cases/validations/acceptance_validation_test.rb
+++ b/activemodel/test/cases/validations/acceptance_validation_test.rb
@@ -7,21 +7,23 @@ require "models/reply"
require "models/person"
class AcceptanceValidationTest < ActiveModel::TestCase
- def teardown
- Topic.clear_validators!
+ teardown do
+ self.class.send(:remove_const, :TestClass)
end
def test_terms_of_service_agreement_no_acceptance
- Topic.validates_acceptance_of(:terms_of_service)
+ klass = define_test_class(Topic)
+ klass.validates_acceptance_of(:terms_of_service)
- t = Topic.new("title" => "We should not be confirmed")
+ t = klass.new("title" => "We should not be confirmed")
assert_predicate t, :valid?
end
def test_terms_of_service_agreement
- Topic.validates_acceptance_of(:terms_of_service)
+ klass = define_test_class(Topic)
+ klass.validates_acceptance_of(:terms_of_service)
- t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "")
+ t = klass.new("title" => "We should be confirmed", "terms_of_service" => "")
assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
@@ -30,9 +32,10 @@ class AcceptanceValidationTest < ActiveModel::TestCase
end
def test_eula
- Topic.validates_acceptance_of(:eula, message: "must be abided")
+ klass = define_test_class(Topic)
+ klass.validates_acceptance_of(:eula, message: "must be abided")
- t = Topic.new("title" => "We should be confirmed", "eula" => "")
+ t = klass.new("title" => "We should be confirmed", "eula" => "")
assert_predicate t, :invalid?
assert_equal ["must be abided"], t.errors[:eula]
@@ -41,9 +44,10 @@ class AcceptanceValidationTest < ActiveModel::TestCase
end
def test_terms_of_service_agreement_with_accept_value
- Topic.validates_acceptance_of(:terms_of_service, accept: "I agree.")
+ klass = define_test_class(Topic)
+ klass.validates_acceptance_of(:terms_of_service, accept: "I agree.")
- t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "")
+ t = klass.new("title" => "We should be confirmed", "terms_of_service" => "")
assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
@@ -52,9 +56,10 @@ class AcceptanceValidationTest < ActiveModel::TestCase
end
def test_terms_of_service_agreement_with_multiple_accept_values
- Topic.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."])
+ klass = define_test_class(Topic)
+ klass.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."])
- t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "")
+ t = klass.new("title" => "We should be confirmed", "terms_of_service" => "")
assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
@@ -66,9 +71,10 @@ class AcceptanceValidationTest < ActiveModel::TestCase
end
def test_validates_acceptance_of_for_ruby_class
- Person.validates_acceptance_of :karma
+ klass = define_test_class(Person)
+ klass.validates_acceptance_of :karma
- p = Person.new
+ p = klass.new
p.karma = ""
assert_predicate p, :invalid?
@@ -76,13 +82,21 @@ class AcceptanceValidationTest < ActiveModel::TestCase
p.karma = "1"
assert_predicate p, :valid?
- ensure
- Person.clear_validators!
end
def test_validates_acceptance_of_true
- Topic.validates_acceptance_of(:terms_of_service)
+ klass = define_test_class(Topic)
+ klass.validates_acceptance_of(:terms_of_service)
- assert_predicate Topic.new(terms_of_service: true), :valid?
+ assert_predicate klass.new(terms_of_service: true), :valid?
end
+
+ private
+
+ # Acceptance validator includes anonymous module into class, which cannot
+ # be cleared, so to avoid multiple inclusions we use a named subclass which
+ # we can remove in teardown.
+ def define_test_class(parent)
+ self.class.const_set(:TestClass, Class.new(parent))
+ end
end
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index ccb565c5bd..35bb918f26 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -6,36 +6,38 @@ require "models/person"
class I18nValidationTest < ActiveModel::TestCase
def setup
Person.clear_validators!
- @person = Person.new
+ @person = person_class.new
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
I18n.load_path.clear
I18n.backend = I18n::Backend::Simple.new
I18n.backend.store_translations("en", errors: { messages: { custom: nil } })
- @original_i18n_full_message = ActiveModel::Errors.i18n_full_message
- ActiveModel::Errors.i18n_full_message = true
+ @original_i18n_customize_full_message = ActiveModel::Errors.i18n_customize_full_message
+ ActiveModel::Errors.i18n_customize_full_message = true
end
def teardown
- Person.clear_validators!
+ person_class.clear_validators!
+ self.class.send(:remove_const, :Person)
+ @person_stub = nil
I18n.load_path.replace @old_load_path
I18n.backend = @old_backend
I18n.backend.reload!
- ActiveModel::Errors.i18n_full_message = @original_i18n_full_message
+ ActiveModel::Errors.i18n_customize_full_message = @original_i18n_customize_full_message
end
def test_full_message_encoding
I18n.backend.store_translations("en", errors: {
messages: { too_short: "猫舌" } })
- Person.validates_length_of :title, within: 3..5
+ person_class.validates_length_of :title, within: 3..5
@person.valid?
assert_equal ["Title 猫舌"], @person.errors.full_messages
end
def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
@person.errors.add(:name, "not found")
- assert_called_with(Person, :human_attribute_name, ["name", default: "Name"], returns: "Person's name") do
+ assert_called_with(person_class, :human_attribute_name, ["name", default: "Name"], returns: "Person's name") do
assert_equal ["Person's name not found"], @person.errors.full_messages
end
end
@@ -47,113 +49,113 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_doesnt_use_attribute_format_without_config
- ActiveModel::Errors.i18n_full_message = false
+ ActiveModel::Errors.i18n_customize_full_message = false
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
- person = Person.new
+ person = person_class.new
assert_equal "Name cannot be blank", person.errors.full_message(:name, "cannot be blank")
assert_equal "Name test cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
end
def test_errors_full_messages_uses_attribute_format
- ActiveModel::Errors.i18n_full_message = true
+ ActiveModel::Errors.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
- person = Person.new
+ person = person_class.new
assert_equal "cannot be blank", person.errors.full_message(:name, "cannot be blank")
assert_equal "Name test cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
end
def test_errors_full_messages_uses_model_format
- ActiveModel::Errors.i18n_full_message = true
+ ActiveModel::Errors.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { format: "%{message}" } } } })
- person = Person.new
+ person = person_class.new
assert_equal "cannot be blank", person.errors.full_message(:name, "cannot be blank")
assert_equal "cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
end
def test_errors_full_messages_uses_deeply_nested_model_attributes_format
- ActiveModel::Errors.i18n_full_message = true
+ ActiveModel::Errors.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
- person = Person.new
+ person = person_class.new
assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.street', "cannot be blank")
assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank")
end
def test_errors_full_messages_uses_deeply_nested_model_model_format
- ActiveModel::Errors.i18n_full_message = true
+ ActiveModel::Errors.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
- person = Person.new
+ person = person_class.new
assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.street', "cannot be blank")
assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank")
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_attributes_format
- ActiveModel::Errors.i18n_full_message = true
+ ActiveModel::Errors.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
- person = Person.new
+ person = person_class.new
assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_model_format
- ActiveModel::Errors.i18n_full_message = true
+ ActiveModel::Errors.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
- person = Person.new
+ person = person_class.new
assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_i18n_attribute_name
- ActiveModel::Errors.i18n_full_message = true
+ ActiveModel::Errors.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
attributes: { 'person/contacts/addresses': { country: "Country" } }
})
- person = Person.new
+ person = person_class.new
assert_equal "Contacts/addresses street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_without_i18n_config
- ActiveModel::Errors.i18n_full_message = false
+ ActiveModel::Errors.i18n_customize_full_message = false
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
- person = Person.new
+ person = person_class.new
assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
assert_equal "Contacts[0]/addresses[0] country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
end
def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
- ActiveModel::Errors.i18n_full_message = false
+ ActiveModel::Errors.i18n_customize_full_message = false
I18n.backend.store_translations("en", activemodel: {
attributes: { 'person/contacts[0]/addresses[0]': { country: "Country" } }
})
- person = Person.new
+ person = person_class.new
assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
end
@@ -174,7 +176,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_confirmation_of on generated message #{name}" do
- Person.validates_confirmation_of :title, validation_options
+ person_class.validates_confirmation_of :title, validation_options
@person.title_confirmation = "foo"
call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: "Title")]
assert_called_with(@person.errors, :generate_message, call) do
@@ -185,7 +187,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_acceptance_of on generated message #{name}" do
- Person.validates_acceptance_of :title, validation_options.merge(allow_nil: false)
+ person_class.validates_acceptance_of :title, validation_options.merge(allow_nil: false)
call = [:title, :accepted, generate_message_options]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@@ -195,7 +197,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_presence_of on generated message #{name}" do
- Person.validates_presence_of :title, validation_options
+ person_class.validates_presence_of :title, validation_options
call = [:title, :blank, generate_message_options]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@@ -205,7 +207,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_length_of for :within on generated message when too short #{name}" do
- Person.validates_length_of :title, validation_options.merge(within: 3..5)
+ person_class.validates_length_of :title, validation_options.merge(within: 3..5)
call = [:title, :too_short, generate_message_options.merge(count: 3)]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@@ -215,7 +217,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_length_of for :too_long generated message #{name}" do
- Person.validates_length_of :title, validation_options.merge(within: 3..5)
+ person_class.validates_length_of :title, validation_options.merge(within: 3..5)
@person.title = "this title is too long"
call = [:title, :too_long, generate_message_options.merge(count: 5)]
assert_called_with(@person.errors, :generate_message, call) do
@@ -226,7 +228,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_length_of for :is on generated message #{name}" do
- Person.validates_length_of :title, validation_options.merge(is: 5)
+ person_class.validates_length_of :title, validation_options.merge(is: 5)
call = [:title, :wrong_length, generate_message_options.merge(count: 5)]
assert_called_with(@person.errors, :generate_message, call) do
@person.valid?
@@ -236,7 +238,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_format_of on generated message #{name}" do
- Person.validates_format_of :title, validation_options.merge(with: /\A[1-9][0-9]*\z/)
+ person_class.validates_format_of :title, validation_options.merge(with: /\A[1-9][0-9]*\z/)
@person.title = "72x"
call = [:title, :invalid, generate_message_options.merge(value: "72x")]
assert_called_with(@person.errors, :generate_message, call) do
@@ -247,7 +249,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_inclusion_of on generated message #{name}" do
- Person.validates_inclusion_of :title, validation_options.merge(in: %w(a b c))
+ person_class.validates_inclusion_of :title, validation_options.merge(in: %w(a b c))
@person.title = "z"
call = [:title, :inclusion, generate_message_options.merge(value: "z")]
assert_called_with(@person.errors, :generate_message, call) do
@@ -258,7 +260,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_inclusion_of using :within on generated message #{name}" do
- Person.validates_inclusion_of :title, validation_options.merge(within: %w(a b c))
+ person_class.validates_inclusion_of :title, validation_options.merge(within: %w(a b c))
@person.title = "z"
call = [:title, :inclusion, generate_message_options.merge(value: "z")]
assert_called_with(@person.errors, :generate_message, call) do
@@ -269,7 +271,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_exclusion_of generated message #{name}" do
- Person.validates_exclusion_of :title, validation_options.merge(in: %w(a b c))
+ person_class.validates_exclusion_of :title, validation_options.merge(in: %w(a b c))
@person.title = "a"
call = [:title, :exclusion, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
@@ -280,7 +282,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_exclusion_of using :within generated message #{name}" do
- Person.validates_exclusion_of :title, validation_options.merge(within: %w(a b c))
+ person_class.validates_exclusion_of :title, validation_options.merge(within: %w(a b c))
@person.title = "a"
call = [:title, :exclusion, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
@@ -291,7 +293,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_numericality_of generated message #{name}" do
- Person.validates_numericality_of :title, validation_options
+ person_class.validates_numericality_of :title, validation_options
@person.title = "a"
call = [:title, :not_a_number, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
@@ -302,7 +304,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_numericality_of for :only_integer on generated message #{name}" do
- Person.validates_numericality_of :title, validation_options.merge(only_integer: true)
+ person_class.validates_numericality_of :title, validation_options.merge(only_integer: true)
@person.title = "0.0"
call = [:title, :not_an_integer, generate_message_options.merge(value: "0.0")]
assert_called_with(@person.errors, :generate_message, call) do
@@ -313,7 +315,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_numericality_of for :odd on generated message #{name}" do
- Person.validates_numericality_of :title, validation_options.merge(only_integer: true, odd: true)
+ person_class.validates_numericality_of :title, validation_options.merge(only_integer: true, odd: true)
@person.title = 0
call = [:title, :odd, generate_message_options.merge(value: 0)]
assert_called_with(@person.errors, :generate_message, call) do
@@ -324,7 +326,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_numericality_of for :less_than on generated message #{name}" do
- Person.validates_numericality_of :title, validation_options.merge(only_integer: true, less_than: 0)
+ person_class.validates_numericality_of :title, validation_options.merge(only_integer: true, less_than: 0)
@person.title = 1
call = [:title, :less_than, generate_message_options.merge(value: 1, count: 0)]
assert_called_with(@person.errors, :generate_message, call) do
@@ -369,67 +371,67 @@ class I18nValidationTest < ActiveModel::TestCase
end
set_expectations_for_validation "validates_confirmation_of", :confirmation do |person, options_to_merge|
- Person.validates_confirmation_of :title, options_to_merge
+ person.class.validates_confirmation_of :title, options_to_merge
person.title_confirmation = "foo"
end
set_expectations_for_validation "validates_acceptance_of", :accepted do |person, options_to_merge|
- Person.validates_acceptance_of :title, options_to_merge.merge(allow_nil: false)
+ person.class.validates_acceptance_of :title, options_to_merge.merge(allow_nil: false)
end
set_expectations_for_validation "validates_presence_of", :blank do |person, options_to_merge|
- Person.validates_presence_of :title, options_to_merge
+ person.class.validates_presence_of :title, options_to_merge
end
set_expectations_for_validation "validates_length_of", :too_short do |person, options_to_merge|
- Person.validates_length_of :title, options_to_merge.merge(within: 3..5)
+ person.class.validates_length_of :title, options_to_merge.merge(within: 3..5)
end
set_expectations_for_validation "validates_length_of", :too_long do |person, options_to_merge|
- Person.validates_length_of :title, options_to_merge.merge(within: 3..5)
+ person.class.validates_length_of :title, options_to_merge.merge(within: 3..5)
person.title = "too long"
end
set_expectations_for_validation "validates_length_of", :wrong_length do |person, options_to_merge|
- Person.validates_length_of :title, options_to_merge.merge(is: 5)
+ person.class.validates_length_of :title, options_to_merge.merge(is: 5)
end
set_expectations_for_validation "validates_format_of", :invalid do |person, options_to_merge|
- Person.validates_format_of :title, options_to_merge.merge(with: /\A[1-9][0-9]*\z/)
+ person.class.validates_format_of :title, options_to_merge.merge(with: /\A[1-9][0-9]*\z/)
end
set_expectations_for_validation "validates_inclusion_of", :inclusion do |person, options_to_merge|
- Person.validates_inclusion_of :title, options_to_merge.merge(in: %w(a b c))
+ person.class.validates_inclusion_of :title, options_to_merge.merge(in: %w(a b c))
end
set_expectations_for_validation "validates_exclusion_of", :exclusion do |person, options_to_merge|
- Person.validates_exclusion_of :title, options_to_merge.merge(in: %w(a b c))
+ person.class.validates_exclusion_of :title, options_to_merge.merge(in: %w(a b c))
person.title = "a"
end
set_expectations_for_validation "validates_numericality_of", :not_a_number do |person, options_to_merge|
- Person.validates_numericality_of :title, options_to_merge
+ person.class.validates_numericality_of :title, options_to_merge
person.title = "a"
end
set_expectations_for_validation "validates_numericality_of", :not_an_integer do |person, options_to_merge|
- Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true)
+ person.class.validates_numericality_of :title, options_to_merge.merge(only_integer: true)
person.title = "1.0"
end
set_expectations_for_validation "validates_numericality_of", :odd do |person, options_to_merge|
- Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, odd: true)
+ person.class.validates_numericality_of :title, options_to_merge.merge(only_integer: true, odd: true)
person.title = 0
end
set_expectations_for_validation "validates_numericality_of", :less_than do |person, options_to_merge|
- Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, less_than: 0)
+ person.class.validates_numericality_of :title, options_to_merge.merge(only_integer: true, less_than: 0)
person.title = 1
end
def test_validations_with_message_symbol_must_translate
I18n.backend.store_translations "en", errors: { messages: { custom_error: "I am a custom error" } }
- Person.validates_presence_of :title, message: :custom_error
+ person_class.validates_presence_of :title, message: :custom_error
@person.title = nil
@person.valid?
assert_equal ["I am a custom error"], @person.errors[:title]
@@ -437,7 +439,7 @@ class I18nValidationTest < ActiveModel::TestCase
def test_validates_with_message_symbol_must_translate_per_attribute
I18n.backend.store_translations "en", activemodel: { errors: { models: { person: { attributes: { title: { custom_error: "I am a custom error" } } } } } }
- Person.validates_presence_of :title, message: :custom_error
+ person_class.validates_presence_of :title, message: :custom_error
@person.title = nil
@person.valid?
assert_equal ["I am a custom error"], @person.errors[:title]
@@ -445,16 +447,20 @@ class I18nValidationTest < ActiveModel::TestCase
def test_validates_with_message_symbol_must_translate_per_model
I18n.backend.store_translations "en", activemodel: { errors: { models: { person: { custom_error: "I am a custom error" } } } }
- Person.validates_presence_of :title, message: :custom_error
+ person_class.validates_presence_of :title, message: :custom_error
@person.title = nil
@person.valid?
assert_equal ["I am a custom error"], @person.errors[:title]
end
def test_validates_with_message_string
- Person.validates_presence_of :title, message: "I am a custom error"
+ person_class.validates_presence_of :title, message: "I am a custom error"
@person.title = nil
@person.valid?
assert_equal ["I am a custom error"], @person.errors[:title]
end
+
+ def person_class
+ @person_stub ||= self.class.const_set(:Person, Class.new(Person))
+ end
end
diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
index bb1b187694..fc4a9e4334 100644
--- a/activemodel/test/models/user.rb
+++ b/activemodel/test/models/user.rb
@@ -10,4 +10,11 @@ class User
has_secure_password :recovery_password, validations: false
attr_accessor :password_digest, :recovery_password_digest
+ attr_accessor :password_called
+
+ def password=(unencrypted_password)
+ self.password_called ||= 0
+ self.password_called += 1
+ super
+ end
end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index adf41cd872..485547f036 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,126 @@
+* Fix dirty tracking after rollback.
+
+ Fixes #15018, #30167, #33868.
+
+ *Ryuta Kamizono*
+
+* Add `ActiveRecord::Relation#cache_version` to support recyclable cache keys via
+ the versioned entries in `ActiveSupport::Cache`. This also means that
+ `ActiveRecord::Relation#cache_key` will now return a stable key that does not
+ include the max timestamp or count any more.
+
+ NOTE: This feature is turned off by default, and `cache_key` will still return
+ cache keys with timestamps until you set `ActiveRecord::Base.collection_cache_versioning = true`.
+ That's the setting for all new apps on Rails 6.0+
+
+ *Lachlan Sylvester*
+
+* Fix dirty tracking for `touch` to track saved changes.
+
+ Fixes #33429.
+
+ *Ryuta Kamzono*
+
+* `change_column_comment` and `change_table_comment` are invertible only if
+ `to` and `from` options are specified.
+
+ *Yoshiyuki Kinjo*
+
+* Don't call commit/rollback callbacks when a record isn't saved.
+
+ Fixes #29747.
+
+ *Ryuta Kamizono*
+
+* Fix circular `autosave: true` causes invalid records to be saved.
+
+ Prior to the fix, when there was a circular series of `autosave: true`
+ associations, the callback for a `has_many` association was run while
+ another instance of the same callback on the same association hadn't
+ finished running. When control returned to the first instance of the
+ callback, the instance variable had changed, and subsequent associated
+ records weren't saved correctly. Specifically, the ID field for the
+ `belongs_to` corresponding to the `has_many` was `nil`.
+
+ Fixes #28080.
+
+ *Larry Reid*
+
+* Raise `ArgumentError` for invalid `:limit` and `:precision` like as other options.
+
+ Before:
+
+ ```ruby
+ add_column :items, :attr1, :binary, size: 10 # => ArgumentError
+ add_column :items, :attr2, :decimal, scale: 10 # => ArgumentError
+ add_column :items, :attr3, :integer, limit: 10 # => ActiveRecordError
+ add_column :items, :attr4, :datetime, precision: 10 # => ActiveRecordError
+ ```
+
+ After:
+
+ ```ruby
+ add_column :items, :attr1, :binary, size: 10 # => ArgumentError
+ add_column :items, :attr2, :decimal, scale: 10 # => ArgumentError
+ add_column :items, :attr3, :integer, limit: 10 # => ArgumentError
+ add_column :items, :attr4, :datetime, precision: 10 # => ArgumentError
+ ```
+
+ *Ryuta Kamizono*
+
+* Association loading isn't to be affected by scoping consistently
+ whether preloaded / eager loaded or not, with the exception of `unscoped`.
+
+ Before:
+
+ ```ruby
+ Post.where("1=0").scoping do
+ Comment.find(1).post # => nil
+ Comment.preload(:post).find(1).post # => #<Post id: 1, ...>
+ Comment.eager_load(:post).find(1).post # => #<Post id: 1, ...>
+ end
+ ```
+
+ After:
+
+ ```ruby
+ Post.where("1=0").scoping do
+ Comment.find(1).post # => #<Post id: 1, ...>
+ Comment.preload(:post).find(1).post # => #<Post id: 1, ...>
+ Comment.eager_load(:post).find(1).post # => #<Post id: 1, ...>
+ end
+ ```
+
+ Fixes #34638, #35398.
+
+ *Ryuta Kamizono*
+
+* Add `rails db:prepare` to migrate or setup a database.
+
+ Runs `db:migrate` if the database exists or `db:setup` if it doesn't.
+
+ *Roberto Miranda*
+
+* Add `after_save_commit` callback as shortcut for `after_commit :hook, on: [ :create, :update ]`.
+
+ *DHH*
+
+* Assign all attributes before calling `build` to ensure the child record is visible in
+ `before_add` and `after_add` callbacks for `has_many :through` associations.
+
+ Fixes #33249.
+
+ *Ryan H. Kerr*
+
+* Add `ActiveRecord::Relation#extract_associated` for extracting associated records from a relation.
+
+ ```
+ account.memberships.extract_associated(:user)
+ # => Returns collection of User records
+ ```
+
+ *DHH*
+
* Add `ActiveRecord::Relation#annotate` for adding SQL comments to its queries.
For example:
@@ -13,7 +136,7 @@
* Support Optimizer Hints.
- In most databases, there is a way to control the optimizer is by using optimizer hints,
+ In most databases, a way to control the optimizer is by using optimizer hints,
which can be specified within individual statements.
Example (for MySQL):
@@ -59,7 +182,7 @@
bulk deletes by `delete_all`.
Supports skipping or upserting duplicates through the `ON CONFLICT` syntax
- for Postgres (9.5+) and Sqlite (3.24+) and `ON DUPLICATE KEY UPDATE` syntax
+ for PostgreSQL (9.5+) and SQLite (3.24+) and `ON DUPLICATE KEY UPDATE` syntax
for MySQL.
*Bob Lail*
@@ -332,7 +455,7 @@
*Gannon McGibbon*
-* Cached columns_hash fields should be excluded from ResultSet#column_types
+* Cached `columns_hash` fields should be excluded from `ResultSet#column_types`.
PR #34528 addresses the inconsistent behaviour when attribute is defined for an ignored column. The following test
was passing for SQLite and MySQL, but failed for PostgreSQL:
@@ -363,12 +486,12 @@
* Make the implicit order column configurable.
- When calling ordered finder methods such as +first+ or +last+ without an
+ When calling ordered finder methods such as `first` or `last` without an
explicit order clause, ActiveRecord sorts records by primary key. This can
result in unpredictable and surprising behaviour when the primary key is
not an auto-incrementing integer, for example when it's a UUID. This change
makes it possible to override the column used for implicit ordering such
- that +first+ and +last+ will return more predictable results.
+ that `first` and `last` will return more predictable results.
Example:
@@ -438,7 +561,7 @@
*Sean Griffin*
-* Add support for hash and url configs in database hash of `ActiveRecord::Base.connected_to`.
+* Add support for hash and URL configs in database hash of `ActiveRecord::Base.connected_to`.
````
User.connected_to(database: { writing: "postgres://foo" }) do
@@ -514,10 +637,10 @@
* Enum raises on invalid definition values
- When defining a Hash enum it can be easy to use [] instead of {}. This
+ When defining a Hash enum it can be easy to use `[]` instead of `{}`. This
commit checks that only valid definition values are provided, those can
be a Hash, an array of Symbols or an array of Strings. Otherwise it
- raises an ArgumentError.
+ raises an `ArgumentError`.
Fixes #33961
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 7d66158f47..fd8d2edf28 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -55,7 +55,6 @@ module ActiveRecord
autoload :Persistence
autoload :QueryCache
autoload :Querying
- autoload :CollectionCacheKey
autoload :ReadonlyAttributes
autoload :RecordInvalid, "active_record/validations"
autoload :Reflection
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 0bb63b97ae..cf22b850b9 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -225,7 +225,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- AssociationRelation.create(klass, self).merge!(klass.all)
+ AssociationRelation.create(klass, self).merge!(klass.scope_for_association)
end
def scope_for_create
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 7c69cd65ee..3b4b243148 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -27,40 +27,32 @@ module ActiveRecord::Associations::Builder # :nodoc:
"Please choose a different association name."
end
- extension = define_extensions model, name, &block
- reflection = create_reflection model, name, scope, options, extension
+ reflection = create_reflection(model, name, scope, options, &block)
define_accessors model, reflection
define_callbacks model, reflection
define_validations model, reflection
reflection
end
- def self.create_reflection(model, name, scope, options, extension = nil)
+ def self.create_reflection(model, name, scope, options, &block)
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
validate_options(options)
- scope = build_scope(scope, extension)
+ extension = define_extensions(model, name, &block)
+ options[:extend] = [*options[:extend], extension] if extension
+
+ scope = build_scope(scope)
ActiveRecord::Reflection.create(macro, name, scope, options, model)
end
- def self.build_scope(scope, extension)
- new_scope = scope
-
+ def self.build_scope(scope)
if scope && scope.arity == 0
- new_scope = proc { instance_exec(&scope) }
- end
-
- if extension
- new_scope = wrap_scope new_scope, extension
+ proc { instance_exec(&scope) }
+ else
+ scope
end
-
- new_scope
- end
-
- def self.wrap_scope(scope, extension)
- scope
end
def self.macro
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 5848cd9112..9fccfcce0c 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -66,17 +66,5 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
CODE
end
-
- def self.wrap_scope(scope, mod)
- if scope
- if scope.arity > 0
- proc { |owner| instance_exec(owner, &scope).extending(mod) }
- else
- proc { instance_exec(&scope).extending(mod) }
- end
- else
- proc { extending(mod) }
- end
- end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 84a9797aa5..0d384950fe 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -57,21 +57,14 @@ module ActiveRecord
@through_records[record.object_id] ||= begin
ensure_mutable
- through_record = through_association.build(*options_for_through_record)
- through_record.send("#{source_reflection.name}=", record)
+ attributes = through_scope_attributes
+ attributes[source_reflection.name] = record
+ attributes[source_reflection.foreign_type] = options[:source_type] if options[:source_type]
- if options[:source_type]
- through_record.send("#{source_reflection.foreign_type}=", options[:source_type])
- end
-
- through_record
+ through_association.build(attributes)
end
end
- def options_for_through_record
- [through_scope_attributes]
- end
-
def through_scope_attributes
scope.where_values_hash(through_association.reflection.name.to_s).
except!(through_association.reflection.foreign_key,
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 8997579527..6b57e5093a 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -143,16 +143,13 @@ module ActiveRecord
def preloaders_for_reflection(reflection, records, scope)
records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
- loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
- loader.run self
- loader
+ preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope).run
end
end
def grouped_records(association, records, polymorphic_parent)
h = {}
records.each do |record|
- next unless record
reflection = record.class._reflect_on_association(association)
next if polymorphic_parent && !reflection || !record.association(association).klass
(h[reflection] ||= []) << record
@@ -166,10 +163,18 @@ module ActiveRecord
@reflection = reflection
end
- def run(preloader); end
+ def run
+ self
+ end
def preloaded_records
- owners.flat_map { |owner| owner.association(reflection.name).target }
+ @preloaded_records ||= records_by_owner.flat_map(&:last)
+ end
+
+ def records_by_owner
+ @records_by_owner ||= owners.each_with_object({}) do |owner, result|
+ result[owner] = Array(owner.association(reflection.name).target)
+ end
end
private
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 7048ff43b8..46532f651e 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -4,26 +4,44 @@ module ActiveRecord
module Associations
class Preloader
class Association #:nodoc:
- attr_reader :preloaded_records
-
def initialize(klass, owners, reflection, preload_scope)
@klass = klass
@owners = owners
@reflection = reflection
@preload_scope = preload_scope
@model = owners.first && owners.first.class
- @preloaded_records = []
end
- def run(preloader)
- records = load_records do |record|
- owner = owners_by_key[convert_key(record[association_key_name])]
- association = owner.association(reflection.name)
- association.set_inverse_instance(record)
+ def run
+ if !preload_scope || preload_scope.empty_scope?
+ owners.each do |owner|
+ associate_records_to_owner(owner, records_by_owner[owner] || [])
+ end
+ else
+ # Custom preload scope is used and
+ # the association can not be marked as loaded
+ # Loading into a Hash instead
+ records_by_owner
end
+ self
+ end
- owners.each do |owner|
- associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || [])
+ def records_by_owner
+ @records_by_owner ||= preloaded_records.each_with_object({}) do |record, result|
+ owners_by_key[convert_key(record[association_key_name])].each do |owner|
+ (result[owner] ||= []) << record
+ end
+ end
+ end
+
+ def preloaded_records
+ return @preloaded_records if defined?(@preloaded_records)
+ return [] if owner_keys.empty?
+ # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
+ # Make several smaller queries if necessary or make one query if the adapter supports it
+ slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
+ @preloaded_records = slices.flat_map do |slice|
+ records_for(slice)
end
end
@@ -54,13 +72,10 @@ module ActiveRecord
end
def owners_by_key
- unless defined?(@owners_by_key)
- @owners_by_key = owners.each_with_object({}) do |owner, h|
- key = convert_key(owner[owner_key_name])
- h[key] = owner if key
- end
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
+ key = convert_key(owner[owner_key_name])
+ (result[key] ||= []) << owner if key
end
- @owners_by_key
end
def key_conversion_required?
@@ -87,23 +102,16 @@ module ActiveRecord
@model.type_for_attribute(owner_key_name).type
end
- def load_records(&block)
- return {} if owner_keys.empty?
- # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
- # Make several smaller queries if necessary or make one query if the adapter supports it
- slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
- @preloaded_records = slices.flat_map do |slice|
- records_for(slice, &block)
- end
- @preloaded_records.group_by do |record|
- convert_key(record[association_key_name])
+ def records_for(ids)
+ scope.where(association_key_name => ids).load do |record|
+ # Processing only the first owner
+ # because the record is modified but not an owner
+ owner = owners_by_key[convert_key(record[association_key_name])].first
+ association = owner.association(reflection.name)
+ association.set_inverse_instance(record)
end
end
- def records_for(ids, &block)
- scope.where(association_key_name => ids).load(&block)
- end
-
def scope
@scope ||= build_scope
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index db89b77629..bec1c4c94a 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -4,45 +4,57 @@ module ActiveRecord
module Associations
class Preloader
class ThroughAssociation < Association # :nodoc:
- def run(preloader)
- already_loaded = owners.first.association(through_reflection.name).loaded?
- through_scope = through_scope()
- through_preloaders = preloader.preload(owners, through_reflection.name, through_scope)
- middle_records = through_preloaders.flat_map(&:preloaded_records)
- preloaders = preloader.preload(middle_records, source_reflection.name, scope)
- @preloaded_records = preloaders.flat_map(&:preloaded_records)
-
- owners.each do |owner|
- through_records = Array(owner.association(through_reflection.name).target)
-
- if already_loaded
+ PRELOADER = ActiveRecord::Associations::Preloader.new
+
+ def initialize(*)
+ super
+ @already_loaded = owners.first.association(through_reflection.name).loaded?
+ end
+
+ def preloaded_records
+ @preloaded_records ||= source_preloaders.flat_map(&:preloaded_records)
+ end
+
+ def records_by_owner
+ return @records_by_owner if defined?(@records_by_owner)
+ source_records_by_owner = source_preloaders.map(&:records_by_owner).reduce(:merge)
+ through_records_by_owner = through_preloaders.map(&:records_by_owner).reduce(:merge)
+
+ @records_by_owner = owners.each_with_object({}) do |owner, result|
+ through_records = through_records_by_owner[owner] || []
+
+ if @already_loaded
if source_type = reflection.options[:source_type]
through_records = through_records.select do |record|
record[reflection.foreign_type] == source_type
end
end
- else
- owner.association(through_reflection.name).reset if through_scope
end
- result = through_records.flat_map do |record|
- record.association(source_reflection.name).target
+ records = through_records.flat_map do |record|
+ source_records_by_owner[record]
end
- result.compact!
- result.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any?
- result.uniq! if scope.distinct_value
- associate_records_to_owner(owner, result)
- end
-
- unless scope.empty_scope?
- middle_records.each do |owner|
- owner.association(source_reflection.name).reset if owner
- end
+ records.compact!
+ records.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any?
+ records.uniq! if scope.distinct_value
+ result[owner] = records
end
end
private
+ def source_preloaders
+ @source_preloaders ||= PRELOADER.preload(middle_records, source_reflection.name, scope)
+ end
+
+ def middle_records
+ through_preloaders.flat_map(&:preloaded_records)
+ end
+
+ def through_preloaders
+ @through_preloaders ||= PRELOADER.preload(owners, through_reflection.name, through_scope)
+ end
+
def through_reflection
reflection.through_reflection
end
@@ -52,8 +64,8 @@ module ActiveRecord
end
def preload_index
- @preload_index ||= @preloaded_records.each_with_object({}).with_index do |(id, result), index|
- result[id] = index
+ @preload_index ||= preloaded_records.each_with_object({}).with_index do |(record, result), index|
+ result[record] = index
end
end
@@ -96,7 +108,7 @@ module ActiveRecord
end
end
- scope unless scope.empty_scope?
+ scope
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index af7e46e649..238ea92da4 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -24,7 +24,7 @@ module ActiveRecord
RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
- class GeneratedAttributeMethodsBuilder < Module #:nodoc:
+ class GeneratedAttributeMethods < Module #:nodoc:
include Mutex_m
end
@@ -35,7 +35,7 @@ module ActiveRecord
end
def initialize_generated_modules # :nodoc:
- @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethodsBuilder.new)
+ @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethods.new)
private_constant :GeneratedAttributeMethods
@attribute_methods_generated = false
include @generated_attribute_methods
@@ -89,7 +89,7 @@ module ActiveRecord
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
# defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, Base) &&
- ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethodsBuilder)
+ ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
defined || super
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index 5941f51a1a..affcf2a4db 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -46,6 +46,7 @@ module ActiveRecord
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
# task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
def read_attribute_before_type_cast(attr_name)
+ sync_with_transaction_state
@attributes[attr_name.to_s].value_before_type_cast
end
@@ -60,17 +61,19 @@ module ActiveRecord
# task.attributes_before_type_cast
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
def attributes_before_type_cast
+ sync_with_transaction_state
@attributes.values_before_type_cast
end
private
- # Handle *_before_type_cast for method_missing.
+ # Dispatch target for <tt>*_before_type_cast</tt> attribute methods.
def attribute_before_type_cast(attribute_name)
read_attribute_before_type_cast(attribute_name)
end
def attribute_came_from_user?(attribute_name)
+ sync_with_transaction_state
@attributes[attribute_name].came_from_user?
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 45e4b8adfa..942fe48635 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -29,9 +29,7 @@ module ActiveRecord
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@mutations_before_last_save = nil
- @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
@mutations_from_database = nil
end
end
@@ -51,7 +49,7 @@ module ActiveRecord
# +to+ When passed, this method will return false unless the value was
# changed to the given value
def saved_change_to_attribute?(attr_name, **options)
- mutations_before_last_save.changed?(attr_name, **options)
+ mutations_before_last_save.changed?(attr_name.to_s, options)
end
# Returns the change to an attribute during the last save. If the
@@ -63,7 +61,7 @@ module ActiveRecord
# invoked as +saved_change_to_name+ instead of
# <tt>saved_change_to_attribute("name")</tt>.
def saved_change_to_attribute(attr_name)
- mutations_before_last_save.change_to_attribute(attr_name)
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
end
# Returns the original value of an attribute before the last save.
@@ -73,7 +71,7 @@ module ActiveRecord
# invoked as +name_before_last_save+ instead of
# <tt>attribute_before_last_save("name")</tt>.
def attribute_before_last_save(attr_name)
- mutations_before_last_save.original_value(attr_name)
+ mutations_before_last_save.original_value(attr_name.to_s)
end
# Did the last call to +save+ have any changes to change?
@@ -101,7 +99,7 @@ module ActiveRecord
# +to+ When passed, this method will return false unless the value will be
# changed to the given value
def will_save_change_to_attribute?(attr_name, **options)
- mutations_from_database.changed?(attr_name, **options)
+ mutations_from_database.changed?(attr_name.to_s, options)
end
# Returns the change to an attribute that will be persisted during the
@@ -115,7 +113,7 @@ module ActiveRecord
# If the attribute will change, the result will be an array containing the
# original value and the new value about to be saved.
def attribute_change_to_be_saved(attr_name)
- mutations_from_database.change_to_attribute(attr_name)
+ mutations_from_database.change_to_attribute(attr_name.to_s)
end
# Returns the value of an attribute in the database, as opposed to the
@@ -127,7 +125,7 @@ module ActiveRecord
# saved. It can be invoked as +name_in_database+ instead of
# <tt>attribute_in_database("name")</tt>.
def attribute_in_database(attr_name)
- mutations_from_database.original_value(attr_name)
+ mutations_from_database.original_value(attr_name.to_s)
end
# Will the next call to +save+ have any changes to persist?
@@ -158,6 +156,16 @@ module ActiveRecord
end
private
+ def mutations_from_database
+ sync_with_transaction_state
+ super
+ end
+
+ def mutations_before_last_save
+ sync_with_transaction_state
+ super
+ end
+
def write_attribute_without_type_cast(attr_name, value)
name = attr_name.to_s
if self.class.attribute_alias?(name)
@@ -168,6 +176,30 @@ module ActiveRecord
result
end
+ def _touch_row(attribute_names, time)
+ @_touch_attr_names = Set.new(attribute_names)
+
+ affected_rows = super
+
+ changes = {}
+ @attributes.keys.each do |attr_name|
+ next if @_touch_attr_names.include?(attr_name)
+
+ if attribute_changed?(attr_name)
+ changes[attr_name] = _read_attribute(attr_name)
+ _write_attribute(attr_name, attribute_was(attr_name))
+ clear_attribute_change(attr_name)
+ end
+ end
+
+ changes_applied
+ changes.each { |attr_name, value| _write_attribute(attr_name, value) }
+
+ affected_rows
+ ensure
+ @_touch_attr_names = nil
+ end
+
def _update_record(attribute_names = attribute_names_for_partial_writes)
affected_rows = super
changes_applied
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 6af5346fa7..feaef72a30 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -16,39 +16,33 @@ module ActiveRecord
# Returns the primary key column's value.
def id
- sync_with_transaction_state
primary_key = self.class.primary_key
_read_attribute(primary_key) if primary_key
end
# Sets the primary key column's value.
def id=(value)
- sync_with_transaction_state
primary_key = self.class.primary_key
_write_attribute(primary_key, value) if primary_key
end
# Queries the primary key column's value.
def id?
- sync_with_transaction_state
query_attribute(self.class.primary_key)
end
# Returns the primary key column's value before type cast.
def id_before_type_cast
- sync_with_transaction_state
read_attribute_before_type_cast(self.class.primary_key)
end
# Returns the primary key column's previous value.
def id_was
- sync_with_transaction_state
attribute_was(self.class.primary_key)
end
# Returns the primary key column's value from the database.
def id_in_database
- sync_with_transaction_state
attribute_in_database(self.class.primary_key)
end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 6811f54b10..0cf67644af 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -32,7 +32,7 @@ module ActiveRecord
end
private
- # Handle *? for method_missing.
+ # Dispatch target for <tt>*?</tt> attribute methods.
def attribute?(attribute_name)
query_attribute(attribute_name)
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index ffac5313ad..84b1ec2fea 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -9,14 +9,11 @@ module ActiveRecord
private
def define_method_attribute(name)
- sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
-
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}
- #{sync_with_transaction_state}
name = #{attr_name_expr}
_read_attribute(name) { |n| missing_attribute(n, caller) }
end
@@ -36,13 +33,13 @@ module ActiveRecord
primary_key = self.class.primary_key
name = primary_key if name == "id" && primary_key
- sync_with_transaction_state if name == primary_key
_read_attribute(name, &block)
end
# This method exists to avoid the expensive primary_key check internally, without
# breaking compatibility with the read_attribute API
def _read_attribute(attr_name, &block) # :nodoc
+ sync_with_transaction_state
@attributes.fetch_value(attr_name.to_s, &block)
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 455e67e19b..d1cfe43bb2 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -13,15 +13,12 @@ module ActiveRecord
private
def define_method_attribute=(name)
- sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
-
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
generated_attribute_methods, name, writer: true,
) do |temp_method_name, attr_name_expr|
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{temp_method_name}(value)
name = #{attr_name_expr}
- #{sync_with_transaction_state}
_write_attribute(name, value)
end
RUBY
@@ -40,25 +37,25 @@ module ActiveRecord
primary_key = self.class.primary_key
name = primary_key if name == "id" && primary_key
- sync_with_transaction_state if name == primary_key
_write_attribute(name, value)
end
# This method exists to avoid the expensive primary_key check internally, without
# breaking compatibility with the write_attribute API
def _write_attribute(attr_name, value) # :nodoc:
+ sync_with_transaction_state
@attributes.write_from_user(attr_name.to_s, value)
value
end
private
def write_attribute_without_type_cast(attr_name, value)
- name = attr_name.to_s
- @attributes.write_cast_value(name, value)
+ sync_with_transaction_state
+ @attributes.write_cast_value(attr_name.to_s, value)
value
end
- # Handle *= for method_missing.
+ # Dispatch target for <tt>*=</tt> attribute methods.
def attribute=(attribute_name, value)
_write_attribute(attribute_name, value)
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index fe94662543..50f29a81a6 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -382,10 +382,14 @@ module ActiveRecord
if association = association_instance_get(reflection.name)
autosave = reflection.options[:autosave]
+ # By saving the instance variable in a local variable,
+ # we make the whole callback re-entrant.
+ new_record_before_save = @new_record_before_save
+
# reconstruct the scope now that we know the owner's id
association.reset_scope
- if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+ if records = associated_records_to_validate_or_save(association, new_record_before_save, autosave)
if autosave
records_to_destroy = records.select(&:marked_for_destruction?)
records_to_destroy.each { |record| association.destroy(record) }
@@ -397,7 +401,7 @@ module ActiveRecord
saved = true
- if autosave != false && (@new_record_before_save || record.new_record?)
+ if autosave != false && (new_record_before_save || record.new_record?)
if autosave
saved = association.insert_record(record, false)
elsif !reflection.nested?
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index db097cb930..2af6d09b53 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -288,7 +288,6 @@ module ActiveRecord #:nodoc:
extend Explain
extend Enum
extend Delegation::DelegateCache
- extend CollectionCacheKey
extend Aggregations::ClassMethods
include Core
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
deleted file mode 100644
index 4b6db8a96c..0000000000
--- a/activerecord/lib/active_record/collection_cache_key.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module CollectionCacheKey
- def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
- query_signature = ActiveSupport::Digest.hexdigest(collection.to_sql)
- key = "#{collection.model_name.cache_key}/query-#{query_signature}"
-
- if collection.loaded? || collection.distinct_value
- size = collection.records.size
- if size > 0
- timestamp = collection.max_by(&timestamp_column)._read_attribute(timestamp_column)
- end
- else
- if collection.eager_loading?
- collection = collection.send(:apply_join_dependency)
- end
- column_type = type_for_attribute(timestamp_column)
- column = connection.visitor.compile(collection.arel_attribute(timestamp_column))
- select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
-
- if collection.has_limit_or_offset?
- query = collection.select("#{column} AS collection_cache_key_timestamp")
- subquery_alias = "subquery_for_cache_key"
- subquery_column = "#{subquery_alias}.collection_cache_key_timestamp"
- subquery = query.arel.as(subquery_alias)
- arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column)
- else
- query = collection.unscope(:order)
- query.select_values = [select_values % column]
- arel = query.arel
- end
-
- result = connection.select_one(arel, nil)
-
- if result.blank?
- size = 0
- timestamp = nil
- else
- size = result["size"]
- timestamp = column_type.deserialize(result["timestamp"])
- end
-
- end
-
- if timestamp
- "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
- else
- "#{key}-#{size}"
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 0ded1a5318..68498b5dc5 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -810,6 +810,7 @@ module ActiveRecord
def new_connection
Base.send(spec.adapter_method, spec.config).tap do |conn|
conn.schema_cache = schema_cache.dup if schema_cache
+ conn.check_version
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index 1305216be2..75e959045e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -5,20 +5,24 @@ require "active_support/deprecation"
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseLimits
+ def max_identifier_length # :nodoc:
+ 64
+ end
+
# Returns the maximum length of a table alias.
def table_alias_length
- 255
+ max_identifier_length
end
# Returns the maximum length of a column name.
def column_name_length
- 64
+ max_identifier_length
end
deprecate :column_name_length
# Returns the maximum length of a table name.
def table_name_length
- 64
+ max_identifier_length
end
deprecate :table_name_length
@@ -33,7 +37,7 @@ module ActiveRecord
# Returns the maximum length of an index name.
def index_name_length
- 64
+ max_identifier_length
end
# Returns the maximum number of columns per table.
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 6aacbe5f88..ef19538447 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -131,7 +131,7 @@ module ActiveRecord
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
- sql, binds = sql_for_insert(sql, pk, sequence_name, binds)
+ sql, binds = sql_for_insert(sql, pk, binds)
exec_query(sql, name, binds)
end
@@ -427,8 +427,7 @@ module ActiveRecord
columns.map do |name, column|
if fixture.key?(name)
type = lookup_cast_type_from_column(column)
- bind = Relation::QueryAttribute.new(name, fixture[name], type)
- with_yaml_fallback(bind.value_for_database)
+ with_yaml_fallback(type.serialize(fixture[name]))
else
default_insert_value(column)
end
@@ -488,7 +487,7 @@ module ActiveRecord
exec_query(sql, name, binds, prepare: true)
end
- def sql_for_insert(sql, pk, sequence_name, binds)
+ def sql_for_insert(sql, pk, binds)
[sql, binds]
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 93b1c4e632..a7753e3e9c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -7,7 +7,8 @@ module ActiveRecord
module QueryCache
class << self
def included(base) #:nodoc:
- dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction
+ dirties_query_cache base, :insert, :update, :delete, :truncate, :truncate_tables,
+ :rollback_to_savepoint, :rollback_db_transaction
base.set_callback :checkout, :after, :configure_query_cache!
base.set_callback :checkin, :after, :disable_query_cache!
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 4861872129..688eea75e8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -416,6 +416,7 @@ module ActiveRecord
#
# t.references(:user)
# t.belongs_to(:supplier, foreign_key: true)
+ # t.belongs_to(:supplier, foreign_key: true, type: :integer)
#
# See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
def references(*args, **options)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 6981ea6ecd..2b64e96450 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -770,6 +770,17 @@ module ActiveRecord
# CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL
#
# Note: only supported by MySQL.
+ #
+ # ====== Creating an index with a specific algorithm
+ #
+ # add_index(:developers, :name, algorithm: :concurrently)
+ # # CREATE INDEX CONCURRENTLY developers_on_name on developers (name)
+ #
+ # Note: only supported by PostgreSQL.
+ #
+ # Concurrently adding an index is not supported in a transaction.
+ #
+ # For more information see the {"Transactional Migrations" section}[rdoc-ref:Migration].
def add_index(table_name, column_name, options = {})
index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}"
@@ -793,6 +804,15 @@ module ActiveRecord
#
# remove_index :accounts, name: :by_branch_party
#
+ # Removes the index named +by_branch_party+ in the +accounts+ table +concurrently+.
+ #
+ # remove_index :accounts, name: :by_branch_party, algorithm: :concurrently
+ #
+ # Note: only supported by PostgreSQL.
+ #
+ # Concurrently removing an index is not supported in a transaction.
+ #
+ # For more information see the {"Transactional Migrations" section}[rdoc-ref:Migration].
def remove_index(table_name, options = {})
index_name = index_name_for_remove(table_name, options)
execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
@@ -966,7 +986,7 @@ module ActiveRecord
# [<tt>:on_update</tt>]
# Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
# [<tt>:validate</tt>]
- # (Postgres only) Specify whether or not the constraint should be validated. Defaults to +true+.
+ # (PostgreSQL only) Specify whether or not the constraint should be validated. Defaults to +true+.
def add_foreign_key(from_table, to_table, options = {})
return unless supports_foreign_keys?
@@ -1097,7 +1117,7 @@ module ActiveRecord
if (0..6) === precision
column_type_sql << "(#{precision})"
else
- raise(ActiveRecordError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6")
+ raise ArgumentError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6"
end
elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
column_type_sql << "(#{limit})"
@@ -1185,12 +1205,22 @@ module ActiveRecord
end
# Changes the comment for a table or removes it if +nil+.
- def change_table_comment(table_name, comment)
+ #
+ # Passing a hash containing +:from+ and +:to+ will make this change
+ # reversible in migration:
+ #
+ # change_table_comment(:posts, from: "old_comment", to: "new_comment")
+ def change_table_comment(table_name, comment_or_changes)
raise NotImplementedError, "#{self.class} does not support changing table comments"
end
# Changes the comment for a column or removes it if +nil+.
- def change_column_comment(table_name, column_name, comment)
+ #
+ # Passing a hash containing +:from+ and +:to+ will make this change
+ # reversible in migration:
+ #
+ # change_column_comment(:posts, :state, from: "old_comment", to: "new_comment")
+ def change_column_comment(table_name, column_name, comment_or_changes)
raise NotImplementedError, "#{self.class} does not support changing column comments"
end
@@ -1374,11 +1404,37 @@ module ActiveRecord
default_or_changes
end
end
+ alias :extract_new_comment_value :extract_new_default_value
def can_remove_index_by_name?(options)
options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty?
end
+ def bulk_change_table(table_name, operations)
+ sql_fragments = []
+ non_combinable_operations = []
+
+ operations.each do |command, args|
+ table, arguments = args.shift, args
+ method = :"#{command}_for_alter"
+
+ if respond_to?(method, true)
+ sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
+ sql_fragments << sqls
+ non_combinable_operations.concat(procs)
+ else
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
+ sql_fragments = []
+ non_combinable_operations = []
+ send(command, table, *arguments)
+ end
+ end
+
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
+ end
+
def add_column_for_alter(table_name, column_name, type, options = {})
td = create_table_definition(table_name)
cd = td.new_column_definition(column_name, type, options)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 7aad306d50..bf0bb84c93 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -133,8 +133,6 @@ module ActiveRecord
@advisory_locks_enabled = self.class.type_cast_config_to_boolean(
config.fetch(:advisory_locks, true)
)
-
- check_version
end
def replica?
@@ -172,8 +170,11 @@ module ActiveRecord
class Version
include Comparable
- def initialize(version_string)
+ attr_reader :full_version_string
+
+ def initialize(version_string, full_version_string = nil)
@version = version_string.split(".").map(&:to_i)
+ @full_version_string = full_version_string
end
def <=>(version_string)
@@ -575,9 +576,17 @@ module ActiveRecord
"INSERT #{insert.into} #{insert.values_list}"
end
+ def get_database_version # :nodoc:
+ end
+
+ def database_version # :nodoc:
+ schema_cache.database_version
+ end
+
+ def check_version # :nodoc:
+ end
+
private
- def check_version
- end
def type_map
@type_map ||= Type::TypeMap.new.tap do |mapping|
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 8ca2cfa9ed..282b2b1838 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -55,24 +55,26 @@ module ActiveRecord
super(connection, logger, config)
end
- def version #:nodoc:
- @version ||= Version.new(version_string)
+ def get_database_version #:nodoc:
+ full_version_string = get_full_version
+ version_string = version_string(full_version_string)
+ Version.new(version_string, full_version_string)
end
def mariadb? # :nodoc:
/mariadb/i.match?(full_version)
end
- def supports_bulk_alter? #:nodoc:
+ def supports_bulk_alter?
true
end
def supports_index_sort_order?
- !mariadb? && version >= "8.0.1"
+ !mariadb? && database_version >= "8.0.1"
end
def supports_expression_index?
- !mariadb? && version >= "8.0.13"
+ !mariadb? && database_version >= "8.0.13"
end
def supports_transaction_isolation?
@@ -96,16 +98,16 @@ module ActiveRecord
end
def supports_datetime_with_precision?
- mariadb? || version >= "5.6.4"
+ mariadb? || database_version >= "5.6.4"
end
def supports_virtual_columns?
- mariadb? || version >= "5.7.5"
+ mariadb? || database_version >= "5.7.5"
end
# See https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html for more details.
def supports_optimizer_hints?
- !mariadb? && version >= "5.7.7"
+ !mariadb? && database_version >= "5.7.7"
end
def supports_advisory_locks?
@@ -285,22 +287,8 @@ module ActiveRecord
SQL
end
- def bulk_change_table(table_name, operations) #:nodoc:
- sqls = operations.flat_map do |command, args|
- table, arguments = args.shift, args
- method = :"#{command}_for_alter"
-
- if respond_to?(method, true)
- send(method, table, *arguments)
- else
- raise "Unknown method called : #{method}(#{arguments.inspect})"
- end
- end.join(", ")
-
- execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
- end
-
- def change_table_comment(table_name, comment) #:nodoc:
+ def change_table_comment(table_name, comment_or_changes) # :nodoc:
+ comment = extract_new_comment_value(comment_or_changes)
comment = "" if comment.nil?
execute("ALTER TABLE #{quote_table_name(table_name)} COMMENT #{quote(comment)}")
end
@@ -356,7 +344,8 @@ module ActiveRecord
change_column table_name, column_name, nil, null: null
end
- def change_column_comment(table_name, column_name, comment) #:nodoc:
+ def change_column_comment(table_name, column_name, comment_or_changes) # :nodoc:
+ comment = extract_new_comment_value(comment_or_changes)
change_column table_name, column_name, nil, comment: comment
end
@@ -516,8 +505,8 @@ module ActiveRecord
sql = +"INSERT #{insert.into} #{insert.values_list}"
if insert.skip_duplicates?
- any_column = quote_column_name(insert.model.columns.first.name)
- sql << " ON DUPLICATE KEY UPDATE #{any_column}=#{any_column}"
+ no_op_column = quote_column_name(insert.keys.first)
+ sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{no_op_column}"
elsif insert.update_duplicates?
sql << " ON DUPLICATE KEY UPDATE "
sql << insert.updatable_columns.map { |column| "#{column}=VALUES(#{column})" }.join(",")
@@ -526,12 +515,13 @@ module ActiveRecord
sql
end
- private
- def check_version
- if version < "5.5.8"
- raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8."
- end
+ def check_version # :nodoc:
+ if database_version < "5.5.8"
+ raise "Your version of MySQL (#{database_version}) is too old. Active Record supports MySQL >= 5.5.8."
end
+ end
+
+ private
def initialize_type_map(m = type_map)
super
@@ -702,7 +692,7 @@ module ActiveRecord
end
def supports_rename_index?
- mariadb? ? false : version >= "5.7.6"
+ mariadb? ? false : database_version >= "5.7.6"
end
def configure_connection
@@ -800,8 +790,8 @@ module ActiveRecord
MismatchedForeignKey.new(options)
end
- def version_string
- full_version.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1]
+ def version_string(full_version_string)
+ full_version_string.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1]
end
class MysqlString < Type::String # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 5d81de9fe1..279d0b9e84 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation, :comment
+ attr_reader :name, :default, :sql_type_metadata, :null, :default_function, :collation, :comment
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
@@ -15,9 +15,8 @@ module ActiveRecord
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil, **)
+ def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **)
@name = name.freeze
- @table_name = table_name
@sql_type_metadata = sql_type_metadata
@null = null
@default = default
@@ -44,7 +43,6 @@ module ActiveRecord
def init_with(coder)
@name = coder["name"]
- @table_name = coder["table_name"]
@sql_type_metadata = coder["sql_type_metadata"]
@null = coder["null"]
@default = coder["default"]
@@ -55,7 +53,6 @@ module ActiveRecord
def encode_with(coder)
coder["name"] = @name
- coder["table_name"] = @table_name
coder["sql_type_metadata"] = @sql_type_metadata
coder["null"] = @null
coder["default"] = @default
@@ -66,19 +63,26 @@ module ActiveRecord
def ==(other)
other.is_a?(Column) &&
- attributes_for_hash == other.attributes_for_hash
+ name == other.name &&
+ default == other.default &&
+ sql_type_metadata == other.sql_type_metadata &&
+ null == other.null &&
+ default_function == other.default_function &&
+ collation == other.collation &&
+ comment == other.comment
end
alias :eql? :==
def hash
- attributes_for_hash.hash
+ Column.hash ^
+ name.hash ^
+ default.hash ^
+ sql_type_metadata.hash ^
+ null.hash ^
+ default_function.hash ^
+ collation.hash ^
+ comment.hash
end
-
- protected
-
- def attributes_for_hash
- [self.class, name, default, sql_type_metadata, null, table_name, default_function, collation]
- end
end
class NullColumn < Column
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
index 1199c0ad1b..2132e5d248 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -61,7 +61,9 @@ module ActiveRecord
def exec_delete(sql, name = nil, binds = [])
if without_prepared_statement?(binds)
- execute_and_free(sql, name) { @connection.affected_rows }
+ @lock.synchronize do
+ execute_and_free(sql, name) { @connection.affected_rows }
+ end
else
exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows }
end
@@ -145,15 +147,12 @@ module ActiveRecord
elsif previous_packet.nil?
true
else
- (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet
+ (current_packet.bytesize + previous_packet.bytesize + 2) > max_allowed_packet
end
end
def max_allowed_packet
- @max_allowed_packet ||= begin
- bytes_margin = 2
- show_variable("max_allowed_packet") - bytes_margin
- end
+ @max_allowed_packet ||= show_variable("max_allowed_packet")
end
def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
index 57518b02fa..234fb25fdf 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -55,7 +55,7 @@ module ActiveRecord
end
def schema_collation(column)
- if column.collation && table_name = column.table_name
+ if column.collation
@table_collation_cache ||= {}
@table_collation_cache[table_name] ||=
@connection.exec_query("SHOW TABLE STATUS LIKE #{@connection.quote(table_name)}", "SCHEMA").first["Collation"]
@@ -64,14 +64,14 @@ module ActiveRecord
end
def extract_expression_for_virtual_column(column)
- if @connection.mariadb? && @connection.version < "10.2.5"
- create_table_info = @connection.send(:create_table_info, column.table_name)
+ if @connection.mariadb? && @connection.database_version < "10.2.5"
+ create_table_info = @connection.send(:create_table_info, table_name)
column_name = @connection.quote_column_name(column.name)
if %r/#{column_name} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info
$~[:expression].inspect
end
else
- scope = @connection.send(:quoted_scope, column.table_name)
+ scope = @connection.send(:quoted_scope, table_name)
column_name = @connection.quote(column.name)
sql = "SELECT generation_expression FROM information_schema.columns" \
" WHERE table_schema = #{scope[:schema]}" \
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
index 4018f0815c..25a1fb234a 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -121,14 +121,18 @@ module ActiveRecord
sql
end
+ def table_alias_length
+ 256 # https://dev.mysql.com/doc/refman/8.0/en/identifiers.html
+ end
+
private
CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"]
def row_format_dynamic_by_default?
if mariadb?
- version >= "10.2.2"
+ database_version >= "10.2.2"
else
- version >= "5.7.9"
+ database_version >= "5.7.9"
end
end
@@ -170,9 +174,8 @@ module ActiveRecord
default,
type_metadata,
field[:Null] == "YES",
- table_name,
default_function,
- field[:Collation],
+ collation: field[:Collation],
comment: field[:Comment].presence
)
end
@@ -240,7 +243,7 @@ module ActiveRecord
when nil, 0x100..0xffff; nil
when 0x10000..0xffffff; "medium"
when 0x1000000..0xffffffff; "long"
- else raise ActiveRecordError, "No #{type} type has byte size #{limit}"
+ else raise ArgumentError, "No #{type} type has byte size #{limit}"
end
end
end
@@ -252,7 +255,7 @@ module ActiveRecord
when 3; "mediumint"
when nil, 4; "int"
when 5..8; "bigint"
- else raise ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead."
+ else raise ArgumentError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead."
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
index 7ad0944d51..9167593064 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
@@ -10,25 +10,21 @@ module ActiveRecord
def initialize(type_metadata, extra: "")
super(type_metadata)
- @type_metadata = type_metadata
@extra = extra
end
def ==(other)
- other.is_a?(MySQL::TypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
+ other.is_a?(TypeMetadata) &&
+ __getobj__ == other.__getobj__ &&
+ extra == other.extra
end
alias eql? ==
def hash
- attributes_for_hash.hash
+ TypeMetadata.hash ^
+ __getobj__.hash ^
+ extra.hash
end
-
- protected
-
- def attributes_for_hash
- [self.class, @type_metadata, extra]
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 9bdaa00336..5b0335c22b 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -43,7 +43,7 @@ module ActiveRecord
end
def supports_json?
- !mariadb? && version >= "5.7.8"
+ !mariadb? && database_version >= "5.7.8"
end
def supports_comments?
@@ -126,7 +126,11 @@ module ActiveRecord
end
def full_version
- @full_version ||= @connection.server_info[:version]
+ schema_cache.database_version.full_version_string
+ end
+
+ def get_full_version
+ @connection.server_info[:version]
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index 3ccc7271ab..ec25bb1e19 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -2,42 +2,29 @@
module ActiveRecord
module ConnectionAdapters
- # PostgreSQL-specific extensions to column definitions in a table.
- class PostgreSQLColumn < Column #:nodoc:
- delegate :array, :oid, :fmod, to: :sql_type_metadata
- alias :array? :array
+ module PostgreSQL
+ class Column < ConnectionAdapters::Column # :nodoc:
+ delegate :oid, :fmod, to: :sql_type_metadata
- def initialize(*, max_identifier_length: 63, **)
- super
- @max_identifier_length = max_identifier_length
- end
-
- def serial?
- return unless default_function
-
- if %r{\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z} =~ default_function
- sequence_name_from_parts(table_name, name, suffix) == sequence_name
+ def initialize(*, serial: nil, **)
+ super
+ @serial = serial
end
- end
-
- private
- attr_reader :max_identifier_length
- def sequence_name_from_parts(table_name, column_name, suffix)
- over_length = [table_name, column_name, suffix].map(&:length).sum + 2 - max_identifier_length
-
- if over_length > 0
- column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min
- over_length -= column_name.length - column_name_length
- column_name = column_name[0, column_name_length - [over_length, 0].min]
- end
+ def serial?
+ @serial
+ end
- if over_length > 0
- table_name = table_name[0, table_name.length - over_length]
- end
+ def array
+ sql_type_metadata.sql_type.end_with?("[]")
+ end
+ alias :array? :array
- "#{table_name}_#{column_name}_#{suffix}"
+ def sql_type
+ super.sub(/\[\]\z/, "")
end
+ end
end
+ PostgreSQLColumn = PostgreSQL::Column # :nodoc:
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index ae7dbd2868..d872bd662f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -110,7 +110,7 @@ module ActiveRecord
end
alias :exec_update :exec_delete
- def sql_for_insert(sql, pk, sequence_name, binds) # :nodoc:
+ def sql_for_insert(sql, pk, binds) # :nodoc:
if pk.nil?
# Extract the table from the insert sql. Yuck.
table_ref = extract_table_ref_from_insert_sql(sql)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index a38c1325c0..40c5e51d92 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -287,7 +287,7 @@ module ActiveRecord
quoted_sequence = quote_table_name(sequence)
max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
if max_pk.nil?
- if postgresql_version >= 100000
+ if database_version >= 100000
minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
else
minvalue = query_value("SELECT min_value FROM #{quoted_sequence}", "SCHEMA")
@@ -368,31 +368,6 @@ module ActiveRecord
SQL
end
- def bulk_change_table(table_name, operations)
- sql_fragments = []
- non_combinable_operations = []
-
- operations.each do |command, args|
- table, arguments = args.shift, args
- method = :"#{command}_for_alter"
-
- if respond_to?(method, true)
- sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
- sql_fragments << sqls
- non_combinable_operations.concat(procs)
- else
- execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
- non_combinable_operations.each(&:call)
- sql_fragments = []
- non_combinable_operations = []
- send(command, table, *arguments)
- end
- end
-
- execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
- non_combinable_operations.each(&:call)
- end
-
# Renames a table.
# Also renames a table's primary key sequence if the sequence name exists and
# matches the Active Record default.
@@ -443,14 +418,16 @@ module ActiveRecord
end
# Adds comment for given table column or drops it if +comment+ is a +nil+
- def change_column_comment(table_name, column_name, comment) # :nodoc:
+ def change_column_comment(table_name, column_name, comment_or_changes) # :nodoc:
clear_cache!
+ comment = extract_new_comment_value(comment_or_changes)
execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}"
end
# Adds comment for given table or drops it if +comment+ is a +nil+
- def change_table_comment(table_name, comment) # :nodoc:
+ def change_table_comment(table_name, comment_or_changes) # :nodoc:
clear_cache!
+ comment = extract_new_comment_value(comment_or_changes)
execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}"
end
@@ -548,21 +525,21 @@ module ActiveRecord
# The hard limit is 1GB, because of a 32-bit size field, and TOAST.
case limit
when nil, 0..0x3fffffff; super(type)
- else raise ActiveRecordError, "No binary type has byte size #{limit}. The limit on binary can be at most 1GB - 1byte."
+ else raise ArgumentError, "No binary type has byte size #{limit}. The limit on binary can be at most 1GB - 1byte."
end
when "text"
# PostgreSQL doesn't support limits on text columns.
# The hard limit is 1GB, according to section 8.3 in the manual.
case limit
when nil, 0..0x3fffffff; super(type)
- else raise ActiveRecordError, "No text type has byte size #{limit}. The limit on text can be at most 1GB - 1byte."
+ else raise ArgumentError, "No text type has byte size #{limit}. The limit on text can be at most 1GB - 1byte."
end
when "integer"
case limit
when 1, 2; "smallint"
when nil, 3, 4; "integer"
when 5..8; "bigint"
- else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.")
+ else raise ArgumentError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead."
end
else
super
@@ -650,16 +627,19 @@ module ActiveRecord
default_value = extract_value_from_default(default)
default_function = extract_default_function(default_value, default)
- PostgreSQLColumn.new(
+ if match = default_function&.match(/\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z/)
+ serial = sequence_name_from_parts(table_name, column_name, match[:suffix]) == match[:sequence_name]
+ end
+
+ PostgreSQL::Column.new(
column_name,
default_value,
type_metadata,
!notnull,
- table_name,
default_function,
- collation,
+ collation: collation,
comment: comment.presence,
- max_identifier_length: max_identifier_length
+ serial: serial
)
end
@@ -672,7 +652,23 @@ module ActiveRecord
precision: cast_type.precision,
scale: cast_type.scale,
)
- PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod)
+ PostgreSQL::TypeMetadata.new(simple_type, oid: oid, fmod: fmod)
+ end
+
+ def sequence_name_from_parts(table_name, column_name, suffix)
+ over_length = [table_name, column_name, suffix].sum(&:length) + 2 - max_identifier_length
+
+ if over_length > 0
+ column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min
+ over_length -= column_name.length - column_name_length
+ column_name = column_name[0, column_name_length - [over_length, 0].min]
+ end
+
+ if over_length > 0
+ table_name = table_name[0, table_name.length - over_length]
+ end
+
+ "#{table_name}_#{column_name}_#{suffix}"
end
def extract_foreign_key_action(specifier)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
index cd69d28139..8bdec623af 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
@@ -3,38 +3,34 @@
module ActiveRecord
# :stopdoc:
module ConnectionAdapters
- class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata)
- undef to_yaml if method_defined?(:to_yaml)
+ module PostgreSQL
+ class TypeMetadata < DelegateClass(SqlTypeMetadata)
+ undef to_yaml if method_defined?(:to_yaml)
- attr_reader :oid, :fmod, :array
+ attr_reader :oid, :fmod
- def initialize(type_metadata, oid: nil, fmod: nil)
- super(type_metadata)
- @type_metadata = type_metadata
- @oid = oid
- @fmod = fmod
- @array = /\[\]$/.match?(type_metadata.sql_type)
- end
-
- def sql_type
- super.gsub(/\[\]$/, "")
- end
-
- def ==(other)
- other.is_a?(PostgreSQLTypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
- end
- alias eql? ==
-
- def hash
- attributes_for_hash.hash
- end
+ def initialize(type_metadata, oid: nil, fmod: nil)
+ super(type_metadata)
+ @oid = oid
+ @fmod = fmod
+ end
- protected
+ def ==(other)
+ other.is_a?(TypeMetadata) &&
+ __getobj__ == other.__getobj__ &&
+ oid == other.oid &&
+ fmod == other.fmod
+ end
+ alias eql? ==
- def attributes_for_hash
- [self.class, @type_metadata, oid, fmod]
+ def hash
+ TypeMetadata.hash ^
+ __getobj__.hash ^
+ oid.hash ^
+ fmod.hash
end
+ end
end
+ PostgreSQLTypeMetadata = PostgreSQL::TypeMetadata
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 29f764e8f4..91318a0af1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -201,7 +201,7 @@ module ActiveRecord
end
def supports_insert_on_conflict?
- postgresql_version >= 90500
+ database_version >= 90500
end
alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
@@ -344,7 +344,7 @@ module ActiveRecord
end
def supports_pgcrypto_uuid?
- postgresql_version >= 90400
+ database_version >= 90400
end
def supports_optimizer_hints?
@@ -400,8 +400,6 @@ module ActiveRecord
def max_identifier_length
@max_identifier_length ||= query_value("SHOW max_identifier_length", "SCHEMA").to_i
end
- alias table_alias_length max_identifier_length
- alias index_name_length max_identifier_length
# Set the authorized user for this session
def session_auth=(user)
@@ -424,9 +422,10 @@ module ActiveRecord
}
# Returns the version of the connected PostgreSQL server.
- def postgresql_version
+ def get_database_version # :nodoc:
@connection.server_version
end
+ alias :postgresql_version :database_version
def default_index_type?(index) # :nodoc:
index.using == :btree || super
@@ -446,12 +445,13 @@ module ActiveRecord
sql
end
- private
- def check_version
- if postgresql_version < 90300
- raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.3."
- end
+ def check_version # :nodoc:
+ if database_version < 90300
+ raise "Your version of PostgreSQL (#{database_version}) is too old. Active Record supports PostgreSQL >= 9.3."
end
+ end
+
+ private
# See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
VALUE_LIMIT_VIOLATION = "22001"
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 07453b4403..dbfe1e4a34 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -26,21 +26,23 @@ module ActiveRecord
end
def encode_with(coder)
- coder["columns"] = @columns
- coder["columns_hash"] = @columns_hash
- coder["primary_keys"] = @primary_keys
- coder["data_sources"] = @data_sources
- coder["indexes"] = @indexes
- coder["version"] = connection.migration_context.current_version
+ coder["columns"] = @columns
+ coder["columns_hash"] = @columns_hash
+ coder["primary_keys"] = @primary_keys
+ coder["data_sources"] = @data_sources
+ coder["indexes"] = @indexes
+ coder["version"] = connection.migration_context.current_version
+ coder["database_version"] = database_version
end
def init_with(coder)
- @columns = coder["columns"]
- @columns_hash = coder["columns_hash"]
- @primary_keys = coder["primary_keys"]
- @data_sources = coder["data_sources"]
- @indexes = coder["indexes"] || {}
- @version = coder["version"]
+ @columns = coder["columns"]
+ @columns_hash = coder["columns_hash"]
+ @primary_keys = coder["primary_keys"]
+ @data_sources = coder["data_sources"]
+ @indexes = coder["indexes"] || {}
+ @version = coder["version"]
+ @database_version = coder["database_version"]
end
def primary_keys(table_name)
@@ -91,6 +93,10 @@ module ActiveRecord
@indexes[table_name] ||= connection.indexes(table_name)
end
+ def database_version # :nodoc:
+ @database_version ||= connection.get_database_version
+ end
+
# Clears out internal caches
def clear!
@columns.clear
@@ -99,6 +105,7 @@ module ActiveRecord
@data_sources.clear
@indexes.clear
@version = nil
+ @database_version = nil
end
def size
@@ -117,11 +124,11 @@ module ActiveRecord
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = connection.migration_context.current_version
- [@version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes]
+ [@version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes, database_version]
end
def marshal_load(array)
- @version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes = array
+ @version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes, @database_version = array
@indexes = @indexes || {}
end
diff --git a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
index 8489bcbf1d..df28df7a7c 100644
--- a/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb
@@ -16,19 +16,22 @@ module ActiveRecord
def ==(other)
other.is_a?(SqlTypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
+ sql_type == other.sql_type &&
+ type == other.type &&
+ limit == other.limit &&
+ precision == other.precision &&
+ scale == other.scale
end
alias eql? ==
def hash
- attributes_for_hash.hash
+ SqlTypeMetadata.hash ^
+ sql_type.hash ^
+ type.hash ^
+ limit.hash ^
+ precision.hash >> 1 ^
+ scale.hash >> 2
end
-
- protected
-
- def attributes_for_hash
- [self.class, sql_type, type, limit, precision, scale]
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
index 84dcae49b9..46ce1a15b5 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
@@ -4,6 +4,82 @@ module ActiveRecord
module ConnectionAdapters
module SQLite3
module DatabaseStatements
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :explain, :select, :pragma, :release, :savepoint, :rollback) # :nodoc:
+ private_constant :READ_QUERY
+
+ def write_query?(sql) # :nodoc:
+ !READ_QUERY.match?(sql)
+ end
+
+ def execute(sql, name = nil) #:nodoc:
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
+ materialize_transactions
+
+ log(sql, name) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.execute(sql)
+ end
+ end
+ end
+
+ def exec_query(sql, name = nil, binds = [], prepare: false)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
+ materialize_transactions
+
+ type_casted_binds = type_casted_binds(binds)
+
+ log(sql, name, binds, type_casted_binds) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ # Don't cache statements if they are not prepared
+ unless prepare
+ stmt = @connection.prepare(sql)
+ begin
+ cols = stmt.columns
+ unless without_prepared_statement?(binds)
+ stmt.bind_params(type_casted_binds)
+ end
+ records = stmt.to_a
+ ensure
+ stmt.close
+ end
+ else
+ stmt = @statements[sql] ||= @connection.prepare(sql)
+ cols = stmt.columns
+ stmt.reset!
+ stmt.bind_params(type_casted_binds)
+ records = stmt.to_a
+ end
+
+ ActiveRecord::Result.new(cols, records)
+ end
+ end
+ end
+
+ def exec_delete(sql, name = "SQL", binds = [])
+ exec_query(sql, name, binds)
+ @connection.changes
+ end
+ alias :exec_update :exec_delete
+
+ def begin_db_transaction #:nodoc:
+ log("begin transaction", nil) { @connection.transaction }
+ end
+
+ def commit_db_transaction #:nodoc:
+ log("commit transaction", nil) { @connection.commit }
+ end
+
+ def exec_rollback_db_transaction #:nodoc:
+ log("rollback transaction", nil) { @connection.rollback }
+ end
+
+
private
def execute_batch(sql, name = nil)
if preventing_writes? && write_query?(sql)
@@ -14,11 +90,15 @@ module ActiveRecord
log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
- @connection.execute_batch(sql)
+ @connection.execute_batch2(sql)
end
end
end
+ def last_inserted_id(result)
+ @connection.last_insert_row_id
+ end
+
def build_fixture_statements(fixture_set)
fixture_set.flat_map do |table_name, fixtures|
next if fixtures.empty?
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index e64e995e1a..e48f59b4f0 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -105,7 +105,7 @@ module ActiveRecord
end
type_metadata = fetch_type_metadata(field["type"])
- Column.new(field["name"], default, type_metadata, field["notnull"].to_i == 0, table_name, nil, field["collation"])
+ Column.new(field["name"], default, type_metadata, field["notnull"].to_i == 0, collation: field["collation"])
end
def data_source_sql(name = nil, type: nil)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index ff23a525b9..f5f5827d04 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -10,7 +10,7 @@ require "active_record/connection_adapters/sqlite3/schema_definitions"
require "active_record/connection_adapters/sqlite3/schema_dumper"
require "active_record/connection_adapters/sqlite3/schema_statements"
-gem "sqlite3", "~> 1.3", ">= 1.3.6"
+gem "sqlite3", "~> 1.4"
require "sqlite3"
module ActiveRecord
@@ -111,7 +111,7 @@ module ActiveRecord
end
def supports_expression_index?
- sqlite_version >= "3.9.0"
+ database_version >= "3.9.0"
end
def requires_reloading?
@@ -135,7 +135,7 @@ module ActiveRecord
end
def supports_insert_on_conflict?
- sqlite_version >= "3.24.0"
+ database_version >= "3.24.0"
end
alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
@@ -204,91 +204,11 @@ module ActiveRecord
#--
# DATABASE STATEMENTS ======================================
#++
-
- READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :explain, :select, :pragma, :release, :savepoint, :rollback) # :nodoc:
- private_constant :READ_QUERY
-
- def write_query?(sql) # :nodoc:
- !READ_QUERY.match?(sql)
- end
-
def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", []))
end
- def exec_query(sql, name = nil, binds = [], prepare: false)
- if preventing_writes? && write_query?(sql)
- raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
- end
-
- materialize_transactions
-
- type_casted_binds = type_casted_binds(binds)
-
- log(sql, name, binds, type_casted_binds) do
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
- # Don't cache statements if they are not prepared
- unless prepare
- stmt = @connection.prepare(sql)
- begin
- cols = stmt.columns
- unless without_prepared_statement?(binds)
- stmt.bind_params(type_casted_binds)
- end
- records = stmt.to_a
- ensure
- stmt.close
- end
- else
- stmt = @statements[sql] ||= @connection.prepare(sql)
- cols = stmt.columns
- stmt.reset!
- stmt.bind_params(type_casted_binds)
- records = stmt.to_a
- end
-
- ActiveRecord::Result.new(cols, records)
- end
- end
- end
-
- def exec_delete(sql, name = "SQL", binds = [])
- exec_query(sql, name, binds)
- @connection.changes
- end
- alias :exec_update :exec_delete
-
- def last_inserted_id(result)
- @connection.last_insert_row_id
- end
-
- def execute(sql, name = nil) #:nodoc:
- if preventing_writes? && write_query?(sql)
- raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
- end
-
- materialize_transactions
-
- log(sql, name) do
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
- @connection.execute(sql)
- end
- end
- end
-
- def begin_db_transaction #:nodoc:
- log("begin transaction", nil) { @connection.transaction }
- end
-
- def commit_db_transaction #:nodoc:
- log("commit transaction", nil) { @connection.commit }
- end
-
- def exec_rollback_db_transaction #:nodoc:
- log("rollback transaction", nil) { @connection.rollback }
- end
-
# SCHEMA STATEMENTS ========================================
def primary_keys(table_name) # :nodoc:
@@ -397,6 +317,16 @@ module ActiveRecord
sql
end
+ def get_database_version # :nodoc:
+ SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)"))
+ end
+
+ def check_version # :nodoc:
+ if database_version < "3.8.0"
+ raise "Your version of SQLite (#{database_version}) is too old. Active Record supports SQLite >= 3.8."
+ end
+ end
+
private
# See https://www.sqlite.org/limits.html,
# the default value is 999 when not configured.
@@ -404,12 +334,6 @@ module ActiveRecord
999
end
- def check_version
- if sqlite_version < "3.8.0"
- raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8."
- end
- end
-
def initialize_type_map(m = type_map)
super
register_class_with_limit m, %r(int)i, SQLite3Integer
@@ -527,10 +451,6 @@ module ActiveRecord
SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}")
end
- def sqlite_version
- @sqlite_version ||= SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)"))
- end
-
def translate_exception(exception, message:, sql:, binds:)
case exception.message
# SQLite 3.8.2 returns a newly formatted error message:
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 53069cd899..040ebdb960 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -85,14 +85,14 @@ module ActiveRecord
# based on the requested role:
#
# ActiveRecord::Base.connected_to(role: :writing) do
- # Dog.create! # creates dog using dog connection
+ # Dog.create! # creates dog using dog writing connection
# end
#
# ActiveRecord::Base.connected_to(role: :reading) do
# Dog.create! # throws exception because we're on a replica
# end
#
- # ActiveRecord::Base.connected_to(role: :unknown_ode) do
+ # ActiveRecord::Base.connected_to(role: :unknown_role) do
# # raises exception due to non-existent role
# end
#
@@ -100,11 +100,20 @@ module ActiveRecord
# you can use +connected_to+ with a +database+ argument. The +database+ argument
# expects a symbol that corresponds to the database key in your config.
#
- # This will connect to a new database for the queries inside the block.
- #
# ActiveRecord::Base.connected_to(database: :animals_slow_replica) do
# Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+
# end
+ #
+ # This will connect to a new database for the queries inside the block. By
+ # default the `:writing` role will be used since all connections must be assigned
+ # a role. If you would like to use a different role you can pass a hash to database:
+ #
+ # ActiveRecord::Base.connected_to(database: { readonly_slow: :animals_slow_replica }) do
+ # # runs a long query while connected to the +animals_slow_replica+ using the readonly_slow role.
+ # Dog.run_a_long_query
+ # end
+ #
+ # When using the database key a new connection will be established every time.
def connected_to(database: nil, role: nil, &blk)
if database && role
raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments."
@@ -112,17 +121,14 @@ module ActiveRecord
if database.is_a?(Hash)
role, database = database.first
role = role.to_sym
- else
- role = database.to_sym
end
config_hash = resolve_config_for_connection(database)
handler = lookup_connection_handler(role)
- with_handler(role) do
- handler.establish_connection(config_hash)
- yield
- end
+ handler.establish_connection(config_hash)
+
+ with_handler(role, &blk)
elsif role
with_handler(role.to_sym, &blk)
else
@@ -154,6 +160,7 @@ module ActiveRecord
end
def lookup_connection_handler(handler_key) # :nodoc:
+ handler_key ||= ActiveRecord::Base.writing_role
connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index eb4b48bc37..04b21b4d00 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -466,6 +466,7 @@ module ActiveRecord
# Returns +true+ if the attributes hash has been frozen.
def frozen?
+ sync_with_transaction_state
@attributes.frozen?
end
@@ -582,12 +583,6 @@ module ActiveRecord
def initialize_internals_callback
end
- def thaw
- if frozen?
- @attributes = @attributes.dup
- end
- end
-
def custom_inspect_method_defined?
self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner
end
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
index 7431a1c759..44b5cfc738 100644
--- a/activerecord/lib/active_record/database_configurations.rb
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -7,7 +7,7 @@ require "active_record/database_configurations/url_config"
module ActiveRecord
# ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig
# objects (either a HashConfig or UrlConfig) that are constructed from the
- # application's database configuration hash or url string.
+ # application's database configuration hash or URL string.
class DatabaseConfigurations
attr_reader :configurations
delegate :any?, to: :configurations
diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb
index d30aee7c00..959e5bd4d7 100644
--- a/activerecord/lib/active_record/insert_all.rb
+++ b/activerecord/lib/active_record/insert_all.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module ActiveRecord
- class InsertAll
+ class InsertAll # :nodoc:
attr_reader :model, :connection, :inserts, :keys
attr_reader :on_duplicate, :returning, :unique_by
@@ -21,7 +21,10 @@ module ActiveRecord
end
def execute
- connection.exec_query to_sql, "Bulk Insert"
+ message = "#{model} "
+ message += "Bulk " if inserts.many?
+ message += (on_duplicate == :update ? "Upsert" : "Insert")
+ connection.exec_query to_sql, message
end
def updatable_columns
@@ -73,15 +76,11 @@ module ActiveRecord
raise ArgumentError, "#{connection.class} does not support :returning"
end
- unless %i{ raise skip update }.member?(on_duplicate)
- raise NotImplementedError, "#{on_duplicate.inspect} is an unknown value for :on_duplicate. Valid values are :raise, :skip, and :update"
- end
-
- if on_duplicate == :skip && !connection.supports_insert_on_duplicate_skip?
+ if skip_duplicates? && !connection.supports_insert_on_duplicate_skip?
raise ArgumentError, "#{connection.class} does not support skipping duplicates"
end
- if on_duplicate == :update && !connection.supports_insert_on_duplicate_update?
+ if update_duplicates? && !connection.supports_insert_on_duplicate_update?
raise ArgumentError, "#{connection.class} does not support upsert"
end
@@ -115,7 +114,7 @@ module ActiveRecord
class Builder
attr_reader :model
- delegate :skip_duplicates?, :update_duplicates?, to: :insert_all
+ delegate :skip_duplicates?, :update_duplicates?, :keys, to: :insert_all
def initialize(insert_all)
@insert_all, @model, @connection = insert_all, insert_all.model, insert_all.connection
@@ -126,27 +125,26 @@ module ActiveRecord
end
def values_list
- types = extract_types_from_columns_on(model.table_name, keys: insert_all.keys)
+ types = extract_types_from_columns_on(model.table_name, keys: keys)
values_list = insert_all.map_key_with_value do |key, value|
- bind = Relation::QueryAttribute.new(key, value, types[key])
- connection.with_yaml_fallback(bind.value_for_database)
+ connection.with_yaml_fallback(types[key].serialize(value))
end
Arel::InsertManager.new.create_values_list(values_list).to_sql
end
def returning
- quote_columns(insert_all.returning).join(",") if insert_all.returning
+ format_columns(insert_all.returning) if insert_all.returning
end
def conflict_target
if index = insert_all.unique_by
- sql = +"(#{quote_columns(index.columns).join(',')})"
+ sql = +"(#{format_columns(index.columns)})"
sql << " WHERE #{index.where}" if index.where
sql
elsif update_duplicates?
- "(#{quote_columns(insert_all.primary_keys).join(',')})"
+ "(#{format_columns(insert_all.primary_keys)})"
end
end
@@ -158,7 +156,7 @@ module ActiveRecord
attr_reader :connection, :insert_all
def columns_list
- quote_columns(insert_all.keys).join(",")
+ format_columns(insert_all.keys)
end
def extract_types_from_columns_on(table_name, keys:)
@@ -170,6 +168,10 @@ module ActiveRecord
keys.map { |key| [ key, connection.lookup_cast_type_from_column(columns[key]) ] }.to_h
end
+ def format_columns(columns)
+ quote_columns(columns).join(",")
+ end
+
def quote_columns(columns)
columns.map(&connection.method(:quote_column_name))
end
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index fa6f0d36ec..573a823dbc 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -22,6 +22,14 @@ module ActiveRecord
#
# This is +true+, by default on Rails 5.2 and above.
class_attribute :cache_versioning, instance_writer: false, default: false
+
+ ##
+ # :singleton-method:
+ # Indicates whether to use a stable #cache_key method that is accompanied
+ # by a changing version in the #cache_version method on collections.
+ #
+ # This is +false+, by default until Rails 6.1.
+ class_attribute :collection_cache_versioning, instance_writer: false, default: false
end
# Returns a +String+, which Action Pack uses for constructing a URL to this
@@ -152,6 +160,10 @@ module ActiveRecord
end
end
end
+
+ def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
+ collection.send(:compute_cache_key, timestamp_column)
+ end
end
private
@@ -180,7 +192,7 @@ module ActiveRecord
# raw_timestamp_to_cache_version(timestamp)
# # => "20181015200215266505"
#
- # Postgres truncates trailing zeros,
+ # PostgreSQL truncates trailing zeros,
# https://github.com/postgres/postgres/commit/3e1beda2cde3495f41290e1ece5d544525810214
# to account for this we pad the output with zeros
def raw_timestamp_to_cache_version(timestamp)
diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb
index 88b0c828ae..e6166581f1 100644
--- a/activerecord/lib/active_record/internal_metadata.rb
+++ b/activerecord/lib/active_record/internal_metadata.rb
@@ -17,7 +17,7 @@ module ActiveRecord
end
def table_name
- "#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}"
+ "#{table_name_prefix}#{internal_metadata_table_name}#{table_name_suffix}"
end
def []=(key, value)
@@ -44,6 +44,10 @@ module ActiveRecord
end
end
end
+
+ def drop_table
+ connection.drop_table table_name, if_exists: true
+ end
end
end
end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 4a3a31fc95..b7eecda59e 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -71,9 +71,8 @@ module ActiveRecord
end
def _touch_row(attribute_names, time)
+ @_touch_attr_names << self.class.locking_column if locking_enabled?
super
- ensure
- clear_attribute_change(self.class.locking_column) if locking_enabled?
end
def _update_row(attribute_names, attempted_action = "update")
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 6b84431343..6248c2f578 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -110,7 +110,7 @@ module ActiveRecord
end
def extract_query_source_location(locations)
- backtrace_cleaner.clean(locations).first
+ backtrace_cleaner.clean(locations.lazy).first
end
end
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 997b7f763a..f20edbeb93 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -4,9 +4,10 @@ require "benchmark"
require "set"
require "zlib"
require "active_support/core_ext/module/attribute_accessors"
+require "active_support/actionable_error"
module ActiveRecord
- class MigrationError < ActiveRecordError#:nodoc:
+ class MigrationError < ActiveRecordError #:nodoc:
def initialize(message = nil)
message = "\n\n#{message}\n\n" if message
super
@@ -87,7 +88,7 @@ module ActiveRecord
class IrreversibleMigration < MigrationError
end
- class DuplicateMigrationVersionError < MigrationError#:nodoc:
+ class DuplicateMigrationVersionError < MigrationError #:nodoc:
def initialize(version = nil)
if version
super("Multiple migrations have the version number #{version}.")
@@ -97,7 +98,7 @@ module ActiveRecord
end
end
- class DuplicateMigrationNameError < MigrationError#:nodoc:
+ class DuplicateMigrationNameError < MigrationError #:nodoc:
def initialize(name = nil)
if name
super("Multiple migrations have the name #{name}.")
@@ -117,7 +118,7 @@ module ActiveRecord
end
end
- class IllegalMigrationNameError < MigrationError#:nodoc:
+ class IllegalMigrationNameError < MigrationError #:nodoc:
def initialize(name = nil)
if name
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).")
@@ -127,7 +128,13 @@ module ActiveRecord
end
end
- class PendingMigrationError < MigrationError#:nodoc:
+ class PendingMigrationError < MigrationError #:nodoc:
+ include ActiveSupport::ActionableError
+
+ action "Run pending migrations" do
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+
def initialize(message = nil)
if !message && defined?(Rails.env)
super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate RAILS_ENV=#{::Rails.env}")
@@ -520,10 +527,10 @@ module ActiveRecord
autoload :Compatibility, "active_record/migration/compatibility"
# This must be defined before the inherited hook, below
- class Current < Migration # :nodoc:
+ class Current < Migration #:nodoc:
end
- def self.inherited(subclass) # :nodoc:
+ def self.inherited(subclass) #:nodoc:
super
if subclass.superclass == Migration
raise StandardError, "Directly inheriting from ActiveRecord::Migration is not supported. " \
@@ -541,7 +548,7 @@ module ActiveRecord
ActiveRecord::VERSION::STRING.to_f
end
- MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc:
+ MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ #:nodoc:
# This class is used to verify that all migrations have been run before
# loading a web page if <tt>config.active_record.migration_error</tt> is set to :page_load
@@ -568,10 +575,10 @@ module ActiveRecord
end
class << self
- attr_accessor :delegate # :nodoc:
- attr_accessor :disable_ddl_transaction # :nodoc:
+ attr_accessor :delegate #:nodoc:
+ attr_accessor :disable_ddl_transaction #:nodoc:
- def nearest_delegate # :nodoc:
+ def nearest_delegate #:nodoc:
delegate || superclass.nearest_delegate
end
@@ -595,13 +602,13 @@ module ActiveRecord
end
end
- def maintain_test_schema! # :nodoc:
+ def maintain_test_schema! #:nodoc:
if ActiveRecord::Base.maintain_test_schema
suppress_messages { load_schema_if_pending! }
end
end
- def method_missing(name, *args, &block) # :nodoc:
+ def method_missing(name, *args, &block) #:nodoc:
nearest_delegate.send(name, *args, &block)
end
@@ -618,7 +625,7 @@ module ActiveRecord
end
end
- def disable_ddl_transaction # :nodoc:
+ def disable_ddl_transaction #:nodoc:
self.class.disable_ddl_transaction
end
@@ -693,7 +700,7 @@ module ActiveRecord
connection.respond_to?(:reverting) && connection.reverting
end
- ReversibleBlockHelper = Struct.new(:reverting) do # :nodoc:
+ ReversibleBlockHelper = Struct.new(:reverting) do #:nodoc:
def up
yield unless reverting
end
@@ -1006,7 +1013,7 @@ module ActiveRecord
end
end
- class MigrationContext # :nodoc:
+ class MigrationContext #:nodoc:
attr_reader :migrations_paths
def initialize(migrations_paths)
@@ -1165,7 +1172,7 @@ module ActiveRecord
end
end
- class Migrator # :nodoc:
+ class Migrator #:nodoc:
class << self
attr_accessor :migrations_paths
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 8e7f596076..efed4b0e26 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -14,6 +14,8 @@ module ActiveRecord
# * change_column
# * change_column_default (must supply a :from and :to option)
# * change_column_null
+ # * change_column_comment (must supply a :from and :to option)
+ # * change_table_comment (must supply a :from and :to option)
# * create_join_table
# * create_table
# * disable_extension
@@ -35,7 +37,8 @@ module ActiveRecord
:change_column_default, :add_reference, :remove_reference, :transaction,
:drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension,
:change_column, :execute, :remove_columns, :change_column_null,
- :add_foreign_key, :remove_foreign_key
+ :add_foreign_key, :remove_foreign_key,
+ :change_column_comment, :change_table_comment
]
include JoinTable
@@ -244,6 +247,26 @@ module ActiveRecord
[:add_foreign_key, reversed_args]
end
+ def invert_change_column_comment(args)
+ table, column, options = *args
+
+ unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
+ raise ActiveRecord::IrreversibleMigration, "change_column_comment is only reversible if given a :from and :to option."
+ end
+
+ [:change_column_comment, [table, column, from: options[:to], to: options[:from]]]
+ end
+
+ def invert_change_table_comment(args)
+ table, options = *args
+
+ unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
+ raise ActiveRecord::IrreversibleMigration, "change_table_comment is only reversible if given a :from and :to option."
+ end
+
+ [:change_table_comment, [table, from: options[:to], to: options[:from]]]
+ end
+
def respond_to_missing?(method, _)
super || delegate.respond_to?(method)
end
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index abc939826b..ff91218696 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -27,6 +27,16 @@ module ActiveRecord
def invert_transaction(args, &block)
[:transaction, args, block]
end
+
+ def invert_change_column_comment(args)
+ table_name, column_name, comment = args
+ [:change_column_comment, [table_name, column_name, from: comment, to: comment]]
+ end
+
+ def invert_change_table_comment(args)
+ table_name, comment = args
+ [:change_table_comment, [table_name, from: comment, to: comment]]
+ end
end
def create_table(table_name, **options)
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index ba03a3773a..8bade8cd28 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -85,14 +85,14 @@ module ActiveRecord
# ==== Options
#
# [:returning]
- # (Postgres-only) An array of attributes to return for all successfully
+ # (PostgreSQL only) An array of attributes to return for all successfully
# inserted records, which by default is the primary key.
# Pass <tt>returning: %w[ id name ]</tt> for both id and name
- # or <tt>returning: false</tt> to omit the underlying RETURNING SQL
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
# clause entirely.
#
# [:unique_by]
- # (Postgres and SQLite only) By default rows are considered to be unique
+ # (PostgreSQL and SQLite only) By default rows are considered to be unique
# by every unique index on the table. Any duplicate rows are skipped.
#
# To skip rows according to just one unique index pass <tt>:unique_by</tt>.
@@ -107,9 +107,9 @@ module ActiveRecord
# unique_by: %i[ author_id name ]
# unique_by: :index_books_on_isbn
#
- # Because it relies on the index information from the database
- # <tt>:unique_by</tt> is recommended to be paired with
- # Active Record's schema_cache.
+ # Because it relies on the index information from the database
+ # <tt>:unique_by</tt> is recommended to be paired with
+ # Active Record's schema_cache.
#
# ==== Example
#
@@ -154,10 +154,10 @@ module ActiveRecord
# ==== Options
#
# [:returning]
- # (Postgres-only) An array of attributes to return for all successfully
+ # (PostgreSQL only) An array of attributes to return for all successfully
# inserted records, which by default is the primary key.
# Pass <tt>returning: %w[ id name ]</tt> for both id and name
- # or <tt>returning: false</tt> to omit the underlying RETURNING SQL
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
# clause entirely.
#
# ==== Examples
@@ -178,7 +178,7 @@ module ActiveRecord
InsertAll.new(self, attributes, on_duplicate: :raise, returning: returning).execute
end
- # Updates or inserts (upserts) multiple records into the database in a
+ # Updates or inserts (upserts) a single record into the database in a
# single SQL INSERT statement. It does not instantiate any models nor does
# it trigger Active Record callbacks or validations. Though passed values
# go through Active Record's type casting and serialization.
@@ -202,14 +202,14 @@ module ActiveRecord
# ==== Options
#
# [:returning]
- # (Postgres-only) An array of attributes to return for all successfully
+ # (PostgreSQL only) An array of attributes to return for all successfully
# inserted records, which by default is the primary key.
# Pass <tt>returning: %w[ id name ]</tt> for both id and name
- # or <tt>returning: false</tt> to omit the underlying RETURNING SQL
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
# clause entirely.
#
# [:unique_by]
- # (Postgres and SQLite only) By default rows are considered to be unique
+ # (PostgreSQL and SQLite only) By default rows are considered to be unique
# by every unique index on the table. Any duplicate rows are skipped.
#
# To skip rows according to just one unique index pass <tt>:unique_by</tt>.
@@ -224,9 +224,9 @@ module ActiveRecord
# unique_by: %i[ author_id name ]
# unique_by: :index_books_on_isbn
#
- # Because it relies on the index information from the database
- # <tt>:unique_by</tt> is recommended to be paired with
- # Active Record's schema_cache.
+ # Because it relies on the index information from the database
+ # <tt>:unique_by</tt> is recommended to be paired with
+ # Active Record's schema_cache.
#
# ==== Examples
#
@@ -238,7 +238,7 @@ module ActiveRecord
# { title: "Eloquent Ruby", author: "Russ", isbn: "1" }
# ], unique_by: :isbn)
#
- # Book.find_by(isbn: "1").title # => "Eloquent Ruby"
+ # Book.find_by(isbn: "1").title # => "Eloquent Ruby"
def upsert_all(attributes, returning: nil, unique_by: nil)
InsertAll.new(self, attributes, on_duplicate: :update, returning: returning, unique_by: unique_by).execute
end
@@ -530,7 +530,6 @@ module ActiveRecord
def destroy
_raise_readonly_record_error if readonly?
destroy_associations
- self.class.connection.add_transaction_record(self)
@_trigger_destroy_callback = if persisted?
destroy_row > 0
else
@@ -568,7 +567,6 @@ module ActiveRecord
became.send(:initialize)
became.instance_variable_set("@attributes", @attributes)
became.instance_variable_set("@mutations_from_database", @mutations_from_database ||= nil)
- became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.errors.copy!(errors)
@@ -853,7 +851,9 @@ module ActiveRecord
end
attribute_names = timestamp_attributes_for_update_in_model
- attribute_names |= names.map(&:to_s)
+ attribute_names |= names.map!(&:to_s).map! { |name|
+ self.class.attribute_alias?(name) ? self.class.attribute_alias(name) : name
+ }
unless attribute_names.empty?
affected_rows = _touch_row(attribute_names, time)
@@ -881,8 +881,7 @@ module ActiveRecord
time ||= current_time_from_proper_timezone
attribute_names.each do |attr_name|
- write_attribute(attr_name, time)
- clear_attribute_change(attr_name)
+ _write_attribute(attr_name, time)
end
_update_row(attribute_names, "touch")
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 81ab502824..08cfc3fe5f 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -10,10 +10,10 @@ module ActiveRecord
:first_or_create, :first_or_create!, :first_or_initialize,
:find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
:create_or_find_by, :create_or_find_by!,
- :destroy_all, :delete_all, :update_all, :destroy_by, :delete_by,
+ :destroy_all, :delete_all, :update_all, :touch_all, :destroy_by, :delete_by,
:find_each, :find_in_batches, :in_batches,
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
- :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
+ :where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
:having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
:count, :average, :minimum, :maximum, :sum, :calculate, :annotate,
:pluck, :pick, :ids
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index f021a8f6c4..e0bc5180c0 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -78,7 +78,7 @@ db_namespace = namespace :db do
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task migrate: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::Tasks::DatabaseTasks.migrate
end
@@ -128,6 +128,8 @@ db_namespace = namespace :db do
# desc 'Runs the "up" for a given migration VERSION.'
task up: :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:up")
+
raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
ActiveRecord::Tasks::DatabaseTasks.check_target_version
@@ -139,8 +141,29 @@ db_namespace = namespace :db do
db_namespace["_dump"].invoke
end
+ namespace :up do
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ task spec_name => :load_config do
+ raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
+
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
+
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+ ActiveRecord::Base.connection.migration_context.run(
+ :up,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
+
+ db_namespace["_dump"].invoke
+ end
+ end
+ end
+
# desc 'Runs the "down" for a given migration VERSION.'
task down: :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:down")
+
raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty?
ActiveRecord::Tasks::DatabaseTasks.check_target_version
@@ -152,9 +175,28 @@ db_namespace = namespace :db do
db_namespace["_dump"].invoke
end
+ namespace :down do
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ task spec_name => :load_config do
+ raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
+
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
+
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+ ActiveRecord::Base.connection.migration_context.run(
+ :down,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
+
+ db_namespace["_dump"].invoke
+ end
+ end
+ end
+
desc "Display status of migrations"
task status: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
ActiveRecord::Tasks::DatabaseTasks.migrate_status
end
@@ -222,6 +264,16 @@ db_namespace = namespace :db do
desc "Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)"
task setup: ["db:schema:load_if_ruby", "db:structure:load_if_sql", :seed]
+ desc "Runs setup if database does not exist, or runs migrations if it does"
+ task prepare: :load_config do
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.config)
+ db_namespace["migrate"].invoke
+ rescue ActiveRecord::NoDatabaseError
+ db_namespace["setup"].invoke
+ end
+ end
+
desc "Loads the seed data from db/seeds.rb"
task seed: :load_config do
db_namespace["abort_if_pending_migrations"].invoke
@@ -285,7 +337,7 @@ db_namespace = namespace :db do
desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record"
task dump: :load_config do
require "active_record/schema_dumper"
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby)
File.open(filename, "w:utf-8") do |file|
ActiveRecord::Base.establish_connection(db_config.config)
@@ -308,7 +360,7 @@ db_namespace = namespace :db do
namespace :cache do
desc "Creates a db/schema_cache.yml file."
task dump: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name)
ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(
@@ -320,7 +372,7 @@ db_namespace = namespace :db do
desc "Clears a db/schema_cache.yml file."
task clear: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name)
rm_f filename, verbose: false
end
@@ -331,7 +383,7 @@ db_namespace = namespace :db do
namespace :structure do
desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql"
task dump: :load_config do
- ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(db_config.config, filename)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 3452cf971b..1312bf6f91 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -21,12 +21,12 @@ module ActiveRecord
def add_reflection(ar, name, reflection)
ar.clear_reflections_cache
- name = name.to_s
+ name = -name.to_s
ar._reflections = ar._reflections.except(name).merge!(name => reflection)
end
def add_aggregate_reflection(ar, name, reflection)
- ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
+ ar.aggregate_reflections = ar.aggregate_reflections.merge(-name.to_s => reflection)
end
private
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 36c2422d84..ab24f67a6d 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -291,31 +291,99 @@ module ActiveRecord
limit_value ? records.many? : size > 1
end
- # Returns a cache key that can be used to identify the records fetched by
- # this query. The cache key is built with a fingerprint of the sql query,
- # the number of records matched by the query and a timestamp of the last
- # updated record. When a new record comes to match the query, or any of
- # the existing records is updated or deleted, the cache key changes.
+ # Returns a stable cache key that can be used to identify this query.
+ # The cache key is built with a fingerprint of the SQL query.
#
- # Product.where("name like ?", "%Cosmic Encounter%").cache_key
- # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
+ # Product.where("name like ?", "%Cosmic Encounter%").cache_key
+ # # => "products/query-1850ab3d302391b85b8693e941286659"
#
- # If the collection is loaded, the method will iterate through the records
- # to generate the timestamp, otherwise it will trigger one SQL query like:
+ # If ActiveRecord::Base.collection_cache_versioning is turned off, as it was
+ # in Rails 6.0 and earlier, the cache key will also include a version.
#
- # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
+ # ActiveRecord::Base.collection_cache_versioning = false
+ # Product.where("name like ?", "%Cosmic Encounter%").cache_key
+ # # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
#
# You can also pass a custom timestamp column to fetch the timestamp of the
# last updated record.
#
# Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at)
- #
- # You can customize the strategy to generate the key on a per model basis
- # overriding ActiveRecord::Base#collection_cache_key.
def cache_key(timestamp_column = :updated_at)
@cache_keys ||= {}
- @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column)
+ @cache_keys[timestamp_column] ||= klass.collection_cache_key(self, timestamp_column)
+ end
+
+ def compute_cache_key(timestamp_column = :updated_at) # :nodoc:
+ query_signature = ActiveSupport::Digest.hexdigest(to_sql)
+ key = "#{klass.model_name.cache_key}/query-#{query_signature}"
+
+ if cache_version(timestamp_column)
+ key
+ else
+ "#{key}-#{compute_cache_version(timestamp_column)}"
+ end
+ end
+ private :compute_cache_key
+
+ # Returns a cache version that can be used together with the cache key to form
+ # a recyclable caching scheme. The cache version is built with the number of records
+ # matching the query, and the timestamp of the last updated record. When a new record
+ # comes to match the query, or any of the existing records is updated or deleted,
+ # the cache version changes.
+ #
+ # If the collection is loaded, the method will iterate through the records
+ # to generate the timestamp, otherwise it will trigger one SQL query like:
+ #
+ # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%')
+ def cache_version(timestamp_column = :updated_at)
+ if collection_cache_versioning
+ @cache_versions ||= {}
+ @cache_versions[timestamp_column] ||= compute_cache_version(timestamp_column)
+ end
+ end
+
+ def compute_cache_version(timestamp_column) # :nodoc:
+ if loaded? || distinct_value
+ size = records.size
+ if size > 0
+ timestamp = max_by(&timestamp_column)._read_attribute(timestamp_column)
+ end
+ else
+ collection = eager_loading? ? apply_join_dependency : self
+
+ column = connection.visitor.compile(arel_attribute(timestamp_column))
+ select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
+
+ if collection.has_limit_or_offset?
+ query = collection.select("#{column} AS collection_cache_key_timestamp")
+ subquery_alias = "subquery_for_cache_key"
+ subquery_column = "#{subquery_alias}.collection_cache_key_timestamp"
+ arel = query.build_subquery(subquery_alias, select_values % subquery_column)
+ else
+ query = collection.unscope(:order)
+ query.select_values = [select_values % column]
+ arel = query.arel
+ end
+
+ result = connection.select_one(arel, nil)
+
+ if result
+ column_type = klass.type_for_attribute(timestamp_column)
+ timestamp = column_type.deserialize(result["timestamp"])
+ size = result["size"]
+ else
+ timestamp = nil
+ size = 0
+ end
+ end
+
+ if timestamp
+ "#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}"
+ else
+ "#{size}"
+ end
end
+ private :compute_cache_version
# Scope all queries to the current scope.
#
@@ -389,8 +457,6 @@ module ActiveRecord
stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
end
- stmt.comment(*arel.comment_node.values) if arel.comment_node
-
@klass.connection.update stmt, "#{@klass} Update All"
end
@@ -506,7 +572,6 @@ module ActiveRecord
stmt.offset(arel.offset)
stmt.order(*arel.orders)
stmt.wheres = arel.constraints
- stmt.comment(*arel.comment_node.values) if arel.comment_node
affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
@@ -679,6 +744,10 @@ module ActiveRecord
@loaded = true
end
+ def null_relation? # :nodoc:
+ is_a?(NullRelation)
+ end
+
private
def already_in_scope?
@delegate_to_klass && begin
@@ -728,7 +797,7 @@ module ActiveRecord
@records =
if eager_loading?
apply_join_dependency do |relation, join_dependency|
- if ActiveRecord::NullRelation === relation
+ if relation.null_relation?
[]
else
relation = join_dependency.apply_column_aliases(relation)
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 4f9ddf302e..801e312658 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -129,11 +129,12 @@ module ActiveRecord
relation = apply_join_dependency
if operation.to_s.downcase == "count"
- relation.distinct!
- # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
- if (column_name == :all || column_name.nil?) && select_values.empty?
- relation.order_values = []
+ unless distinct_value || distinct_select?(column_name || select_for_count)
+ relation.distinct!
+ relation.select_values = [ klass.primary_key || table[Arel.star] ]
end
+ # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
+ relation.order_values = []
end
relation.calculate(operation, column_name)
@@ -307,25 +308,22 @@ module ActiveRecord
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
- group_attrs = group_values
+ group_fields = group_values
- if group_attrs.first.respond_to?(:to_sym)
- association = @klass._reflect_on_association(group_attrs.first)
- associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
- group_fields = Array(associated ? association.foreign_key : group_attrs)
- else
- group_fields = group_attrs
+ if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)
+ association = klass._reflect_on_association(group_fields.first)
+ associated = association && association.belongs_to? # only count belongs_to associations
+ group_fields = Array(association.foreign_key) if associated
end
group_fields = arel_columns(group_fields)
- group_aliases = group_fields.map { |field| column_alias_for(field) }
+ group_aliases = group_fields.map { |field|
+ field = connection.visitor.compile(field) if Arel.arel_node?(field)
+ column_alias_for(field.to_s.downcase)
+ }
group_columns = group_aliases.zip(group_fields)
- if operation == "count" && column_name == :all
- aggregate_alias = "count_all"
- else
- aggregate_alias = column_alias_for([operation, column_name].join(" "))
- end
+ aggregate_alias = column_alias_for("#{operation}_#{column_name.to_s.downcase}")
select_values = [
operation_over_aggregate_column(
@@ -344,7 +342,7 @@ module ActiveRecord
}
relation = except(:group).distinct!(false)
- relation.group_values = group_fields
+ relation.group_values = group_aliases
relation.select_values = select_values
calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil) }
@@ -370,25 +368,23 @@ module ActiveRecord
end]
end
- # Converts the given keys to the value that the database adapter returns as
+ # Converts the given field to the value that the database adapter returns as
# a usable column name:
#
# column_alias_for("users.id") # => "users_id"
# column_alias_for("sum(id)") # => "sum_id"
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
- def column_alias_for(keys)
- if keys.respond_to? :name
- keys = "#{keys.relation.name}.#{keys.name}"
- end
+ def column_alias_for(field)
+ return field if field.match?(/\A\w{,#{connection.table_alias_length}}\z/)
- table_name = keys.to_s.downcase
- table_name.gsub!(/\*/, "all")
- table_name.gsub!(/\W+/, " ")
- table_name.strip!
- table_name.gsub!(/ +/, "_")
+ column_alias = +field
+ column_alias.gsub!(/\*/, "all")
+ column_alias.gsub!(/\W+/, " ")
+ column_alias.strip!
+ column_alias.gsub!(/ +/, "_")
- @klass.connection.table_alias_for(table_name)
+ connection.table_alias_for(column_alias)
end
def type_for(field, &block)
@@ -416,16 +412,17 @@ module ActiveRecord
def build_count_subquery(relation, column_name, distinct)
if column_name == :all
+ column_alias = Arel.star
relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
else
column_alias = Arel.sql("count_column")
relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
end
- subquery = relation.arel.as(Arel.sql("subquery_for_count"))
- select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false)
+ subquery_alias = Arel.sql("subquery_for_count")
+ select_value = operation_over_aggregate_column(column_alias, "count", false)
- Arel::SelectManager.new(subquery).project(select_value)
+ relation.build_subquery(subquery_alias, select_value)
end
end
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 7a53a9d1c7..d59331053e 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -45,7 +45,10 @@ module ActiveRecord
private
def generated_relation_methods
- @generated_relation_methods ||= GeneratedRelationMethods.new
+ @generated_relation_methods ||= GeneratedRelationMethods.new.tap do |mod|
+ const_set(:GeneratedRelationMethods, mod)
+ private_constant :GeneratedRelationMethods
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index e2efd4aa0d..9450e4d3c5 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -314,7 +314,7 @@ module ActiveRecord
relation = construct_relation_for_exists(conditions)
- skip_query_cache_if_necessary { connection.select_one(relation.arel, "#{name} Exists") } ? true : false
+ skip_query_cache_if_necessary { connection.select_one(relation.arel, "#{name} Exists?") } ? true : false
end
# This method is called whenever no records are found with either a single
@@ -370,12 +370,6 @@ module ActiveRecord
relation
end
- def construct_join_dependency(associations)
- ActiveRecord::Associations::JoinDependency.new(
- klass, table, associations
- )
- end
-
def apply_join_dependency(eager_loading: group_values.empty?)
join_dependency = construct_join_dependency(eager_load_values + includes_values)
relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 4de7465128..6bb77b355c 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -117,16 +117,14 @@ module ActiveRecord
if other.klass == relation.klass
relation.joins!(*other.joins_values)
else
- joins_dependency = other.joins_values.map do |join|
+ associations, others = other.joins_values.partition do |join|
case join
- when Hash, Symbol, Array
- other.send(:construct_join_dependency, join)
- else
- join
+ when Hash, Symbol, Array; true
end
end
- relation.joins!(*joins_dependency)
+ join_dependency = other.construct_join_dependency(associations)
+ relation.joins!(join_dependency, *others)
end
end
@@ -136,16 +134,9 @@ module ActiveRecord
if other.klass == relation.klass
relation.left_outer_joins!(*other.left_outer_joins_values)
else
- joins_dependency = other.left_outer_joins_values.map do |join|
- case join
- when Hash, Symbol, Array
- other.send(:construct_join_dependency, join)
- else
- join
- end
- end
-
- relation.left_outer_joins!(*joins_dependency)
+ associations = other.left_outer_joins_values
+ join_dependency = other.construct_join_dependency(associations)
+ relation.joins!(join_dependency)
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 6f0f2125dc..90b5e9a118 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -100,7 +100,7 @@ module ActiveRecord
#
# === conditions
#
- # If you want to add conditions to your included models you'll have
+ # If you want to add string conditions to your included models, you'll have
# to explicitly reference them. For example:
#
# User.includes(:posts).where('posts.name = ?', 'example')
@@ -111,6 +111,12 @@ module ActiveRecord
#
# Note that #includes works with association names while #references needs
# the actual table name.
+ #
+ # If you pass the conditions via hash, you don't need to call #references
+ # explicitly, as #where references the tables for you. For example, this
+ # will work correctly:
+ #
+ # User.includes(:posts).where(posts: { name: 'example' })
def includes(*args)
check_if_method_has_arguments!(:includes, args)
spawn.includes!(*args)
@@ -154,6 +160,19 @@ module ActiveRecord
self
end
+ # Extracts a named +association+ from the relation. The named association is first preloaded,
+ # then the individual association records are collected from the relation. Like so:
+ #
+ # account.memberships.extract_associated(:user)
+ # # => Returns collection of User records
+ #
+ # This is short-hand for:
+ #
+ # account.memberships.preload(:user).collect(&:user)
+ def extract_associated(association)
+ preload(association).collect(&association)
+ end
+
# Use to indicate that the given +table_names+ are referenced by an SQL string,
# and should therefore be JOINed in any query rather than loaded separately.
# This method only works in conjunction with #includes.
@@ -233,9 +252,6 @@ module ActiveRecord
def _select!(*fields) # :nodoc:
fields.reject!(&:blank?)
fields.flatten!
- fields.map! do |field|
- klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
- end
self.select_values += fields
self
end
@@ -973,6 +989,21 @@ module ActiveRecord
@arel ||= build_arel(aliases)
end
+ def construct_join_dependency(associations) # :nodoc:
+ ActiveRecord::Associations::JoinDependency.new(
+ klass, table, associations
+ )
+ end
+
+ protected
+ def build_subquery(subquery_alias, select_value) # :nodoc:
+ subquery = except(:optimizer_hints).arel.as(subquery_alias)
+
+ Arel::SelectManager.new(subquery).project(select_value).tap do |arel|
+ arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty?
+ end
+ end
+
private
# Returns a relation value with a given name
def get_value(name)
@@ -993,8 +1024,11 @@ module ActiveRecord
def build_arel(aliases)
arel = Arel::SelectManager.new(table)
- aliases = build_joins(arel, joins_values.flatten, aliases) unless joins_values.empty?
- build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases) unless left_outer_joins_values.empty?
+ if !joins_values.empty?
+ build_joins(arel, joins_values.flatten, aliases)
+ elsif !left_outer_joins_values.empty?
+ build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases)
+ end
arel.where(where_clause.ast) unless where_clause.empty?
arel.having(having_clause.ast) unless having_clause.empty?
@@ -1044,22 +1078,28 @@ module ActiveRecord
end
end
- def build_left_outer_joins(manager, outer_joins, aliases)
- buckets = outer_joins.group_by do |join|
- case join
+ def valid_association_list(associations)
+ associations.each do |association|
+ case association
when Hash, Symbol, Array
- :association_join
- when ActiveRecord::Associations::JoinDependency
- :stashed_join
+ # valid
else
raise ArgumentError, "only Hash, Symbol and Array are allowed"
end
end
+ end
+ def build_left_outer_joins(manager, outer_joins, aliases)
+ buckets = { association_join: valid_association_list(outer_joins) }
build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases)
end
def build_joins(manager, joins, aliases)
+ unless left_outer_joins_values.empty?
+ left_joins = valid_association_list(left_outer_joins_values.flatten)
+ joins << construct_join_dependency(left_joins)
+ end
+
buckets = joins.group_by do |join|
case join
when String
@@ -1121,9 +1161,9 @@ module ActiveRecord
case field
when Symbol
field = field.to_s
- arel_column(field) { connection.quote_table_name(field) }
+ arel_column(field, &connection.method(:quote_table_name))
when String
- arel_column(field) { field }
+ arel_column(field, &:itself)
when Proc
field.call
else
@@ -1139,7 +1179,7 @@ module ActiveRecord
if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
arel_attribute(field)
else
- yield
+ yield field
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index d475e77444..2f7cc07221 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -47,6 +47,7 @@ module ActiveRecord
end
private
+ attr_accessor :table_name
def initialize(connection, options = {})
@connection = connection
@@ -110,6 +111,8 @@ HEADER
def table(table, stream)
columns = @connection.columns(table)
begin
+ self.table_name = table
+
tbl = StringIO.new
# first dump primary key column
@@ -159,6 +162,8 @@ HEADER
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
stream.puts "# #{e.message}"
stream.puts
+ ensure
+ self.table_name = nil
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 1fca1a18f6..74547de862 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -19,7 +19,7 @@ module ActiveRecord
end
def table_name
- "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
+ "#{table_name_prefix}#{schema_migrations_table_name}#{table_name_suffix}"
end
def table_exists?
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 681a5c6250..cd9801b7a0 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -58,7 +58,7 @@ module ActiveRecord
end
def default_extensions # :nodoc:
- if scope = current_scope || build_default_scope
+ if scope = scope_for_association || build_default_scope
scope.extensions
else
[]
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 3537e2d008..6fecb06897 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -11,6 +11,12 @@ module ActiveRecord
# of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
# already built around just accessing attributes on the model.
#
+ # Every accessor comes with dirty tracking methods (+key_changed?+, +key_was+ and +key_change+) and
+ # methods to access the changes made during the last save (+saved_change_to_key?+, +saved_change_to_key+ and
+ # +key_before_last_save+).
+ #
+ # NOTE: There is no +key_will_change!+ method for accessors, use +store_will_change!+ instead.
+ #
# Make sure that you declare the database column used for the serialized store as a text, so there's
# plenty of room.
#
@@ -49,6 +55,12 @@ module ActiveRecord
# u.settings[:country] # => 'Denmark'
# u.settings['country'] # => 'Denmark'
#
+ # # Dirty tracking
+ # u.color = 'green'
+ # u.color_changed? # => true
+ # u.color_was # => 'black'
+ # u.color_change # => ['black', 'red']
+ #
# # Add additional accessors to an existing store through store_accessor
# class SuperUser < User
# store_accessor :settings, :privileges, :servants
@@ -127,6 +139,42 @@ module ActiveRecord
define_method(accessor_key) do
read_store_attribute(store_attribute, key)
end
+
+ define_method("#{accessor_key}_changed?") do
+ return false unless attribute_changed?(store_attribute)
+ prev_store, new_store = changes[store_attribute]
+ prev_store&.dig(key) != new_store&.dig(key)
+ end
+
+ define_method("#{accessor_key}_change") do
+ return unless attribute_changed?(store_attribute)
+ prev_store, new_store = changes[store_attribute]
+ [prev_store&.dig(key), new_store&.dig(key)]
+ end
+
+ define_method("#{accessor_key}_was") do
+ return unless attribute_changed?(store_attribute)
+ prev_store, _new_store = changes[store_attribute]
+ prev_store&.dig(key)
+ end
+
+ define_method("saved_change_to_#{accessor_key}?") do
+ return false unless saved_change_to_attribute?(store_attribute)
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
+ prev_store&.dig(key) != new_store&.dig(key)
+ end
+
+ define_method("saved_change_to_#{accessor_key}") do
+ return unless saved_change_to_attribute?(store_attribute)
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
+ [prev_store&.dig(key), new_store&.dig(key)]
+ end
+
+ define_method("#{accessor_key}_before_last_save") do
+ return unless saved_change_to_attribute?(store_attribute)
+ prev_store, _new_store = saved_change_to_attribute(store_attribute)
+ prev_store&.dig(key)
+ end
end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 155d2b0b98..c79ed8db60 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -142,6 +142,8 @@ module ActiveRecord
end
def for_each
+ return {} unless defined?(Rails)
+
databases = Rails.application.config.load_database_yaml
database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env)
@@ -153,6 +155,20 @@ module ActiveRecord
end
end
+ def raise_for_multi_db(environment = env, command:)
+ db_configs = ActiveRecord::Base.configurations.configs_for(env_name: environment)
+
+ if db_configs.count > 1
+ dbs_list = []
+
+ db_configs.each do |db|
+ dbs_list << "#{command}:#{db.spec_name}"
+ end
+
+ raise "You're using a multiple database application. To use `#{command}` you must run the namespaced task with a VERSION. Available tasks are #{dbs_list.to_sentence}."
+ end
+ end
+
def create_current(environment = env)
each_current_configuration(environment) { |configuration|
create configuration
@@ -186,8 +202,8 @@ module ActiveRecord
ActiveRecord::Base.connected_to(database: { truncation: configuration }) do
table_names = ActiveRecord::Base.connection.tables
table_names -= [
- ActiveRecord::Base.schema_migrations_table_name,
- ActiveRecord::Base.internal_metadata_table_name
+ SchemaMigration.table_name,
+ InternalMetadata.table_name
]
ActiveRecord::Base.connection.truncate_tables(*table_names)
diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb
index f70b7c50a2..980e42664b 100644
--- a/activerecord/lib/active_record/touch_later.rb
+++ b/activerecord/lib/active_record/touch_later.rb
@@ -2,7 +2,7 @@
module ActiveRecord
# = Active Record Touch Later
- module TouchLater
+ module TouchLater # :nodoc:
extend ActiveSupport::Concern
included do
@@ -22,7 +22,7 @@ module ActiveRecord
@_touch_time = current_time_from_proper_timezone
surreptitiously_touch @_defer_touch_attrs
- self.class.connection.add_transaction_record self
+ add_to_transaction
# touch the parents as we are not calling the after_save callbacks
self.class.reflect_on_all_associations(:belongs_to).each do |r|
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index fe3842b905..ea288456b9 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -234,6 +234,12 @@ module ActiveRecord
set_callback(:commit, :after, *args, &block)
end
+ # Shortcut for <tt>after_commit :hook, on: [ :create, :update ]</tt>.
+ def after_save_commit(*args, &block)
+ set_options_for_callbacks!(args, on: [ :create, :update ])
+ set_callback(:commit, :after, *args, &block)
+ end
+
# Shortcut for <tt>after_commit :hook, on: :create</tt>.
def after_create_commit(*args, &block)
set_options_for_callbacks!(args, on: :create)
@@ -349,18 +355,6 @@ module ActiveRecord
clear_transaction_record_state
end
- # Add the record to the current transaction so that the #after_rollback and #after_commit callbacks
- # can be called.
- def add_to_transaction
- if has_transactional_callbacks?
- self.class.connection.add_transaction_record(self)
- else
- sync_with_transaction_state
- set_transaction_state(self.class.connection.transaction_state)
- end
- remember_transaction_record_state
- end
-
# Executes +method+ within a transaction and captures its return value as a
# status flag. If the status is true the transaction is committed, otherwise
# a ROLLBACK is issued. In any case the status flag is returned.
@@ -370,9 +364,19 @@ module ActiveRecord
def with_transaction_returning_status
status = nil
self.class.transaction do
- add_to_transaction
+ unless has_transactional_callbacks?
+ sync_with_transaction_state
+ @transaction_state = self.class.connection.transaction_state
+ end
+ remember_transaction_record_state
+
status = yield
raise ActiveRecord::Rollback unless status
+ ensure
+ if has_transactional_callbacks? &&
+ (@_new_record_before_last_commit && !new_record? || _trigger_update_callback || _trigger_destroy_callback)
+ add_to_transaction
+ end
end
status
end
@@ -386,6 +390,7 @@ module ActiveRecord
id: id,
new_record: @new_record,
destroyed: @destroyed,
+ attributes: @attributes,
frozen?: frozen?,
)
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
@@ -409,20 +414,27 @@ module ActiveRecord
# Force to clear the transaction record state.
def force_clear_transaction_record_state
@_start_transaction_state.clear
+ @transaction_state = nil
end
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
- def restore_transaction_record_state(force = false)
+ def restore_transaction_record_state(force_restore_state = false)
unless @_start_transaction_state.empty?
transaction_level = (@_start_transaction_state[:level] || 0) - 1
- if transaction_level < 1 || force
+ if transaction_level < 1 || force_restore_state
restore_state = @_start_transaction_state
- thaw
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
+ @attributes = restore_state[:attributes].map do |attr|
+ value = @attributes.fetch_value(attr.name)
+ attr = attr.with_value_from_user(value) if attr.value != value
+ attr
+ end
+ @mutations_from_database = nil
+ @mutations_before_last_save = nil
pk = self.class.primary_key
- if pk && _read_attribute(pk) != restore_state[:id]
- _write_attribute(pk, restore_state[:id])
+ if pk && @attributes.fetch_value(pk) != restore_state[:id]
+ @attributes.write_from_user(pk, restore_state[:id])
end
freeze if restore_state[:frozen?]
end
@@ -443,8 +455,10 @@ module ActiveRecord
end
end
- def set_transaction_state(state)
- @transaction_state = state
+ # Add the record to the current transaction so that the #after_rollback and #after_commit
+ # callbacks can be called.
+ def add_to_transaction
+ self.class.connection.add_transaction_record(self)
end
def has_transactional_callbacks?
@@ -464,19 +478,17 @@ module ActiveRecord
# This method checks to see if the ActiveRecord object's state reflects
# the TransactionState, and rolls back or commits the Active Record object
# as appropriate.
- #
- # Since Active Record objects can be inside multiple transactions, this
- # method recursively goes through the parent of the TransactionState and
- # checks if the Active Record object reflects the state of the object.
def sync_with_transaction_state
- update_attributes_from_transaction_state(@transaction_state)
- end
-
- def update_attributes_from_transaction_state(transaction_state)
- if transaction_state && transaction_state.finalized?
- restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback?
- force_clear_transaction_record_state if transaction_state.fully_committed?
- clear_transaction_record_state if transaction_state.fully_completed?
+ if (transaction_state = @transaction_state)&.finalized?
+ if transaction_state.fully_committed?
+ force_clear_transaction_record_state
+ elsif transaction_state.committed?
+ clear_transaction_record_state
+ elsif transaction_state.rolledback?
+ force_restore_state = transaction_state.fully_rolledback?
+ restore_transaction_record_state(force_restore_state)
+ clear_transaction_record_state
+ end
end
end
end
diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb
index 56249b2bad..a419975335 100644
--- a/activerecord/lib/arel/nodes/delete_statement.rb
+++ b/activerecord/lib/arel/nodes/delete_statement.rb
@@ -3,7 +3,7 @@
module Arel # :nodoc: all
module Nodes
class DeleteStatement < Arel::Nodes::Node
- attr_accessor :left, :right, :orders, :limit, :offset, :key, :comment
+ attr_accessor :left, :right, :orders, :limit, :offset, :key
alias :relation :left
alias :relation= :left=
@@ -18,18 +18,16 @@ module Arel # :nodoc: all
@limit = nil
@offset = nil
@key = nil
- @comment = nil
end
def initialize_copy(other)
super
@left = @left.clone if @left
@right = @right.clone if @right
- @comment = @comment.clone if @comment
end
def hash
- [self.class, @left, @right, @orders, @limit, @offset, @key, @comment].hash
+ [self.class, @left, @right, @orders, @limit, @offset, @key].hash
end
def eql?(other)
@@ -39,8 +37,7 @@ module Arel # :nodoc: all
self.orders == other.orders &&
self.limit == other.limit &&
self.offset == other.offset &&
- self.key == other.key &&
- self.comment == other.comment
+ self.key == other.key
end
alias :== :eql?
end
diff --git a/activerecord/lib/arel/nodes/insert_statement.rb b/activerecord/lib/arel/nodes/insert_statement.rb
index 8430dd23da..d28fd1f6c8 100644
--- a/activerecord/lib/arel/nodes/insert_statement.rb
+++ b/activerecord/lib/arel/nodes/insert_statement.rb
@@ -3,7 +3,7 @@
module Arel # :nodoc: all
module Nodes
class InsertStatement < Arel::Nodes::Node
- attr_accessor :relation, :columns, :values, :select, :comment
+ attr_accessor :relation, :columns, :values, :select
def initialize
super()
@@ -11,7 +11,6 @@ module Arel # :nodoc: all
@columns = []
@values = nil
@select = nil
- @comment = nil
end
def initialize_copy(other)
@@ -19,11 +18,10 @@ module Arel # :nodoc: all
@columns = @columns.clone
@values = @values.clone if @values
@select = @select.clone if @select
- @comment = @comment.clone if @comment
end
def hash
- [@relation, @columns, @values, @select, @comment].hash
+ [@relation, @columns, @values, @select].hash
end
def eql?(other)
@@ -31,8 +29,7 @@ module Arel # :nodoc: all
self.relation == other.relation &&
self.columns == other.columns &&
self.select == other.select &&
- self.values == other.values &&
- self.comment == other.comment
+ self.values == other.values
end
alias :== :eql?
end
diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb
index b6154b7ff4..11b4f39ece 100644
--- a/activerecord/lib/arel/nodes/select_core.rb
+++ b/activerecord/lib/arel/nodes/select_core.rb
@@ -40,7 +40,6 @@ module Arel # :nodoc: all
@groups = @groups.clone
@havings = @havings.clone
@windows = @windows.clone
- @comment = @comment.clone if @comment
end
def hash
diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb
index 015bcd7613..cfaa19e392 100644
--- a/activerecord/lib/arel/nodes/update_statement.rb
+++ b/activerecord/lib/arel/nodes/update_statement.rb
@@ -3,7 +3,7 @@
module Arel # :nodoc: all
module Nodes
class UpdateStatement < Arel::Nodes::Node
- attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key, :comment
+ attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key
def initialize
@relation = nil
@@ -13,18 +13,16 @@ module Arel # :nodoc: all
@limit = nil
@offset = nil
@key = nil
- @comment = nil
end
def initialize_copy(other)
super
@wheres = @wheres.clone
@values = @values.clone
- @comment = @comment.clone if @comment
end
def hash
- [@relation, @wheres, @values, @orders, @limit, @offset, @key, @comment].hash
+ [@relation, @wheres, @values, @orders, @limit, @offset, @key].hash
end
def eql?(other)
@@ -35,8 +33,7 @@ module Arel # :nodoc: all
self.orders == other.orders &&
self.limit == other.limit &&
self.offset == other.offset &&
- self.key == other.key &&
- self.comment == other.comment
+ self.key == other.key
end
alias :== :eql?
end
diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb
index 4e9f527235..ddc9e394dd 100644
--- a/activerecord/lib/arel/select_manager.rb
+++ b/activerecord/lib/arel/select_manager.rb
@@ -249,10 +249,6 @@ module Arel # :nodoc: all
self
end
- def comment_node
- @ctx.comment
- end
-
private
def collapse(exprs)
exprs = exprs.compact
diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb
index 326c4f995c..0476399618 100644
--- a/activerecord/lib/arel/tree_manager.rb
+++ b/activerecord/lib/arel/tree_manager.rb
@@ -36,11 +36,6 @@ module Arel # :nodoc: all
@ast.wheres << expr
self
end
-
- def comment(*values)
- @ast.comment = Nodes::Comment.new(values)
- self
- end
end
attr_reader :ast
diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb
index f96bf65ee5..500974dff5 100644
--- a/activerecord/lib/arel/visitors/oracle.rb
+++ b/activerecord/lib/arel/visitors/oracle.rb
@@ -87,6 +87,50 @@ module Arel # :nodoc: all
collector << " )"
end
+ def visit_Arel_Nodes_In(o, collector)
+ if Array === o.right && !o.right.empty?
+ o.right.delete_if { |value| unboundable?(value) }
+ end
+
+ if Array === o.right && o.right.empty?
+ collector << "1=0"
+ else
+ first = true
+ o.right.each_slice(in_clause_length) do |sliced_o_right|
+ collector << " OR " unless first
+ first = false
+
+ collector = visit o.left, collector
+ collector << " IN ("
+ visit(sliced_o_right, collector)
+ collector << ")"
+ end
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_NotIn(o, collector)
+ if Array === o.right && !o.right.empty?
+ o.right.delete_if { |value| unboundable?(value) }
+ end
+
+ if Array === o.right && o.right.empty?
+ collector << "1=1"
+ else
+ first = true
+ o.right.each_slice(in_clause_length) do |sliced_o_right|
+ collector << " AND " unless first
+ first = false
+
+ collector = visit o.left, collector
+ collector << " NOT IN ("
+ visit(sliced_o_right, collector)
+ collector << ")"
+ end
+ end
+ collector
+ end
+
def visit_Arel_Nodes_UpdateStatement(o, collector)
# Oracle does not allow ORDER BY/LIMIT in UPDATEs.
if o.orders.any? && o.limit.nil?
@@ -154,6 +198,10 @@ module Arel # :nodoc: all
collector = visit [o.left, o.right, 0, 1], collector
collector << ")"
end
+
+ def in_clause_length
+ 1000
+ end
end
end
end
diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb
index 9a7fe4d626..8e0f07fca9 100644
--- a/activerecord/lib/arel/visitors/oracle12.rb
+++ b/activerecord/lib/arel/visitors/oracle12.rb
@@ -41,6 +41,50 @@ module Arel # :nodoc: all
collector << " )"
end
+ def visit_Arel_Nodes_In(o, collector)
+ if Array === o.right && !o.right.empty?
+ o.right.delete_if { |value| unboundable?(value) }
+ end
+
+ if Array === o.right && o.right.empty?
+ collector << "1=0"
+ else
+ first = true
+ o.right.each_slice(in_clause_length) do |sliced_o_right|
+ collector << " OR " unless first
+ first = false
+
+ collector = visit o.left, collector
+ collector << " IN ("
+ visit(sliced_o_right, collector)
+ collector << ")"
+ end
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_NotIn(o, collector)
+ if Array === o.right && !o.right.empty?
+ o.right.delete_if { |value| unboundable?(value) }
+ end
+
+ if Array === o.right && o.right.empty?
+ collector << "1=1"
+ else
+ first = true
+ o.right.each_slice(in_clause_length) do |sliced_o_right|
+ collector << " AND " unless first
+ first = false
+
+ collector = visit o.left, collector
+ collector << " NOT IN ("
+ visit(sliced_o_right, collector)
+ collector << ")"
+ end
+ end
+ collector
+ end
+
def visit_Arel_Nodes_UpdateStatement(o, collector)
# Oracle does not allow ORDER BY/LIMIT in UPDATEs.
if o.orders.any? && o.limit.nil?
@@ -62,6 +106,10 @@ module Arel # :nodoc: all
collector = visit [o.left, o.right, 0, 1], collector
collector << ")"
end
+
+ def in_clause_length
+ 1000
+ end
end
end
end
diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb
index 4192d9efdc..277d553e6c 100644
--- a/activerecord/lib/arel/visitors/to_sql.rb
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -35,7 +35,6 @@ module Arel # :nodoc: all
collect_nodes_for o.wheres, collector, " WHERE ", " AND "
collect_nodes_for o.orders, collector, " ORDER BY "
maybe_visit o.limit, collector
- maybe_visit o.comment, collector
end
def visit_Arel_Nodes_UpdateStatement(o, collector)
@@ -48,7 +47,6 @@ module Arel # :nodoc: all
collect_nodes_for o.wheres, collector, " WHERE ", " AND "
collect_nodes_for o.orders, collector, " ORDER BY "
maybe_visit o.limit, collector
- maybe_visit o.comment, collector
end
def visit_Arel_Nodes_InsertStatement(o, collector)
@@ -64,9 +62,9 @@ module Arel # :nodoc: all
maybe_visit o.values, collector
elsif o.select
maybe_visit o.select, collector
+ else
+ collector
end
-
- maybe_visit o.comment, collector
end
def visit_Arel_Nodes_Exists(o, collector)
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
index 5f7201cfe1..562543f981 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
@@ -6,7 +6,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
t.string :password_digest<%= attribute.inject_options %>
<% elsif attribute.token? -%>
t.string :<%= attribute.name %><%= attribute.inject_options %>
-<% else -%>
+<% elsif !attribute.virtual? -%>
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
<% end -%>
<% end -%>
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
index 481c70201b..c07380bec9 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
@@ -7,7 +7,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
<%- elsif attribute.token? -%>
add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
- <%- else -%>
+ <%- elsif !attribute.virtual? -%>
add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- if attribute.has_index? -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
@@ -21,7 +21,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
<%- attributes.each do |attribute| -%>
<%- if attribute.reference? -%>
t.references :<%= attribute.name %><%= attribute.inject_options %>
- <%- else -%>
+ <%- elsif !attribute.virtual? -%>
<%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %>
<%- end -%>
<%- end -%>
@@ -37,7 +37,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
<%- if attribute.has_index? -%>
remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<%- end -%>
+ <%- if !attribute.virtual? %>
remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
+ <%- end -%>
<%- end -%>
<%- end -%>
<%- end -%>
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
index 55dc65c8ad..77b9ea1c86 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
+++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
@@ -1,7 +1,16 @@
<% module_namespacing do -%>
class <%= class_name %> < <%= parent_class_name.classify %>
<% attributes.select(&:reference?).each do |attribute| -%>
- belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %>
+ belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
+<% end -%>
+<% attributes.select(&:rich_text?).each do |attribute| -%>
+ has_rich_text :<%= attribute.name %>
+<% end -%>
+<% attributes.select(&:attachment?).each do |attribute| -%>
+ has_one_attached :<%= attribute.name %>
+<% end -%>
+<% attributes.select(&:attachments?).each do |attribute| -%>
+ has_many_attached :<%= attribute.name %>
<% end -%>
<% attributes.select(&:token?).each do |attribute| -%>
has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 2b20d842e8..ce2ed06c1d 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -485,23 +485,57 @@ module ActiveRecord
end
def test_truncate
- assert_operator @connection.query_value("SELECT COUNT(*) FROM posts"), :>, 0
+ assert_operator Post.count, :>, 0
@connection.truncate("posts")
- assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM posts")
+ assert_equal 0, Post.count
+ ensure
+ reset_fixtures("posts")
+ end
+
+ def test_truncate_with_query_cache
+ @connection.enable_query_cache!
+
+ assert_operator Post.count, :>, 0
+
+ @connection.truncate("posts")
+
+ assert_equal 0, Post.count
+ ensure
+ reset_fixtures("posts")
+ @connection.disable_query_cache!
end
def test_truncate_tables
- assert_operator @connection.query_value("SELECT COUNT(*) FROM posts"), :>, 0
- assert_operator @connection.query_value("SELECT COUNT(*) FROM authors"), :>, 0
- assert_operator @connection.query_value("SELECT COUNT(*) FROM author_addresses"), :>, 0
+ assert_operator Post.count, :>, 0
+ assert_operator Author.count, :>, 0
+ assert_operator AuthorAddress.count, :>, 0
+
+ @connection.truncate_tables("author_addresses", "authors", "posts")
+
+ assert_equal 0, Post.count
+ assert_equal 0, Author.count
+ assert_equal 0, AuthorAddress.count
+ ensure
+ reset_fixtures("posts", "authors", "author_addresses")
+ end
+
+ def test_truncate_tables_with_query_cache
+ @connection.enable_query_cache!
+
+ assert_operator Post.count, :>, 0
+ assert_operator Author.count, :>, 0
+ assert_operator AuthorAddress.count, :>, 0
@connection.truncate_tables("author_addresses", "authors", "posts")
- assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM posts")
- assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM authors")
- assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM author_addresses")
+ assert_equal 0, Post.count
+ assert_equal 0, Author.count
+ assert_equal 0, AuthorAddress.count
+ ensure
+ reset_fixtures("posts", "authors", "author_addresses")
+ @connection.disable_query_cache!
end
# test resetting sequences in odd tables in PostgreSQL
@@ -523,6 +557,16 @@ module ActiveRecord
assert_nothing_raised { sub.save! }
end
end
+
+ private
+
+ def reset_fixtures(*fixture_names)
+ ActiveRecord::FixtureSet.reset_cache
+
+ fixture_names.each do |fixture_name|
+ ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, fixture_name)
+ end
+ end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index 88c2ac5d0a..c2c357d0c1 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -10,7 +10,15 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
ActiveRecord::Base.connection.send(:default_row_format)
ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_without_stub, :execute
- def execute(sql, name = nil) sql end
+ def execute(sql, name = nil)
+ ActiveSupport::Notifications.instrumenter.instrument(
+ "sql.active_record",
+ sql: sql,
+ name: name,
+ connection: self) do
+ sql
+ end
+ end
end
end
@@ -89,17 +97,19 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)"
- actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
- t.index :last_name, type: type
+ assert_sql(expected) do
+ ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
+ t.index :last_name, type: type
+ end
end
- assert_equal expected, actual
end
expected = "ALTER TABLE `people` ADD INDEX `index_people_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY"
- actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
- t.index :last_name, length: 10, using: :btree, algorithm: :copy
+ assert_sql(expected) do
+ ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
+ t.index :last_name, length: 10, using: :btree, algorithm: :copy
+ end
end
- assert_equal expected, actual
end
def test_drop_table
diff --git a/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb b/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb
new file mode 100644
index 0000000000..4d361e405c
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/connection_helper"
+require "models/author"
+require "models/bulb"
+
+module ActiveRecord
+ class CountDeletedRowsWithLockTest < ActiveRecord::Mysql2TestCase
+ test "delete and create in different threads synchronize correctly" do
+ Bulb.unscoped.delete_all
+ Bulb.create!(name: "Jimmy", color: "blue")
+
+ delete_thread = Thread.new do
+ Bulb.unscoped.delete_all
+ end
+
+ create_thread = Thread.new do
+ Author.create!(name: "Tommy")
+ end
+
+ delete_thread.join
+ create_thread.join
+
+ assert_equal 1, delete_thread.value
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
index 00a075e063..cbe55f1d53 100644
--- a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
@@ -46,10 +46,7 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase
def stub_version(full_version_string)
@connection.stub(:full_version, full_version_string) do
- @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
yield
end
- ensure
- @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
index a5b53f76b4..6ade2eec24 100644
--- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
@@ -200,7 +200,7 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
def test_doesnt_error_when_a_set_query_is_called_while_preventing_writes
@conn.while_preventing_writes do
- assert_nil @conn.execute("SET NAMES utf8")
+ assert_nil @conn.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci")
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb
index b9794c5710..628802b216 100644
--- a/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb
@@ -15,6 +15,14 @@ if supports_optimizer_hints?
end
end
+ def test_optimizer_hints_with_count_subquery
+ assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
+ posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)")
+ posts = posts.select(:id).where(author_id: [0, 1]).limit(5)
+ assert_equal 5, posts.count
+ end
+ end
+
def test_optimizer_hints_is_sanitized
assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb
index 1283b0642c..b8f51acba0 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -41,7 +41,7 @@ module ActiveRecord
column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_24" }
column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_25" }
- # Mysql floats are precision 0..24, Mysql doubles are precision 25..53
+ # MySQL floats are precision 0..24, MySQL doubles are precision 25..53
assert_equal 24, column_no_limit.limit
assert_equal 24, column_short.limit
assert_equal 53, column_long.limit
diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb
index 7b6dce71e9..626ef59570 100644
--- a/activerecord/test/cases/adapters/mysql2/sp_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb
@@ -9,7 +9,7 @@ class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase
def setup
@connection = ActiveRecord::Base.connection
- unless ActiveRecord::Base.connection.version >= "5.6.0"
+ unless ActiveRecord::Base.connection.database_version >= "5.6.0"
skip("no stored procedure support")
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index 3988c2adca..531e6b2328 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -35,7 +35,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
def test_binary_columns_are_limitless_the_upper_limit_is_one_GB
assert_equal "bytea", @connection.type_to_sql(:binary, limit: 100_000)
- assert_raise ActiveRecord::ActiveRecordError do
+ assert_raise ArgumentError do
@connection.type_to_sql(:binary, limit: 4294967295)
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
index b7535d5c9a..562cf1f2d1 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -64,7 +64,7 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase
def test_text_columns_are_limitless_the_upper_limit_is_one_GB
assert_equal "text", @connection.type_to_sql(:text, limit: 100_000)
- assert_raise ActiveRecord::ActiveRecordError do
+ assert_raise ArgumentError do
@connection.type_to_sql(:text, limit: 4294967295)
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
index df97ab11e7..0fd7b2c6ed 100644
--- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
@@ -22,23 +22,26 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
- @old_schema_migration_table_name = ActiveRecord::SchemaMigration.table_name
@old_table_name_prefix = ActiveRecord::Base.table_name_prefix
@old_table_name_suffix = ActiveRecord::Base.table_name_suffix
ActiveRecord::Base.table_name_prefix = "p_"
ActiveRecord::Base.table_name_suffix = "_s"
+ ActiveRecord::SchemaMigration.reset_table_name
+ ActiveRecord::InternalMetadata.reset_table_name
+
ActiveRecord::SchemaMigration.delete_all rescue nil
- ActiveRecord::SchemaMigration.table_name = "p_schema_migrations_s"
ActiveRecord::Migration.verbose = false
end
def teardown
- ActiveRecord::Base.table_name_prefix = @old_table_name_prefix
- ActiveRecord::Base.table_name_suffix = @old_table_name_suffix
ActiveRecord::SchemaMigration.delete_all rescue nil
ActiveRecord::Migration.verbose = true
- ActiveRecord::SchemaMigration.table_name = @old_schema_migration_table_name
+
+ ActiveRecord::Base.table_name_prefix = @old_table_name_prefix
+ ActiveRecord::Base.table_name_suffix = @old_table_name_suffix
+ ActiveRecord::SchemaMigration.reset_table_name
+ ActiveRecord::InternalMetadata.reset_table_name
super
end
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
index 8c6f046553..14c262f4ce 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -247,7 +247,7 @@ class PostgreSQLGeometricLineTest < ActiveRecord::PostgreSQLTestCase
class PostgresqlLine < ActiveRecord::Base; end
setup do
- unless ActiveRecord::Base.connection.send(:postgresql_version) >= 90400
+ unless ActiveRecord::Base.connection.database_version >= 90400
skip("line type is not fully implemented")
end
@connection = ActiveRecord::Base.connection
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index cd45975f70..671d8211a7 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -153,6 +153,22 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
assert_equal "GMT", y.timezone
end
+ def test_changes_with_store_accessors
+ x = Hstore.new(language: "de")
+ assert x.language_changed?
+ assert_nil x.language_was
+ assert_equal [nil, "de"], x.language_change
+ x.save!
+
+ assert_not x.language_changed?
+ x.reload
+
+ x.settings = nil
+ assert x.language_changed?
+ assert_equal "de", x.language_was
+ assert_equal ["de", nil], x.language_change
+ end
+
def test_changes_in_place
hstore = Hstore.create!(settings: { "one" => "two" })
hstore.settings["three"] = "four"
diff --git a/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb
index 5e4bf232e1..5b9f5e0832 100644
--- a/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb
@@ -19,6 +19,14 @@ if supports_optimizer_hints?
end
end
+ def test_optimizer_hints_with_count_subquery
+ assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
+ posts = Post.optimizer_hints("SeqScan(posts)")
+ posts = posts.select(:id).where(author_id: [0, 1]).limit(5)
+ assert_equal 5, posts.count
+ end
+ end
+
def test_optimizer_hints_is_sanitized
assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
diff --git a/activerecord/test/cases/adapters/postgresql/partitions_test.rb b/activerecord/test/cases/adapters/postgresql/partitions_test.rb
index 0ac9ca1200..4015bc94f9 100644
--- a/activerecord/test/cases/adapters/postgresql/partitions_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/partitions_test.rb
@@ -12,7 +12,7 @@ class PostgreSQLPartitionsTest < ActiveRecord::PostgreSQLTestCase
end
def test_partitions_table_exists
- skip unless ActiveRecord::Base.connection.postgresql_version >= 100000
+ skip unless ActiveRecord::Base.connection.database_version >= 100000
@connection.create_table :partitioned_events, force: true, id: false,
options: "partition by range (issued_at)" do |t|
t.timestamp :issued_at
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index 9d88b14dab..7aa6d089c5 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -51,11 +51,11 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
assert_equal 7, @connection.migration_context.current_version
end
- def test_schema_define_w_table_name_prefix
- table_name = ActiveRecord::SchemaMigration.table_name
+ def test_schema_define_with_table_name_prefix
old_table_name_prefix = ActiveRecord::Base.table_name_prefix
ActiveRecord::Base.table_name_prefix = "nep_"
- ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}"
+ ActiveRecord::SchemaMigration.reset_table_name
+ ActiveRecord::InternalMetadata.reset_table_name
ActiveRecord::Schema.define(version: 7) do
create_table :fruits do |t|
t.column :color, :string
@@ -67,7 +67,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
assert_equal 7, @connection.migration_context.current_version
ensure
ActiveRecord::Base.table_name_prefix = old_table_name_prefix
- ActiveRecord::SchemaMigration.table_name = table_name
+ ActiveRecord::SchemaMigration.reset_table_name
+ ActiveRecord::InternalMetadata.reset_table_name
end
def test_schema_raises_an_error_for_invalid_column_type
@@ -159,7 +160,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
end
if subsecond_precision_supported?
- def test_timestamps_sets_presicion_on_create_table
+ def test_timestamps_sets_precision_on_create_table
ActiveRecord::Schema.define do
create_table :has_timestamps do |t|
t.timestamps
@@ -170,7 +171,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
end
- def test_timestamps_sets_presicion_on_change_table
+ def test_timestamps_sets_precision_on_change_table
ActiveRecord::Schema.define do
create_table :has_timestamps
@@ -184,7 +185,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
end
if ActiveRecord::Base.connection.supports_bulk_alter?
- def test_timestamps_sets_presicion_on_change_table_with_bulk
+ def test_timestamps_sets_precision_on_change_table_with_bulk
ActiveRecord::Schema.define do
create_table :has_timestamps
@@ -198,7 +199,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
end
end
- def test_timestamps_sets_presicion_on_add_timestamps
+ def test_timestamps_sets_precision_on_add_timestamps
ActiveRecord::Schema.define do
create_table :has_timestamps
add_timestamps :has_timestamps, default: Time.now
diff --git a/activerecord/test/cases/arel/delete_manager_test.rb b/activerecord/test/cases/arel/delete_manager_test.rb
index 63cd1bffe3..0bad02f4d2 100644
--- a/activerecord/test/cases/arel/delete_manager_test.rb
+++ b/activerecord/test/cases/arel/delete_manager_test.rb
@@ -49,23 +49,5 @@ module Arel
dm.where(table[:id].eq(10)).must_equal dm
end
end
-
- describe "comment" do
- it "chains" do
- manager = Arel::DeleteManager.new
- manager.comment("deleting").must_equal manager
- end
-
- it "appends a comment to the generated query" do
- table = Table.new(:users)
- dm = Arel::DeleteManager.new
- dm.from table
- dm.comment("deletion")
- assert_match(%r{DELETE FROM "users" /\* deletion \*/}, dm.to_sql)
-
- dm.comment("deletion", "with", "comment")
- assert_match(%r{DELETE FROM "users" /\* deletion \*/ /\* with \*/ /\* comment \*/}, dm.to_sql)
- end
- end
end
end
diff --git a/activerecord/test/cases/arel/nodes/delete_statement_test.rb b/activerecord/test/cases/arel/nodes/delete_statement_test.rb
index 8ba268653d..3f078063a4 100644
--- a/activerecord/test/cases/arel/nodes/delete_statement_test.rb
+++ b/activerecord/test/cases/arel/nodes/delete_statement_test.rb
@@ -18,10 +18,8 @@ describe Arel::Nodes::DeleteStatement do
it "is equal with equal ivars" do
statement1 = Arel::Nodes::DeleteStatement.new
statement1.wheres = %w[a b c]
- statement1.comment = Arel::Nodes::Comment.new(["comment"])
statement2 = Arel::Nodes::DeleteStatement.new
statement2.wheres = %w[a b c]
- statement2.comment = Arel::Nodes::Comment.new(["comment"])
array = [statement1, statement2]
assert_equal 1, array.uniq.size
end
@@ -29,14 +27,8 @@ describe Arel::Nodes::DeleteStatement do
it "is not equal with different ivars" do
statement1 = Arel::Nodes::DeleteStatement.new
statement1.wheres = %w[a b c]
- statement1.comment = Arel::Nodes::Comment.new(["comment"])
statement2 = Arel::Nodes::DeleteStatement.new
statement2.wheres = %w[1 2 3]
- statement2.comment = Arel::Nodes::Comment.new(["comment"])
- array = [statement1, statement2]
- assert_equal 2, array.uniq.size
- statement2.wheres = %w[a b c]
- statement2.comment = Arel::Nodes::Comment.new(["other"])
array = [statement1, statement2]
assert_equal 2, array.uniq.size
end
diff --git a/activerecord/test/cases/arel/nodes/insert_statement_test.rb b/activerecord/test/cases/arel/nodes/insert_statement_test.rb
index 036576b231..252a0d0d0b 100644
--- a/activerecord/test/cases/arel/nodes/insert_statement_test.rb
+++ b/activerecord/test/cases/arel/nodes/insert_statement_test.rb
@@ -23,11 +23,9 @@ describe Arel::Nodes::InsertStatement do
statement1 = Arel::Nodes::InsertStatement.new
statement1.columns = %w[a b c]
statement1.values = %w[x y z]
- statement1.comment = Arel::Nodes::Comment.new(["comment"])
statement2 = Arel::Nodes::InsertStatement.new
statement2.columns = %w[a b c]
statement2.values = %w[x y z]
- statement2.comment = Arel::Nodes::Comment.new(["comment"])
array = [statement1, statement2]
assert_equal 1, array.uniq.size
end
@@ -36,15 +34,9 @@ describe Arel::Nodes::InsertStatement do
statement1 = Arel::Nodes::InsertStatement.new
statement1.columns = %w[a b c]
statement1.values = %w[x y z]
- statement1.comment = Arel::Nodes::Comment.new(["comment"])
statement2 = Arel::Nodes::InsertStatement.new
statement2.columns = %w[a b c]
statement2.values = %w[1 2 3]
- statement2.comment = Arel::Nodes::Comment.new(["comment"])
- array = [statement1, statement2]
- assert_equal 2, array.uniq.size
- statement2.values = %w[x y z]
- statement2.comment = Arel::Nodes::Comment.new("other")
array = [statement1, statement2]
assert_equal 2, array.uniq.size
end
diff --git a/activerecord/test/cases/arel/nodes/update_statement_test.rb b/activerecord/test/cases/arel/nodes/update_statement_test.rb
index f133ddf7eb..a83ce32f68 100644
--- a/activerecord/test/cases/arel/nodes/update_statement_test.rb
+++ b/activerecord/test/cases/arel/nodes/update_statement_test.rb
@@ -27,7 +27,6 @@ describe Arel::Nodes::UpdateStatement do
statement1.orders = %w[x y z]
statement1.limit = 42
statement1.key = "zomg"
- statement1.comment = Arel::Nodes::Comment.new(["comment"])
statement2 = Arel::Nodes::UpdateStatement.new
statement2.relation = "zomg"
statement2.wheres = 2
@@ -35,7 +34,6 @@ describe Arel::Nodes::UpdateStatement do
statement2.orders = %w[x y z]
statement2.limit = 42
statement2.key = "zomg"
- statement2.comment = Arel::Nodes::Comment.new(["comment"])
array = [statement1, statement2]
assert_equal 1, array.uniq.size
end
@@ -48,7 +46,6 @@ describe Arel::Nodes::UpdateStatement do
statement1.orders = %w[x y z]
statement1.limit = 42
statement1.key = "zomg"
- statement1.comment = Arel::Nodes::Comment.new(["comment"])
statement2 = Arel::Nodes::UpdateStatement.new
statement2.relation = "zomg"
statement2.wheres = 2
@@ -56,11 +53,6 @@ describe Arel::Nodes::UpdateStatement do
statement2.orders = %w[x y z]
statement2.limit = 42
statement2.key = "wth"
- statement2.comment = Arel::Nodes::Comment.new(["comment"])
- array = [statement1, statement2]
- assert_equal 2, array.uniq.size
- statement2.key = "zomg"
- statement2.comment = Arel::Nodes::Comment.new(["other"])
array = [statement1, statement2]
assert_equal 2, array.uniq.size
end
diff --git a/activerecord/test/cases/arel/update_manager_test.rb b/activerecord/test/cases/arel/update_manager_test.rb
index e13cb6aa52..cc1b9ac5b3 100644
--- a/activerecord/test/cases/arel/update_manager_test.rb
+++ b/activerecord/test/cases/arel/update_manager_test.rb
@@ -122,29 +122,5 @@ module Arel
@um.key.must_equal @table[:foo]
end
end
-
- describe "comment" do
- it "chains" do
- manager = Arel::UpdateManager.new
- manager.comment("updating").must_equal manager
- end
-
- it "appends a comment to the generated query" do
- table = Table.new :users
-
- manager = Arel::UpdateManager.new
- manager.table table
-
- manager.comment("updating")
- manager.to_sql.must_be_like %{
- UPDATE "users" /* updating */
- }
-
- manager.comment("updating", "with", "comment")
- manager.to_sql.must_be_like %{
- UPDATE "users" /* updating */ /* with */ /* comment */
- }
- end
- end
end
end
diff --git a/activerecord/test/cases/arel/visitors/oracle12_test.rb b/activerecord/test/cases/arel/visitors/oracle12_test.rb
index 4ce5cab4db..ebea12910d 100644
--- a/activerecord/test/cases/arel/visitors/oracle12_test.rb
+++ b/activerecord/test/cases/arel/visitors/oracle12_test.rb
@@ -8,6 +8,7 @@ module Arel
before do
@visitor = Oracle12.new Table.engine.connection
@table = Table.new(:users)
+ @attr = @table[:id]
end
def compile(node)
@@ -95,6 +96,26 @@ module Arel
sql.must_be_like %{ "users"."name" IS NOT NULL }
end
end
+
+ describe "Nodes::In" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.in ary
+ compile(node).must_be_like %{
+ "users"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) OR \"users\".\"id\" IN (1001)
+ }
+ end
+ end
+
+ describe "Nodes::NotIn" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.not_in ary
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) AND \"users\".\"id\" NOT IN (1001)
+ }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/arel/visitors/oracle_test.rb b/activerecord/test/cases/arel/visitors/oracle_test.rb
index 893edc7f74..f69b201855 100644
--- a/activerecord/test/cases/arel/visitors/oracle_test.rb
+++ b/activerecord/test/cases/arel/visitors/oracle_test.rb
@@ -8,6 +8,7 @@ module Arel
before do
@visitor = Oracle.new Table.engine.connection
@table = Table.new(:users)
+ @attr = @table[:id]
end
def compile(node)
@@ -231,6 +232,26 @@ module Arel
sql.must_be_like %{ "users"."name" IS NOT NULL }
end
end
+
+ describe "Nodes::In" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.in ary
+ compile(node).must_be_like %{
+ "users"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) OR \"users\".\"id\" IN (1001)
+ }
+ end
+ end
+
+ describe "Nodes::NotIn" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.not_in ary
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) AND \"users\".\"id\" NOT IN (1001)
+ }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index b9e16cab21..49f754be63 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -121,7 +121,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
assert reply.save
topics = Topic.all.merge!(includes: :replies, order: ["topics.id", "replies_topics.id"]).to_a
- assert_no_queries do
+ assert_queries(0) do
assert_equal 2, topics[0].replies.size
assert_equal 0, topics[1].replies.size
end
diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
index 5fca972aee..673d5f1dcf 100644
--- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
+++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
@@ -21,7 +21,7 @@ module PolymorphicFullStiClassNamesSharedTest
ActiveRecord::Base.store_full_sti_class = store_full_sti_class
post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1)
- @tagging = Tagging.create(taggable: post)
+ @tagging = post.create_tagging!
end
def teardown
diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
index 525ad3197a..849939de75 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -110,10 +110,10 @@ class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase
end
teardown do
- @davey_mcdave.destroy
- @first_post.destroy
@first_comment.destroy
@first_categorization.destroy
+ @davey_mcdave.destroy
+ @first_post.destroy
end
def test_missing_data_in_a_nested_include_should_not_cause_errors_when_constructing_objects
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index cd9c8a5285..594d161fa3 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -1400,11 +1400,24 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal expected, FirstPost.unscoped.find(2)
end
- test "preload ignores the scoping" do
- assert_equal(
- Comment.find(1).post,
- Post.where("1 = 0").scoping { Comment.preload(:post).find(1).post }
- )
+ test "belongs_to association ignores the scoping" do
+ post = Comment.find(1).post
+
+ Post.where("1=0").scoping do
+ assert_equal post, Comment.find(1).post
+ assert_equal post, Comment.preload(:post).find(1).post
+ assert_equal post, Comment.eager_load(:post).find(1).post
+ end
+ end
+
+ test "has_many association ignores the scoping" do
+ comments = Post.find(1).comments.to_a
+
+ Comment.where("1=0").scoping do
+ assert_equal comments, Post.find(1).comments
+ assert_equal comments, Post.preload(:comments).find(1).comments
+ assert_equal comments, Post.eager_load(:comments).find(1).comments
+ end
end
test "deep preload" do
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 6a7efe2121..32285f269a 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -2578,22 +2578,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
test "association with extend option" do
post = posts(:welcome)
- assert_equal "lifo", post.comments_with_extend.author
- assert_equal "hello", post.comments_with_extend.greeting
+ assert_equal "lifo", post.comments_with_extend.author
+ assert_equal "hello :)", post.comments_with_extend.greeting
end
test "association with extend option with multiple extensions" do
post = posts(:welcome)
- assert_equal "lifo", post.comments_with_extend_2.author
- assert_equal "hullo", post.comments_with_extend_2.greeting
+ assert_equal "lifo", post.comments_with_extend_2.author
+ assert_equal "hullo :)", post.comments_with_extend_2.greeting
end
test "extend option affects per association" do
post = posts(:welcome)
- assert_equal "lifo", post.comments_with_extend.author
- assert_equal "lifo", post.comments_with_extend_2.author
- assert_equal "hello", post.comments_with_extend.greeting
- assert_equal "hullo", post.comments_with_extend_2.greeting
+ assert_equal "lifo", post.comments_with_extend.author
+ assert_equal "lifo", post.comments_with_extend_2.author
+ assert_equal "hello :)", post.comments_with_extend.greeting
+ assert_equal "hullo :)", post.comments_with_extend_2.greeting
end
test "delete record with complex joins" do
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index 67e013c6e0..c13789f7ec 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -33,6 +33,9 @@ require "models/organization"
require "models/user"
require "models/family"
require "models/family_tree"
+require "models/section"
+require "models/seminar"
+require "models/session"
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
@@ -1474,6 +1477,37 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [subscription2], post.subscriptions.to_a
end
+ def test_child_is_visible_to_join_model_in_add_association_callbacks
+ [:before_add, :after_add].each do |callback_name|
+ sentient_treasure = Class.new(Treasure) do
+ def self.name; "SentientTreasure"; end
+
+ has_many :pet_treasures, foreign_key: :treasure_id, callback_name => :check_pet!
+ has_many :pets, through: :pet_treasures
+
+ def check_pet!(added)
+ raise "No pet!" if added.pet.nil?
+ end
+ end
+
+ treasure = sentient_treasure.new
+ assert_nothing_raised { treasure.pets << pets(:mochi) }
+ end
+ end
+
+ def test_circular_autosave_association_correctly_saves_multiple_records
+ cs180 = Seminar.new(name: "CS180")
+ fall = Session.new(name: "Fall")
+ sections = [
+ cs180.sections.build(short_name: "A"),
+ cs180.sections.build(short_name: "B"),
+ ]
+ fall.sections << sections
+ fall.save!
+ fall.reload
+ assert_equal sections, fall.sections.sort_by(&:id)
+ end
+
private
def make_model(name)
Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index c33dcdee61..e0dac01f4a 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -29,7 +29,10 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations_with_left_outer_joins
sql = Person.joins(agents: :agents).left_outer_joins(agents: :agents).to_sql
- assert_match(/agents_people_4/i, sql)
+ assert_match(/agents_people_2/i, sql)
+ assert_match(/INNER JOIN/i, sql)
+ assert_no_match(/agents_people_4/i, sql)
+ assert_no_match(/LEFT OUTER JOIN/i, sql)
end
def test_construct_finder_sql_does_not_table_name_collide_with_string_joins
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index da3a42e2b5..669e176dcb 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -8,6 +8,7 @@ require "models/zine"
require "models/club"
require "models/sponsor"
require "models/rating"
+require "models/post"
require "models/comment"
require "models/car"
require "models/bulb"
@@ -62,6 +63,14 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
assert_equal rating_reflection, comment_reflection.inverse_of, "The Comment reflection's inverse should be the Rating reflection"
end
+ def test_has_many_and_belongs_to_should_find_inverse_automatically_for_extension_block
+ comment_reflection = Comment.reflect_on_association(:post)
+ post_reflection = Post.reflect_on_association(:comments)
+
+ assert_predicate post_reflection, :has_inverse?
+ assert_equal comment_reflection, post_reflection.inverse_of
+ end
+
def test_has_many_and_belongs_to_should_find_inverse_automatically_for_sti
author_reflection = Author.reflect_on_association(:posts)
author_child_reflection = Author.reflect_on_association(:special_posts)
diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb
index 0e54e8c1b0..0a8863c35d 100644
--- a/activerecord/test/cases/associations/left_outer_join_association_test.rb
+++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb
@@ -46,6 +46,12 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
assert queries.any? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
end
+ def test_left_outer_joins_is_deduped_when_same_association_is_joined
+ queries = capture_sql { Author.joins(:posts).left_outer_joins(:posts).to_a }
+ assert queries.any? { |sql| /INNER JOIN/i.match?(sql) }
+ assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
+ end
+
def test_construct_finder_sql_ignores_empty_left_outer_joins_hash
queries = capture_sql { Author.left_outer_joins({}).to_a }
assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
@@ -60,6 +66,10 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
assert_raise(ArgumentError) { Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a }
end
+ def test_left_outer_joins_with_string_join
+ assert_equal 16, Author.left_outer_joins(:posts).joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id").count
+ end
+
def test_join_conditions_added_to_join_clause
queries = capture_sql { Author.left_outer_joins(:essays).to_a }
assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1|\:a1)/i.match?(sql) }
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 0b83fd8421..35da74102d 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -548,6 +548,15 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_through_association_preload_doesnt_reset_source_association_if_already_preloaded
+ blue = tags(:blue)
+ authors = Author.preload(posts: :first_blue_tags_2, misc_post_first_blue_tags_2: {}).to_a.sort_by(&:id)
+
+ assert_no_queries do
+ assert_equal [blue], authors[2].posts.first.first_blue_tags_2
+ end
+ end
+
def test_nested_has_many_through_with_conditions_on_source_associations_preload_via_joins
# Pointless condition to force single-query loading
assert_includes_and_joins_equal(
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 9fd62dcf72..5cbe5d796d 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -1081,9 +1081,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal ["title"], model.accessed_fields
end
- test "generated attribute methods ancestors have correct class" do
+ test "generated attribute methods ancestors have correct module" do
mod = Topic.send(:generated_attribute_methods)
- assert_match %r(Topic::GeneratedAttributeMethods), mod.inspect
+ assert_equal "Topic::GeneratedAttributeMethods", mod.inspect
end
private
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 866818b2ab..ddafa468ed 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -67,6 +67,16 @@ end
class BasicsTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :author_addresses, :categorizations, :categories, :posts
+ def test_generated_association_methods_module_name
+ mod = Post.send(:generated_association_methods)
+ assert_equal "Post::GeneratedAssociationMethods", mod.inspect
+ end
+
+ def test_generated_relation_methods_module_name
+ mod = Post.send(:generated_relation_methods)
+ assert_equal "Post::GeneratedRelationMethods", mod.inspect
+ end
+
def test_column_names_are_escaped
conn = ActiveRecord::Base.connection
classname = conn.class.name[/[^:]*$/]
@@ -1035,11 +1045,6 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- def test_find_last
- last = Developer.last
- assert_equal last, Developer.all.merge!(order: "id desc").first
- end
-
def test_last
assert_equal Developer.all.merge!(order: "id desc").first, Developer.last
end
@@ -1210,6 +1215,8 @@ class BasicsTest < ActiveRecord::TestCase
wr.close
assert Marshal.load rd.read
rd.close
+ ensure
+ self.class.send(:remove_const, "Post") if self.class.const_defined?("Post", false)
end
end
diff --git a/activerecord/test/cases/boolean_test.rb b/activerecord/test/cases/boolean_test.rb
index ab9f974e2c..18824004d2 100644
--- a/activerecord/test/cases/boolean_test.rb
+++ b/activerecord/test/cases/boolean_test.rb
@@ -40,4 +40,13 @@ class BooleanTest < ActiveRecord::TestCase
assert_equal b_false, Boolean.find_by(value: "false")
assert_equal b_true, Boolean.find_by(value: "true")
end
+
+ def test_find_by_falsy_boolean_symbol
+ ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
+ b_false = Boolean.create!(value: value)
+
+ assert_not_predicate b_false, :value?
+ assert_equal b_false, Boolean.find_by(id: b_false.id, value: value.to_s.to_sym)
+ end
+ end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index f1e35d6ab9..16c2a3661d 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -243,6 +243,12 @@ class CalculationsTest < ActiveRecord::TestCase
assert_queries(1) { assert_equal 11, posts.count(:all) }
end
+ def test_count_with_eager_loading_and_custom_select_and_order
+ posts = Post.includes(:comments).order("comments.id").select(:type)
+ assert_queries(1) { assert_equal 11, posts.count }
+ assert_queries(1) { assert_equal 11, posts.count(:all) }
+ end
+
def test_count_with_eager_loading_and_custom_order_and_distinct
posts = Post.includes(:comments).order("comments.id").distinct
assert_queries(1) { assert_equal 11, posts.count }
@@ -357,6 +363,17 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 60, c[2]
end
+ def test_should_calculate_grouped_with_longer_field
+ field = "a" * Account.connection.max_identifier_length
+
+ Account.update_all("#{field} = credit_limit")
+
+ c = Account.group(:firm_id).sum(field)
+ assert_equal 50, c[1]
+ assert_equal 105, c[6]
+ assert_equal 60, c[2]
+ end
+
def test_should_calculate_with_invalid_field
assert_equal 6, Account.calculate(:count, "*")
assert_equal 6, Account.calculate(:count, :all)
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 4d6a112af5..b4026078f1 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -458,10 +458,6 @@ class CallbacksTest < ActiveRecord::TestCase
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :before_validation, :throwing_abort ],
- [ :after_rollback, :block ],
- [ :after_rollback, :object ],
- [ :after_rollback, :proc ],
- [ :after_rollback, :method ],
], david.history
end
diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb
index 483383257b..f07f3c42e6 100644
--- a/activerecord/test/cases/collection_cache_key_test.rb
+++ b/activerecord/test/cases/collection_cache_key_test.rb
@@ -171,5 +171,39 @@ module ActiveRecord
assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
end
+
+ test "cache_key should be stable when using collection_cache_versioning" do
+ with_collection_cache_versioning do
+ developers = Developer.where(salary: 100000)
+
+ assert_match(/\Adevelopers\/query-(\h+)\z/, developers.cache_key)
+
+ /\Adevelopers\/query-(\h+)\z/ =~ developers.cache_key
+
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
+ end
+ end
+
+ test "cache_version for relation" do
+ with_collection_cache_versioning do
+ developers = Developer.where(salary: 100000).order(updated_at: :desc)
+ last_developer_timestamp = developers.first.updated_at
+
+ assert_match(/(\d+)-(\d+)\z/, developers.cache_version)
+
+ /(\d+)-(\d+)\z/ =~ developers.cache_version
+
+ assert_equal developers.count.to_s, $1
+ assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $2
+ end
+ end
+
+ def with_collection_cache_versioning(value = true)
+ @old_collection_cache_versioning = ActiveRecord::Base.collection_cache_versioning
+ ActiveRecord::Base.collection_cache_versioning = value
+ yield
+ ensure
+ ActiveRecord::Base.collection_cache_versioning = @old_collection_cache_versioning
+ end
end
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
index 36591097b6..d3184f39f5 100644
--- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
@@ -203,26 +203,53 @@ module ActiveRecord
assert_equal "must provide a `database` or a `role`.", error.message
end
- def test_switching_connections_with_database_symbol
+ def test_switching_connections_with_database_symbol_uses_default_role
previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
config = {
"default_env" => {
- "readonly" => { adapter: "sqlite3", database: "db/readonly.sqlite3" },
- "primary" => { adapter: "sqlite3", database: "db/primary.sqlite3" }
+ "animals" => { adapter: "sqlite3", database: "db/animals.sqlite3" },
+ "primary" => { adapter: "sqlite3", database: "db/primary.sqlite3" }
}
}
@prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
- ActiveRecord::Base.connected_to(database: :readonly) do
- assert_equal :readonly, ActiveRecord::Base.current_role
- assert ActiveRecord::Base.connected_to?(role: :readonly)
+ ActiveRecord::Base.connected_to(database: :animals) do
+ assert_equal :writing, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :writing)
handler = ActiveRecord::Base.connection_handler
- assert_equal handler, ActiveRecord::Base.connection_handlers[:readonly]
+ assert_equal handler, ActiveRecord::Base.connection_handlers[:writing]
+
+ assert_not_nil pool = handler.retrieve_connection_pool("primary")
+ assert_equal(config["default_env"]["animals"], pool.spec.config)
+ end
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ ENV["RAILS_ENV"] = previous_env
+ end
+
+ def test_switching_connections_with_database_hash_uses_passed_role_and_database
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "animals" => { adapter: "sqlite3", database: "db/animals.sqlite3" },
+ "primary" => { adapter: "sqlite3", database: "db/primary.sqlite3" }
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.connected_to(database: { writing: :primary }) do
+ assert_equal :writing, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+
+ handler = ActiveRecord::Base.connection_handler
+ assert_equal handler, ActiveRecord::Base.connection_handlers[:writing]
assert_not_nil pool = handler.retrieve_connection_pool("primary")
- assert_equal(config["default_env"]["readonly"], pool.spec.config)
+ assert_equal(config["default_env"]["primary"], pool.spec.config)
end
ensure
ActiveRecord::Base.configurations = @prev_configs
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index 5113548091..28e232b88f 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -6,8 +6,9 @@ module ActiveRecord
module ConnectionAdapters
class SchemaCacheTest < ActiveRecord::TestCase
def setup
- @connection = ActiveRecord::Base.connection
- @cache = SchemaCache.new @connection
+ @connection = ActiveRecord::Base.connection
+ @cache = SchemaCache.new @connection
+ @database_version = @connection.get_database_version
end
def test_primary_key
@@ -28,6 +29,7 @@ module ActiveRecord
assert new_cache.data_sources("posts")
assert_equal "id", new_cache.primary_keys("posts")
assert_equal 1, new_cache.indexes("posts").size
+ assert_equal @database_version.to_s, new_cache.database_version.to_s
end
end
@@ -55,6 +57,20 @@ module ActiveRecord
@connection.schema_cache = old_cache
end
+ def test_yaml_loads_5_1_dump_without_database_version_still_queries_for_database_version
+ @cache = YAML.load(File.read(schema_dump_path))
+
+ # Simulate assignment in railtie after loading the cache.
+ old_cache, @connection.schema_cache = @connection.schema_cache, @cache
+
+ # We can't verify queries get executed because the database version gets
+ # cached in both MySQL and PostgreSQL outside of the schema cache.
+ assert_nil @cache.instance_variable_get(:@database_version)
+ assert_equal @database_version.to_s, @cache.database_version.to_s
+ ensure
+ @connection.schema_cache = old_cache
+ end
+
def test_primary_key_for_non_existent_table
assert_nil @cache.primary_keys("omgponies")
end
@@ -74,6 +90,18 @@ module ActiveRecord
assert_equal indexes, @cache.indexes("posts")
end
+ def test_caches_database_version
+ @cache.database_version # cache database_version
+
+ assert_no_queries do
+ assert_equal @database_version.to_s, @cache.database_version.to_s
+
+ if current_adapter?(:Mysql2Adapter)
+ assert_not_nil @cache.database_version.full_version_string
+ end
+ end
+ end
+
def test_clearing
@cache.columns("posts")
@cache.columns_hash("posts")
@@ -84,6 +112,7 @@ module ActiveRecord
@cache.clear!
assert_equal 0, @cache.size
+ assert_nil @cache.instance_variable_get(:@database_version)
end
def test_dump_and_load
@@ -101,6 +130,7 @@ module ActiveRecord
assert @cache.data_sources("posts")
assert_equal "id", @cache.primary_keys("posts")
assert_equal 1, @cache.indexes("posts").size
+ assert_equal @database_version.to_s, @cache.database_version.to_s
end
end
diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb
index 9d1af9362d..79d63949ca 100644
--- a/activerecord/test/cases/date_time_precision_test.rb
+++ b/activerecord/test/cases/date_time_precision_test.rb
@@ -82,7 +82,7 @@ if subsecond_precision_supported?
end
def test_invalid_datetime_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
+ assert_raises ArgumentError do
@connection.create_table(:foos, force: true) do |t|
t.timestamps precision: 7
end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 5d02e59ef6..50a86b0a19 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -89,7 +89,7 @@ if current_adapter?(:PostgreSQLAdapter)
test "schema dump includes default expression" do
output = dump_table_schema("defaults")
- if ActiveRecord::Base.connection.postgresql_version >= 100000
+ if ActiveRecord::Base.connection.database_version >= 100000
assert_match %r/t\.date\s+"modified_date",\s+default: -> { "CURRENT_DATE" }/, output
assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "CURRENT_TIMESTAMP" }/, output
else
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index f95d082907..543a0aeb39 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -53,7 +53,7 @@ def supports_default_expression?
true
elsif current_adapter?(:Mysql2Adapter)
conn = ActiveRecord::Base.connection
- !conn.mariadb? && conn.version >= "8.0.13"
+ !conn.mariadb? && conn.database_version >= "8.0.13"
end
end
@@ -202,3 +202,5 @@ module InTimeZone
ActiveRecord::Base.time_zone_aware_attributes = old_tz
end
end
+
+require_relative "../../../tools/test_common"
diff --git a/activerecord/test/cases/insert_all_test.rb b/activerecord/test/cases/insert_all_test.rb
index 0818d7c1ab..f24c63031c 100644
--- a/activerecord/test/cases/insert_all_test.rb
+++ b/activerecord/test/cases/insert_all_test.rb
@@ -11,6 +11,20 @@ class InsertAllTest < ActiveRecord::TestCase
fixtures :books
def test_insert
+ skip unless supports_insert_on_duplicate_skip?
+
+ id = 1_000_000
+
+ assert_difference "Book.count", +1 do
+ Book.insert(id: id, name: "Rework", author_id: 1)
+ end
+
+ Book.upsert(id: id, name: "Remote", author_id: 1)
+
+ assert_equal "Remote", Book.find(id).name
+ end
+
+ def test_insert!
assert_difference "Book.count", +1 do
Book.insert! name: "Rework", author_id: 1
end
@@ -90,6 +104,44 @@ class InsertAllTest < ActiveRecord::TestCase
end
end
+ def test_insert_all_with_skip_duplicates_and_autonumber_id_not_given
+ skip unless supports_insert_on_duplicate_skip?
+
+ assert_difference "Book.count", 1 do
+ # These two books are duplicates according to an index on %i[author_id name]
+ # but their IDs are not specified so they will be assigned different IDs
+ # by autonumber. We will get an exception from MySQL if we attempt to skip
+ # one of these records by assigning its ID.
+ Book.insert_all [
+ { author_id: 8, name: "Refactoring" },
+ { author_id: 8, name: "Refactoring" }
+ ]
+ end
+ end
+
+ def test_insert_all_with_skip_duplicates_and_autonumber_id_given
+ skip unless supports_insert_on_duplicate_skip?
+
+ assert_difference "Book.count", 1 do
+ Book.insert_all [
+ { id: 200, author_id: 8, name: "Refactoring" },
+ { id: 201, author_id: 8, name: "Refactoring" }
+ ]
+ end
+ end
+
+ def test_skip_duplicates_strategy_does_not_secretly_upsert
+ skip unless supports_insert_on_duplicate_skip?
+
+ book = Book.create!(author_id: 8, name: "Refactoring", format: "EXPECTED")
+
+ assert_no_difference "Book.count" do
+ Book.insert(author_id: 8, name: "Refactoring", format: "UNEXPECTED")
+ end
+
+ assert_equal "EXPECTED", book.reload.format
+ end
+
def test_insert_all_will_raise_if_duplicates_are_skipped_only_for_a_certain_conflict_target
skip unless supports_insert_on_duplicate_skip? && supports_insert_conflict_target?
@@ -129,6 +181,42 @@ class InsertAllTest < ActiveRecord::TestCase
end
end
+ def test_insert_logs_message_including_model_name
+ skip unless supports_insert_conflict_target?
+
+ capture_log_output do |output|
+ Book.insert(name: "Rework", author_id: 1)
+ assert_match "Book Insert", output.string
+ end
+ end
+
+ def test_insert_all_logs_message_including_model_name
+ skip unless supports_insert_conflict_target?
+
+ capture_log_output do |output|
+ Book.insert_all [{ name: "Remote", author_id: 1 }, { name: "Renote", author_id: 1 }]
+ assert_match "Book Bulk Insert", output.string
+ end
+ end
+
+ def test_upsert_logs_message_including_model_name
+ skip unless supports_insert_on_duplicate_update?
+
+ capture_log_output do |output|
+ Book.upsert(name: "Remote", author_id: 1)
+ assert_match "Book Upsert", output.string
+ end
+ end
+
+ def test_upsert_all_logs_message_including_model_name
+ skip unless supports_insert_on_duplicate_update?
+
+ capture_log_output do |output|
+ Book.upsert_all [{ name: "Remote", author_id: 1 }, { name: "Renote", author_id: 1 }]
+ assert_match "Book Bulk Upsert", output.string
+ end
+ end
+
def test_upsert_all_updates_existing_records
skip unless supports_insert_on_duplicate_update?
@@ -172,4 +260,17 @@ class InsertAllTest < ActiveRecord::TestCase
Book.insert_all! [{ unknown_attribute: "Test" }]
end
end
+
+ private
+
+ def capture_log_output
+ output = StringIO.new
+ old_logger, ActiveRecord::Base.logger = ActiveRecord::Base.logger, ActiveSupport::Logger.new(output)
+
+ begin
+ yield output
+ ensure
+ ActiveRecord::Base.logger = old_logger
+ end
+ end
end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index d68cc40107..7f67b945f0 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -103,6 +103,32 @@ module ActiveRecord
end
end
+ class ChangeColumnComment1 < SilentMigration
+ def change
+ create_table("horses") do |t|
+ t.column :name, :string, comment: "Sekitoba"
+ end
+ end
+ end
+
+ class ChangeColumnComment2 < SilentMigration
+ def change
+ change_column_comment :horses, :name, from: "Sekitoba", to: "Diomed"
+ end
+ end
+
+ class ChangeTableComment1 < SilentMigration
+ def change
+ create_table("horses", comment: "Sekitoba")
+ end
+ end
+
+ class ChangeTableComment2 < SilentMigration
+ def change
+ change_table_comment :horses, from: "Sekitoba", to: "Diomed"
+ end
+ end
+
class DisableExtension1 < SilentMigration
def change
enable_extension "hstore"
@@ -290,6 +316,7 @@ module ActiveRecord
def test_migrate_revert_change_column_default
migration1 = ChangeColumnDefault1.new
migration1.migrate(:up)
+ Horse.reset_column_information
assert_equal "Sekitoba", Horse.new.name
migration2 = ChangeColumnDefault2.new
@@ -302,6 +329,38 @@ module ActiveRecord
assert_equal "Sekitoba", Horse.new.name
end
+ if ActiveRecord::Base.connection.supports_comments?
+ def test_migrate_revert_change_column_comment
+ migration1 = ChangeColumnComment1.new
+ migration1.migrate(:up)
+ Horse.reset_column_information
+ assert_equal "Sekitoba", Horse.columns_hash["name"].comment
+
+ migration2 = ChangeColumnComment2.new
+ migration2.migrate(:up)
+ Horse.reset_column_information
+ assert_equal "Diomed", Horse.columns_hash["name"].comment
+
+ migration2.migrate(:down)
+ Horse.reset_column_information
+ assert_equal "Sekitoba", Horse.columns_hash["name"].comment
+ end
+
+ def test_migrate_revert_change_table_comment
+ connection = ActiveRecord::Base.connection
+ migration1 = ChangeTableComment1.new
+ migration1.migrate(:up)
+ assert_equal "Sekitoba", connection.table_comment("horses")
+
+ migration2 = ChangeTableComment2.new
+ migration2.migrate(:up)
+ assert_equal "Diomed", connection.table_comment("horses")
+
+ migration2.migrate(:down)
+ assert_equal "Sekitoba", connection.table_comment("horses")
+ end
+ end
+
if current_adapter?(:PostgreSQLAdapter)
def test_migrate_enable_and_disable_extension
migration1 = InvertibleMigration.new
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 33bd74e114..04f9b26960 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -182,7 +182,9 @@ class OptimisticLockingTest < ActiveRecord::TestCase
p1.touch
assert_equal 1, p1.lock_version
- assert_not p1.changed?, "Changes should have been cleared"
+ assert_not_predicate p1, :changed?, "Changes should have been cleared"
+ assert_predicate p1, :saved_changes?
+ assert_equal ["lock_version", "updated_at"], p1.saved_changes.keys.sort
end
def test_touch_stale_object
@@ -193,6 +195,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::StaleObjectError) do
stale_person.touch
end
+
+ assert_not_predicate stale_person, :saved_changes?
end
def test_update_with_dirty_primary_key
@@ -296,6 +300,9 @@ class OptimisticLockingTest < ActiveRecord::TestCase
t1.touch
assert_equal 1, t1.lock_version
+ assert_not_predicate t1, :changed?
+ assert_predicate t1, :saved_changes?
+ assert_equal ["lock_version", "updated_at"], t1.saved_changes.keys.sort
end
def test_touch_stale_object_with_lock_without_default
@@ -307,6 +314,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::StaleObjectError) do
stale_object.touch
end
+
+ assert_not_predicate stale_object, :saved_changes?
end
def test_lock_without_default_should_work_with_null_in_the_database
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index 6f9190c110..b6064500ee 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -176,9 +176,9 @@ module ActiveRecord
if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
def test_out_of_range_limit_should_raise
- assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, limit: 10 }
- assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff }
- assert_raise(ActiveRecordError) { add_column :test_models, :binary_too_big, :binary, limit: 0xfffffffff }
+ assert_raise(ArgumentError) { add_column :test_models, :integer_too_big, :integer, limit: 10 }
+ assert_raise(ArgumentError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff }
+ assert_raise(ArgumentError) { add_column :test_models, :binary_too_big, :binary, limit: 0xfffffffff }
end
end
end
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index dbbba9c5fa..cce3461e18 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -136,7 +136,7 @@ module ActiveRecord
def test_remove_column_with_multi_column_index
# MariaDB starting with 10.2.8
# Dropping a column that is part of a multi-column UNIQUE constraint is not permitted.
- skip if current_adapter?(:Mysql2Adapter) && connection.mariadb? && connection.version >= "10.2.8"
+ skip if current_adapter?(:Mysql2Adapter) && connection.mariadb? && connection.database_version >= "10.2.8"
add_column "test_models", :hat_size, :integer
add_column "test_models", :hat_style, :string, limit: 100
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index 01f8628fc5..c9f3756b1f 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -182,6 +182,40 @@ module ActiveRecord
assert_equal [:change_column_default, [:table, :column, from: false, to: true]], change
end
+ if ActiveRecord::Base.connection.supports_comments?
+ def test_invert_change_column_comment
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column_comment, [:table, :column, "comment"]
+ end
+ end
+
+ def test_invert_change_column_comment_with_from_and_to
+ change = @recorder.inverse_of :change_column_comment, [:table, :column, from: "old_value", to: "new_value"]
+ assert_equal [:change_column_comment, [:table, :column, from: "new_value", to: "old_value"]], change
+ end
+
+ def test_invert_change_column_comment_with_from_and_to_with_nil
+ change = @recorder.inverse_of :change_column_comment, [:table, :column, from: nil, to: "new_value"]
+ assert_equal [:change_column_comment, [:table, :column, from: "new_value", to: nil]], change
+ end
+
+ def test_invert_change_table_comment
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column_comment, [:table, :column, "comment"]
+ end
+ end
+
+ def test_invert_change_table_comment_with_from_and_to
+ change = @recorder.inverse_of :change_table_comment, [:table, from: "old_value", to: "new_value"]
+ assert_equal [:change_table_comment, [:table, from: "new_value", to: "old_value"]], change
+ end
+
+ def test_invert_change_table_comment_with_from_and_to_with_nil
+ change = @recorder.inverse_of :change_table_comment, [:table, from: nil, to: "new_value"]
+ assert_equal [:change_table_comment, [:table, from: "new_value", to: nil]], change
+ end
+ end
+
def test_invert_change_column_null
add = @recorder.inverse_of :change_column_null, [:table, :column, true]
assert_equal [:change_column_null, [:table, :column, false]], add
diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb
index 5753bd7117..726ccf925e 100644
--- a/activerecord/test/cases/migration/compatibility_test.rb
+++ b/activerecord/test/cases/migration/compatibility_test.rb
@@ -220,6 +220,35 @@ module ActiveRecord
end
end
+ if ActiveRecord::Base.connection.supports_comments?
+ def test_change_column_comment_can_be_reverted
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ revert do
+ change_column_comment(:testings, :foo, "comment")
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+ assert connection.column_exists?(:testings, :foo, comment: "comment")
+ end
+
+ def test_change_table_comment_can_be_reverted
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ revert do
+ change_table_comment(:testings, "comment")
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert_equal "comment", connection.table_comment("testings")
+ end
+ end
+
if current_adapter?(:PostgreSQLAdapter)
class Testing < ActiveRecord::Base
end
@@ -242,9 +271,9 @@ module ActiveRecord
private
def precision_implicit_default
if current_adapter?(:Mysql2Adapter)
- { presicion: 0 }
+ { precision: 0 }
else
- { presicion: nil }
+ { precision: nil }
end
end
end
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
index ba21923d79..5f1057f093 100644
--- a/activerecord/test/cases/migration/foreign_key_test.rb
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -155,7 +155,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
class ForeignKeyChangeColumnWithSuffixTest < ForeignKeyChangeColumnTest
setup do
- ActiveRecord::Base.table_name_suffix = "_p"
+ ActiveRecord::Base.table_name_suffix = "_s"
end
teardown do
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 788c8c36b8..8e8ed494d9 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -385,6 +385,7 @@ class MigrationTest < ActiveRecord::TestCase
assert_equal "changed", ActiveRecord::SchemaMigration.table_name
ensure
ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name
+ ActiveRecord::SchemaMigration.reset_table_name
Reminder.reset_table_name
end
@@ -405,6 +406,7 @@ class MigrationTest < ActiveRecord::TestCase
assert_equal "changed", ActiveRecord::InternalMetadata.table_name
ensure
ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name
+ ActiveRecord::InternalMetadata.reset_table_name
Reminder.reset_table_name
end
@@ -581,7 +583,7 @@ class MigrationTest < ActiveRecord::TestCase
if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
def test_out_of_range_integer_limit_should_raise
- e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
+ e = assert_raise(ArgumentError) do
Person.connection.create_table :test_integer_limits, force: true do |t|
t.column :bigone, :integer, limit: 10
end
@@ -593,7 +595,7 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_out_of_range_text_limit_should_raise
- e = assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
+ e = assert_raise(ArgumentError) do
Person.connection.create_table :test_text_limits, force: true do |t|
t.text :bigtext, limit: 0xfffffffff
end
@@ -605,15 +607,15 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_out_of_range_binary_limit_should_raise
- e = assert_raise(ActiveRecord::ActiveRecordError) do
- Person.connection.create_table :test_text_limits, force: true do |t|
+ e = assert_raise(ArgumentError) do
+ Person.connection.create_table :test_binary_limits, force: true do |t|
t.binary :bigbinary, limit: 0xfffffffff
end
end
assert_includes e.message, "No binary type has byte size #{0xfffffffff}"
ensure
- Person.connection.drop_table :test_text_limits, if_exists: true
+ Person.connection.drop_table :test_binary_limits, if_exists: true
end
end
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
index 172fa20bc3..085006c9a2 100644
--- a/activerecord/test/cases/relation/delegation_test.rb
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -51,12 +51,12 @@ module ActiveRecord
ActiveRecord::SpawnMethods.public_instance_methods(false) - [:spawn, :merge!] +
ActiveRecord::QueryMethods.public_instance_methods(false).reject { |method|
method.to_s.end_with?("=", "!", "value", "values", "clause")
- } - [:reverse_order, :arel, :extensions] + [
+ } - [:reverse_order, :arel, :extensions, :construct_join_dependency] + [
:any?, :many?, :none?, :one?,
:first_or_create, :first_or_create!, :first_or_initialize,
:find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
:create_or_find_by, :create_or_find_by!,
- :destroy_all, :delete_all, :update_all, :delete_by, :destroy_by
+ :destroy_all, :delete_all, :update_all, :touch_all, :delete_by, :destroy_by
]
def test_delegate_querying_methods
diff --git a/activerecord/test/cases/relation/delete_all_test.rb b/activerecord/test/cases/relation/delete_all_test.rb
index 9b76936b7e..d1c13fa1b5 100644
--- a/activerecord/test/cases/relation/delete_all_test.rb
+++ b/activerecord/test/cases/relation/delete_all_test.rb
@@ -99,23 +99,4 @@ class DeleteAllTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) }
assert posts(:welcome)
end
-
- def test_delete_all_with_annotation_includes_a_query_comment
- davids = Author.where(name: "David").annotate("deleting all")
-
- assert_sql(%r{/\* deleting all \*/}) do
- assert_difference("Author.count", -1) { davids.delete_all }
- end
- end
-
- def test_delete_all_without_annotation_does_not_include_an_empty_comment
- davids = Author.where(name: "David")
-
- log = capture_sql do
- assert_difference("Author.count", -1) { davids.delete_all }
- end
-
- assert_not_predicate log, :empty?
- assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
- end
end
diff --git a/activerecord/test/cases/relation/update_all_test.rb b/activerecord/test/cases/relation/update_all_test.rb
index 526f926841..e45531b4a9 100644
--- a/activerecord/test/cases/relation/update_all_test.rb
+++ b/activerecord/test/cases/relation/update_all_test.rb
@@ -241,31 +241,36 @@ class UpdateAllTest < ActiveRecord::TestCase
end
end
- def test_update_all_with_annotation_includes_a_query_comment
- tag = Tag.first
+ def test_klass_level_update_all
+ travel 5.seconds do
+ now = Time.now.utc
- assert_sql(%r{/\* updating all \*/}) do
- Post.tagged_with(tag.id).annotate("updating all").update_all(title: "rofl")
- end
+ Person.all.each do |person|
+ assert_not_equal now, person.updated_at
+ end
- posts = Post.tagged_with(tag.id).all.to_a
- assert_operator posts.length, :>, 0
- posts.each { |post| assert_equal "rofl", post.title }
+ Person.update_all(updated_at: now)
+
+ Person.all.each do |person|
+ assert_equal now, person.updated_at
+ end
+ end
end
- def test_update_all_without_annotation_does_not_include_an_empty_comment
- tag = Tag.first
+ def test_klass_level_touch_all
+ travel 5.seconds do
+ now = Time.now.utc
- log = capture_sql do
- Post.tagged_with(tag.id).update_all(title: "rofl")
- end
+ Person.all.each do |person|
+ assert_not_equal now, person.updated_at
+ end
- assert_not_predicate log, :empty?
- assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+ Person.touch_all(time: now)
- posts = Post.tagged_with(tag.id).all.to_a
- assert_operator posts.length, :>, 0
- posts.each { |post| assert_equal "rofl", post.title }
+ Person.all.each do |person|
+ assert_equal now, person.updated_at
+ end
+ end
end
# Oracle UPDATE does not support ORDER BY
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 00a7b3841f..3f370e5ede 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -101,6 +101,9 @@ module ActiveRecord
relation.merge!(relation)
assert_predicate relation, :empty_scope?
+
+ assert_not_predicate NullPost.all, :empty_scope?
+ assert_not_predicate FirstPost.all, :empty_scope?
end
def test_bad_constants_raise_errors
@@ -289,6 +292,7 @@ module ActiveRecord
klass.create!(description: "foo")
assert_equal ["foo"], klass.select(:description).from(klass.all).map(&:desc)
+ assert_equal ["foo"], klass.reselect(:description).from(klass.all).map(&:desc)
end
def test_relation_merging_with_merged_joins_as_strings
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 36b4000018..2417775ef1 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -602,6 +602,13 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ def test_extracted_association
+ relation_authors = assert_queries(2) { Post.all.extract_associated(:author) }
+ root_authors = assert_queries(2) { Post.extract_associated(:author) }
+ assert_equal relation_authors, root_authors
+ assert_equal Post.all.collect(&:author), relation_authors
+ end
+
def test_find_with_included_associations
assert_queries(2) do
posts = Post.includes(:comments).order("posts.id")
@@ -978,6 +985,12 @@ class RelationTest < ActiveRecord::TestCase
assert_queries(1) { assert_equal 11, posts.load.size }
end
+ def test_size_with_eager_loading_and_custom_select_and_order
+ posts = Post.includes(:comments).order("comments.id").select(:type)
+ assert_queries(1) { assert_equal 11, posts.size }
+ assert_queries(1) { assert_equal 11, posts.load.size }
+ end
+
def test_size_with_eager_loading_and_custom_order_and_distinct
posts = Post.includes(:comments).order("comments.id").distinct
assert_queries(1) { assert_equal 11, posts.size }
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index a95ab0f429..50b514d464 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -411,7 +411,19 @@ class HasManyScopingTest < ActiveRecord::TestCase
def test_nested_scope_finder
Comment.where("1=0").scoping do
- assert_equal 0, @welcome.comments.count
+ assert_equal 2, @welcome.comments.count
+ assert_equal "a comment...", @welcome.comments.what_are_you
+ end
+
+ Comment.where("1=1").scoping do
+ assert_equal 2, @welcome.comments.count
+ assert_equal "a comment...", @welcome.comments.what_are_you
+ end
+ end
+
+ def test_none_scoping
+ Comment.none.scoping do
+ assert_equal 2, @welcome.comments.count
assert_equal "a comment...", @welcome.comments.what_are_you
end
@@ -452,7 +464,19 @@ class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase
def test_nested_scope_finder
Category.where("1=0").scoping do
- assert_equal 0, @welcome.categories.count
+ assert_equal 2, @welcome.categories.count
+ assert_equal "a category...", @welcome.categories.what_are_you
+ end
+
+ Category.where("1=1").scoping do
+ assert_equal 2, @welcome.categories.count
+ assert_equal "a category...", @welcome.categories.what_are_you
+ end
+ end
+
+ def test_none_scoping
+ Category.none.scoping do
+ assert_equal 2, @welcome.categories.count
assert_equal "a category...", @welcome.categories.what_are_you
end
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index 4457cfbd37..91c0e959f4 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -79,6 +79,74 @@ class StoreTest < ActiveRecord::TestCase
assert_not_predicate @john, :settings_changed?
end
+ test "updating the store will mark accessor as changed" do
+ @john.color = "red"
+ assert @john.color_changed?
+ end
+
+ test "new record and no accessors changes" do
+ user = Admin::User.new
+ assert_not user.color_changed?
+ assert_nil user.color_was
+ assert_nil user.color_change
+
+ user.color = "red"
+ assert user.color_changed?
+ assert_nil user.color_was
+ assert_equal "red", user.color_change[1]
+ end
+
+ test "updating the store won't mark accessor as changed if the whole store was updated" do
+ @john.settings = { color: @john.color, some: "thing" }
+ assert @john.settings_changed?
+ assert_not @john.color_changed?
+ end
+
+ test "updating the store populates the accessor changed array correctly" do
+ @john.color = "red"
+ assert_equal "black", @john.color_was
+ assert_equal "black", @john.color_change[0]
+ assert_equal "red", @john.color_change[1]
+ end
+
+ test "updating the store won't mark accessor as changed if the value isn't changed" do
+ @john.color = @john.color
+ assert_not @john.color_changed?
+ end
+
+ test "nullifying the store mark accessor as changed" do
+ color = @john.color
+ @john.settings = nil
+ assert @john.color_changed?
+ assert_equal color, @john.color_was
+ assert_equal [color, nil], @john.color_change
+ end
+
+ test "dirty methods for suffixed accessors" do
+ @john.configs[:two_factor_auth] = true
+ assert @john.two_factor_auth_configs_changed?
+ assert_nil @john.two_factor_auth_configs_was
+ assert_equal [nil, true], @john.two_factor_auth_configs_change
+ end
+
+ test "dirty methods for prefixed accessors" do
+ @john.spouse[:name] = "Lena"
+ assert @john.partner_name_changed?
+ assert_equal "Dallas", @john.partner_name_was
+ assert_equal ["Dallas", "Lena"], @john.partner_name_change
+ end
+
+ test "saved changes tracking for accessors" do
+ @john.spouse[:name] = "Lena"
+ assert @john.partner_name_changed?
+
+ @john.save!
+ assert_not @john.partner_name_change
+ assert @john.saved_change_to_partner_name?
+ assert_equal ["Dallas", "Lena"], @john.saved_change_to_partner_name
+ assert_equal "Dallas", @john.partner_name_before_last_save
+ end
+
test "object initialization with not nullable column" do
assert_equal true, @john.remember_login
end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 06f11108f9..ffe94eee0f 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -50,6 +50,8 @@ module ActiveRecord
protected_environments = ActiveRecord::Base.protected_environments
current_env = ActiveRecord::Base.connection.migration_context.current_environment
+ InternalMetadata[:environment] = current_env
+
assert_called_on_instance_of(
ActiveRecord::MigrationContext,
:current_version,
@@ -73,6 +75,9 @@ module ActiveRecord
def test_raises_an_error_when_called_with_protected_environment_which_name_is_a_symbol
protected_environments = ActiveRecord::Base.protected_environments
current_env = ActiveRecord::Base.connection.migration_context.current_environment
+
+ InternalMetadata[:environment] = current_env
+
assert_called_on_instance_of(
ActiveRecord::MigrationContext,
:current_version,
@@ -755,7 +760,7 @@ module ActiveRecord
end
class DatabaseTasksMigrateTest < DatabaseTasksMigrationTestCase
- def test_migrate_set_and_unset_verbose_and_version_env_vars
+ def test_can_migrate_from_pending_migration_error_action_dispatch
verbose, version = ENV["VERBOSE"], ENV["VERSION"]
ENV["VERSION"] = "2"
ENV["VERBOSE"] = "false"
@@ -767,7 +772,9 @@ module ActiveRecord
ENV.delete("VERBOSE")
# re-run up migration
- assert_includes capture_migration_output, "migrating"
+ assert_includes(capture(:stdout) do
+ ActiveSupport::ActionableError.dispatch ActiveRecord::PendingMigrationError, "Run pending migrations"
+ end, "migrating")
ensure
ENV["VERBOSE"], ENV["VERSION"] = verbose, version
end
@@ -951,11 +958,22 @@ module ActiveRecord
fixtures :authors, :author_addresses
+ def setup
+ SchemaMigration.create_table
+ SchemaMigration.create!(version: SchemaMigration.table_name)
+ InternalMetadata.create_table
+ InternalMetadata.create!(key: InternalMetadata.table_name)
+ end
+
def teardown
+ SchemaMigration.delete_all
+ InternalMetadata.delete_all
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
end
def test_truncate_tables
+ assert_operator SchemaMigration.count, :>, 0
+ assert_operator InternalMetadata.count, :>, 0
assert_operator Author.count, :>, 0
assert_operator AuthorAddress.count, :>, 0
@@ -969,12 +987,46 @@ module ActiveRecord
)
end
+ assert_operator SchemaMigration.count, :>, 0
+ assert_operator InternalMetadata.count, :>, 0
assert_equal 0, Author.count
assert_equal 0, AuthorAddress.count
ensure
ActiveRecord::Base.configurations = old_configurations
end
end
+
+ class DatabaseTasksTruncateAllWithPrefixTest < DatabaseTasksTruncateAllTest
+ setup do
+ ActiveRecord::Base.table_name_prefix = "p_"
+
+ SchemaMigration.reset_table_name
+ InternalMetadata.reset_table_name
+ end
+
+ teardown do
+ ActiveRecord::Base.table_name_prefix = nil
+
+ SchemaMigration.reset_table_name
+ InternalMetadata.reset_table_name
+ end
+ end
+
+ class DatabaseTasksTruncateAllWithSuffixTest < DatabaseTasksTruncateAllTest
+ setup do
+ ActiveRecord::Base.table_name_suffix = "_s"
+
+ SchemaMigration.reset_table_name
+ InternalMetadata.reset_table_name
+ end
+
+ teardown do
+ ActiveRecord::Base.table_name_suffix = nil
+
+ SchemaMigration.reset_table_name
+ InternalMetadata.reset_table_name
+ end
+ end
end
class DatabaseTasksTruncateAllWithMultipleDatabasesTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb
index 2f534ea110..1abd857216 100644
--- a/activerecord/test/cases/time_precision_test.rb
+++ b/activerecord/test/cases/time_precision_test.rb
@@ -75,7 +75,7 @@ if subsecond_precision_supported?
end
def test_invalid_time_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
+ assert_raises ArgumentError do
@connection.create_table(:foos, force: true) do |t|
t.time :start, precision: 7
t.time :finish, precision: 7
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 75ecd6fc40..232e018e03 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -40,17 +40,25 @@ class TimestampTest < ActiveRecord::TestCase
assert_not_equal @previously_updated_at, @developer.updated_at
assert_equal previous_salary + 10000, @developer.salary
- assert @developer.salary_changed?, "developer salary should have changed"
- assert @developer.changed?, "developer should be marked as changed"
+ assert_predicate @developer, :salary_changed?, "developer salary should have changed"
+ assert_predicate @developer, :changed?, "developer should be marked as changed"
+ assert_equal ["salary"], @developer.changed
+ assert_predicate @developer, :saved_changes?
+ assert_equal ["updated_at", "updated_on"], @developer.saved_changes.keys.sort
+
@developer.reload
assert_equal previous_salary, @developer.salary
end
def test_touching_a_record_with_default_scope_that_excludes_it_updates_its_timestamp
developer = @developer.becomes(DeveloperCalledJamis)
-
developer.touch
+
assert_not_equal @previously_updated_at, developer.updated_at
+ assert_not_predicate developer, :changed?
+ assert_predicate developer, :saved_changes?
+ assert_equal ["updated_at", "updated_on"], developer.saved_changes.keys.sort
+
developer.reload
assert_not_equal @previously_updated_at, developer.updated_at
end
diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb
index cd3d5ed7d1..f1a9cf2d05 100644
--- a/activerecord/test/cases/touch_later_test.rb
+++ b/activerecord/test/cases/touch_later_test.rb
@@ -10,7 +10,7 @@ require "models/tree"
class TouchLaterTest < ActiveRecord::TestCase
fixtures :nodes, :trees
- def test_touch_laster_raise_if_non_persisted
+ def test_touch_later_raise_if_non_persisted
invoice = Invoice.new
Invoice.transaction do
assert_not_predicate invoice, :persisted?
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index aa6b7915a2..53fe31e087 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -38,6 +38,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
before_commit { |record| record.do_before_commit(nil) }
after_commit { |record| record.do_after_commit(nil) }
+ after_save_commit { |record| record.do_after_commit(:save) }
after_create_commit { |record| record.do_after_commit(:create) }
after_update_commit { |record| record.do_after_commit(:update) }
after_destroy_commit { |record| record.do_after_commit(:destroy) }
@@ -110,6 +111,43 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal [:after_commit], @first.history
end
+ def test_dont_call_any_callbacks_after_transaction_commits_for_invalid_record
+ @first.after_commit_block { |r| r.history << :after_commit }
+ @first.after_rollback_block { |r| r.history << :after_rollback }
+
+ def @first.valid?(*)
+ false
+ end
+
+ assert_not @first.save
+ assert_equal [], @first.history
+ end
+
+ def test_dont_call_any_callbacks_after_explicit_transaction_commits_for_invalid_record
+ @first.after_commit_block { |r| r.history << :after_commit }
+ @first.after_rollback_block { |r| r.history << :after_rollback }
+
+ def @first.valid?(*)
+ false
+ end
+
+ @first.transaction do
+ assert_not @first.save
+ end
+ assert_equal [], @first.history
+ end
+
+ def test_only_call_after_commit_on_save_after_transaction_commits_for_saving_record
+ record = TopicWithCallbacks.new(title: "New topic", written_on: Date.today)
+ record.after_commit_block(:save) { |r| r.history << :after_save }
+
+ record.save!
+ assert_equal [:after_save], record.history
+
+ record.update!(title: "Another topic")
+ assert_equal [:after_save, :after_save], record.history
+ end
+
def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record
add_transaction_execution_blocks @first
@@ -586,7 +624,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
@topic.content = "foo"
@topic.save!
end
- @topic.class.connection.add_transaction_record(@topic)
+ @topic.send(:add_to_transaction)
end
assert_equal [:before_commit, :after_commit], @topic.history
end
@@ -596,7 +634,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
@topic.transaction(requires_new: true) do
@topic.content = "foo"
@topic.save!
- @topic.class.connection.add_transaction_record(@topic)
+ @topic.send(:add_to_transaction)
end
end
assert_equal [:before_commit, :after_commit], @topic.history
@@ -617,7 +655,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
@topic.content = "foo"
@topic.save!
end
- @topic.class.connection.add_transaction_record(@topic)
+ @topic.send(:add_to_transaction)
raise ActiveRecord::Rollback
end
assert_equal [:rollback], @topic.history
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 1009dd0f99..6795996cca 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -18,6 +18,65 @@ class TransactionTest < ActiveRecord::TestCase
@first, @second = Topic.find(1, 2).sort_by(&:id)
end
+ def test_rollback_dirty_changes
+ topic = topics(:fifth)
+
+ ActiveRecord::Base.transaction do
+ topic.update(title: "Ruby on Rails")
+ raise ActiveRecord::Rollback
+ end
+
+ title_change = ["The Fifth Topic of the day", "Ruby on Rails"]
+ assert_equal title_change, topic.changes["title"]
+ end
+
+ def test_rollback_dirty_changes_multiple_saves
+ topic = topics(:fifth)
+
+ ActiveRecord::Base.transaction do
+ topic.update(title: "Ruby on Rails")
+ topic.update(title: "Another Title")
+ raise ActiveRecord::Rollback
+ end
+
+ title_change = ["The Fifth Topic of the day", "Another Title"]
+ assert_equal title_change, topic.changes["title"]
+ end
+
+ def test_rollback_dirty_changes_then_retry_save
+ topic = topics(:fifth)
+
+ ActiveRecord::Base.transaction do
+ topic.update(title: "Ruby on Rails")
+ raise ActiveRecord::Rollback
+ end
+
+ title_change = ["The Fifth Topic of the day", "Ruby on Rails"]
+ assert_equal title_change, topic.changes["title"]
+
+ assert topic.save
+
+ assert_equal title_change, topic.saved_changes["title"]
+ assert_equal topic.title, topic.reload.title
+ end
+
+ def test_rollback_dirty_changes_then_retry_save_on_new_record
+ topic = Topic.new(title: "Ruby on Rails")
+
+ ActiveRecord::Base.transaction do
+ topic.save
+ raise ActiveRecord::Rollback
+ end
+
+ title_change = [nil, "Ruby on Rails"]
+ assert_equal title_change, topic.changes["title"]
+
+ assert topic.save
+
+ assert_equal title_change, topic.saved_changes["title"]
+ assert_equal topic.title, topic.reload.title
+ end
+
def test_persisted_in_a_model_with_custom_primary_key_after_failed_save
movie = Movie.create
assert_not_predicate movie, :persisted?
@@ -26,28 +85,31 @@ class TransactionTest < ActiveRecord::TestCase
def test_raise_after_destroy
assert_not_predicate @first, :frozen?
- assert_raises(RuntimeError) {
- Topic.transaction do
- @first.destroy
- assert_predicate @first, :frozen?
- raise
+ assert_not_called(@first, :rolledback!) do
+ assert_raises(RuntimeError) do
+ Topic.transaction do
+ @first.destroy
+ assert_predicate @first, :frozen?
+ raise
+ end
end
- }
+ end
- assert @first.reload
assert_not_predicate @first, :frozen?
end
def test_successful
- Topic.transaction do
- @first.approved = true
- @second.approved = false
- @first.save
- @second.save
+ assert_not_called(@first, :committed!) do
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ end
end
- assert Topic.find(1).approved?, "First should have been approved"
- assert_not Topic.find(2).approved?, "Second should have been unapproved"
+ assert_predicate Topic.find(1), :approved?, "First should have been approved"
+ assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved"
end
def transaction_with_return
@@ -62,7 +124,7 @@ class TransactionTest < ActiveRecord::TestCase
def test_add_to_null_transaction
topic = Topic.new
- topic.add_to_transaction
+ topic.send(:add_to_transaction)
end
def test_successful_with_return
@@ -76,11 +138,13 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- transaction_with_return
+ assert_not_called(@first, :committed!) do
+ transaction_with_return
+ end
assert committed
- assert Topic.find(1).approved?, "First should have been approved"
- assert_not Topic.find(2).approved?, "Second should have been unapproved"
+ assert_predicate Topic.find(1), :approved?, "First should have been approved"
+ assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved"
ensure
Topic.connection.class_eval do
remove_method :commit_db_transaction
@@ -99,9 +163,11 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- Topic.transaction do
- @first.approved = true
- @first.save!
+ assert_not_called(@first, :committed!) do
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
end
assert_equal 0, num
@@ -113,19 +179,21 @@ class TransactionTest < ActiveRecord::TestCase
end
def test_successful_with_instance_method
- @first.transaction do
- @first.approved = true
- @second.approved = false
- @first.save
- @second.save
+ assert_not_called(@first, :committed!) do
+ @first.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ end
end
- assert Topic.find(1).approved?, "First should have been approved"
- assert_not Topic.find(2).approved?, "Second should have been unapproved"
+ assert_predicate Topic.find(1), :approved?, "First should have been approved"
+ assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved"
end
def test_failing_on_exception
- begin
+ assert_not_called(@first, :rolledback!) do
Topic.transaction do
@first.approved = true
@second.approved = false
@@ -137,11 +205,11 @@ class TransactionTest < ActiveRecord::TestCase
# caught it
end
- assert @first.approved?, "First should still be changed in the objects"
- assert_not @second.approved?, "Second should still be changed in the objects"
+ assert_predicate @first, :approved?, "First should still be changed in the objects"
+ assert_not_predicate @second, :approved?, "Second should still be changed in the objects"
- assert_not Topic.find(1).approved?, "First shouldn't have been approved"
- assert Topic.find(2).approved?, "Second should still be approved"
+ assert_not_predicate Topic.find(1), :approved?, "First shouldn't have been approved"
+ assert_predicate Topic.find(2), :approved?, "Second should still be approved"
end
def test_raising_exception_in_callback_rollbacks_in_save
@@ -150,8 +218,10 @@ class TransactionTest < ActiveRecord::TestCase
end
@first.approved = true
- e = assert_raises(RuntimeError) { @first.save }
- assert_equal "Make the transaction rollback", e.message
+ assert_not_called(@first, :rolledback!) do
+ e = assert_raises(RuntimeError) { @first.save }
+ assert_equal "Make the transaction rollback", e.message
+ end
assert_not_predicate Topic.find(1), :approved?
end
@@ -159,13 +229,15 @@ class TransactionTest < ActiveRecord::TestCase
def @first.before_save_for_transaction
raise ActiveRecord::Rollback
end
- assert_not @first.approved
+ assert_not_predicate @first, :approved?
- Topic.transaction do
- @first.approved = true
- @first.save!
+ assert_not_called(@first, :rolledback!) do
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
end
- assert_not Topic.find(@first.id).approved?, "Should not commit the approved flag"
+ assert_not_predicate Topic.find(@first.id), :approved?, "Should not commit the approved flag"
end
def test_raising_exception_in_nested_transaction_restore_state_in_save
@@ -175,11 +247,13 @@ class TransactionTest < ActiveRecord::TestCase
raise "Make the transaction rollback"
end
- assert_raises(RuntimeError) do
- Topic.transaction { topic.save }
+ assert_not_called(topic, :rolledback!) do
+ assert_raises(RuntimeError) do
+ Topic.transaction { topic.save }
+ end
end
- assert topic.new_record?, "#{topic.inspect} should be new record"
+ assert_predicate topic, :new_record?, "#{topic.inspect} should be new record"
end
def test_transaction_state_is_cleared_when_record_is_persisted
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 9a70934b7e..4f98a6b7fc 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -145,15 +145,17 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validates_acceptance_of_with_undefined_attribute_methods
- Topic.validates_acceptance_of(:approved)
- topic = Topic.new(approved: true)
- Topic.undefine_attribute_methods
+ klass = Class.new(Topic)
+ klass.validates_acceptance_of(:approved)
+ topic = klass.new(approved: true)
+ klass.undefine_attribute_methods
assert topic.approved
end
def test_validates_acceptance_of_as_database_column
- Topic.validates_acceptance_of(:approved)
- topic = Topic.create("approved" => true)
+ klass = Class.new(Topic)
+ klass.validates_acceptance_of(:approved)
+ topic = klass.create("approved" => true)
assert topic["approved"]
end
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 61e5f14100..c34968590f 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -11,6 +11,10 @@ class Post < ActiveRecord::Base
def author
"lifo"
end
+
+ def greeting
+ super + " :)"
+ end
end
module NamedExtension2
@@ -203,6 +207,10 @@ end
class SubAbstractStiPost < AbstractStiPost; end
+class NullPost < Post
+ default_scope { none }
+end
+
class FirstPost < ActiveRecord::Base
self.inheritance_column = :disabled
self.table_name = "posts"
diff --git a/activerecord/test/models/section.rb b/activerecord/test/models/section.rb
new file mode 100644
index 0000000000..f8b4cc7936
--- /dev/null
+++ b/activerecord/test/models/section.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Section < ActiveRecord::Base
+ belongs_to :session, inverse_of: :sections, autosave: true
+ belongs_to :seminar, inverse_of: :sections, autosave: true
+end
diff --git a/activerecord/test/models/seminar.rb b/activerecord/test/models/seminar.rb
new file mode 100644
index 0000000000..c18aa86433
--- /dev/null
+++ b/activerecord/test/models/seminar.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Seminar < ActiveRecord::Base
+ has_many :sections, inverse_of: :seminar, autosave: true, dependent: :destroy
+ has_many :sessions, through: :sections
+end
diff --git a/activerecord/test/models/session.rb b/activerecord/test/models/session.rb
new file mode 100644
index 0000000000..db66b5297e
--- /dev/null
+++ b/activerecord/test/models/session.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Session < ActiveRecord::Base
+ has_many :sections, inverse_of: :session, autosave: true, dependent: :destroy
+ has_many :seminars, through: :sections
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index ead4de2a13..7d9b8afeb6 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -19,6 +19,7 @@ ActiveRecord::Schema.define do
t.references :firm, index: false
t.string :firm_name
t.integer :credit_limit
+ t.integer "a" * max_identifier_length
end
create_table :admin_accounts, force: true do |t|
@@ -791,6 +792,24 @@ ActiveRecord::Schema.define do
t.integer :lock_version, default: 0
end
+ disable_referential_integrity do
+ create_table :seminars, force: :cascade do |t|
+ t.string :name
+ end
+
+ create_table :sessions, force: :cascade do |t|
+ t.date :start_date
+ t.date :end_date
+ t.string :name
+ end
+
+ create_table :sections, force: :cascade do |t|
+ t.string :short_name
+ t.belongs_to :session, foreign_key: true
+ t.belongs_to :seminar, foreign_key: true
+ end
+ end
+
create_table :shape_expressions, force: true do |t|
t.string :paint_type
t.integer :paint_id
diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec
index 34029ac8ad..0ae2dcdd3e 100644
--- a/activestorage/activestorage.gemspec
+++ b/activestorage/activestorage.gemspec
@@ -28,7 +28,8 @@ Gem::Specification.new do |s|
# NOTE: Please read our dependency guidelines before updating versions:
# https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
- s.add_dependency "actionpack", version
+ s.add_dependency "actionpack", version
+ s.add_dependency "activejob", version
s.add_dependency "activerecord", version
s.add_dependency "marcel", "~> 0.3.1"
diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb
index df8d73cc91..df3116afd7 100644
--- a/activestorage/app/controllers/active_storage/disk_controller.rb
+++ b/activestorage/app/controllers/active_storage/disk_controller.rb
@@ -3,7 +3,7 @@
# Serves files stored with the disk service in the same way that the cloud services do.
# This means using expiring, signed URLs that are meant for immediate access, not permanent linking.
# Always go through the BlobsController, or your own authenticated controller, rather than directly
-# to the service url.
+# to the service URL.
class ActiveStorage::DiskController < ActiveStorage::BaseController
skip_forgery_protection
diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb
index 13758d9179..874ba80ca8 100644
--- a/activestorage/app/models/active_storage/attachment.rb
+++ b/activestorage/app/models/active_storage/attachment.rb
@@ -46,3 +46,5 @@ class ActiveStorage::Attachment < ActiveRecord::Base
record.attachment_reflections[name]&.options[:dependent]
end
end
+
+ActiveSupport.run_load_hooks :active_storage_attachment, ActiveStorage::Attachment
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb
index 6ca7d49bc1..c9fbafad1f 100644
--- a/activestorage/app/models/active_storage/blob.rb
+++ b/activestorage/app/models/active_storage/blob.rb
@@ -193,17 +193,18 @@ class ActiveStorage::Blob < ActiveRecord::Base
#
# The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
#
- # By default, the tempfile is created in <tt>Dir.tmpdir</tt>. Pass +tempdir:+ to create it in a different directory:
+ # By default, the tempfile is created in <tt>Dir.tmpdir</tt>. Pass +tmpdir:+ to create it in a different directory:
#
- # blob.open(tempdir: "/path/to/tmp") do |file|
+ # blob.open(tmpdir: "/path/to/tmp") do |file|
# # ...
# end
#
# The tempfile is automatically closed and unlinked after the given block is executed.
#
# Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
- def open(tempdir: nil, &block)
- ActiveStorage::Downloader.new(self, tempdir: tempdir).download_blob_to_tempfile(&block)
+ def open(tmpdir: nil, &block)
+ service.open key, checksum: checksum,
+ name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &block
end
@@ -272,6 +273,6 @@ class ActiveStorage::Blob < ActiveRecord::Base
{ content_type: content_type }
end
end
-
- ActiveSupport.run_load_hooks(:active_storage_blob, self)
end
+
+ActiveSupport.run_load_hooks :active_storage_blob, ActiveStorage::Blob
diff --git a/activestorage/lib/active_storage/analyzer.rb b/activestorage/lib/active_storage/analyzer.rb
index caa25418a5..26414ffbc2 100644
--- a/activestorage/lib/active_storage/analyzer.rb
+++ b/activestorage/lib/active_storage/analyzer.rb
@@ -24,14 +24,14 @@ module ActiveStorage
private
# Downloads the blob to a tempfile on disk. Yields the tempfile.
def download_blob_to_tempfile(&block) #:doc:
- blob.open tempdir: tempdir, &block
+ blob.open tmpdir: tmpdir, &block
end
def logger #:doc:
ActiveStorage.logger
end
- def tempdir #:doc:
+ def tmpdir #:doc:
Dir.tmpdir
end
end
diff --git a/activestorage/lib/active_storage/downloader.rb b/activestorage/lib/active_storage/downloader.rb
index 87be6efb05..4d7e832af5 100644
--- a/activestorage/lib/active_storage/downloader.rb
+++ b/activestorage/lib/active_storage/downloader.rb
@@ -2,24 +2,23 @@
module ActiveStorage
class Downloader #:nodoc:
- def initialize(blob, tempdir: nil)
- @blob = blob
- @tempdir = tempdir
+ attr_reader :service
+
+ def initialize(service)
+ @service = service
end
- def download_blob_to_tempfile
- open_tempfile do |file|
- download_blob_to file
- verify_integrity_of file
+ def open(key, checksum:, name: "ActiveStorage-", tmpdir: nil)
+ open_tempfile(name, tmpdir) do |file|
+ download key, file
+ verify_integrity_of file, checksum: checksum
yield file
end
end
private
- attr_reader :blob, :tempdir
-
- def open_tempfile
- file = Tempfile.open([ "ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter ], tempdir)
+ def open_tempfile(name, tmpdir = nil)
+ file = Tempfile.open(name, tmpdir)
begin
yield file
@@ -28,15 +27,15 @@ module ActiveStorage
end
end
- def download_blob_to(file)
+ def download(key, file)
file.binmode
- blob.download { |chunk| file.write(chunk) }
+ service.download(key) { |chunk| file.write(chunk) }
file.flush
file.rewind
end
- def verify_integrity_of(file)
- unless Digest::MD5.file(file).base64digest == blob.checksum
+ def verify_integrity_of(file, checksum:)
+ unless Digest::MD5.file(file).base64digest == checksum
raise ActiveStorage::IntegrityError
end
end
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
index 384e6ebfa6..fc75a8f816 100644
--- a/activestorage/lib/active_storage/engine.rb
+++ b/activestorage/lib/active_storage/engine.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
require "rails"
+require "action_controller/railtie"
+require "active_job/railtie"
+require "active_record/railtie"
+
require "active_storage"
require "active_storage/previewer/poppler_pdf_previewer"
diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb
index 95a041fd16..af6bcadd4c 100644
--- a/activestorage/lib/active_storage/previewer.rb
+++ b/activestorage/lib/active_storage/previewer.rb
@@ -26,7 +26,7 @@ module ActiveStorage
private
# Downloads the blob to a tempfile on disk. Yields the tempfile.
def download_blob_to_tempfile(&block) #:doc:
- blob.open tempdir: tempdir, &block
+ blob.open tmpdir: tmpdir, &block
end
# Executes a system command, capturing its binary output in a tempfile. Yields the tempfile.
@@ -42,7 +42,7 @@ module ActiveStorage
# end
# end
#
- # The output tempfile is opened in the directory returned by #tempdir.
+ # The output tempfile is opened in the directory returned by #tmpdir.
def draw(*argv) #:doc:
open_tempfile do |file|
instrument :preview, key: blob.key do
@@ -54,7 +54,7 @@ module ActiveStorage
end
def open_tempfile
- tempfile = Tempfile.open("ActiveStorage-", tempdir)
+ tempfile = Tempfile.open("ActiveStorage-", tmpdir)
begin
yield tempfile
@@ -77,7 +77,7 @@ module ActiveStorage
ActiveStorage.logger
end
- def tempdir #:doc:
+ def tmpdir #:doc:
Dir.tmpdir
end
end
diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb
index c18fccbb1d..aac1e62e7f 100644
--- a/activestorage/lib/active_storage/service.rb
+++ b/activestorage/lib/active_storage/service.rb
@@ -82,6 +82,10 @@ module ActiveStorage
raise NotImplementedError
end
+ def open(*args, &block)
+ ActiveStorage::Downloader.new(self).open(*args, &block)
+ end
+
# Delete the file at the +key+.
def delete(key)
raise NotImplementedError
diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb
index 54cf9e2b8a..9fd75a1b4a 100644
--- a/activestorage/test/models/blob_test.rb
+++ b/activestorage/test/models/blob_test.rb
@@ -104,19 +104,17 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
end
end
- test "open in a custom tempdir" do
- tempdir = Dir.mktmpdir
-
- create_file_blob(filename: "racecar.jpg").open(tempdir: tempdir) do |file|
+ test "open in a custom tmpdir" do
+ create_file_blob(filename: "racecar.jpg").open(tmpdir: tmpdir = Dir.mktmpdir) do |file|
assert file.binmode?
assert_equal 0, file.pos
assert_match(/\.jpg\z/, file.path)
- assert file.path.starts_with?(tempdir)
+ assert file.path.starts_with?(tmpdir)
assert_equal file_fixture("racecar.jpg").binread, file.read, "Expected downloaded file to match fixture file"
end
end
- test "urls expiring in 5 minutes" do
+ test "URLs expiring in 5 minutes" do
blob = create_blob
freeze_time do
@@ -125,7 +123,7 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
end
end
- test "urls force content_type to binary and attachment as content disposition for content types served as binary" do
+ test "URLs force content_type to binary and attachment as content disposition for content types served as binary" do
blob = create_blob(content_type: "text/html")
freeze_time do
@@ -134,7 +132,7 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
end
end
- test "urls force attachment as content disposition when the content type is not allowed inline" do
+ test "URLs force attachment as content disposition when the content type is not allowed inline" do
blob = create_blob(content_type: "application/zip")
freeze_time do
@@ -143,7 +141,7 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
end
end
- test "urls allow for custom filename" do
+ test "URLs allow for custom filename" do
blob = create_blob(filename: "original.txt")
new_filename = ActiveStorage::Filename.new("new.txt")
@@ -155,7 +153,7 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
end
end
- test "urls allow for custom options" do
+ test "URLs allow for custom options" do
blob = create_blob(filename: "original.txt")
arguments = [
diff --git a/activestorage/test/service/disk_service_test.rb b/activestorage/test/service/disk_service_test.rb
index a0218bff1c..f3c4dd26bd 100644
--- a/activestorage/test/service/disk_service_test.rb
+++ b/activestorage/test/service/disk_service_test.rb
@@ -7,7 +7,7 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase
include ActiveStorage::Service::SharedServiceTests
- test "url generation" do
+ test "URL generation" do
assert_match(/^https:\/\/example.com\/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
@service.url(@key, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png"))
end
diff --git a/activestorage/test/template/image_tag_test.rb b/activestorage/test/template/image_tag_test.rb
index f0b166c225..258cf702ad 100644
--- a/activestorage/test/template/image_tag_test.rb
+++ b/activestorage/test/template/image_tag_test.rb
@@ -37,7 +37,7 @@ class ActiveStorage::ImageTagTest < ActionView::TestCase
assert_raises(ArgumentError) { image_tag(@user.avatar) }
end
- test "error when object can't be resolved into url" do
+ test "error when object can't be resolved into URL" do
unresolvable_object = ActionView::Helpers::AssetTagHelper
assert_raises(ArgumentError) { image_tag(unresolvable_object) }
end
diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb
index 144c224421..b34d0d64bb 100644
--- a/activestorage/test/test_helper.rb
+++ b/activestorage/test/test_helper.rb
@@ -101,3 +101,5 @@ end
class Group < ActiveRecord::Base
has_one_attached :avatar
end
+
+require_relative "../../tools/test_common"
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 03c9dfc546..4c7b134c35 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,94 @@
+* Introduce `ActiveSupport::ActionableError`.
+
+ Actionable errors let's you dispatch actions from Rails' error pages. This
+ can help you save time if you have a clear action for the resolution of
+ common development errors.
+
+ The de-facto example are pending migrations. Every time pending migrations
+ are found, a middleware raises an error. With actionable errors, you can
+ run the migrations right from the error page. Other examples include Rails
+ plugins that need to run a rake task to setup themselves. They can now
+ raise actionable errors to run the setup straight from the error pages.
+
+ Here is how to define an actionable error:
+
+ ```ruby
+ class PendingMigrationError < MigrationError #:nodoc:
+ include ActiveSupport::ActionableError
+
+ action "Run pending migrations" do
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+ end
+ ```
+
+ To make an error actionable, include the `ActiveSupport::ActionableError`
+ module and invoke the `action` class macro to define the action. An action
+ needs a name and a procedure to execute. The name is shown as the name of a
+ button on the error pages. Once clicked, it will invoke the given
+ procedure.
+
+ *Vipul A M*, *Yao Jie*, *Genadi Samokovarov*
+
+* Preserve `html_safe?` status on `ActiveSupport::SafeBuffer#*`.
+
+ Before:
+
+ ("<br />".html_safe * 2).html_safe? #=> nil
+
+ After:
+
+ ("<br />".html_safe * 2).html_safe? #=> true
+
+ *Ryo Nakamura*
+
+* Calling test methods with `with_info_handler` method to allow minitest-hooks
+ plugin to work.
+
+ *Mauri Mustonen*
+
+* The Zeitwerk compatibility interface for `ActiveSupport::Dependencies` no
+ longer implements `autoloaded_constants` or `autoloaded?` (undocumented,
+ anyway). Experience shows introspection does not have many use cases, and
+ troubleshooting is done by logging. With this design trade-off we are able
+ to use even less memory in all environments.
+
+ *Xavier Noria*
+
+* Depends on Zeitwerk 2, which stores less metadata if reloading is disabled
+ and hence uses less memory when `config.cache_classes` is `true`, a standard
+ setup in production.
+
+ *Xavier Noria*
+
+* In `:zeitwerk` mode, eager load directories in engines and applications only
+ if present in their respective `config.eager_load_paths`.
+
+ A common use case for this is adding `lib` to `config.autoload_paths`, but
+ not to `config.eager_load_paths`. In that configuration, for example, files
+ in the `lib` directory should not be eager loaded.
+
+ *Xavier Noria*
+
+* Fix bug in Range comparisons when comparing to an excluded-end Range
+
+ Before:
+
+ (1..10).cover?(1...11) # => false
+
+ After:
+
+ (1..10).cover?(1...11) # => true
+
+ With the same change for `Range#include?` and `Range#===`.
+
+ *Owen Stephens*
+
+* Use weak references in descendants tracker to allow anonymous subclasses to
+ be garbage collected.
+
+ *Edgars Beigarts*
+
* Update `ActiveSupport::Notifications::Instrumenter#instrument` to make
passing a block optional. This will let users use
`ActiveSupport::Notifications` messaging features outside of
@@ -8,11 +99,11 @@
* Fix `Time#advance` to work with dates before 1001-03-07
Before:
-
+
Time.utc(1001, 3, 6).advance(years: -1) # => 1000-03-05 00:00:00 UTC
-
+
After
-
+
Time.utc(1001, 3, 6).advance(years: -1) # => 1000-03-06 00:00:00 UTC
Note that this doesn't affect `DateTime#advance` as that doesn't use a proleptic calendar.
@@ -27,27 +118,27 @@
I18n.backend.store_translations(:de, i18n: { transliterate: { rule: { "ü" => "ue" } } })
- ActiveSupport::Inflector.transliterate("ü", locale: :de) => "ue"
- "Fünf autos".parameterize(locale: :de) => "fuenf-autos"
- ActiveSupport::Inflector.parameterize("Fünf autos", locale: :de) => "fuenf-autos"
+ ActiveSupport::Inflector.transliterate("ü", locale: :de) # => "ue"
+ "Fünf autos".parameterize(locale: :de) # => "fuenf-autos"
+ ActiveSupport::Inflector.parameterize("Fünf autos", locale: :de) # => "fuenf-autos"
*Kaan Ozkan*, *Sharang Dashputre*
-* Allow Array#excluding and Enumerable#excluding to deal with a passed array gracefully.
+* Allow `Array#excluding` and `Enumerable#excluding` to deal with a passed array gracefully.
- [ 1, 2, 3, 4, 5 ].excluding([4, 5]) => [ 1, 2, 3 ]
+ [ 1, 2, 3, 4, 5 ].excluding([4, 5]) # => [ 1, 2, 3 ]
*DHH*
-* Renamed Array#without and Enumerable#without to Array#excluding and Enumerable#excluding, to create parity with
- Array#including and Enumerable#including. Retained the old names as aliases.
+* Renamed `Array#without` and `Enumerable#without` to `Array#excluding` and `Enumerable#excluding`, to create parity with
+ `Array#including` and `Enumerable#including`. Retained the old names as aliases.
*DHH*
-* Added Array#including and Enumerable#including to conveniently enlarge a collection with more members using a method rather than an operator:
+* Added `Array#including` and `Enumerable#including` to conveniently enlarge a collection with more members using a method rather than an operator:
- [ 1, 2, 3 ].including(4, 5) => [ 1, 2, 3, 4, 5 ]
- post.authors.including(Current.person) => All the authors plus the current person!
+ [ 1, 2, 3 ].including(4, 5) # => [ 1, 2, 3, 4, 5 ]
+ post.authors.including(Current.person) # => All the authors plus the current person!
*DHH*
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index be041944f6..dd2340cdd3 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -34,5 +34,5 @@ Gem::Specification.new do |s|
s.add_dependency "tzinfo", "~> 1.1"
s.add_dependency "minitest", "~> 5.1"
s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2"
- s.add_dependency "zeitwerk", "~> 1.4", ">= 1.4.2"
+ s.add_dependency "zeitwerk", "~> 2.1", ">= 2.1.2"
end
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 5589c71281..9e242ddeaa 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -34,6 +34,7 @@ module ActiveSupport
extend ActiveSupport::Autoload
autoload :Concern
+ autoload :ActionableError
autoload :CurrentAttributes
autoload :Dependencies
autoload :DescendantsTracker
diff --git a/activesupport/lib/active_support/actionable_error.rb b/activesupport/lib/active_support/actionable_error.rb
new file mode 100644
index 0000000000..7db14cd178
--- /dev/null
+++ b/activesupport/lib/active_support/actionable_error.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ # Actionable errors let's you define actions to resolve an error.
+ #
+ # To make an error actionable, include the <tt>ActiveSupport::ActionableError</tt>
+ # module and invoke the +action+ class macro to define the action. An action
+ # needs a name and a block to execute.
+ module ActionableError
+ extend Concern
+
+ class NonActionable < StandardError; end
+
+ included do
+ class_attribute :_actions, default: {}
+ end
+
+ def self.actions(error) # :nodoc:
+ case error
+ when ActionableError, -> it { Class === it && it < ActionableError }
+ error._actions
+ else
+ {}
+ end
+ end
+
+ def self.dispatch(error, name) # :nodoc:
+ actions(error).fetch(name).call
+ rescue KeyError
+ raise NonActionable, "Cannot find action \"#{name}\""
+ end
+
+ module ClassMethods
+ # Defines an action that can resolve the error.
+ #
+ # class PendingMigrationError < MigrationError
+ # include ActiveSupport::ActionableError
+ #
+ # action "Run pending migrations" do
+ # ActiveRecord::Tasks::DatabaseTasks.migrate
+ # end
+ # end
+ def action(name, &block)
+ _actions[name] = block
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb
index 62973eca58..02cbfbaee6 100644
--- a/activesupport/lib/active_support/backtrace_cleaner.rb
+++ b/activesupport/lib/active_support/backtrace_cleaner.rb
@@ -122,7 +122,11 @@ module ActiveSupport
end
def noise(backtrace)
- backtrace - silence(backtrace)
+ backtrace.select do |line|
+ @silencers.any? do |s|
+ s.call(line)
+ end
+ end
end
end
end
diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb
index 9a55e49e27..bb092fcde9 100644
--- a/activesupport/lib/active_support/cache/redis_cache_store.rb
+++ b/activesupport/lib/active_support/cache/redis_cache_store.rb
@@ -152,12 +152,14 @@ module ActiveSupport
# Creates a new Redis cache store.
#
- # Handles three options: block provided to instantiate, single URL
- # provided, and multiple URLs provided.
+ # Handles four options: :redis block, :redis instance, single :url
+ # string, and multiple :url strings.
#
- # :redis Proc -> options[:redis].call
- # :url String -> Redis.new(url: …)
- # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
+ # Option Class Result
+ # :redis Proc -> options[:redis].call
+ # :redis Object -> options[:redis]
+ # :url String -> Redis.new(url: …)
+ # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
#
# No namespace is set by default. Provide one if the Redis cache
# server is shared with other apps: <tt>namespace: 'myapp-cache'</tt>.
@@ -361,6 +363,7 @@ module ActiveSupport
def read_multi_mget(*names)
options = names.extract_options!
options = merged_options(options)
+ return {} if names == []
keys = names.map { |name| normalize_key(name, options) }
diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb
index 5d356a0ab6..708c445031 100644
--- a/activesupport/lib/active_support/concern.rb
+++ b/activesupport/lib/active_support/concern.rb
@@ -110,7 +110,7 @@ module ActiveSupport
base.instance_variable_set(:@_dependencies, [])
end
- def append_features(base)
+ def append_features(base) #:nodoc:
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
false
@@ -123,6 +123,9 @@ module ActiveSupport
end
end
+ # Evaluate given block in context of base class,
+ # so that you can write class macros here.
+ # When you define more than one +included+ block, it raises an exception.
def included(base = nil, &block)
if base.nil?
if instance_variable_defined?(:@_included_block)
@@ -137,6 +140,26 @@ module ActiveSupport
end
end
+ # Define class methods from given block.
+ # You can define private class methods as well.
+ #
+ # module Example
+ # extend ActiveSupport::Concern
+ #
+ # class_methods do
+ # def foo; puts 'foo'; end
+ #
+ # private
+ # def bar; puts 'bar'; end
+ # end
+ # end
+ #
+ # class Buzz
+ # include Example
+ # end
+ #
+ # Buzz.foo # => "foo"
+ # Buzz.bar # => private method 'bar' called for Buzz:Class(NoMethodError)
def class_methods(&class_methods_module_definition)
mod = const_defined?(:ClassMethods, false) ?
const_get(:ClassMethods) :
diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb
index 10e4c6b09d..ea01e5891c 100644
--- a/activesupport/lib/active_support/core_ext/array/access.rb
+++ b/activesupport/lib/active_support/core_ext/array/access.rb
@@ -31,16 +31,16 @@ class Array
# Returns a new array that includes the passed elements.
#
- # [ 1, 2, 3 ].including(4, 5) => [ 1, 2, 3, 4, 5 ]
- # [ [ 0, 1 ] ].including([ [ 1, 0 ] ]) => [ [ 0, 1 ], [ 1, 0 ] ]
+ # [ 1, 2, 3 ].including(4, 5) # => [ 1, 2, 3, 4, 5 ]
+ # [ [ 0, 1 ] ].including([ [ 1, 0 ] ]) # => [ [ 0, 1 ], [ 1, 0 ] ]
def including(*elements)
self + elements.flatten(1)
end
# Returns a copy of the Array excluding the specified elements.
#
- # ["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd") => ["David", "Rafael"]
- # [ [ 0, 1 ], [ 1, 0 ] ].excluding([ [ 1, 0 ] ]) => [ [ 0, 1 ] ]
+ # ["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd") # => ["David", "Rafael"]
+ # [ [ 0, 1 ], [ 1, 0 ] ].excluding([ [ 1, 0 ] ]) # => [ [ 0, 1 ] ]
#
# Note: This is an optimization of <tt>Enumerable#excluding</tt> that uses <tt>Array#-</tt>
# instead of <tt>Array#reject</tt> for performance reasons.
diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb
index 6258610c98..5013812460 100644
--- a/activesupport/lib/active_support/core_ext/hash/except.rb
+++ b/activesupport/lib/active_support/core_ext/hash/except.rb
@@ -10,7 +10,7 @@ class Hash
# This is useful for limiting a set of parameters to everything but a few known toggles:
# @person.update(params[:person].except(:admin))
def except(*keys)
- dup.except!(*keys)
+ slice(*self.keys - keys)
end
# Removes the given keys from hash and returns it.
diff --git a/activesupport/lib/active_support/core_ext/range/compare_range.rb b/activesupport/lib/active_support/core_ext/range/compare_range.rb
index 6f6d2a27bb..ea1dc29a76 100644
--- a/activesupport/lib/active_support/core_ext/range/compare_range.rb
+++ b/activesupport/lib/active_support/core_ext/range/compare_range.rb
@@ -3,9 +3,10 @@
module ActiveSupport
module CompareWithRange
# Extends the default Range#=== to support range comparisons.
- # (1..5) === (1..5) # => true
- # (1..5) === (2..3) # => true
- # (1..5) === (2..6) # => false
+ # (1..5) === (1..5) # => true
+ # (1..5) === (2..3) # => true
+ # (1..5) === (1...6) # => true
+ # (1..5) === (2..6) # => false
#
# The native Range#=== behavior is untouched.
# ('a'..'f') === ('c') # => true
@@ -13,17 +14,20 @@ module ActiveSupport
def ===(value)
if value.is_a?(::Range)
# 1...10 includes 1..9 but it does not include 1..10.
+ # 1..10 includes 1...11 but it does not include 1...12.
operator = exclude_end? && !value.exclude_end? ? :< : :<=
- super(value.first) && value.last.send(operator, last)
+ value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
+ super(value.first) && value_max.send(operator, last)
else
super
end
end
# Extends the default Range#include? to support range comparisons.
- # (1..5).include?(1..5) # => true
- # (1..5).include?(2..3) # => true
- # (1..5).include?(2..6) # => false
+ # (1..5).include?(1..5) # => true
+ # (1..5).include?(2..3) # => true
+ # (1..5).include?(1...6) # => true
+ # (1..5).include?(2..6) # => false
#
# The native Range#include? behavior is untouched.
# ('a'..'f').include?('c') # => true
@@ -31,17 +35,20 @@ module ActiveSupport
def include?(value)
if value.is_a?(::Range)
# 1...10 includes 1..9 but it does not include 1..10.
+ # 1..10 includes 1...11 but it does not include 1...12.
operator = exclude_end? && !value.exclude_end? ? :< : :<=
- super(value.first) && value.last.send(operator, last)
+ value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
+ super(value.first) && value_max.send(operator, last)
else
super
end
end
# Extends the default Range#cover? to support range comparisons.
- # (1..5).cover?(1..5) # => true
- # (1..5).cover?(2..3) # => true
- # (1..5).cover?(2..6) # => false
+ # (1..5).cover?(1..5) # => true
+ # (1..5).cover?(2..3) # => true
+ # (1..5).cover?(1...6) # => true
+ # (1..5).cover?(2..6) # => false
#
# The native Range#cover? behavior is untouched.
# ('a'..'f').cover?('c') # => true
@@ -49,8 +56,10 @@ module ActiveSupport
def cover?(value)
if value.is_a?(::Range)
# 1...10 covers 1..9 but it does not cover 1..10.
+ # 1..10 covers 1...11 but it does not cover 1...12.
operator = exclude_end? && !value.exclude_end? ? :< : :<=
- super(value.first) && value.last.send(operator, last)
+ value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
+ super(value.first) && value_max.send(operator, last)
else
super
end
diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb
index 0b40c3d799..645b1fea17 100644
--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb
+++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb
@@ -135,10 +135,12 @@ module ActiveSupport #:nodoc:
class SafeBuffer < String
UNSAFE_STRING_METHODS = %w(
capitalize chomp chop delete delete_prefix delete_suffix
- downcase gsub lstrip next reverse rstrip slice squeeze strip
- sub succ swapcase tr tr_s unicode_normalize upcase
+ downcase lstrip next reverse rstrip slice squeeze strip
+ succ swapcase tr tr_s unicode_normalize upcase
)
+ UNSAFE_STRING_METHODS_WITH_BACKREF = %w(gsub sub)
+
alias_method :original_concat, :concat
private :original_concat
@@ -211,6 +213,12 @@ module ActiveSupport #:nodoc:
dup.concat(other)
end
+ def *(*)
+ new_safe_buffer = super
+ new_safe_buffer.instance_variable_set(:@html_safe, @html_safe)
+ new_safe_buffer
+ end
+
def %(args)
case args
when Hash
@@ -253,11 +261,44 @@ module ActiveSupport #:nodoc:
end
end
+ UNSAFE_STRING_METHODS_WITH_BACKREF.each do |unsafe_method|
+ if unsafe_method.respond_to?(unsafe_method)
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
+ def #{unsafe_method}(*args, &block) # def gsub(*args, &block)
+ if block # if block
+ to_str.#{unsafe_method}(*args) { |*params| # to_str.gsub(*args) { |*params|
+ set_block_back_references(block, $~) # set_block_back_references(block, $~)
+ block.call(*params) # block.call(*params)
+ } # }
+ else # else
+ to_str.#{unsafe_method}(*args) # to_str.gsub(*args)
+ end # end
+ end # end
+
+ def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block)
+ @html_safe = false # @html_safe = false
+ if block # if block
+ super(*args) { |*params| # super(*args) { |*params|
+ set_block_back_references(block, $~) # set_block_back_references(block, $~)
+ block.call(*params) # block.call(*params)
+ } # }
+ else # else
+ super # super
+ end # end
+ end # end
+ EOT
+ end
+ end
+
private
def html_escape_interpolated_argument(arg)
(!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
end
+
+ def set_block_back_references(block, match_data)
+ block.binding.eval("proc { |m| $~ = m }").call(match_data)
+ end
end
end
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index d5d00b5e6e..82f07c085e 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -70,6 +70,11 @@ module ActiveSupport #:nodoc:
# only once. All directories in this set must also be present in +autoload_paths+.
mattr_accessor :autoload_once_paths, default: []
+ # This is a private set that collects all eager load paths during bootstrap.
+ # Useful for Zeitwerk integration. Its public interface is the config.* path
+ # accessors of each engine.
+ mattr_accessor :_eager_load_paths, default: Set.new
+
# An array of qualified constant names that have been loaded. Adding a name
# to this array will cause it to be unloaded the next time Dependencies are
# cleared.
diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
index c6fdade006..d5dc7c2ff4 100644
--- a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
+++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require "set"
require "active_support/core_ext/string/inflections"
module ActiveSupport
@@ -9,6 +10,8 @@ module ActiveSupport
def clear
Dependencies.unload_interlock do
Rails.autoloaders.main.reload
+ rescue Zeitwerk::ReloadingDisabledError
+ raise "reloading is disabled because config.cache_classes is true"
end
end
@@ -20,17 +23,8 @@ module ActiveSupport
ActiveSupport::Inflector.safe_constantize(cpath)
end
- def autoloaded_constants
- cpaths = []
- Rails.autoloaders.each do |autoloader|
- cpaths.concat(autoloader.loaded_cpaths.to_a)
- end
- cpaths
- end
-
- def autoloaded?(object)
- cpath = object.is_a?(Module) ? object.name : object.to_s
- Rails.autoloaders.any? { |autoloader| autoloader.loaded?(cpath) }
+ def to_unload?(cpath)
+ Rails.autoloaders.main.to_unload?(cpath)
end
def verbose=(verbose)
@@ -50,27 +44,28 @@ module ActiveSupport
end
class << self
- def take_over
- setup_autoloaders
- freeze_autoload_paths
+ def take_over(enable_reloading:)
+ setup_autoloaders(enable_reloading)
+ freeze_paths
decorate_dependencies
end
private
- def setup_autoloaders
+ def setup_autoloaders(enable_reloading)
Dependencies.autoload_paths.each do |autoload_path|
# Zeitwerk only accepts existing directories in `push_dir` to
# prevent misconfigurations.
next unless File.directory?(autoload_path)
- if autoload_once?(autoload_path)
- Rails.autoloaders.once.push_dir(autoload_path)
- else
- Rails.autoloaders.main.push_dir(autoload_path)
- end
+ autoloader = \
+ autoload_once?(autoload_path) ? Rails.autoloaders.once : Rails.autoloaders.main
+
+ autoloader.push_dir(autoload_path)
+ autoloader.do_not_eager_load(autoload_path) unless eager_load?(autoload_path)
end
+ Rails.autoloaders.main.enable_reloading if enable_reloading
Rails.autoloaders.each(&:setup)
end
@@ -78,9 +73,14 @@ module ActiveSupport
Dependencies.autoload_once_paths.include?(autoload_path)
end
- def freeze_autoload_paths
+ def eager_load?(autoload_path)
+ Dependencies._eager_load_paths.member?(autoload_path)
+ end
+
+ def freeze_paths
Dependencies.autoload_paths.freeze
Dependencies.autoload_once_paths.freeze
+ Dependencies._eager_load_paths.freeze
end
def decorate_dependencies
diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb
index d99571790f..7c0a54a1d0 100644
--- a/activesupport/lib/active_support/deprecation/method_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/module/redefine_method"
module ActiveSupport
class Deprecation
@@ -52,29 +53,17 @@ module ActiveSupport
options = method_names.extract_options!
deprecator = options.delete(:deprecator) || self
method_names += options.keys
- mod = Module.new
+ mod = nil
method_names.each do |method_name|
if target_module.method_defined?(method_name) || target_module.private_method_defined?(method_name)
- aliased_method, punctuation = method_name.to_s.sub(/([?!=])$/, ""), $1
- with_method = "#{aliased_method}_with_deprecation#{punctuation}"
- without_method = "#{aliased_method}_without_deprecation#{punctuation}"
-
- target_module.define_method(with_method) do |*args, &block|
+ method = target_module.instance_method(method_name)
+ target_module.redefine_method(method_name) do |*args, &block|
deprecator.deprecation_warning(method_name, options[method_name])
- send(without_method, *args, &block)
- end
-
- target_module.alias_method(without_method, method_name)
- target_module.alias_method(method_name, with_method)
-
- case
- when target_module.protected_method_defined?(without_method)
- target_module.send(:protected, method_name)
- when target_module.private_method_defined?(without_method)
- target_module.send(:private, method_name)
+ method.bind(self).call(*args, &block)
end
else
+ mod ||= Module.new
mod.define_method(method_name) do |*args, &block|
deprecator.deprecation_warning(method_name, options[method_name])
super(*args, &block)
@@ -82,7 +71,7 @@ module ActiveSupport
end
end
- target_module.prepend(mod) unless mod.instance_methods(false).empty?
+ target_module.prepend(mod) if mod
end
end
end
diff --git a/activesupport/lib/active_support/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb
index 05236d3162..fe0c6991aa 100644
--- a/activesupport/lib/active_support/descendants_tracker.rb
+++ b/activesupport/lib/active_support/descendants_tracker.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "weakref"
+
module ActiveSupport
# This module provides an internal implementation to track descendants
# which is faster than iterating through ObjectSpace.
@@ -8,7 +10,8 @@ module ActiveSupport
class << self
def direct_descendants(klass)
- @@direct_descendants[klass] || []
+ descendants = @@direct_descendants[klass]
+ descendants ? descendants.to_a : []
end
def descendants(klass)
@@ -19,11 +22,18 @@ module ActiveSupport
def clear
if defined? ActiveSupport::Dependencies
+ # to_unload? is only defined in Zeitwerk mode.
+ to_unload = if Dependencies.respond_to?(:to_unload?)
+ ->(klass) { Dependencies.to_unload?(klass.name) }
+ else
+ ->(klass) { Dependencies.autoloaded?(klass) }
+ end
+
@@direct_descendants.each do |klass, descendants|
- if ActiveSupport::Dependencies.autoloaded?(klass)
+ if to_unload[klass]
@@direct_descendants.delete(klass)
else
- descendants.reject! { |v| ActiveSupport::Dependencies.autoloaded?(v) }
+ descendants.reject! { |v| to_unload[v] }
end
end
else
@@ -34,15 +44,17 @@ module ActiveSupport
# This is the only method that is not thread safe, but is only ever called
# during the eager loading phase.
def store_inherited(klass, descendant)
- (@@direct_descendants[klass] ||= []) << descendant
+ (@@direct_descendants[klass] ||= DescendantsArray.new) << descendant
end
private
def accumulate_descendants(klass, acc)
if direct_descendants = @@direct_descendants[klass]
- acc.concat(direct_descendants)
- direct_descendants.each { |direct_descendant| accumulate_descendants(direct_descendant, acc) }
+ direct_descendants.each do |direct_descendant|
+ acc << direct_descendant
+ accumulate_descendants(direct_descendant, acc)
+ end
end
end
end
@@ -59,5 +71,46 @@ module ActiveSupport
def descendants
DescendantsTracker.descendants(self)
end
+
+ # DescendantsArray is an array that contains weak references to classes.
+ class DescendantsArray # :nodoc:
+ include Enumerable
+
+ def initialize
+ @refs = []
+ end
+
+ def initialize_copy(orig)
+ @refs = @refs.dup
+ end
+
+ def <<(klass)
+ cleanup!
+ @refs << WeakRef.new(klass)
+ end
+
+ def each
+ @refs.each do |ref|
+ yield ref.__getobj__
+ rescue WeakRef::RefError
+ end
+ end
+
+ def refs_size
+ @refs.size
+ end
+
+ def cleanup!
+ @refs.delete_if { |ref| !ref.weakref_alive? }
+ end
+
+ def reject!
+ @refs.reject! do |ref|
+ yield ref.__getobj__
+ rescue WeakRef::RefError
+ true
+ end
+ end
+ end
end
end
diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb
index 97b4634d7b..a30bd11a87 100644
--- a/activesupport/lib/active_support/duration.rb
+++ b/activesupport/lib/active_support/duration.rb
@@ -4,7 +4,6 @@ require "active_support/core_ext/array/conversions"
require "active_support/core_ext/module/delegation"
require "active_support/core_ext/object/acts_like"
require "active_support/core_ext/string/filters"
-require "active_support/deprecation"
module ActiveSupport
# Provides accurate date and time measurements using Date#advance and
diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb
index 3a2b2652c4..42ae7e9b7b 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -225,8 +225,8 @@ module ActiveSupport
# hash[:a] = 'x'
# hash[:b] = 'y'
# hash.values_at('a', 'b') # => ["x", "y"]
- def values_at(*indices)
- indices.collect { |key| self[convert_key(key)] }
+ def values_at(*keys)
+ super(*keys.map { |key| convert_key(key) })
end
# Returns an array of the values at the specified indices, but also
@@ -239,7 +239,7 @@ module ActiveSupport
# hash.fetch_values('a', 'c') { |key| 'z' } # => ["x", "z"]
# hash.fetch_values('a', 'c') # => KeyError: key not found: "c"
def fetch_values(*indices, &block)
- indices.collect { |key| fetch(key, &block) }
+ super(*indices.map { |key| convert_key(key) }, &block)
end
# Returns a shallow copy of the hash.
@@ -293,6 +293,9 @@ module ActiveSupport
super(convert_key(key))
end
+ def except(*keys)
+ slice(*self.keys - keys.map { |key| convert_key(key) })
+ end
alias_method :without, :except
def stringify_keys!; self end
diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb
index 584930e413..8faa93a3e4 100644
--- a/activesupport/lib/active_support/i18n_railtie.rb
+++ b/activesupport/lib/active_support/i18n_railtie.rb
@@ -97,7 +97,8 @@ module I18n
If you desire the default locale to be included in the defaults, please
explicitly configure it with `config.i18n.fallbacks.defaults =
[I18n.default_locale]` or `config.i18n.fallbacks = [I18n.default_locale,
- {...}]`
+ {...}]`. If you want to opt-in to the new behavior, use
+ `config.i18n.fallbacks.defaults = [nil, {...}]`.
MSG
args.unshift I18n.default_locale
end
diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb
index c506b35b1e..8812b67f63 100644
--- a/activesupport/lib/active_support/notifications/fanout.rb
+++ b/activesupport/lib/active_support/notifications/fanout.rb
@@ -180,13 +180,13 @@ module ActiveSupport
def start(name, id, payload)
timestack = Thread.current[:_timestack] ||= []
- timestack.push Time.now
+ timestack.push Concurrent.monotonic_time
end
def finish(name, id, payload)
timestack = Thread.current[:_timestack]
started = timestack.pop
- @delegate.call(name, started, Time.now, id, payload)
+ @delegate.call(name, started, Concurrent.monotonic_time, id, payload)
end
end
diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb
index a03e7e483e..12546511a8 100644
--- a/activesupport/lib/active_support/notifications/instrumenter.rb
+++ b/activesupport/lib/active_support/notifications/instrumenter.rb
@@ -68,9 +68,8 @@ module ActiveSupport
@transaction_id = transaction_id
@end = ending
@children = []
- @duration = nil
- @cpu_time_start = nil
- @cpu_time_finish = nil
+ @cpu_time_start = 0
+ @cpu_time_finish = 0
@allocation_count_start = 0
@allocation_count_finish = 0
end
@@ -125,7 +124,7 @@ module ActiveSupport
#
# @event.duration # => 1000.138
def duration
- @duration ||= 1000.0 * (self.end - time)
+ 1000.0 * (self.end - time)
end
def <<(event)
diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb
index 20b6b9cd3f..5e455fca57 100644
--- a/activesupport/lib/active_support/security_utils.rb
+++ b/activesupport/lib/active_support/security_utils.rb
@@ -24,7 +24,7 @@ module ActiveSupport
# The values are first processed by SHA256, so that we don't leak length info
# via timing attacks.
def secure_compare(a, b)
- fixed_length_secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b)) && a == b
+ fixed_length_secure_compare(::Digest::SHA256.digest(a), ::Digest::SHA256.digest(b)) && a == b
end
module_function :secure_compare
end
diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb
index f3e902f9dd..c3cd175a52 100644
--- a/activesupport/lib/active_support/subscriber.rb
+++ b/activesupport/lib/active_support/subscriber.rb
@@ -24,6 +24,10 @@ module ActiveSupport
# After configured, whenever a "sql.active_record" notification is published,
# it will properly dispatch the event (ActiveSupport::Notifications::Event) to
# the +sql+ method.
+ #
+ # We can detach a subscriber as well:
+ #
+ # ActiveRecord::StatsSubscriber.detach_from(:active_record)
class Subscriber
class << self
# Attach the subscriber to a namespace.
@@ -40,6 +44,25 @@ module ActiveSupport
end
end
+ # Detach the subscriber from a namespace.
+ def detach_from(namespace, notifier = ActiveSupport::Notifications)
+ @namespace = namespace
+ @subscriber = find_attached_subscriber
+ @notifier = notifier
+
+ return unless subscriber
+
+ subscribers.delete(subscriber)
+
+ # Remove event subscribers of all existing methods on the class.
+ subscriber.public_methods(false).each do |event|
+ remove_event_subscriber(event)
+ end
+
+ # Reset notifier so that event subscribers will not add for new methods added to the class.
+ @notifier = nil
+ end
+
# Adds event subscribers for all new methods added to the class.
def method_added(event)
# Only public methods are added as subscribers, and only if a notifier
@@ -58,15 +81,41 @@ module ActiveSupport
attr_reader :subscriber, :notifier, :namespace
def add_event_subscriber(event) # :doc:
- return if %w{ start finish }.include?(event.to_s)
+ return if invalid_event?(event.to_s)
- pattern = "#{event}.#{namespace}"
+ pattern = prepare_pattern(event)
# Don't add multiple subscribers (eg. if methods are redefined).
- return if subscriber.patterns.include?(pattern)
+ return if pattern_subscribed?(pattern)
+
+ subscriber.patterns[pattern] = notifier.subscribe(pattern, subscriber)
+ end
+
+ def remove_event_subscriber(event) # :doc:
+ return if invalid_event?(event.to_s)
+
+ pattern = prepare_pattern(event)
+
+ return unless pattern_subscribed?(pattern)
+
+ notifier.unsubscribe(subscriber.patterns[pattern])
+ subscriber.patterns.delete(pattern)
+ end
+
+ def find_attached_subscriber
+ subscribers.find { |attached_subscriber| attached_subscriber.instance_of?(self) }
+ end
+
+ def invalid_event?(event)
+ %w{ start finish }.include?(event.to_s)
+ end
+
+ def prepare_pattern(event)
+ "#{event}.#{namespace}"
+ end
- subscriber.patterns << pattern
- notifier.subscribe(pattern, subscriber)
+ def pattern_subscribed?(pattern)
+ subscriber.patterns.key?(pattern)
end
end
@@ -74,7 +123,7 @@ module ActiveSupport
def initialize
@queue_key = [self.class.name, object_id].join "-"
- @patterns = []
+ @patterns = {}
super
end
diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb
index 63440069b1..e760bf5ce3 100644
--- a/activesupport/lib/active_support/testing/parallelization.rb
+++ b/activesupport/lib/active_support/testing/parallelization.rb
@@ -71,7 +71,9 @@ module ActiveSupport
fork do
DRb.stop_service
- after_fork(worker)
+ begin
+ after_fork(worker)
+ rescue => setup_exception; end
queue = DRbObject.new_with_uri(@url)
@@ -79,7 +81,11 @@ module ActiveSupport
klass = job[0]
method = job[1]
reporter = job[2]
- result = Minitest.run_one_method(klass, method)
+ result = klass.with_info_handler reporter do
+ Minitest.run_one_method(klass, method)
+ end
+
+ add_setup_exception(result, setup_exception) if setup_exception
begin
queue.record(reporter, result)
@@ -104,6 +110,11 @@ module ActiveSupport
@queue_size.times { @queue << nil }
@pool.each { |pid| Process.waitpid pid }
end
+
+ private
+ def add_setup_exception(result, setup_exception)
+ result.failures.prepend Minitest::UnexpectedError.new(setup_exception)
+ end
end
end
end
diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb
index 62356e4d46..01e60abd99 100644
--- a/activesupport/test/abstract_unit.rb
+++ b/activesupport/test/abstract_unit.rb
@@ -40,3 +40,5 @@ class ActiveSupport::TestCase
skip message if defined?(JRUBY_VERSION)
end
end
+
+require_relative "../../tools/test_common"
diff --git a/activesupport/test/actionable_error_test.rb b/activesupport/test/actionable_error_test.rb
new file mode 100644
index 0000000000..63046b937c
--- /dev/null
+++ b/activesupport/test/actionable_error_test.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/actionable_error"
+
+class ActionableErrorTest < ActiveSupport::TestCase
+ NonActionableError = Class.new(StandardError)
+
+ class DispatchableError < StandardError
+ include ActiveSupport::ActionableError
+
+ class_attribute :flip1, default: false
+ class_attribute :flip2, default: false
+
+ action "Flip 1" do
+ self.flip1 = true
+ end
+
+ action "Flip 2" do
+ self.flip2 = true
+ end
+ end
+
+ test "returns all action of an actionable error" do
+ assert_equal ["Flip 1", "Flip 2"], ActiveSupport::ActionableError.actions(DispatchableError).keys
+ assert_equal ["Flip 1", "Flip 2"], ActiveSupport::ActionableError.actions(DispatchableError.new).keys
+ end
+
+ test "returns no actions for non-actionable errors" do
+ assert ActiveSupport::ActionableError.actions(Exception).empty?
+ assert ActiveSupport::ActionableError.actions(Exception.new).empty?
+ end
+
+ test "dispatches actions from error and name" do
+ assert_changes "DispatchableError.flip1", from: false, to: true do
+ ActiveSupport::ActionableError.dispatch DispatchableError, "Flip 1"
+ end
+ end
+
+ test "cannot dispatch missing actions" do
+ err = assert_raises ActiveSupport::ActionableError::NonActionable do
+ ActiveSupport::ActionableError.dispatch NonActionableError, "action"
+ end
+
+ assert_equal 'Cannot find action "action"', err.to_s
+ end
+end
diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb
index 1d87f74347..790534cd3c 100644
--- a/activesupport/test/cache/stores/redis_cache_store_test.rb
+++ b/activesupport/test/cache/stores/redis_cache_store_test.rb
@@ -140,6 +140,12 @@ module ActiveSupport::Cache::RedisCacheStoreTests
end
end
+ def test_fetch_multi_without_names
+ assert_not_called(@cache.redis, :mget) do
+ @cache.fetch_multi() { }
+ end
+ end
+
def test_increment_expires_in
assert_called_with @cache.redis, :incrby, [ "#{@namespace}:foo", 1 ] do
assert_called_with @cache.redis, :expire, [ "#{@namespace}:foo", 60 ] do
diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb
index 4b8efb8a93..16d6a4c2f2 100644
--- a/activesupport/test/core_ext/range_ext_test.rb
+++ b/activesupport/test/core_ext/range_ext_test.rb
@@ -57,7 +57,7 @@ class RangeTest < ActiveSupport::TestCase
end
def test_should_include_other_with_exclusive_end
- assert((1..10).include?(1...10))
+ assert((1..10).include?(1...11))
end
def test_should_compare_identical_inclusive
@@ -69,7 +69,7 @@ class RangeTest < ActiveSupport::TestCase
end
def test_should_compare_other_with_exclusive_end
- assert((1..10) === (1...10))
+ assert((1..10) === (1...11))
end
def test_exclusive_end_should_not_include_identical_with_inclusive_end
@@ -93,6 +93,10 @@ class RangeTest < ActiveSupport::TestCase
assert range.method(:include?) != range.method(:cover?)
end
+ def test_should_cover_other_with_exclusive_end
+ assert((1..10).cover?(1...11))
+ end
+
def test_overlaps_on_time
time_range_1 = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30)
time_range_2 = Time.utc(2005, 12, 10, 17, 00)..Time.utc(2005, 12, 10, 18, 00)
diff --git a/activesupport/test/deprecation/method_wrappers_test.rb b/activesupport/test/deprecation/method_wrappers_test.rb
index 18729941bc..0aa3233aab 100644
--- a/activesupport/test/deprecation/method_wrappers_test.rb
+++ b/activesupport/test/deprecation/method_wrappers_test.rb
@@ -89,12 +89,4 @@ class MethodWrappersTest < ActiveSupport::TestCase
warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/
assert_deprecated(warning) { assert_equal "abc", @klass.old_method }
end
-
- def test_method_with_without_deprecation_is_exposed
- ActiveSupport::Deprecation.deprecate_methods(@klass, old_method: :new_method)
-
- warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/
- assert_deprecated(warning) { assert_equal "abc", @klass.new.old_method_with_deprecation }
- assert_equal "abc", @klass.new.old_method_without_deprecation
- end
end
diff --git a/activesupport/test/descendants_tracker_test_cases.rb b/activesupport/test/descendants_tracker_test_cases.rb
index 2c94c3c56c..f8752688d2 100644
--- a/activesupport/test/descendants_tracker_test_cases.rb
+++ b/activesupport/test/descendants_tracker_test_cases.rb
@@ -27,6 +27,15 @@ module DescendantsTrackerTestCases
assert_equal_sets [], Child2.descendants
end
+ def test_descendants_with_garbage_collected_classes
+ 1.times do
+ child_klass = Class.new(Parent)
+ assert_equal_sets [Child1, Grandchild1, Grandchild2, Child2, child_klass], Parent.descendants
+ end
+ GC.start
+ assert_equal_sets [Child1, Grandchild1, Grandchild2, Child2], Parent.descendants
+ end
+
def test_direct_descendants
assert_equal_sets [Child1, Child2], Parent.direct_descendants
assert_equal_sets [Grandchild1, Grandchild2], Child1.direct_descendants
diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb
index bb20d26a25..0af59764b5 100644
--- a/activesupport/test/notifications_test.rb
+++ b/activesupport/test/notifications_test.rb
@@ -302,7 +302,7 @@ module Notifications
class EventTest < TestCase
def test_events_are_initialized_with_details
- time = Time.now
+ time = Concurrent.monotonic_time
event = event(:foo, time, time + 0.01, random_id, {})
assert_equal :foo, event.name
@@ -310,15 +310,24 @@ module Notifications
assert_in_delta 10.0, event.duration, 0.00001
end
+ def test_event_cpu_time_and_idle_time_when_start_and_finish_is_not_called
+ time = Concurrent.monotonic_time
+ event = event(:foo, time, time + 0.01, random_id, {})
+
+ assert_equal 0, event.cpu_time
+ assert_in_delta 10.0, event.idle_time, 0.00001
+ end
+
+
def test_events_consumes_information_given_as_payload
- event = event(:foo, Time.now, Time.now + 1, random_id, payload: :bar)
+ event = event(:foo, Concurrent.monotonic_time, Concurrent.monotonic_time + 1, random_id, payload: :bar)
assert_equal Hash[payload: :bar], event.payload
end
def test_event_is_parent_based_on_children
- time = Time.utc(2009, 01, 01, 0, 0, 1)
+ time = Concurrent.monotonic_time
- parent = event(:foo, Time.utc(2009), Time.utc(2009) + 100, random_id, {})
+ parent = event(:foo, Concurrent.monotonic_time, Concurrent.monotonic_time + 100, random_id, {})
child = event(:foo, time, time + 10, random_id, {})
not_child = event(:foo, time, time + 100, random_id, {})
diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb
index 32e1d2d5bf..f475e05c9a 100644
--- a/activesupport/test/safe_buffer_test.rb
+++ b/activesupport/test/safe_buffer_test.rb
@@ -150,6 +150,14 @@ class SafeBufferTest < ActiveSupport::TestCase
assert_equal "hello&lt;&gt;", clean + @buffer
end
+ test "Should preserve html_safe? status on multiplication" do
+ multiplied_safe_buffer = "<br />".html_safe * 2
+ assert_predicate multiplied_safe_buffer, :html_safe?
+
+ multiplied_unsafe_buffer = @buffer.gsub("", "<>") * 2
+ assert_not_predicate multiplied_unsafe_buffer, :html_safe?
+ end
+
test "Should concat as a normal string when safe" do
clean = "hello".html_safe
@buffer.gsub!("", "<>")
@@ -248,4 +256,22 @@ class SafeBufferTest < ActiveSupport::TestCase
x = "Hello".html_safe
assert_nil x[/a/, 1]
end
+
+ test "Should set back references" do
+ a = "foo123".html_safe
+ a2 = a.sub(/([a-z]+)([0-9]+)/) { $2 + $1 }
+ assert_equal "123foo", a2
+ assert_not_predicate a2, :html_safe?
+ a.sub!(/([a-z]+)([0-9]+)/) { $2 + $1 }
+ assert_equal "123foo", a
+ assert_not_predicate a, :html_safe?
+
+ b = "foo123 bar456".html_safe
+ b2 = b.gsub(/([a-z]+)([0-9]+)/) { $2 + $1 }
+ assert_equal "123foo 456bar", b2
+ assert_not_predicate b2, :html_safe?
+ b.gsub!(/([a-z]+)([0-9]+)/) { $2 + $1 }
+ assert_equal "123foo 456bar", b
+ assert_not_predicate b, :html_safe?
+ end
end
diff --git a/activesupport/test/subscriber_test.rb b/activesupport/test/subscriber_test.rb
index 6b012e43af..bc8d8f1c13 100644
--- a/activesupport/test/subscriber_test.rb
+++ b/activesupport/test/subscriber_test.rb
@@ -23,6 +23,21 @@ class TestSubscriber < ActiveSupport::Subscriber
end
end
+class TestSubscriber2 < ActiveSupport::Subscriber
+ attach_to :doodle
+ detach_from :doodle
+
+ cattr_reader :events
+
+ def self.clear
+ @@events = []
+ end
+
+ def open_party(event)
+ events << event
+ end
+end
+
# Monkey patch subscriber to test that only one subscriber per method is added.
class TestSubscriber
remove_method :open_party
@@ -34,6 +49,7 @@ end
class SubscriberTest < ActiveSupport::TestCase
def setup
TestSubscriber.clear
+ TestSubscriber2.clear
end
def test_attaches_subscribers
@@ -53,4 +69,11 @@ class SubscriberTest < ActiveSupport::TestCase
assert_equal [], TestSubscriber.events
end
+
+ def test_detaches_subscribers
+ ActiveSupport::Notifications.instrument("open_party.doodle")
+
+ assert_equal [], TestSubscriber2.events
+ assert_equal 1, TestSubscriber.events.size
+ end
end
diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md
index beace1ed6f..798354550b 100644
--- a/guides/CHANGELOG.md
+++ b/guides/CHANGELOG.md
@@ -1,3 +1,8 @@
+* Added documentation about the `variants` option to `render`
+
+ *Edward Rudd*
+
+
## Rails 6.0.0.beta3 (March 11, 2019) ##
* No changes.
diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb
index f186ac526f..7f14c28bbc 100644
--- a/guides/rails_guides/markdown/renderer.rb
+++ b/guides/rails_guides/markdown/renderer.rb
@@ -16,7 +16,7 @@ HTML
end
def link(url, title, content)
- if url.start_with?("http://api.rubyonrails.org")
+ if %r{https?://api\.rubyonrails\.org}.match?(url)
%(<a href="#{api_link(url)}">#{content}</a>)
elsif title
%(<a href="#{url}" title="#{title}">#{content}</a>)
@@ -115,7 +115,7 @@ HTML
end
def api_link(url)
- if %r{http://api\.rubyonrails\.org/v\d+\.}.match?(url)
+ if %r{https?://api\.rubyonrails\.org/v\d+\.}.match?(url)
url
elsif edge
url.sub("api", "edgeapi")
diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md
index 9549b0b911..09faeea614 100644
--- a/guides/source/3_1_release_notes.md
+++ b/guides/source/3_1_release_notes.md
@@ -240,7 +240,7 @@ Action Pack
* Added `config.action_controller.include_all_helpers`. By default `helper :all` is done in `ActionController::Base`, which includes all the helpers by default. Setting `include_all_helpers` to `false` will result in including only application_helper and the helper corresponding to controller (like foo_helper for foo_controller).
-* `url_for` and named url helpers now accept `:subdomain` and `:domain` as options.
+* `url_for` and named URL helpers now accept `:subdomain` and `:domain` as options.
* Added `Base.http_basic_authenticate_with` to do simple http basic authentication with a single class method call.
@@ -293,7 +293,7 @@ Action Pack
You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](https://api.rubyonrails.org/v3.1.0/classes/ActionController/Streaming.html) for more information.
-* The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused.
+* The redirect route method now also accepts a hash of options which will only change the parts of the URL in question, or an object which responds to call, allowing for redirects to be reused.
### Action Dispatch
diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md
index 0cf9ca09c7..f1f41dc8fc 100644
--- a/guides/source/6_0_release_notes.md
+++ b/guides/source/6_0_release_notes.md
@@ -138,7 +138,7 @@ Please refer to the [Changelog][railties] for detailed changes.
the generators.
([Pull Request](https://github.com/rails/rails/pull/34021))
-* Add support for multi environment credentials=.
+* Add support for multi environment credentials.
([Pull Request](https://github.com/rails/rails/pull/33521))
* Make `null_store` as default cache store in test environment.
@@ -211,10 +211,237 @@ Please refer to the [Changelog][active-record] for detailed changes.
### Removals
+* Remove deprecated `#set_state` from the transaction object.
+ ([Commit](https://github.com/rails/rails/commit/6c745b0c5152a4437163a67707e02f4464493983))
+
+* Remove deprecated `#supports_statement_cache?` from the database adapters.
+ ([Commit](https://github.com/rails/rails/commit/5f3ed8784383fb4eb0f9959f31a9c28a991b7553))
+
+* Remove deprecated `#insert_fixtures` from the database adapters.
+ ([Commit](https://github.com/rails/rails/commit/400ba786e1d154448235f5f90183e48a1043eece))
+
+* Remove deprecated `ActiveRecord::ConnectionAdapters::SQLite3Adapter#valid_alter_table_type?`.
+ ([Commit](https://github.com/rails/rails/commit/45b4d5f81f0c0ca72c18d0dea4a3a7b2ecc589bf))
+
+* Remove support for passing the column name to `sum` when a block is passed.
+ ([Commit](https://github.com/rails/rails/commit/91ddb30083430622188d76eb9f29b78131df67f9))
+
+* Remove support for passing the column name to `count` when a block is passed.
+ ([Commit](https://github.com/rails/rails/commit/67356f2034ab41305af7218f7c8b2fee2d614129))
+
+* Remove support for delegation of missing methods in a relation to arel.
+ ([Commit](https://github.com/rails/rails/commit/d97980a16d76ad190042b4d8578109714e9c53d0))
+
+* Remove support for delegating missing methods in a relation to private methods of the class.
+ ([Commit](https://github.com/rails/rails/commit/a7becf147afc85c354e5cfa519911a948d25fc4d))
+
+* Remove support for specifying a timestamp name for `#cache_key`.
+ ([Commit](https://github.com/rails/rails/commit/0bef23e630f62e38f20b5ae1d1d5dbfb087050ea))
+
+* Remove deprecated `ActiveRecord::Migrator.migrations_path=`.
+ ([Commit](https://github.com/rails/rails/commit/90d7842186591cae364fab3320b524e4d31a7d7d))
+
+* Remove deprecated `expand_hash_conditions_for_aggregates`.
+ ([Commit](https://github.com/rails/rails/commit/27b252d6a85e300c7236d034d55ec8e44f57a83e))
+
+
### Deprecations
+* Deprecate mismatched case-sensitivity collation comparisons for uniqueness validator.
+ ([Commit](https://github.com/rails/rails/commit/9def05385f1cfa41924bb93daa187615e88c95b9))
+
+* Deprecate using class level querying methods if the receiver scope has leaked.
+ ([Pull Request](https://github.com/rails/rails/pull/35280))
+
+* Deprecate `config.activerecord.sqlite3.represent_boolean_as_integer`.
+ ([Commit](https://github.com/rails/rails/commit/f59b08119bc0c01a00561d38279b124abc82561b))
+
+* Deprecate passing `migrations_paths` to `connection.assume_migrated_upto_version`.
+ ([Commit](https://github.com/rails/rails/commit/c1b14aded27e063ead32fa911aa53163d7cfc21a))
+
+* Deprecate `ActiveRecord::Result#to_hash` in favor of `ActiveRecord::Result#to_a`.
+ ([Commit](https://github.com/rails/rails/commit/16510d609c601aa7d466809f3073ec3313e08937))
+
+* Deprecate methods in `DatabaseLimits`: `column_name_length`, `table_name_length`,
+ `columns_per_table`, `indexes_per_table`, `columns_per_multicolumn_index`,
+ `sql_query_length`, and `joins_per_query`.
+ ([Commit](https://github.com/rails/rails/commit/e0a1235f7df0fa193c7e299a5adee88db246b44f))
+
+* Deprecate `update_attributes`/`!` in favor of `update`/`!`.
+ ([Commit](https://github.com/rails/rails/commit/5645149d3a27054450bd1130ff5715504638a5f5))
+
### Notable changes
+* Bump the minimum sqlite3 version to 1.4.
+ ([Pull Request](https://github.com/rails/rails/pull/35844))
+
+* Add `rails db:prepare` to create a database if it doesn't exist, and run its migrations.
+ ([Pull Request](https://github.com/rails/rails/pull/35768))
+
+* Add `after_save_commit` callback as shortcut for `after_commit :hook, on: [ :create, :update ]`.
+ ([Pull Request](https://github.com/rails/rails/pull/35804))
+
+* Add `ActiveRecord::Relation#extract_associated` for extracting associated records from a relation.
+ ([Pull Request](https://github.com/rails/rails/pull/35784))
+
+* Add `ActiveRecord::Relation#annotate` for adding SQL comments to ActiveRecord::Relation queries.
+ ([Pull Request](https://github.com/rails/rails/pull/35617))
+
+* Add support for setting Optimizer Hints on databases.
+ ([Pull Request](https://github.com/rails/rails/pull/35615))
+
+* Add `insert_all`/`insert_all!`/`upsert_all` methods for doing bulk inserts.
+ ([Pull Request](https://github.com/rails/rails/pull/35631))
+
+* Add `rails db:seed:replant` that truncates tables of each database
+ for ther current environment and loads the seeds.
+ ([Pull Request](https://github.com/rails/rails/pull/34779))
+
+* Add `reselect` method, which is a short-hand for `unscope(:select).select(fields)`.
+ ([Pull Request](https://github.com/rails/rails/pull/33611))
+
+* Add negative scopes for all enum values.
+ ([Pull Request](https://github.com/rails/rails/pull/35381))
+
+* Add `#destroy_by` and `#delete_by` for conditional removals.
+ ([Pull Request](https://github.com/rails/rails/pull/35316))
+
+* Add the ability to automatically switch database connections.
+ ([Pull Request](https://github.com/rails/rails/pull/35073))
+
+* Add the ability to prevent writes to a database for the duration of a block.
+ ([Pull Request](https://github.com/rails/rails/pull/34505))
+
+* Add an API for switching connections to support multiple databases.
+ ([Pull Request](https://github.com/rails/rails/pull/34052))
+
+* Make timestamps with precision the default for migrations.
+ ([Pull Request](https://github.com/rails/rails/pull/34970))
+
+* Support `:size` option to change text and blob size in MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/35071))
+
+* Set both the foreign key and the foreign type columns to NULL for
+ polymorphic associations on `dependent: :nullify` strategy.
+ ([Pull Request](https://github.com/rails/rails/pull/28078))
+
+* Allow a permitted instance of `ActionController::Parameters` to be passed as an
+ argument to `ActiveRecord::Relation#exists?`.
+ ([Pull Request](https://github.com/rails/rails/pull/34891))
+
+* Add support in `#where` for endless ranges introduced in Ruby 2.6.
+ ([Pull Request](https://github.com/rails/rails/pull/34906))
+
+* Make `ROW_FORMAT=DYNAMIC` a default create table option for MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/34742))
+
+* Add the ability to disable scopes generated by `ActiveRecord.enum`.
+ ([Pull Request](https://github.com/rails/rails/pull/34605/files))
+
+* Make implicit ordering configurable for a column.
+ ([Pull Request](https://github.com/rails/rails/pull/34480))
+
+* Bump the minimum PostgreSQL version to 9.3, dropping support for 9.1 and 9.2.
+ ([Pull Request](https://github.com/rails/rails/pull/34520))
+
+* Make the values of an enum frozen, raising an error when attempting to modify them.
+ ([Pull Request](https://github.com/rails/rails/pull/34517))
+
+* Make the SQL of `ActiveRecord::StatementInvalid` errors its own error property
+ and include SQL binds as a separate error property.
+ ([Pull Request](https://github.com/rails/rails/pull/34468))
+
+* Add an `:if_not_exists` option to `create_table`.
+ ([Pull Request](https://github.com/rails/rails/pull/31382))
+
+* Add support for multiple databases to `rails db:schema:cache:dump`
+ and `rails db:schema:cache:clear`.
+ ([Pull Request](https://github.com/rails/rails/pull/34181))
+
+* Add support for hash and url configs in database hash of `ActiveRecord::Base.connected_to`.
+ ([Pull Request](https://github.com/rails/rails/pull/34196))
+
+* Add support for default expressions and expression indexes for MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/34307))
+
+* Add an `index` option for `change_table` migration helpers.
+ ([Pull Request](https://github.com/rails/rails/pull/23593))
+
+* Fix `transaction` reverting for migrations. Previously, commands inside of a `transaction`
+ in a reverted migration ran uninverted. This change fixes that.
+ ([Pull Request](https://github.com/rails/rails/pull/31604))
+
+* Allow `ActiveRecord::Base.configurations=` to be set with a symbolized hash.
+ ([Pull Request](https://github.com/rails/rails/pull/33968))
+
+* Fix the counter cache to only update if the record is actually saved.
+ ([Pull Request](https://github.com/rails/rails/pull/33913))
+
+* Add expression indexes support for the SQLite adapter.
+ ([Pull Request](https://github.com/rails/rails/pull/33874))
+
+* Allow subclasses to redefine autosave callbacks for associated records.
+ ([Pull Request](https://github.com/rails/rails/pull/33378))
+
+* Bump the minimum MySQL version to 5.5.8.
+ ([Pull Request](https://github.com/rails/rails/pull/33853))
+
+* Use the utf8mb4 character set by default in MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/33608))
+
+* Add the ability to filter out sensitive data in `#inspect`
+ ([Pull Request](https://github.com/rails/rails/pull/33756), [Pull Request](https://github.com/rails/rails/pull/34208))
+
+* Change `ActiveRecord::Base.configurations` to return an object instead of a hash.
+ ([Pull Request](https://github.com/rails/rails/pull/33637))
+
+* Add database configuration to disable advisory locks.
+ ([Pull Request](https://github.com/rails/rails/pull/33691))
+
+* Update SQLite3 adapter `alter_table` method to restore foreign keys.
+ ([Pull Request](https://github.com/rails/rails/pull/33585))
+
+* Allow the `:to_table` option of `remove_foreign_key` to be invertible.
+ ([Pull Request](https://github.com/rails/rails/pull/33530))
+
+* Fix default value for mysql time types with specified precision.
+ ([Pull Request](https://github.com/rails/rails/pull/33280))
+
+* Fix the `touch` option to behave consistently with `Persistence#touch` method.
+ ([Pull Request](https://github.com/rails/rails/pull/33107))
+
+* Raise an exception for duplicate column definitions in Migrations.
+ ([Pull Request](https://github.com/rails/rails/pull/33029))
+
+* Bump the minimum SQLite version to 3.8.
+ ([Pull Request](https://github.com/rails/rails/pull/32923))
+
+* Fix parent records to not get saved with duplicate children records.
+ ([Pull Request](https://github.com/rails/rails/pull/32952))
+
+* Ensure `Associations::CollectionAssociation#size` and `Associations::CollectionAssociation#empty?`
+ use loaded association ids if present.
+ ([Pull Request](https://github.com/rails/rails/pull/32617))
+
+* Add support to preload associations of polymorphic associations when not all the records have the requested associations.
+ ([Commit](https://github.com/rails/rails/commit/75ef18c67c29b1b51314b6c8a963cee53394080b))
+
+* Add `touch_all` method to `ActiveRecord::Relation`.
+ ([Pull Request](https://github.com/rails/rails/pull/31513))
+
+* Add `ActiveRecord::Base.base_class?` predicate.
+ ([Pull Request](https://github.com/rails/rails/pull/32417))
+
+* Add custom prefix/suffix options to `ActiveRecord::Store.store_accessor`.
+ ([Pull Request](https://github.com/rails/rails/pull/32306))
+
+* Add `ActiveRecord::Base.create_or_find_by`/`!` to deal with the SELECT/INSERT race condition in
+ `ActiveRecord::Base.find_or_create_by`/`!` by leaning on unique constraints in the database.
+ ([Pull Request](https://github.com/rails/rails/pull/31989))
+
+* Add `Relation#pick` as short-hand for single-value plucks.
+ ([Pull Request](https://github.com/rails/rails/pull/31941))
+
Active Storage
--------------
@@ -237,6 +464,18 @@ Please refer to the [Changelog][active-model] for detailed changes.
### Notable changes
+* Add a configuration option to customize format of the `ActiveModel::Errors#full_message`.
+ ([Pull Request](https://github.com/rails/rails/pull/32956))
+
+* Add support for configuring attribute name for `has_secure_password`.
+ ([Pull Request](https://github.com/rails/rails/pull/26764))
+
+* Add `#slice!` method to `ActiveModel::Errors`.
+ ([Pull Request](https://github.com/rails/rails/pull/34489))
+
+* Add `ActiveModel::Errors#of_kind?` to check presence of a specific error.
+ ([Pull Request](https://github.com/rails/rails/pull/34866))
+
Active Support
--------------
@@ -255,10 +494,56 @@ Please refer to the [Changelog][active-job] for detailed changes.
### Removals
+* Remove support for Qu gem.
+ ([Pull Request](https://github.com/rails/rails/pull/32300))
+
### Deprecations
### Notable changes
+* Add support for custom serializers for Active Job arguments.
+ ([Pull Request](https://github.com/rails/rails/pull/30941))
+
+* Add support for executing Active Jobs in the timezone in which
+ they were enqueued.
+ ([Pull Request](https://github.com/rails/rails/pull/32085))
+
+* Allow passing multiple exceptions to `retry_on`/`discard_on`.
+ ([Commit](https://github.com/rails/rails/commit/3110caecbebdad7300daaf26bfdff39efda99e25))
+
+* Allow calling `assert_enqueued_with` and `assert_enqueued_email_with` without a block.
+ ([Pull Request](https://github.com/rails/rails/pull/33258))
+
+* Wrap the notifications for `enqueue` and `enqueue_at` in the `around_enqueue`
+ callback instead of `after_enqueue` callback.
+ ([Pull Request](https://github.com/rails/rails/pull/33171))
+
+* Allow calling `perform_enqueued_jobs` without a block.
+ ([Pull Request](https://github.com/rails/rails/pull/33626))
+
+* Allow calling `assert_performed_with` without a block.
+ ([Pull Request](https://github.com/rails/rails/pull/33635))
+
+* Add `:queue` option to job assertions and helpers.
+ ([Pull Request](https://github.com/rails/rails/pull/33635))
+
+* Add hooks to Active Job around retries and discards.
+ ([Pull Request](https://github.com/rails/rails/pull/33751))
+
+* Add a way to test for subset of arguments when performing jobs.
+ ([Pull Request](https://github.com/rails/rails/pull/33995))
+
+* Include deserialized arguments in jobs returned by Active Job
+ test helpers.
+ ([Pull Request](https://github.com/rails/rails/pull/34204))
+
+* Allow Active Job assertion helpers to accept Proc for `only`
+ keyword.
+ ([Pull Request](https://github.com/rails/rails/pull/34339))
+
+* Drop microseconds and nanoseconds from the job arguments in assertion helpers.
+ ([Pull Request](https://github.com/rails/rails/pull/35713))
+
Ruby on Rails Guides
--------------------
diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md
index c531b6eee2..f1e2a0081f 100644
--- a/guides/source/action_cable_overview.md
+++ b/guides/source/action_cable_overview.md
@@ -190,6 +190,23 @@ This will ready a consumer that'll connect against `/cable` on your server by de
The connection won't be established until you've also specified at least one subscription
you're interested in having.
+The consumer can optionally take an argument that specifies the URL to connect to. This
+can be a string, or a function that returns a string that will be called when the
+WebSocket is opened.
+
+```js
+// Specify a different URL to connect to
+createConsumer('https://ws.example.com/cable')
+
+// Use a function to dynamically generate the URL
+createConsumer(getWebSocketURL)
+
+function getWebSocketURL {
+ const token = localStorage.get('auth-token')
+ return `https://ws.example.com/cable?token=${token}`
+}
+```
+
#### Subscriber
A consumer becomes a subscriber by creating a subscription to a given channel:
diff --git a/guides/source/action_mailbox_basics.md b/guides/source/action_mailbox_basics.md
index c90892d456..de92401226 100644
--- a/guides/source/action_mailbox_basics.md
+++ b/guides/source/action_mailbox_basics.md
@@ -19,9 +19,9 @@ Introduction
------------
Action Mailbox routes incoming emails to controller-like mailboxes for
-processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill,
-Postmark, and SendGrid. You can also handle inbound mails directly via the
-built-in Exim, Postfix, and Qmail ingresses.
+processing in Rails. It ships with ingresses for Mailgun, Mandrill, Postmark,
+and SendGrid. You can also handle inbound mails directly via the built-in Exim,
+Postfix, and Qmail ingresses.
The inbound emails are turned into `InboundEmail` records using Active Record
and feature lifecycle tracking, storage of the original email on cloud storage
@@ -43,28 +43,6 @@ $ rails db:migrate
## Configuration
-### Amazon SES
-
-Install the [`aws-sdk-sns`](https://rubygems.org/gems/aws-sdk-sns) gem:
-
-```ruby
-# Gemfile
-gem "aws-sdk-sns", ">= 1.9.0", require: false
-```
-
-Tell Action Mailbox to accept emails from SES:
-
-```ruby
-# config/environments/production.rb
-config.action_mailbox.ingress = :amazon
-```
-
-[Configure SES](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html)
-to deliver emails to your application via POST requests to
-`/rails/action_mailbox/amazon/inbound_emails`. If your application lived at
-`https://example.com`, you would specify the fully-qualified URL
-`https://example.com/rails/action_mailbox/amazon/inbound_emails`.
-
### Exim
Tell Action Mailbox to accept emails from an SMTP relay:
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index df74b4ebd0..f600cf29ce 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -573,7 +573,7 @@ web addresses. Thus, you should always use the "_url" variant of named route
helpers.
If you did not configure the `:host` option globally make sure to pass it to the
-url helper.
+URL helper.
```erb
<%= user_url(@user, host: 'example.com') %>
diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md
index 7f17b19a13..d765e32ac7 100644
--- a/guides/source/active_record_basics.md
+++ b/guides/source/active_record_basics.md
@@ -105,7 +105,7 @@ depending on the purpose of these columns.
fields that Active Record will look for when you create associations between
your models.
* **Primary keys** - By default, Active Record will use an integer column named
- `id` as the table's primary key (`bigint` for Postgres and MYSQL, `integer`
+ `id` as the table's primary key (`bigint` for PostgreSQL and MySQL, `integer`
for SQLite). When using [Active Record Migrations](active_record_migrations.html)
to create your tables, this column will be automatically created.
diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md
index 614737c342..8f54e78224 100644
--- a/guides/source/active_record_callbacks.md
+++ b/guides/source/active_record_callbacks.md
@@ -473,10 +473,33 @@ end
=> User was saved to database
```
-To register callbacks for both create and update actions, use `after_commit` instead.
+There is also an alias for using the `after_commit` callback for both create and update together:
+
+* `after_save_commit`
+
+```ruby
+class User < ApplicationRecord
+ after_save_commit :log_user_saved_to_db
+
+ private
+ def log_user_saved_to_db
+ puts 'User was saved to database'
+ end
+end
+
+# creating a User
+>> @user = User.create
+=> User was saved to database
+
+# updating @user
+>> @user.save
+=> User was saved to database
+```
+
+To register callbacks for both create and destroy actions, use `after_commit` instead.
```ruby
class User < ApplicationRecord
- after_commit :log_user_saved_to_db, on: [:create, :update]
+ after_commit :log_user_saved_to_db, on: [:create, :destroy]
end
```
diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md
index be0bc495f7..270e4a3bf9 100644
--- a/guides/source/active_record_migrations.md
+++ b/guides/source/active_record_migrations.md
@@ -465,7 +465,6 @@ number of digits after the decimal point.
* `default` Allows to set a default value on the column. Note that if you
are using a dynamic value (such as a date), the default will only be calculated
the first time (i.e. on the date the migration is applied).
-* `index` Adds an index for the column.
* `comment` Adds a comment for the column.
Some adapters may support additional options; see the adapter specific API docs
@@ -948,7 +947,7 @@ If `:ruby` is selected, then the schema is stored in `db/schema.rb`. If you look
at this file you'll find that it looks an awful lot like one very big migration:
```ruby
-ActiveRecord::Schema.define(version: 20080906171750) do
+ActiveRecord::Schema.define(version: 2008_09_06_171750) do
create_table "authors", force: true do |t|
t.string "name"
t.datetime "created_at"
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index cc6e08aaec..e40f16e62d 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -65,6 +65,7 @@ The methods are:
* `distinct`
* `eager_load`
* `extending`
+* `extract_associated`
* `from`
* `group`
* `having`
diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md
index d7dbc5cea8..4868b00bbe 100644
--- a/guides/source/active_support_instrumentation.md
+++ b/guides/source/active_support_instrumentation.md
@@ -545,6 +545,14 @@ Active Storage
| `:key` | Secure token |
| `:service` | Name of the service |
+### service_download_chunk.active_storage
+
+| Key | Value |
+| ------------ | ------------------------------- |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+| `:range` | Byte range attempted to be read |
+
### service_download.active_storage
| Key | Value |
@@ -580,7 +588,24 @@ Active Storage
| ------------ | ------------------- |
| `:key` | Secure token |
| `:service` | Name of the service |
-| `:url` | Generated url |
+| `:url` | Generated URL |
+
+### service_update_metadata.active_storage
+
+| Key | Value |
+| --------------- | ------------------------------ |
+| `:key` | Secure token |
+| `:service` | Name of the service |
+| `:content_type` | HTTP Content-Type field |
+| `:disposition` | HTTP Content-Disposition field |
+
+INFO. The only ActiveStorage service that provides this hook so far is GCS.
+
+### preview.active_storage
+
+| Key | Value |
+| ------------ | ------------------- |
+| `:key` | Secure token |
Railties
--------
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index 454613e733..d853559440 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -33,13 +33,11 @@ passing the `--skip-sprockets` option.
rails new appname --skip-sprockets
```
-Rails automatically adds the `sass-rails`, `coffee-rails` and `uglifier`
-gems to your `Gemfile`, which are used by Sprockets for asset compression:
+Rails automatically adds the `sass-rails` gem to your `Gemfile`, which is used
+by Sprockets for asset compression:
```ruby
gem 'sass-rails'
-gem 'uglifier'
-gem 'coffee-rails'
```
Using the `--skip-sprockets` option will prevent Rails from adding
@@ -176,8 +174,7 @@ in `app/assets` are never served directly in production.
### Controller Specific Assets
-When you generate a scaffold or a controller, Rails also generates a JavaScript
-file (or CoffeeScript file if the `coffee-rails` gem is in the `Gemfile`) and a
+When you generate a scaffold or a controller, Rails also generates a
Cascading Style Sheet file (or SCSS file if `sass-rails` is in the `Gemfile`)
for that controller. Additionally, when generating a scaffold, Rails generates
the file `scaffolds.css` (or `scaffolds.scss` if `sass-rails` is in the
@@ -434,9 +431,8 @@ one file rather than many, the load time of pages can be greatly reduced because
the browser makes fewer requests. Compression also reduces file size, enabling
the browser to download them faster.
-
-For example, a new Rails application includes a default
-`app/assets/javascripts/application.js` file containing the following lines:
+For example, with a `app/assets/javascripts/application.js` file containing the
+following lines:
```js
// ...
@@ -476,8 +472,7 @@ which contains these lines:
*/
```
-Rails creates both `app/assets/javascripts/application.js` and
-`app/assets/stylesheets/application.css` regardless of whether the
+Rails create `app/assets/stylesheets/application.css` regardless of whether the
--skip-sprockets option is used when creating a new Rails application. This is
so you can easily add asset pipelining later if you like.
@@ -517,8 +512,7 @@ The file extensions used on an asset determine what preprocessing is applied.
When a controller or a scaffold is generated with the default Rails gemset, a
CoffeeScript file and a SCSS file are generated in place of a regular JavaScript
and CSS file. The example used before was a controller called "projects", which
-generated an `app/assets/javascripts/projects.coffee` and an
-`app/assets/stylesheets/projects.scss` file.
+generated an `app/assets/stylesheets/projects.scss` file.
In development mode, or if the asset pipeline is disabled, when these files are
requested they are processed by the processors provided by the `coffee-script`
@@ -1083,7 +1077,7 @@ Possible options for JavaScript compression are `:closure`, `:uglifier` and
`:yui`. These require the use of the `closure-compiler`, `uglifier` or
`yui-compressor` gems, respectively.
-The default `Gemfile` includes [uglifier](https://github.com/lautis/uglifier).
+Take the `uglifier` gem, for example.
This gem wraps [UglifyJS](https://github.com/mishoo/UglifyJS) (written for
NodeJS) in Ruby. It compresses your code by removing white space and comments,
shortening local variable names, and performing other micro-optimizations such
@@ -1230,4 +1224,3 @@ it as a preprocessor for your mime type.
```ruby
Sprockets.register_preprocessor 'text/css', AddComment
```
-
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index e076f10ece..a60ce7fb32 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -384,7 +384,7 @@ end
The corresponding migration might look like this:
```ruby
-class CreateSuppliers < ActiveRecord::Migration[5.0]
+class CreateSuppliers < ActiveRecord::Migration[5.2]
def change
create_table :suppliers do |t|
t.string :name
@@ -392,7 +392,7 @@ class CreateSuppliers < ActiveRecord::Migration[5.0]
end
create_table :accounts do |t|
- t.integer :supplier_id
+ t.bigint :supplier_id
t.string :account_number
t.timestamps
end
@@ -402,7 +402,7 @@ class CreateSuppliers < ActiveRecord::Migration[5.0]
end
```
-NOTE: Using `t.integer :supplier_id` makes the foreign key naming obvious and explicit. In current versions of Rails, you can abstract away this implementation detail by using `t.references :supplier` instead.
+NOTE: Using `t.bigint :supplier_id` makes the foreign key naming obvious and explicit. In current versions of Rails, you can abstract away this implementation detail by using `t.references :supplier` instead.
### Choosing Between `has_many :through` and `has_and_belongs_to_many`
@@ -466,11 +466,11 @@ Similarly, you can retrieve `@product.pictures`.
If you have an instance of the `Picture` model, you can get to its parent via `@picture.imageable`. To make this work, you need to declare both a foreign key column and a type column in the model that declares the polymorphic interface:
```ruby
-class CreatePictures < ActiveRecord::Migration[5.0]
+class CreatePictures < ActiveRecord::Migration[5.2]
def change
create_table :pictures do |t|
t.string :name
- t.integer :imageable_id
+ t.bigint :imageable_id
t.string :imageable_type
t.timestamps
end
@@ -619,11 +619,11 @@ end
These need to be backed up by a migration to create the `assemblies_parts` table. This table should be created without a primary key:
```ruby
-class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0]
+class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.2]
def change
create_table :assemblies_parts, id: false do |t|
- t.integer :assembly_id
- t.integer :part_id
+ t.bigint :assembly_id
+ t.bigint :part_id
end
add_index :assemblies_parts, :assembly_id
@@ -1305,6 +1305,21 @@ The `:source` option specifies the source association name for a `has_one :throu
The `:source_type` option specifies the source association type for a `has_one :through` association that proceeds through a polymorphic association.
+```ruby
+class Book < ApplicationRecord
+ has_one :format, polymorphic: true
+ has_one :dust_jacket, through: :format, source: :dust_jacket, source_type: "Hardback"
+end
+
+class Paperback < ApplicationRecord; end
+
+class Hardback < ApplicationRecord
+ has_one :dust_jacket
+end
+
+class DustJacket < ApplicationRecord; end
+```
+
##### `:through`
The `:through` option specifies a join model through which to perform the query. `has_one :through` associations were discussed in detail [earlier in this guide](#the-has-one-through-association).
@@ -1717,6 +1732,20 @@ The `:source` option specifies the source association name for a `has_many :thro
The `:source_type` option specifies the source association type for a `has_many :through` association that proceeds through a polymorphic association.
+```ruby
+class Author < ApplicationRecord
+ has_many :books
+ has_many :paperbacks, through: :books, source: :format, source_type: "Paperback"
+end
+
+class Book < ApplicationRecord
+ has_one :format, polymorphic: true
+end
+
+class Hardback < ApplicationRecord; end
+class Paperback < ApplicationRecord; end
+```
+
##### `:through`
The `:through` option specifies a join model through which to perform the query. `has_many :through` associations provide a way to implement many-to-many relationships, as discussed [earlier in this guide](#the-has-many-through-association).
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index f6612ba338..3f013fff3a 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -302,7 +302,7 @@ class Product < ApplicationRecord
end
```
-NOTE: Notice that in this example we used the `cache_key_with_version` method, so the resulting cache key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key_with_version` generates a string based on the model's `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key.
+NOTE: Notice that in this example we used the `cache_key_with_version` method, so the resulting cache key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key_with_version` generates a string based on the model's class name, `id`, and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key.
### SQL Caching
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index 04c8352b90..4ad143d105 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -257,7 +257,7 @@ We will set up a simple resource called "HighScore" that will keep track of our
```bash
$ rails generate scaffold HighScore game:string score:integer
invoke active_record
- create db/migrate/20130717151933_create_high_scores.rb
+ create db/migrate/20190416145729_create_high_scores.rb
create app/models/high_score.rb
invoke test_unit
create test/models/high_score_test.rb
@@ -275,20 +275,19 @@ $ rails generate scaffold HighScore game:string score:integer
create app/views/high_scores/_form.html.erb
invoke test_unit
create test/controllers/high_scores_controller_test.rb
+ create test/system/high_scores_test.rb
invoke helper
create app/helpers/high_scores_helper.rb
+ invoke test_unit
invoke jbuilder
create app/views/high_scores/index.json.jbuilder
create app/views/high_scores/show.json.jbuilder
- invoke test_unit
- create test/system/high_scores_test.rb
+ create app/views/high_scores/_high_score.json.jbuilder
invoke assets
- invoke coffee
- create app/assets/javascripts/high_scores.coffee
invoke scss
create app/assets/stylesheets/high_scores.scss
invoke scss
- identical app/assets/stylesheets/scaffolds.scss
+ create app/assets/stylesheets/scaffolds.scss
```
The generator checks that there exist the directories for models, controllers, helpers, layouts, functional and unit tests, stylesheets, creates the views, controller, model and database migration for HighScore (creating the `high_scores` table and fields), takes care of the route for the **resource**, and new tests for everything.
@@ -344,7 +343,7 @@ irb(main):001:0>
Inside the `rails console` you have access to the `app` and `helper` instances.
-With the `app` method you can access url and path helpers, as well as do requests.
+With the `app` method you can access URL and path helpers, as well as do requests.
```bash
>> app.root_path
@@ -481,6 +480,22 @@ lib/school.rb:
* [ 17] [FIXME]
```
+#### Tags
+
+You can add more default tags to search for by using `config.annotations.register_tags`. It receives a list of tags.
+
+```ruby
+config.annotations.register_tags("DEPRECATEME", "TESTME")
+```
+
+```bash
+$ rails notes
+app/controllers/admin/users_controller.rb:
+ * [ 20] [TODO] do A/B testing on this
+ * [ 42] [TESTME] this needs more functional tests
+ * [132] [DEPRECATEME] ensure this method is deprecated in next release
+```
+
#### Directories
You can add more default directories to search from by using `config.annotations.register_directories`. It receives a list of directory names.
@@ -606,7 +621,7 @@ $ rails "task_name[value 1]" # entire argument string should be quoted
$ rails db:nothing
```
-NOTE: If your need to interact with your application models, perform database queries, and so on, your task should depend on the `environment` task, which will load your application code.
+NOTE: If you need to interact with your application models, perform database queries, and so on, your task should depend on the `environment` task, which will load your application code.
The Rails Advanced Command Line
-------------------------------
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index ed739aa0ab..e1c9fad232 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -310,7 +310,7 @@ All these configuration options are delegated to the `I18n` library.
### Configuring Active Model
-* `config.active_model.i18n_full_message` is a boolean value which controls whether the `full_message` error format can be overridden at the attribute or model level in the locale files. This is `false` by default.
+* `config.active_model.i18n_customize_full_message` is a boolean value which controls whether the `full_message` error format can be overridden at the attribute or model level in the locale files. This is `false` by default.
### Configuring Active Record
@@ -727,7 +727,7 @@ There are a few configuration options available in Active Support:
* `ActiveSupport::Deprecation.silence` takes a block in which all deprecation warnings are silenced.
-* `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings.
+* `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings. The default is `false`.
### Configuring Active Job
@@ -1162,7 +1162,7 @@ Imagine you have a server which mirrors the production environment but is only u
That environment is no different than the default ones, start a server with `rails server -e staging`, a console with `rails console -e staging`, `Rails.env.staging?` works, etc.
-### Deploy to a subdirectory (relative url root)
+### Deploy to a subdirectory (relative URL root)
By default Rails expects that your application is running at the root
(eg. `/`). This section explains how to run your application inside a directory.
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index 569f52652f..f86589bdf1 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -247,7 +247,7 @@ Rails follows a simple set of coding style conventions:
* Two spaces, no tabs (for indentation).
* No trailing whitespace. Blank lines should not have any spaces.
-* Indent after private/protected.
+* Indent and no blank line after private/protected.
* Use Ruby >= 1.9 syntax for hashes. Prefer `{ a: :b }` over `{ :a => :b }`.
* Prefer `&&`/`||` over `and`/`or`.
* Prefer class << self over self.method for class methods.
@@ -302,7 +302,7 @@ the recommended workflow with the [rails-dev-box](https://github.com/rails/rails
As a compromise, test what your code obviously affects, and if the change is
not in railties, run the whole test suite of the affected component. If all
tests are passing, that's enough to propose your contribution. We have
-[Travis CI](https://travis-ci.org/rails/rails) as a safety net for catching
+[Buildkite](https://buildkite.com/rails/rails) as a safety net for catching
unexpected breakages elsewhere.
#### Entire Rails:
@@ -418,7 +418,7 @@ To run a single test against all adapters, use:
$ bundle exec rake TEST=test/cases/associations/has_many_associations_test.rb
```
-You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server.
+You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests.
### Warnings
@@ -677,7 +677,7 @@ $ git apply ~/my_changes.patch
This works well for simple changes. However, if your changes are complicated or if the code in master has deviated significantly from your target branch, it might require more work on your part. The difficulty of a backport varies greatly from case to case, and sometimes it is simply not worth the effort.
-Once you have resolved all conflicts and made sure all the tests are passing, push your changes and open a separate pull request for your backport. It is also worth noting that older branches might have a different set of build targets than master. When possible, it is best to first test your backport locally against the Ruby versions listed in `.travis.yml` before submitting your pull request.
+Once you have resolved all conflicts and made sure all the tests are passing, push your changes and open a separate pull request for your backport. It is also worth noting that older branches might have a different set of build targets than master. When possible, it is best to first test your backport locally against the oldest Ruby version permitted by the target branch's `rails.gemspec` before submitting your pull request.
And then... think about your next contribution!
diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md
index 77513c3a84..170c22905b 100644
--- a/guides/source/debugging_rails_applications.md
+++ b/guides/source/debugging_rails_applications.md
@@ -962,15 +962,8 @@ Plugins for Debugging
There are some Rails plugins to help you to find errors and debug your
application. Here is a list of useful plugins for debugging:
-* [Footnotes](https://github.com/josevalim/rails-footnotes) Every Rails page has
-footnotes that give request information and link back to your source via
-TextMate.
* [Query Trace](https://github.com/ruckus/active-record-query-trace/tree/master) Adds query
origin tracing to your logs.
-* [Query Reviewer](https://github.com/nesquena/query_reviewer) This Rails plugin
-not only runs "EXPLAIN" before each of your select queries in development, but
-provides a small DIV in the rendered output of each page with the summary of
-warnings for each query that it analyzed.
* [Exception Notifier](https://github.com/smartinez87/exception_notification/tree/master)
Provides a mailer object and a default set of templates for sending email
notifications when errors occur in a Rails application.
diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml
index 25e4fdb4e6..1e67b2bce7 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -155,6 +155,11 @@
name: Using Rails for API-only Applications
url: api_app.html
description: This guide explains how to effectively use Rails to develop a JSON API application.
+ -
+ name: Active Record and PostgreSQL
+ work_in_progress: true
+ url: active_record_postgresql.html
+ description: This guide covers PostgreSQL specific usage of Active Record.
-
name: Extending Rails
diff --git a/guides/source/engines.md b/guides/source/engines.md
index a00311bffb..3031c62928 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -217,10 +217,8 @@ important parts about namespacing, and is discussed later in the
#### `app` Directory
Inside the `app` directory are the standard `assets`, `controllers`, `helpers`,
-`mailers`, `models` and `views` directories that you should be familiar with
-from an application. The `helpers`, `mailers` and `models` directories are
-empty, so they aren't described in this section. We'll look more into models in
-a future section, when we're writing the engine.
+`jobs`, `mailers`, `models`, and `views` directories that you should be familiar with
+from an application. We'll look more into models in a future section, when we're writing the engine.
Within the `app/assets` directory, there are the `images`, `javascripts` and
`stylesheets` directories which, again, you should be familiar with due to their
@@ -261,6 +259,30 @@ WARNING: Don't use `require` because it will break the automatic reloading of cl
in the development environment - using `require_dependency` ensures that classes are
loaded and unloaded in the correct manner.
+Within the `app/helpers` directory there is a `blorgh` directory that
+contains a file called `application_helper.rb`. This file will provide any
+common functionality for the helpers of the engine. The `blorgh` directory
+is where the other helpers for the engine will go. By placing them within
+this namespaced directory, you prevent them from possibly clashing with
+identically-named helpers within other engines or even within the
+application.
+
+Within the `app/jobs` directory there is a `blorgh` directory that
+contains a file called `application_job.rb`. This file will provide any
+common functionality for the jobs of the engine. The `blorgh` directory
+is where the other jobs for the engine will go. By placing them within
+this namespaced directory, you prevent them from possibly clashing with
+identically-named jobs within other engines or even within the
+application.
+
+Within the `app/mailers` directory there is a `blorgh` directory that
+contains a file called `application_mailer.rb`. This file will provide any
+common functionality for the mailers of the engine. The `blorgh` directory
+is where the other mailers for the engine will go. By placing them within
+this namespaced directory, you prevent them from possibly clashing with
+identically-named mailers within other engines or even within the
+application.
+
Lastly, the `app/views` directory contains a `layouts` folder, which contains a
file at `blorgh/application.html.erb`. This file allows you to specify a layout
for the engine. If this engine is to be used as a stand-alone engine, then you
@@ -347,14 +369,11 @@ create app/views/blorgh/articles/new.html.erb
create app/views/blorgh/articles/_form.html.erb
invoke test_unit
create test/controllers/blorgh/articles_controller_test.rb
+create test/system/blorgh/articles_test.rb
invoke helper
create app/helpers/blorgh/articles_helper.rb
-invoke test_unit
-create test/application_system_test_case.rb
-create test/system/articles_test.rb
+invoke test_unit
invoke assets
-invoke js
-create app/assets/javascripts/blorgh/articles.js
invoke css
create app/assets/stylesheets/blorgh/articles.css
invoke css
@@ -394,9 +413,8 @@ be isolated from those routes that are within the application. The
Next, the `scaffold_controller` generator is invoked, generating a controller
called `Blorgh::ArticlesController` (at
`app/controllers/blorgh/articles_controller.rb`) and its related views at
-`app/views/blorgh/articles`. This generator also generates a test for the
-controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper
-(`app/helpers/blorgh/articles_helper.rb`).
+`app/views/blorgh/articles`. This generator also generates tests for the
+controller (`test/controllers/blorgh/articles_controller_test.rb` and `test/system/blorgh/articles_test.rb`) and a helper (`app/helpers/blorgh/articles_helper.rb`).
Everything this generator has created is neatly namespaced. The controller's
class is defined within the `Blorgh` module:
@@ -425,10 +443,7 @@ end
This helps prevent conflicts with any other engine or application that may have
an article resource as well.
-Finally, the assets for this resource are generated in two files:
-`app/assets/javascripts/blorgh/articles.js` and
-`app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little
-later.
+Finally, the assets for this resource are generated in one file: `app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little later.
You can see what the engine has so far by running `rails db:migrate` at the root
of our engine to run the migration generated by the scaffold generator, and then
@@ -579,9 +594,8 @@ invoke test_unit
create test/controllers/blorgh/comments_controller_test.rb
invoke helper
create app/helpers/blorgh/comments_helper.rb
+invoke test_unit
invoke assets
-invoke js
-create app/assets/javascripts/blorgh/comments.js
invoke css
create app/assets/stylesheets/blorgh/comments.css
```
@@ -1137,10 +1151,11 @@ end
```
```ruby
-# Blorgh/app/models/article.rb
-
-class Article < ApplicationRecord
- has_many :comments
+# Blorgh/app/models/blorgh/article.rb
+module Blorgh
+ class Article < ApplicationRecord
+ has_many :comments
+ end
end
```
@@ -1158,12 +1173,13 @@ end
```
```ruby
-# Blorgh/app/models/article.rb
-
-class Article < ApplicationRecord
- has_many :comments
- def summary
- "#{title}"
+# Blorgh/app/models/blorgh/article.rb
+module Blorgh
+ class Article < ApplicationRecord
+ has_many :comments
+ def summary
+ "#{title}"
+ end
end
end
```
@@ -1195,10 +1211,11 @@ end
```
```ruby
-# Blorgh/app/models/article.rb
-
-class Article < ApplicationRecord
- include Blorgh::Concerns::Models::Article
+# Blorgh/app/models/blorgh/article.rb
+module Blorgh
+ class Article < ApplicationRecord
+ include Blorgh::Concerns::Models::Article
+ end
end
```
@@ -1401,7 +1418,7 @@ s.add_development_dependency "moo"
Both kinds of dependencies will be installed when `bundle install` is run inside
of the application. The development dependencies for the gem will only be used
-when the tests for the engine are running.
+when the development and tests for the engine are running.
Note that if you want to immediately require dependencies when the engine is
required, you should require them before the engine's initialization. For
@@ -1518,6 +1535,7 @@ To hook into the initialization process of one of the following classes use the
| `ActiveJob::Base` | `active_job` |
| `ActiveJob::TestCase` | `active_job_test_case` |
| `ActiveRecord::Base` | `active_record` |
+| `ActiveStorage::Attachment` | `active_storage_attachment` |
| `ActiveStorage::Blob` | `active_storage_blob` |
| `ActiveSupport::TestCase` | `active_support_test_case` |
| `i18n` | `i18n` |
@@ -1535,4 +1553,6 @@ These are the available configuration hooks. They do not hook into any particula
### Example
-`config.before_configuration { puts 'I am called before any initializers' }`
+```ruby
+config.before_configuration { puts 'I am called before any initializers' }
+```
diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md
index b6674ae7dd..6418005921 100644
--- a/guides/source/form_helpers.md
+++ b/guides/source/form_helpers.md
@@ -248,7 +248,7 @@ There are a few things to note here:
* `@article` is the actual object being edited.
* There is a single hash of options. HTML options (except `id` and `class`) are passed in the `:html` hash. Also you can provide a `:namespace` option for your form to ensure uniqueness of id attributes on form elements. The scope attribute will be prefixed with underscore on the generated HTML id.
* The `form_with` method yields a **form builder** object (the `f` variable).
-* If you wish to direct your form request to a particular url, you would use `form_with url: my_nifty_url_path` instead. To see more in depth options on what `form_with` accepts be sure to [check out the API documentation](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with).
+* If you wish to direct your form request to a particular URL, you would use `form_with url: my_nifty_url_path` instead. To see more in depth options on what `form_with` accepts be sure to [check out the API documentation](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_with).
* Methods to create form controls are called **on** the form builder object `f`.
The resulting HTML is:
diff --git a/guides/source/i18n.md b/guides/source/i18n.md
index dab73bfbc2..d6fccadb28 100644
--- a/guides/source/i18n.md
+++ b/guides/source/i18n.md
@@ -1206,7 +1206,7 @@ The I18n API described in this guide is primarily intended for translating inter
Several gems can help with this:
* [Globalize](https://github.com/globalize/globalize): Store translations on separate translation tables, one for each translated model
-* [Mobility](https://github.com/shioyama/mobility): Provides support for storing translations in many formats, including translation tables, json columns (Postgres), etc.
+* [Mobility](https://github.com/shioyama/mobility): Provides support for storing translations in many formats, including translation tables, json columns (PostgreSQL), etc.
* [Traco](https://github.com/barsoom/traco): Translatable columns for Rails 3 and 4, stored in the model table itself
Conclusion
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index a2ae4ea59e..2808527141 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -296,6 +296,7 @@ Calls to the `render` method generally accept five options:
* `:location`
* `:status`
* `:formats`
+* `:variants`
##### The `:content_type` Option
@@ -417,6 +418,44 @@ render formats: [:json, :xml]
If a template with the specified format does not exist an `ActionView::MissingTemplate` error is raised.
+##### The `:variants` Option
+
+This tells Rails to look for template variations of the same format.
+You can specify a list of variants by passing the `:variants` option with a symbol or an array.
+
+An example of use would be this.
+
+```ruby
+# called in HomeController#index
+render variants: [:mobile, :desktop]
+```
+
+With this set of variants Rails will look for the following set of templates and use the first that exists.
+
+- `app/views/home/index.html+mobile.erb`
+- `app/views/home/index.html+desktop.erb`
+- `app/views/home/index.html.erb`
+
+If a template with the specified format does not exist an `ActionView::MissingTemplate` error is raised.
+
+Instead of setting the variant on the render call you may also set it on the request object in your controller action.
+
+```ruby
+def index
+ request.variant = determine_variant
+end
+
+private
+
+def determine_variant
+ variant = nil
+ # some code to determine the variant(s) to use
+ variant = :mobile if session[:use_mobile]
+
+ variant
+end
+```
+
#### Finding Layouts
To find the current layout, Rails first looks for a file in `app/views/layouts` with the same base name as the controller. For example, rendering actions from the `PhotosController` class will use `app/views/layouts/photos.html.erb` (or `app/views/layouts/photos.builder`). If there is no such controller-specific layout, Rails will use `app/views/layouts/application.html.erb` or `app/views/layouts/application.builder`. If there is no `.erb` layout, Rails will use a `.builder` layout if one exists. Rails also provides several ways to more precisely assign specific layouts to individual controllers and actions.
diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md
index 69b5f254bf..d60e53b052 100644
--- a/guides/source/rails_on_rack.md
+++ b/guides/source/rails_on_rack.md
@@ -13,7 +13,7 @@ After reading this guide, you will know:
--------------------------------------------------------------------------------
-WARNING: This guide assumes a working knowledge of Rack protocol and Rack concepts such as middlewares, url maps, and `Rack::Builder`.
+WARNING: This guide assumes a working knowledge of Rack protocol and Rack concepts such as middlewares, URL maps, and `Rack::Builder`.
Introduction to Rack
--------------------
diff --git a/guides/source/testing.md b/guides/source/testing.md
index 04f0297480..26448958ea 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -1404,7 +1404,7 @@ If you find your helpers are cluttering `test_helper.rb`, you can extract them i
```ruby
# lib/test/multiple_assertions.rb
module MultipleAssertions
- def assert_multiple_of_fourty_two(number)
+ def assert_multiple_of_forty_two(number)
assert (number % 42 == 0), 'expected #{number} to be a multiple of 42'
end
end
@@ -1419,8 +1419,8 @@ require 'test/multiple_assertions'
class NumberTest < ActiveSupport::TestCase
include MultipleAssertions
- test '420 is a multiple of fourty two' do
- assert_multiple_of_fourty_two 420
+ test '420 is a multiple of forty two' do
+ assert_multiple_of_forty_two 420
end
end
```
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 9d026ad4fd..78a94f7c28 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,7 +1,43 @@
+* `null: false` is set in the migrations by default for column pointed by
+ `belongs_to` / `references` association generated by model generator.
+
+ Also deprecate passing {required} to the model generator.
+
+ *Prathamesh Sonpatki*
+
+* New applications get `config.cache_classes = false` in `config/environments/test.rb`
+ unless `--skip-spring`.
+
+ *Xavier Noria*
+
+* Autoloading during initialization is deprecated.
+
+ *Xavier Noria*
+
+* Only force `:async` ActiveJob adapter to `:inline` during seeding.
+
+ *BatedUrGonnaDie*
+
+* The `connection` option of `rails dbconsole` command is deprecated in
+ favor of `database` option.
+
+ *Yuji Yaginuma*
+
+* Replace `chromedriver-helper` gem with `webdrivers` in default Gemfile.
+ `chromedriver-helper` is deprecated as of March 31, 2019 and won't
+ receive any further updates.
+
+ *Guillermo Iguaran‮*
+
+* Applications running in `:zeitwerk` mode that use `bootsnap` need
+ to upgrade `bootsnap` to at least 1.4.2.
+
+ *Xavier Noria*
+
* Add `config.disable_sandbox` option to Rails console.
This setting will disable `rails console --sandbox` mode, preventing
- developer from accidentally starting a sandbox console,
+ developer from accidentally starting a sandbox console,
which when left inactive, can cause the database server to run out of memory.
*Prem Sichanugrist*
@@ -10,6 +46,7 @@
*Yuji Yaginuma*
+
## Rails 6.0.0.beta3 (March 11, 2019) ##
* Generate random development secrets
@@ -24,7 +61,6 @@
*Eileen M. Uchitelle*, *Aaron Patterson*, *John Hawthorn*
-
## Rails 6.0.0.beta2 (February 25, 2019) ##
* Fix non-symbol access to nested hashes returned from `Rails::Application.config_for`
diff --git a/railties/Rakefile b/railties/Rakefile
index 4ae546b202..0f305ea332 100644
--- a/railties/Rakefile
+++ b/railties/Rakefile
@@ -11,6 +11,33 @@ task test: "test:isolated"
namespace :test do
task :isolated do
+ estimated_duration = {
+ "test/application/test_runner_test.rb" => 201,
+ "test/application/assets_test.rb" => 131,
+ "test/application/rake/migrations_test.rb" => 65,
+ "test/generators/scaffold_generator_test.rb" => 57,
+ "test/generators/plugin_test_runner_test.rb" => 57,
+ "test/application/test_test.rb" => 52,
+ "test/application/configuration_test.rb" => 49,
+ "test/generators/app_generator_test.rb" => 43,
+ "test/application/rake/dbs_test.rb" => 43,
+ "test/application/rake_test.rb" => 33,
+ "test/generators/plugin_generator_test.rb" => 30,
+ "test/railties/engine_test.rb" => 27,
+ "test/generators/scaffold_controller_generator_test.rb" => 23,
+ "test/railties/generators_test.rb" => 19,
+ "test/application/console_test.rb" => 16,
+ "test/engine/commands_test.rb" => 15,
+ "test/application/routing_test.rb" => 15,
+ "test/application/mailer_previews_test.rb" => 15,
+ "test/application/rake/multi_dbs_test.rb" => 13,
+ "test/application/asset_debugging_test.rb" => 12,
+ "test/application/bin_setup_test.rb" => 11,
+ "test/engine/test_test.rb" => 10,
+ "test/application/runner_test.rb" => 10,
+ }
+ estimated_duration.default = 1
+
dash_i = [
"test",
"lib",
@@ -39,13 +66,23 @@ namespace :test do
test_patterns = dirs.map { |dir| "test/#{dir}/*_test.rb" }
test_files = Dir[*test_patterns].select do |file|
!file.start_with?("test/fixtures/") && !file.start_with?("test/isolation/assets/")
- end.sort
+ end
if ENV["BUILDKITE_PARALLEL_JOB_COUNT"]
n = ENV["BUILDKITE_PARALLEL_JOB"].to_i
m = ENV["BUILDKITE_PARALLEL_JOB_COUNT"].to_i
- test_files = test_files.each_slice(m).map { |slice| slice[n] }.compact
+ buckets = Array.new(m) { [] }
+ allocations = Array.new(m) { 0 }
+ test_files.sort_by { |file| [-estimated_duration[file], file] }.each do |file|
+ idx = allocations.index(allocations.min)
+ buckets[idx] << file
+ allocations[idx] += estimated_duration[file]
+ end
+
+ puts "Running #{buckets[n].size} of #{test_files.size} test files, estimated duration #{allocations[n]}s"
+
+ test_files = buckets[n]
end
test_files.each do |file|
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index b79dbdbc6f..d743c1c0d9 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -142,6 +142,10 @@ module Rails
active_storage.queues.analysis = :active_storage_analysis
active_storage.queues.purge = :active_storage_purge
end
+
+ if respond_to?(:active_record)
+ active_record.collection_cache_versioning = true
+ end
else
raise "Unknown version #{target_version.to_s.inspect}"
end
diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb
index 193cc59f3a..9800b19274 100644
--- a/railties/lib/rails/application/default_middleware_stack.rb
+++ b/railties/lib/rails/application/default_middleware_stack.rb
@@ -49,6 +49,7 @@ module Rails
middleware.use ::Rails::Rack::Logger, config.log_tags
middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app
middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format
+ middleware.use ::ActionDispatch::ActionableExceptions
unless config.cache_classes
middleware.use ::ActionDispatch::Reloader, app.reloader
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index 8d2c13d2a8..109c560c80 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+require "active_support/core_ext/string/inflections"
+require "active_support/core_ext/array/conversions"
+
module Rails
class Application
module Finisher
@@ -21,10 +24,44 @@ module Rails
end
end
+ # This will become an error if/when we remove classic mode. The plan is
+ # autoloaders won't be configured up to this point in the finisher, so
+ # constants just won't be found, raising regular NameError exceptions.
+ initializer :warn_if_autoloaded, before: :let_zeitwerk_take_over do
+ next if config.cache_classes
+ next if ActiveSupport::Dependencies.autoloaded_constants.empty?
+
+ autoloaded = ActiveSupport::Dependencies.autoloaded_constants
+ constants = "constant".pluralize(autoloaded.size)
+ enum = autoloaded.to_sentence
+ have = autoloaded.size == 1 ? "has" : "have"
+ these = autoloaded.size == 1 ? "This" : "These"
+ example = autoloaded.first
+ example_klass = example.constantize.class
+
+ ActiveSupport::DescendantsTracker.clear
+ ActiveSupport::Dependencies.clear
+
+ ActiveSupport::Deprecation.warn(<<~WARNING)
+ Initialization autoloaded the #{constants} #{enum}.
+
+ Being able to do this is deprecated. Autoloading during initialization is going
+ to be an error condition in future versions of Rails.
+
+ Reloading does not reboot the application, and therefore code executed during
+ initialization does not run again. So, if you reload #{example}, for example,
+ the expected changes won't be reflected in that stale #{example_klass} object.
+
+ #{these} autoloaded #{constants} #{have} been unloaded.
+
+ Please, check the "Autoloading and Reloading Constants" guide for solutions.
+ WARNING
+ end
+
initializer :let_zeitwerk_take_over do
if config.autoloader == :zeitwerk
require "active_support/dependencies/zeitwerk_integration"
- ActiveSupport::Dependencies::ZeitwerkIntegration.take_over
+ ActiveSupport::Dependencies::ZeitwerkIntegration.take_over(enable_reloading: !config.cache_classes)
end
end
diff --git a/railties/lib/rails/command/environment_argument.rb b/railties/lib/rails/command/environment_argument.rb
index 0cb3f1ce1e..9945fd1430 100644
--- a/railties/lib/rails/command/environment_argument.rb
+++ b/railties/lib/rails/command/environment_argument.rb
@@ -9,7 +9,9 @@ module Rails
extend ActiveSupport::Concern
included do
- class_attribute :environment_desc, default: "Specifies the environment to run this #{self.command_name} under (test/development/production)."
+ no_commands do
+ class_attribute :environment_desc, default: "Specifies the environment to run this #{self.command_name} under (test/development/production)."
+ end
class_option :environment, aliases: "-e", type: :string, desc: environment_desc
end
diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
index 0fac7d34a0..72f3235ce3 100644
--- a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
+++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/deprecation"
+require "active_support/core_ext/string/filters"
require "rails/command/environment_argument"
module Rails
@@ -89,15 +91,15 @@ module Rails
def config
@config ||= begin
- # We need to check whether the user passed the connection the
+ # We need to check whether the user passed the database the
# first time around to show a consistent error message to people
# relying on 2-level database configuration.
- if @options["connection"] && configurations[connection].blank?
- raise ActiveRecord::AdapterNotSpecified, "'#{connection}' connection is not configured. Available configuration: #{configurations.inspect}"
- elsif configurations[environment].blank? && configurations[connection].blank?
+ if @options["database"] && configurations[database].blank?
+ raise ActiveRecord::AdapterNotSpecified, "'#{database}' database is not configured. Available configuration: #{configurations.inspect}"
+ elsif configurations[environment].blank? && configurations[database].blank?
raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}"
else
- configurations[connection] || configurations[environment].presence
+ configurations[database] || configurations[environment].presence
end
end
end
@@ -106,8 +108,8 @@ module Rails
Rails.respond_to?(:env) ? Rails.env : Rails::Command.environment
end
- def connection
- @options.fetch(:connection, "primary")
+ def database
+ @options.fetch(:database, "primary")
end
private
@@ -156,12 +158,22 @@ module Rails
class_option :connection, aliases: "-c", type: :string,
desc: "Specifies the connection to use."
+ class_option :database, aliases: "--db", type: :string,
+ desc: "Specifies the database to use."
+
def perform
extract_environment_option_from_argument
# RAILS_ENV needs to be set before config/application is required.
ENV["RAILS_ENV"] = options[:environment]
+ if options["connection"]
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `connection` option is deprecated and will be removed in Rails 6.1. Please use `database` option instead.
+ MSG
+ options["database"] = options["connection"]
+ end
+
require_application_and_environment!
Rails::DBConsole.start(options)
end
diff --git a/railties/lib/rails/commands/dev/dev_command.rb b/railties/lib/rails/commands/dev/dev_command.rb
index a3f02f3172..9b2cb2b04a 100644
--- a/railties/lib/rails/commands/dev/dev_command.rb
+++ b/railties/lib/rails/commands/dev/dev_command.rb
@@ -5,8 +5,10 @@ require "rails/dev_caching"
module Rails
module Command
class DevCommand < Base # :nodoc:
- def help
- say "rails dev:cache # Toggle development mode caching on/off."
+ no_commands do
+ def help
+ say "rails dev:cache # Toggle development mode caching on/off."
+ end
end
def cache
diff --git a/railties/lib/rails/commands/notes/notes_command.rb b/railties/lib/rails/commands/notes/notes_command.rb
index 64b339b3cd..94cf183855 100644
--- a/railties/lib/rails/commands/notes/notes_command.rb
+++ b/railties/lib/rails/commands/notes/notes_command.rb
@@ -5,7 +5,7 @@ require "rails/source_annotation_extractor"
module Rails
module Command
class NotesCommand < Base # :nodoc:
- class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array, default: %w(OPTIMIZE FIXME TODO)
+ class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array, default: Rails::SourceAnnotationExtractor::Annotation.tags
def perform(*)
require_application_and_environment!
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 07bd56c978..eb2f0e8fca 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -230,7 +230,7 @@ module Rails
#
# If +MyEngine+ is isolated, The routes above will point to
# <tt>MyEngine::ArticlesController</tt>. You also don't need to use longer
- # url helpers like +my_engine_articles_path+. Instead, you should simply use
+ # URL helpers like +my_engine_articles_path+. Instead, you should simply use
# +articles_path+, like you would do with your main application.
#
# To make this behavior consistent with other parts of the framework,
@@ -238,7 +238,7 @@ module Rails
# normal Rails app, when you use a namespaced model such as
# <tt>Namespace::Article</tt>, <tt>ActiveModel::Naming</tt> will generate
# names with the prefix "namespace". In an isolated engine, the prefix will
- # be omitted in url helpers and form fields, for convenience.
+ # be omitted in URL helpers and form fields, for convenience.
#
# polymorphic_url(MyEngine::Article.new)
# # => "articles_path" # not "my_engine_articles_path"
@@ -286,11 +286,11 @@ module Rails
# Note that the <tt>:as</tt> option given to mount takes the <tt>engine_name</tt> as default, so most of the time
# you can simply omit it.
#
- # Finally, if you want to generate a url to an engine's route using
+ # Finally, if you want to generate a URL to an engine's route using
# <tt>polymorphic_url</tt>, you also need to pass the engine helper. Let's
# say that you want to create a form pointing to one of the engine's routes.
# All you need to do is pass the helper as the first element in array with
- # attributes for url:
+ # attributes for URL:
#
# form_for([my_engine, @user])
#
@@ -469,13 +469,16 @@ module Rails
self
end
- # Eager load the application by loading all ruby
- # files inside eager_load paths.
def eager_load!
- if Rails.autoloaders.zeitwerk_enabled?
- eager_load_with_zeitwerk!
- else
- eager_load_with_dependencies!
+ # Already done by Zeitwerk::Loader.eager_load_all in the finisher.
+ return if Rails.autoloaders.zeitwerk_enabled?
+
+ config.eager_load_paths.each do |load_path|
+ # Starts after load_path plus a slash, ends before ".rb".
+ relname_range = (load_path.to_s.length + 1)...-3
+ Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
+ require_dependency file[relname_range]
+ end
end
end
@@ -547,7 +550,13 @@ module Rails
# Blog::Engine.load_seed
def load_seed
seed_file = paths["db/seeds.rb"].existent.first
- with_inline_jobs { load(seed_file) } if seed_file
+ return unless seed_file
+
+ if config.active_job.queue_adapter == :async
+ with_inline_jobs { load(seed_file) }
+ else
+ load(seed_file)
+ end
end
# Add configured load paths to Ruby's load path, and remove duplicate entries.
@@ -567,12 +576,15 @@ module Rails
ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths)
ActiveSupport::Dependencies.autoload_once_paths.unshift(*_all_autoload_once_paths)
- # Freeze so future modifications will fail rather than do nothing mysteriously
config.autoload_paths.freeze
- config.eager_load_paths.freeze
config.autoload_once_paths.freeze
end
+ initializer :set_eager_load_paths, before: :bootstrap_hook do
+ ActiveSupport::Dependencies._eager_load_paths.merge(config.eager_load_paths)
+ config.eager_load_paths.freeze
+ end
+
initializer :add_routing_paths do |app|
routing_paths = paths["config/routes.rb"].existent
@@ -651,22 +663,6 @@ module Rails
private
- def eager_load_with_zeitwerk!
- (config.eager_load_paths - Zeitwerk::Loader.all_dirs).each do |path|
- Dir.glob("#{path}/**/*.rb").sort.each { |file| require file }
- end
- end
-
- def eager_load_with_dependencies!
- config.eager_load_paths.each do |load_path|
- # Starts after load_path plus a slash, ends before ".rb".
- relname_range = (load_path.to_s.length + 1)...-3
- Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
- require_dependency file[relname_range]
- end
- end
- end
-
def load_config_initializer(initializer) # :doc:
ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do
load(initializer)
diff --git a/railties/lib/rails/generators/app_name.rb b/railties/lib/rails/generators/app_name.rb
index c4f71694d8..5bb735c4e8 100644
--- a/railties/lib/rails/generators/app_name.rb
+++ b/railties/lib/rails/generators/app_name.rb
@@ -7,11 +7,11 @@ module Rails
private
def app_name
- @app_name ||= original_app_name.tr("-", "_")
+ @app_name ||= original_app_name.tr('\\', "").tr("-. ", "_")
end
def original_app_name
- @original_app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', "").tr(". ", "_")
+ @original_app_name ||= defined_app_const_base? ? defined_app_name : File.basename(destination_root)
end
def defined_app_name
diff --git a/railties/lib/rails/generators/database.rb b/railties/lib/rails/generators/database.rb
index 18fc7be3ff..cc6e7b50e5 100644
--- a/railties/lib/rails/generators/database.rb
+++ b/railties/lib/rails/generators/database.rb
@@ -15,7 +15,7 @@ module Rails
case database
when "mysql" then ["mysql2", [">= 0.4.4"]]
when "postgresql" then ["pg", [">= 0.18", "< 2.0"]]
- when "sqlite3" then ["sqlite3", ["~> 1.3", ">= 1.3.6"]]
+ when "sqlite3" then ["sqlite3", ["~> 1.4"]]
when "oracle" then ["activerecord-oracle_enhanced-adapter", nil]
when "frontbase" then ["ruby-frontbase", nil]
when "sqlserver" then ["activerecord-sqlserver-adapter", nil]
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt
index b80c1280ce..1dddc3d698 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt
+++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt
@@ -21,6 +21,9 @@
<div class="field">
<%%= form.label :password_confirmation %>
<%%= form.password_field :password_confirmation %>
+<% elsif attribute.attachments? -%>
+ <%%= form.label :<%= attribute.column_name %> %>
+ <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, multiple: true %>
<% else -%>
<%%= form.label :<%= attribute.column_name %> %>
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt
index 7deba07926..6b216001d2 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt
+++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt
@@ -3,7 +3,15 @@
<% attributes.reject(&:password_digest?).each do |attribute| -%>
<p>
<strong><%= attribute.human_name %>:</strong>
+<% if attribute.attachment? -%>
+ <%%= link_to @<%= singular_table_name %>.<%= attribute.column_name %>.filename, @<%= singular_table_name %>.<%= attribute.column_name %> %>
+<% elsif attribute.attachments? -%>
+ <%% @<%= singular_table_name %>.<%= attribute.column_name %>.each do |<%= attribute.singular_name %>| %>
+ <div><%%= link_to <%= attribute.singular_name %>.filename, <%= attribute.singular_name %> %></div>
+ <%% end %>
+<% else -%>
<%%= @<%= singular_table_name %>.<%= attribute.column_name %> %>
+<% end -%>
</p>
<% end -%>
diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb
index a8f7729fd3..1a80e71eae 100644
--- a/railties/lib/rails/generators/generated_attribute.rb
+++ b/railties/lib/rails/generators/generated_attribute.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "active_support/time"
+require "active_support/deprecation"
module Rails
module Generators
@@ -51,6 +52,12 @@ module Rails
type = $1
provided_options = $2.split(/[,.-]/)
options = Hash[provided_options.map { |opt| [opt.to_sym, true] }]
+
+ if options[:required]
+ ActiveSupport::Deprecation.warn("Passing {required} option has no effect on the model generator. It will be removed in Rails 6.1.\n")
+ options.delete(:required)
+ end
+
return type, options
else
return type, {}
@@ -68,13 +75,15 @@ module Rails
def field_type
@field_type ||= case type
- when :integer then :number_field
- when :float, :decimal then :text_field
- when :time then :time_select
- when :datetime, :timestamp then :datetime_select
- when :date then :date_select
- when :text then :text_area
- when :boolean then :check_box
+ when :integer then :number_field
+ when :float, :decimal then :text_field
+ when :time then :time_select
+ when :datetime, :timestamp then :datetime_select
+ when :date then :date_select
+ when :text then :text_area
+ when :rich_text then :rich_text_area
+ when :boolean then :check_box
+ when :attachment, :attachments then :file_field
else
:text_field
end
@@ -90,7 +99,9 @@ module Rails
when :string then name == "type" ? "" : "MyString"
when :text then "MyText"
when :boolean then false
- when :references, :belongs_to then nil
+ when :references, :belongs_to,
+ :attachment, :attachments,
+ :rich_text then nil
else
""
end
@@ -133,7 +144,7 @@ module Rails
end
def required?
- attr_options[:required]
+ reference? && Rails.application.config.active_record.belongs_to_required_by_default
end
def has_index?
@@ -152,6 +163,22 @@ module Rails
type == :token
end
+ def rich_text?
+ type == :rich_text
+ end
+
+ def attachment?
+ type == :attachment
+ end
+
+ def attachments?
+ type == :attachments
+ end
+
+ def virtual?
+ rich_text? || attachment? || attachments?
+ end
+
def inject_options
(+"").tap { |s| options_for_migration.each { |k, v| s << ", #{k}: #{v.inspect}" } }
end
@@ -163,7 +190,6 @@ module Rails
def options_for_migration
@attr_options.dup.tap do |options|
if required?
- options.delete(:required)
options[:null] = false
end
diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb
index d6732f8ff1..42e64cd11f 100644
--- a/railties/lib/rails/generators/named_base.rb
+++ b/railties/lib/rails/generators/named_base.rb
@@ -187,6 +187,7 @@ module Rails
def attributes_names # :doc:
@attributes_names ||= attributes.each_with_object([]) do |a, names|
+ next if a.attachments?
names << a.column_name
names << "password_confirmation" if a.password_digest?
names << "#{a.name}_type" if a.polymorphic?
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
index 88cd502b53..d7221453e7 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
@@ -28,7 +28,7 @@ ruby <%= "'#{RUBY_VERSION}'" -%>
<% if depend_on_bootsnap? -%>
# Reduces boot times through caching; required in config/boot.rb
-gem 'bootsnap', '>= 1.4.1', require: false
+gem 'bootsnap', '>= 1.4.2', require: false
<%- end -%>
<%- if options.api? -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt
index 3f73bae3da..5928deb6aa 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt
+++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt
@@ -8,7 +8,8 @@ def system!(*args)
end
FileUtils.chdir APP_ROOT do
- # This script is a starting point to setup your application.
+ # This script is a way to setup or update your development environment automatically.
+ # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
# Add necessary setup steps to this file.
puts '== Installing dependencies =='
@@ -27,7 +28,7 @@ FileUtils.chdir APP_ROOT do
# end
puts "\n== Preparing database =="
- system! 'bin/rails db:setup'
+ system! 'bin/rails db:prepare'
<% end -%>
puts "\n== Removing old logs and tempfiles =="
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update.tt b/railties/lib/rails/generators/rails/app/templates/bin/update.tt
deleted file mode 100644
index 03b77d0d46..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/bin/update.tt
+++ /dev/null
@@ -1,33 +0,0 @@
-require 'fileutils'
-
-# path to your application root.
-APP_ROOT = File.expand_path('..', __dir__)
-
-def system!(*args)
- system(*args) || abort("\n== Command #{args} failed ==")
-end
-
-FileUtils.chdir APP_ROOT do
- # This script is a way to update your development environment automatically.
- # Add necessary update steps to this file.
-
- puts '== Installing dependencies =='
- system! 'gem install bundler --conservative'
- system('bundle check') || system!('bundle install')
-<% unless options.skip_javascript? -%>
-
- # Install JavaScript dependencies
- # system('bin/yarn')
-<% end -%>
-<% unless options.skip_active_record? -%>
-
- puts "\n== Updating database =="
- system! 'rails db:migrate'
-<% end -%>
-
- puts "\n== Removing old logs and tempfiles =="
- system! 'rails log:clear tmp:clear'
-
- puts "\n== Restarting application server =="
- system! 'rails restart'
-end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
index 63ed3fa952..c66e349442 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
@@ -1,11 +1,16 @@
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
-
- # The test environment is used exclusively to run your application's
- # test suite. You never need to work with it otherwise. Remember that
- # your test database is "scratch space" for the test suite and is wiped
- # and recreated between test runs. Don't rely on the data there!
+ <%# Spring executes the reloaders when files change. %>
+ <%- if spring_install? -%>
+ config.cache_classes = false
+ <%- else -%>
config.cache_classes = true
+ <%- end -%>
# Do not eager load code on boot. This avoids loading your whole application
# just for the purpose of running a single test. If you are using a tool that
diff --git a/railties/lib/rails/generators/rails/app/templates/ruby-version.tt b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt
index bac1339923..096cfd36a8 100644
--- a/railties/lib/rails/generators/rails/app/templates/ruby-version.tt
+++ b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt
@@ -1 +1 @@
-<%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" -%>
+<%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" %>
diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
index 79a06648b5..895b3b2e92 100644
--- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -144,17 +144,6 @@ task default: :test
end
end
- def javascripts
- return if options.skip_javascript?
-
- if mountable?
- template "rails/javascripts.js",
- "app/assets/javascripts/#{namespaced_name}/application.js"
- elsif full?
- empty_directory_with_keep_file "app/assets/javascripts/#{namespaced_name}"
- end
- end
-
def bin(force = false)
bin_file = engine? ? "bin/rails.tt" : "bin/test.tt"
template bin_file, force: force do |content|
@@ -236,10 +225,6 @@ task default: :test
build(:stylesheets) unless api?
end
- def create_javascript_files
- build(:javascripts) unless api?
- end
-
def create_bin_files
build(:bin)
end
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
index 7030561a33..8b46eb88ae 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
+++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
@@ -32,6 +32,14 @@ module Rails
hook_for :helper, as: :scaffold do |invoked|
invoke invoked, [ controller_name ]
end
+
+ private
+
+ def permitted_params
+ params = attributes_names.map { |name| ":#{name}" }.join(", ")
+ params += attributes.select(&:attachments?).map { |a| ", #{a.name}: []" }.join
+ params
+ end
end
end
end
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt
index 400afec6dc..bb26370276 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt
+++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt
@@ -54,7 +54,7 @@ class <%= controller_class_name %>Controller < ApplicationController
<%- if attributes_names.empty? -%>
params.fetch(:<%= singular_table_name %>, {})
<%- else -%>
- params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
+ params.require(:<%= singular_table_name %>).permit(<%= permitted_params %>)
<%- end -%>
end
end
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt
index 05f1c2b2d3..82b43987b4 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt
+++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt
@@ -61,7 +61,7 @@ class <%= controller_class_name %>Controller < ApplicationController
<%- if attributes_names.empty? -%>
params.fetch(:<%= singular_table_name %>, {})
<%- else -%>
- params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
+ params.require(:<%= singular_table_name %>).permit(<%= permitted_params %>)
<%- end -%>
end
end
diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt
index ee4ae47727..0fd9f305d7 100644
--- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt
+++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt
@@ -7,7 +7,7 @@
password_digest: <%%= BCrypt::Password.create('secret') %>
<%- elsif attribute.reference? -%>
<%= yaml_key_value(attribute.column_name.sub(/_id$/, ''), attribute.default || name) %>
- <%- else -%>
+ <%- elsif !attribute.virtual? -%>
<%= yaml_key_value(attribute.column_name, attribute.default) %>
<%- end -%>
<%- if attribute.polymorphic? -%>
diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb
index 5cffa52860..4a1942790b 100644
--- a/railties/lib/rails/mailers_controller.rb
+++ b/railties/lib/rails/mailers_controller.rb
@@ -5,8 +5,9 @@ require "rails/application_controller"
class Rails::MailersController < Rails::ApplicationController # :nodoc:
prepend_view_path ActionDispatch::DebugView::RESCUES_TEMPLATE_PATH
+ around_action :set_locale, only: :preview
+ before_action :find_preview, only: :preview
before_action :require_local!, unless: :show_previews?
- before_action :find_preview, :set_locale, only: :preview
helper_method :part_query, :locale_query
@@ -92,6 +93,8 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
end
def set_locale
- I18n.locale = params[:locale] || I18n.default_locale
+ I18n.with_locale(params[:locale] || I18n.default_locale) do
+ yield
+ end
end
end
diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb
index d7170e6282..9ce22b96a6 100644
--- a/railties/lib/rails/source_annotation_extractor.rb
+++ b/railties/lib/rails/source_annotation_extractor.rb
@@ -29,6 +29,16 @@ module Rails
directories.push(*dirs)
end
+ def self.tags
+ @@tags ||= %w(OPTIMIZE FIXME TODO)
+ end
+
+ # Registers additional tags
+ # Rails::SourceAnnotationExtractor::Annotation.register_tags("TESTME", "DEPRECATEME")
+ def self.register_tags(*additional_tags)
+ tags.push(*additional_tags)
+ end
+
def self.extensions
@@extensions ||= {}
end
@@ -66,6 +76,8 @@ module Rails
# Prints all annotations with tag +tag+ under the root directories +app+,
# +config+, +db+, +lib+, and +test+ (recursively).
#
+ # If +tag+ is <tt>nil</tt>, annotations with either default or registered tags are printed.
+ #
# Specific directories can be explicitly set using the <tt>:dirs</tt> key in +options+.
#
# Rails::SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true
@@ -75,7 +87,8 @@ module Rails
# See <tt>#find_in</tt> for a list of file extensions that will be taken into account.
#
# This class method is the single entry point for the `rails notes` command.
- def self.enumerate(tag, options = {})
+ def self.enumerate(tag = nil, options = {})
+ tag ||= Annotation.tags.join("|")
extractor = new(tag)
dirs = options.delete(:dirs) || Annotation.directories
extractor.display(extractor.find(dirs), options)
diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb
index 9c866263f0..9600194ed6 100644
--- a/railties/test/abstract_unit.rb
+++ b/railties/test/abstract_unit.rb
@@ -32,3 +32,5 @@ class ActiveSupport::TestCase
skip message if defined?(JRUBY_VERSION)
end
end
+
+require_relative "../../tools/test_common"
diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb
index 3e0f31860b..7623e8e352 100644
--- a/railties/test/application/asset_debugging_test.rb
+++ b/railties/test/application/asset_debugging_test.rb
@@ -95,7 +95,7 @@ module ApplicationTests
end
end
- test "public url methods are not over-written by the asset pipeline" do
+ test "public URL methods are not over-written by the asset pipeline" do
contents = "doesnotexist"
cases = {
asset_url: %r{http://example.org/#{contents}},
diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb
index 46ee0d670e..a80581211b 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -450,7 +450,7 @@ module ApplicationTests
assert_equal 0, files.length, "Expected application.js asset to be removed, but still exists"
end
- test "asset urls should use the request's protocol by default" do
+ test "asset URLs should use the request's protocol by default" do
app_with_assets_in_view
add_to_config "config.asset_host = 'example.com'"
add_to_env_config "development", "config.assets.digest = false"
@@ -466,7 +466,7 @@ module ApplicationTests
assert_match('src="https://example.com/assets/application.self.js', last_response.body)
end
- test "asset urls should be protocol-relative if no request is in scope" do
+ test "asset URLs should be protocol-relative if no request is in scope" do
app_file "app/assets/images/rails.png", "notreallyapng"
app_file "app/assets/javascripts/image_loader.js.erb", "var src='<%= image_path('rails.png') %>';"
add_to_config "config.assets.precompile = %w{rails.png image_loader.js}"
diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb
index a952d2466b..aa0da0931d 100644
--- a/railties/test/application/bin_setup_test.rb
+++ b/railties/test/application/bin_setup_test.rb
@@ -6,21 +6,12 @@ module ApplicationTests
class BinSetupTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
- def setup
- build_app
- end
-
- def teardown
- teardown_app
- end
+ setup :build_app
+ teardown :teardown_app
def test_bin_setup
Dir.chdir(app_path) do
- app_file "db/schema.rb", <<-RUBY
- ActiveRecord::Schema.define(version: 20140423102712) do
- create_table(:articles) {}
- end
- RUBY
+ rails "generate", "model", "article"
list_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip }
File.write("log/test.log", "zomg!")
@@ -28,15 +19,20 @@ module ApplicationTests
assert_equal "[]", list_tables.call
assert_equal 5, File.size("log/test.log")
assert_not File.exist?("tmp/restart.txt")
+
`bin/setup 2>&1`
assert_equal 0, File.size("log/test.log")
- assert_equal '["articles", "schema_migrations", "ar_internal_metadata"]', list_tables.call
+ assert_equal '["schema_migrations", "ar_internal_metadata", "articles"]', list_tables.call
assert File.exist?("tmp/restart.txt")
end
end
def test_bin_setup_output
Dir.chdir(app_path) do
+ # SQLite3 seems to auto-create the database on first checkout.
+ rails "db:system:change", "--to=postgresql"
+ rails "db:drop"
+
app_file "db/schema.rb", ""
output = `bin/setup 2>&1`
@@ -53,8 +49,8 @@ module ApplicationTests
The Gemfile's dependencies are satisfied
== Preparing database ==
- Created database 'db/development.sqlite3'
- Created database 'db/test.sqlite3'
+ Created database 'app_development'
+ Created database 'app_test'
== Removing old logs and tempfiles ==
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index b8e167b488..62d9b1c813 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -1704,6 +1704,61 @@ module ApplicationTests
end
end
+ test "autoloading during initialization gets deprecation message and clearing if config.cache_classes is false" do
+ app_file "lib/c.rb", <<~EOS
+ class C
+ extend ActiveSupport::DescendantsTracker
+ end
+
+ class X < C
+ end
+ EOS
+
+ app_file "app/models/d.rb", <<~EOS
+ require "c"
+
+ class D < C
+ end
+ EOS
+
+ app_file "config/initializers/autoload.rb", "D.class"
+
+ app "development"
+
+ # TODO: Test deprecation message, assert_depcrecated { app "development" }
+ # does not collect it.
+
+ assert_equal [X], C.descendants
+ assert_empty ActiveSupport::Dependencies.autoloaded_constants
+ end
+
+ test "autoloading during initialization triggers nothing if config.cache_classes is true" do
+ app_file "lib/c.rb", <<~EOS
+ class C
+ extend ActiveSupport::DescendantsTracker
+ end
+
+ class X < C
+ end
+ EOS
+
+ app_file "app/models/d.rb", <<~EOS
+ require "c"
+
+ class D < C
+ end
+ EOS
+
+ app_file "config/initializers/autoload.rb", "D.class"
+
+ app "production"
+
+ # TODO: Test no deprecation message is issued.
+
+ assert_equal [X, D], C.descendants
+ end
+
+
test "raises with proper error message if no database configuration found" do
FileUtils.rm("#{app_path}/config/database.yml")
err = assert_raises RuntimeError do
diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb
index 3cd4b8fe33..a35247fc43 100644
--- a/railties/test/application/initializers/frameworks_test.rb
+++ b/railties/test/application/initializers/frameworks_test.rb
@@ -39,7 +39,7 @@ module ApplicationTests
assert_equal expanded_path, ActionMailer::Base.view_paths[0].to_s
end
- test "allows me to configure default url options for ActionMailer" do
+ test "allows me to configure default URL options for ActionMailer" do
app_file "config/environments/development.rb", <<-RUBY
Rails.application.configure do
config.action_mailer.default_url_options = { :host => "test.rails" }
@@ -61,7 +61,7 @@ module ApplicationTests
assert_equal "https", ActionMailer::Base.default_url_options[:protocol]
end
- test "includes url helpers as action methods" do
+ test "includes URL helpers as action methods" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get "/foo", :to => lambda { |env| [200, {}, []] }, :as => :foo
diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb
index fb84276b8a..fa9ed868c4 100644
--- a/railties/test/application/mailer_previews_test.rb
+++ b/railties/test/application/mailer_previews_test.rb
@@ -515,6 +515,13 @@ module ApplicationTests
assert_match '<option selected value="locale=ja">ja', last_response.body
end
+ test "preview does not leak I18n global setting changes" do
+ I18n.with_locale(:en) do
+ get "/rails/mailers/notifier/foo.txt?locale=ja"
+ assert_equal :en, I18n.locale
+ end
+ end
+
test "mailer previews create correct links when loaded on a subdirectory" do
mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb
index 2d659ade8d..17df78ed4e 100644
--- a/railties/test/application/middleware/exceptions_test.rb
+++ b/railties/test/application/middleware/exceptions_test.rb
@@ -60,7 +60,7 @@ module ApplicationTests
assert_equal "YOU FAILED", last_response.body
end
- test "url generation error when action_dispatch.show_exceptions is set raises an exception" do
+ test "URL generation error when action_dispatch.show_exceptions is set raises an exception" do
controller :foo, <<-RUBY
class FooController < ActionController::Base
def index
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index 4242daf39a..54b2e95d75 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -38,6 +38,7 @@ module ApplicationTests
"Rails::Rack::Logger",
"ActionDispatch::ShowExceptions",
"ActionDispatch::DebugExceptions",
+ "ActionDispatch::ActionableExceptions",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
"ActiveRecord::Migration::CheckPending",
@@ -70,6 +71,7 @@ module ApplicationTests
"Rails::Rack::Logger",
"ActionDispatch::ShowExceptions",
"ActionDispatch::DebugExceptions",
+ "ActionDispatch::ActionableExceptions",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
"Rack::Head",
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index a1e237fa7b..258066a7e6 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -40,12 +40,12 @@ module ApplicationTests
end
end
- test "db:create and db:drop without database url" do
+ test "db:create and db:drop without database URL" do
require "#{app_path}/config/environment"
db_create_and_drop ActiveRecord::Base.configurations[Rails.env]["database"]
end
- test "db:create and db:drop with database url" do
+ test "db:create and db:drop with database URL" do
require "#{app_path}/config/environment"
set_database_url
db_create_and_drop database_url_db_name
@@ -553,6 +553,22 @@ module ApplicationTests
end
end
end
+
+ test "db:prepare setup the database" do
+ Dir.chdir(app_path) do
+ rails "generate", "model", "book", "title:string"
+ output = rails("db:prepare")
+ assert_match(/CreateBooks: migrated/, output)
+
+ output = rails("db:drop")
+ assert_match(/Dropped database/, output)
+
+ rails "generate", "model", "recipe", "title:string"
+ output = rails("db:prepare")
+ assert_match(/CreateBooks: migrated/, output)
+ assert_match(/CreateRecipes: migrated/, output)
+ end
+ end
end
end
end
diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb
index d676e7486e..31ea2246a9 100644
--- a/railties/test/application/rake/multi_dbs_test.rb
+++ b/railties/test/application/rake/multi_dbs_test.rb
@@ -24,7 +24,6 @@ module ApplicationTests
assert_no_match(/already exists/, output)
assert File.exist?(expected_database)
-
output = rails("db:drop")
assert_match(/Dropped database/, output)
assert_match_namespace(namespace, output)
@@ -137,6 +136,51 @@ module ApplicationTests
end
end
+ def db_up_and_down(version, namespace = nil)
+ Dir.chdir(app_path) do
+ generate_models_for_animals
+ rails("db:migrate")
+
+ if namespace
+ down_output = rails("db:migrate:down:#{namespace}", "VERSION=#{version}")
+ up_output = rails("db:migrate:up:#{namespace}", "VERSION=#{version}")
+ else
+ assert_raises RuntimeError, /You're using a multiple database application/ do
+ down_output = rails("db:migrate:down", "VERSION=#{version}")
+ end
+
+ assert_raises RuntimeError, /You're using a multiple database application/ do
+ up_output = rails("db:migrate:up", "VERSION=#{version}")
+ end
+ end
+
+ case namespace
+ when "primary"
+ assert_match(/OneMigration: reverting/, down_output)
+ assert_match(/OneMigration: migrated/, up_output)
+ when nil
+ else
+ assert_match(/TwoMigration: reverting/, down_output)
+ assert_match(/TwoMigration: migrated/, up_output)
+ end
+ end
+ end
+
+ def db_prepare
+ Dir.chdir(app_path) do
+ generate_models_for_animals
+ output = rails("db:prepare")
+
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ if db_config.spec_name == "primary"
+ assert_match(/CreateBooks: migrated/, output)
+ else
+ assert_match(/CreateDogs: migrated/, output)
+ end
+ end
+ end
+ end
+
def write_models_for_animals
# make a directory for the animals migration
FileUtils.mkdir_p("#{app_path}/db/animals_migrate")
@@ -204,6 +248,34 @@ module ApplicationTests
end
end
+ test "db:migrate:down and db:migrate:up without a namespace raises in a multi-db application" do
+ require "#{app_path}/config/environment"
+
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ db_up_and_down "01"
+ end
+
+ test "db:migrate:down:namespace and db:migrate:up:namespace works" do
+ require "#{app_path}/config/environment"
+
+ app_file "db/migrate/01_one_migration.rb", <<-MIGRATION
+ class OneMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ app_file "db/animals_migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
+
+ db_up_and_down "01", "primary"
+ db_up_and_down "02", "animals"
+ end
+
test "db:migrate:status works on all databases" do
require "#{app_path}/config/environment"
db_migrate_and_migrate_status
@@ -226,6 +298,11 @@ module ApplicationTests
require "#{app_path}/config/environment"
db_migrate_and_schema_cache_dump_and_schema_cache_clear
end
+
+ test "db:prepare works on all databases" do
+ require "#{app_path}/config/environment"
+ db_prepare
+ end
end
end
end
diff --git a/railties/test/application/rake/routes_test.rb b/railties/test/application/rake/routes_test.rb
index 9879d1f047..dffdae7bde 100644
--- a/railties/test/application/rake/routes_test.rb
+++ b/railties/test/application/rake/routes_test.rb
@@ -20,7 +20,6 @@ module ApplicationTests
assert_equal <<~MESSAGE, run_rake_routes
Prefix Verb URI Pattern Controller#Action
cart GET /cart(.:format) cart#show
- rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create
rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create
rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create
diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb
index c82b37d07d..40d06ee999 100644
--- a/railties/test/application/zeitwerk_integration_test.rb
+++ b/railties/test/application/zeitwerk_integration_test.rb
@@ -98,24 +98,35 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert_nil deps.safe_constantize("Admin")
end
- test "autoloaded_constants returns autoloaded constant paths" do
- app_file "app/models/admin/user.rb", "class Admin::User; end"
+ test "to_unload? says if a constant would be unloaded (main)" do
+ app_file "app/models/user.rb", "class User; end"
app_file "app/models/post.rb", "class Post; end"
boot
- assert Admin::User
- assert_equal ["Admin", "Admin::User"], deps.autoloaded_constants
+ assert Post
+ assert deps.to_unload?("Post")
+ assert_not deps.to_unload?("User")
+ end
+
+ test "to_unload? says if a constant would be unloaded (once)" do
+ add_to_config 'config.autoload_once_paths << "#{Rails.root}/extras"'
+ app_file "extras/foo.rb", "class Foo; end"
+ app_file "extras/bar.rb", "class Bar; end"
+ boot
+
+ assert Foo
+ assert_not deps.to_unload?("Foo")
+ assert_not deps.to_unload?("Bar")
end
- test "autoloaded? says if a constant has been autoloaded" do
+ test "to_unload? says if a constant would be unloaded (reloading disabled)" do
app_file "app/models/user.rb", "class User; end"
app_file "app/models/post.rb", "class Post; end"
- boot
+ boot("production")
assert Post
- assert deps.autoloaded?("Post")
- assert deps.autoloaded?(Post)
- assert_not deps.autoloaded?("User")
+ assert_not deps.to_unload?("Post")
+ assert_not deps.to_unload?("User")
end
test "eager loading loads the application code" do
@@ -124,12 +135,49 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true"
app_file "app/models/post.rb", "class Post; end; $zeitwerk_integration_test_post = true"
+
boot("production")
assert $zeitwerk_integration_test_user
assert $zeitwerk_integration_test_post
end
+ test "reloading is enabled if config.cache_classes is false" do
+ boot
+
+ assert Rails.autoloaders.main.reloading_enabled?
+ assert_not Rails.autoloaders.once.reloading_enabled?
+ end
+
+ test "reloading is disabled if config.cache_classes is true" do
+ boot("production")
+
+ assert_not Rails.autoloaders.main.reloading_enabled?
+ assert_not Rails.autoloaders.once.reloading_enabled?
+ end
+
+ test "reloading raises if config.cache_classes is true" do
+ boot("production")
+
+ e = assert_raises(StandardError) do
+ deps.clear
+ end
+ assert_equal "reloading is disabled because config.cache_classes is true", e.message
+ end
+
+ test "eager loading loads code in engines" do
+ $test_blog_engine_eager_loaded = false
+
+ engine("blog") do |bukkit|
+ bukkit.write("lib/blog.rb", "class BlogEngine < Rails::Engine; end")
+ bukkit.write("app/models/post.rb", "Post = $test_blog_engine_eager_loaded = true")
+ end
+
+ boot("production")
+
+ assert $test_blog_engine_eager_loaded
+ end
+
test "eager loading loads anything managed by Zeitwerk" do
$zeitwerk_integration_test_user = false
app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true"
@@ -149,6 +197,34 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert $zeitwerk_integration_test_extras
end
+ test "autoload directories not present in eager load paths are not eager loaded" do
+ $zeitwerk_integration_test_user = false
+ app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true"
+
+ $zeitwerk_integration_test_lib = false
+ app_dir "lib"
+ app_file "lib/webhook_hacks.rb", "WebhookHacks = 1; $zeitwerk_integration_test_lib = true"
+
+ $zeitwerk_integration_test_extras = false
+ app_dir "extras"
+ app_file "extras/websocket_hacks.rb", "WebsocketHacks = 1; $zeitwerk_integration_test_extras = true"
+
+ add_to_config "config.autoload_paths << '#{app_path}/lib'"
+ add_to_config "config.autoload_once_paths << '#{app_path}/extras'"
+
+ boot("production")
+
+ assert $zeitwerk_integration_test_user
+ assert_not $zeitwerk_integration_test_lib
+ assert_not $zeitwerk_integration_test_extras
+
+ assert WebhookHacks
+ assert WebsocketHacks
+
+ assert $zeitwerk_integration_test_lib
+ assert $zeitwerk_integration_test_extras
+ end
+
test "autoload_paths are set as root dirs of main, and in the same order" do
boot
@@ -259,4 +335,46 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert_nil autoloader.logger
end
end
+
+ # This is here because to guarantee classic mode works as always, Zeitwerk
+ # integration does not touch anything in classic. The descendants tracker is a
+ # very small one-liner exception. We leave its main test suite untouched, and
+ # add some minimal safety net here.
+ #
+ # When time passes, things are going to be reorganized (famous last words).
+ test "descendants tracker" do
+ class ::ZeitwerkDTIntegrationTestRoot
+ extend ActiveSupport::DescendantsTracker
+ end
+ class ::ZeitwerkDTIntegrationTestChild < ::ZeitwerkDTIntegrationTestRoot; end
+ class ::ZeitwerkDTIntegrationTestGrandchild < ::ZeitwerkDTIntegrationTestChild; end
+
+ begin
+ app_file "app/models/user.rb", "class User < ZeitwerkDTIntegrationTestRoot; end"
+ app_file "app/models/post.rb", "class Post < ZeitwerkDTIntegrationTestRoot; end"
+ app_file "app/models/tutorial.rb", "class Tutorial < Post; end"
+ boot
+
+ assert User
+ assert Tutorial
+
+ direct_descendants = [ZeitwerkDTIntegrationTestChild, User, Post].to_set
+ assert_equal direct_descendants, ZeitwerkDTIntegrationTestRoot.direct_descendants.to_set
+
+ descendants = direct_descendants.merge([ZeitwerkDTIntegrationTestGrandchild, Tutorial])
+ assert_equal descendants, ZeitwerkDTIntegrationTestRoot.descendants.to_set
+
+ ActiveSupport::DescendantsTracker.clear
+
+ direct_descendants = [ZeitwerkDTIntegrationTestChild].to_set
+ assert_equal direct_descendants, ZeitwerkDTIntegrationTestRoot.direct_descendants.to_set
+
+ descendants = direct_descendants.merge([ZeitwerkDTIntegrationTestGrandchild])
+ assert_equal descendants, ZeitwerkDTIntegrationTestRoot.descendants.to_set
+ ensure
+ Object.send(:remove_const, :ZeitwerkDTIntegrationTestRoot)
+ Object.send(:remove_const, :ZeitwerkDTIntegrationTestChild)
+ Object.send(:remove_const, :ZeitwerkDTIntegrationTestGrandchild)
+ end
+ end
end
diff --git a/railties/test/backtrace_cleaner_test.rb b/railties/test/backtrace_cleaner_test.rb
index ec512b6b64..6de23acebe 100644
--- a/railties/test/backtrace_cleaner_test.rb
+++ b/railties/test/backtrace_cleaner_test.rb
@@ -17,6 +17,16 @@ class BacktraceCleanerTest < ActiveSupport::TestCase
assert_equal 1, result.length
end
+ test "can filter for noise" do
+ backtrace = [ "(irb):1",
+ "/Path/to/rails/railties/lib/rails/commands/console.rb:77:in `start'",
+ "bin/rails:4:in `<main>'" ]
+ result = @cleaner.clean(backtrace, :noise)
+ assert_equal "/Path/to/rails/railties/lib/rails/commands/console.rb:77:in `start'", result[0]
+ assert_equal "bin/rails:4:in `<main>'", result[1]
+ assert_equal 2, result.length
+ end
+
test "should omit ActionView template methods names" do
method_name = ActionView::Template.new(nil, "app/views/application/index.html.erb", nil, locals: []).send :method_name
backtrace = [ "app/views/application/index.html.erb:4:in `block in #{method_name}'"]
diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb
index 2f2c50de6c..0ee36081c0 100644
--- a/railties/test/commands/credentials_test.rb
+++ b/railties/test/commands/credentials_test.rb
@@ -63,7 +63,7 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
end
end
- test "edit command properly expand environment option" do
+ test "edit command properly expands environment option" do
assert_match(/access_key_id: 123/, run_edit_command(environment: "prod"))
Dir.chdir(app_path) do
assert File.exist?("config/credentials/production.key")
@@ -79,7 +79,7 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
assert_match(/access_key_id: 123/, run_edit_command(environment: "qa"))
end
- test "edit command generate template file when the file does not exist" do
+ test "edit command generates template file when the file does not exist" do
FileUtils.rm("#{app_path}/config/credentials.yml.enc")
run_edit_command
@@ -92,7 +92,7 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
assert_match(/access_key_id: 123/, run_show_command)
end
- test "show command raise error when require_master_key is specified and key does not exist" do
+ test "show command raises error when require_master_key is specified and key does not exist" do
remove_file "config/master.key"
add_to_config "config.require_master_key = true"
@@ -112,7 +112,7 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
assert_match(/access_key_id: 123/, run_show_command(environment: "production"))
end
- test "show command properly expand environment option" do
+ test "show command properly expands environment option" do
run_edit_command(environment: "production")
output = run_show_command(environment: "prod")
diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb
index 65f6916acb..76a7cd055f 100644
--- a/railties/test/commands/dbconsole_test.rb
+++ b/railties/test/commands/dbconsole_test.rb
@@ -216,22 +216,22 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
end
end
- def test_specifying_a_custom_connection_and_environment
+ def test_specifying_a_custom_database_and_environment
stub_available_environments(["development"]) do
- dbconsole = parse_arguments(["-c", "custom", "-e", "development"])
+ dbconsole = parse_arguments(["--db", "custom", "-e", "development"])
assert_equal "development", dbconsole[:environment]
- assert_equal "custom", dbconsole.connection
+ assert_equal "custom", dbconsole.database
end
end
- def test_specifying_a_missing_connection
+ def test_specifying_a_missing_database
app_db_config({}) do
e = assert_raises(ActiveRecord::AdapterNotSpecified) do
- Rails::Command.invoke(:dbconsole, ["-c", "i_do_not_exist"])
+ Rails::Command.invoke(:dbconsole, ["--db", "i_do_not_exist"])
end
- assert_includes e.message, "'i_do_not_exist' connection is not configured."
+ assert_includes e.message, "'i_do_not_exist' database is not configured."
end
end
@@ -245,6 +245,18 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
end
end
+ def test_connection_options_is_deprecate
+ command = Rails::Command::DbconsoleCommand.new([], ["-c", "custom"])
+ Rails::DBConsole.stub(:start, nil) do
+ assert_deprecated("`connection` option is deprecated") do
+ command.perform
+ end
+ end
+
+ assert_equal "custom", command.options["connection"]
+ assert_equal "custom", command.options["database"]
+ end
+
def test_print_help_short
stdout = capture(:stdout) do
Rails::Command.invoke(:dbconsole, ["-h"])
diff --git a/railties/test/commands/notes_test.rb b/railties/test/commands/notes_test.rb
index 147019e299..9182541413 100644
--- a/railties/test/commands/notes_test.rb
+++ b/railties/test/commands/notes_test.rb
@@ -121,6 +121,47 @@ class Rails::Command::NotesTest < ActiveSupport::TestCase
OUTPUT
end
+ test "displays results from additional tags added to the default tags from a config file" do
+ app_file "app/models/profile.rb", "# TESTME: some method to test"
+ app_file "app/controllers/hello_controller.rb", "# DEPRECATEME: this action is no longer needed"
+ app_file "db/some_seeds.rb", "# TODO: default tags such as TODO are still present"
+
+ add_to_config 'config.annotations.register_tags "TESTME", "DEPRECATEME"'
+
+ assert_equal <<~OUTPUT, run_notes_command
+ app/controllers/hello_controller.rb:
+ * [1] [DEPRECATEME] this action is no longer needed
+
+ app/models/profile.rb:
+ * [1] [TESTME] some method to test
+
+ db/some_seeds.rb:
+ * [1] [TODO] default tags such as TODO are still present
+
+ OUTPUT
+ end
+
+ test "does not display results from tags that are neither default nor registered" do
+ app_file "app/models/profile.rb", "# TESTME: some method to test"
+ app_file "app/controllers/hello_controller.rb", "# DEPRECATEME: this action is no longer needed"
+ app_file "db/some_seeds.rb", "# TODO: default tags such as TODO are still present"
+ app_file "db/some_other_seeds.rb", "# BAD: this note should not be listed"
+
+ add_to_config 'config.annotations.register_tags "TESTME", "DEPRECATEME"'
+
+ assert_equal <<~OUTPUT, run_notes_command
+ app/controllers/hello_controller.rb:
+ * [1] [DEPRECATEME] this action is no longer needed
+
+ app/models/profile.rb:
+ * [1] [TESTME] some method to test
+
+ db/some_seeds.rb:
+ * [1] [TODO] default tags such as TODO are still present
+
+ OUTPUT
+ end
+
private
def run_notes_command(args = [])
rails "notes", args
diff --git a/railties/test/commands/routes_test.rb b/railties/test/commands/routes_test.rb
index b4f927060e..a2dbd944f5 100644
--- a/railties/test/commands/routes_test.rb
+++ b/railties/test/commands/routes_test.rb
@@ -62,7 +62,6 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
assert_equal <<~MESSAGE, run_routes_command([ "-g", "POST" ])
Prefix Verb URI Pattern Controller#Action
POST /cart(.:format) cart#create
- rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create
rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create
rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create
@@ -166,7 +165,6 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
assert_equal <<~MESSAGE, run_routes_command
Prefix Verb URI Pattern Controller#Action
- rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create
rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create
rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create
@@ -207,101 +205,96 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
URI | /cart(.:format)
Controller#Action | cart#show
--[ Route 2 ]--------------
- Prefix | rails_amazon_inbound_emails
- Verb | POST
- URI | /rails/action_mailbox/amazon/inbound_emails(.:format)
- Controller#Action | action_mailbox/ingresses/amazon/inbound_emails#create
- --[ Route 3 ]--------------
Prefix | rails_mandrill_inbound_emails
Verb | POST
URI | /rails/action_mailbox/mandrill/inbound_emails(.:format)
Controller#Action | action_mailbox/ingresses/mandrill/inbound_emails#create
- --[ Route 4 ]--------------
+ --[ Route 3 ]--------------
Prefix | rails_postmark_inbound_emails
Verb | POST
URI | /rails/action_mailbox/postmark/inbound_emails(.:format)
Controller#Action | action_mailbox/ingresses/postmark/inbound_emails#create
- --[ Route 5 ]--------------
+ --[ Route 4 ]--------------
Prefix | rails_relay_inbound_emails
Verb | POST
URI | /rails/action_mailbox/relay/inbound_emails(.:format)
Controller#Action | action_mailbox/ingresses/relay/inbound_emails#create
- --[ Route 6 ]--------------
+ --[ Route 5 ]--------------
Prefix | rails_sendgrid_inbound_emails
Verb | POST
URI | /rails/action_mailbox/sendgrid/inbound_emails(.:format)
Controller#Action | action_mailbox/ingresses/sendgrid/inbound_emails#create
- --[ Route 7 ]--------------
+ --[ Route 6 ]--------------
Prefix | rails_mailgun_inbound_emails
Verb | POST
URI | /rails/action_mailbox/mailgun/inbound_emails/mime(.:format)
Controller#Action | action_mailbox/ingresses/mailgun/inbound_emails#create
- --[ Route 8 ]--------------
+ --[ Route 7 ]--------------
Prefix | rails_conductor_inbound_emails
Verb | GET
URI | /rails/conductor/action_mailbox/inbound_emails(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#index
- --[ Route 9 ]--------------
+ --[ Route 8 ]--------------
Prefix |
Verb | POST
URI | /rails/conductor/action_mailbox/inbound_emails(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#create
- --[ Route 10 ]-------------
+ --[ Route 9 ]--------------
Prefix | new_rails_conductor_inbound_email
Verb | GET
URI | /rails/conductor/action_mailbox/inbound_emails/new(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#new
- --[ Route 11 ]-------------
+ --[ Route 10 ]-------------
Prefix | edit_rails_conductor_inbound_email
Verb | GET
URI | /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#edit
- --[ Route 12 ]-------------
+ --[ Route 11 ]-------------
Prefix | rails_conductor_inbound_email
Verb | GET
URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#show
- --[ Route 13 ]-------------
+ --[ Route 12 ]-------------
Prefix |
Verb | PATCH
URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#update
- --[ Route 14 ]-------------
+ --[ Route 13 ]-------------
Prefix |
Verb | PUT
URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#update
- --[ Route 15 ]-------------
+ --[ Route 14 ]-------------
Prefix |
Verb | DELETE
URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#destroy
- --[ Route 16 ]-------------
+ --[ Route 15 ]-------------
Prefix | rails_conductor_inbound_email_reroute
Verb | POST
URI | /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format)
Controller#Action | rails/conductor/action_mailbox/reroutes#create
- --[ Route 17 ]-------------
+ --[ Route 16 ]-------------
Prefix | rails_service_blob
Verb | GET
URI | /rails/active_storage/blobs/:signed_id/*filename(.:format)
Controller#Action | active_storage/blobs#show
- --[ Route 18 ]-------------
+ --[ Route 17 ]-------------
Prefix | rails_blob_representation
Verb | GET
URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format)
Controller#Action | active_storage/representations#show
- --[ Route 19 ]-------------
+ --[ Route 18 ]-------------
Prefix | rails_disk_service
Verb | GET
URI | /rails/active_storage/disk/:encoded_key/*filename(.:format)
Controller#Action | active_storage/disk#show
- --[ Route 20 ]-------------
+ --[ Route 19 ]-------------
Prefix | update_rails_disk_service
Verb | PUT
URI | /rails/active_storage/disk/:encoded_token(.:format)
Controller#Action | active_storage/disk#update
- --[ Route 21 ]-------------
+ --[ Route 20 ]-------------
Prefix | rails_direct_uploads
Verb | POST
URI | /rails/active_storage/direct_uploads(.:format)
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 44d4e92256..d913bb5438 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -200,7 +200,7 @@ class ActionsTest < Rails::Generators::TestCase
run_generator
action :environment do
- _ = "# This wont be added"# assignment to silence parse-time warning "unused literal ignored"
+ _ = "# This wont be added" # assignment to silence parse-time warning "unused literal ignored"
"# This will be added"
end
diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb
index 4b9878187b..503564beec 100644
--- a/railties/test/generators/api_app_generator_test.rb
+++ b/railties/test/generators/api_app_generator_test.rb
@@ -120,7 +120,6 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
bin/rails
bin/rake
bin/setup
- bin/update
config/application.rb
config/boot.rb
config/cable.yml
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index d5a3599f67..5b439fdcba 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -41,7 +41,6 @@ DEFAULT_APP_FILES = %w(
bin/rails
bin/rake
bin/setup
- bin/update
bin/yarn
config/application.rb
config/boot.rb
@@ -321,10 +320,6 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "#{app_root}/bin/setup" do |content|
assert_no_match(/system\('bin\/yarn'\)/, content)
end
-
- assert_file "#{app_root}/bin/update" do |content|
- assert_no_match(/system\('bin\/yarn'\)/, content)
- end
end
end
@@ -345,39 +340,44 @@ class AppGeneratorTest < Rails::Generators::TestCase
app_root = File.join(destination_root, "myapp")
run_generator [app_root, "--skip-spring"]
- FileUtils.cd(app_root) do
- quietly { system("bin/rails app:update") }
- end
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_spring: true }, { destination_root: app_root, shell: @shell }
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
- assert_no_file "#{app_root}/config/spring.rb"
+ assert_no_file "#{app_root}/config/spring.rb"
+ end
end
def test_app_update_does_not_generate_action_cable_contents_when_skip_action_cable_is_given
app_root = File.join(destination_root, "myapp")
run_generator [app_root, "--skip-action-cable"]
- FileUtils.cd(app_root) do
- quietly { system("bin/rails app:update") }
- end
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_action_cable: true }, { destination_root: app_root, shell: @shell }
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
- assert_no_file "#{app_root}/config/cable.yml"
- assert_file "#{app_root}/config/environments/production.rb" do |content|
- assert_no_match(/config\.action_cable/, content)
+ assert_no_file "#{app_root}/config/cable.yml"
+ assert_file "#{app_root}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.action_cable/, content)
+ end
+ assert_no_file "#{app_root}/test/channels/application_cable/connection_test.rb"
end
-
- assert_no_file "#{app_root}/test/channels/application_cable/connection_test.rb"
end
def test_app_update_does_not_generate_bootsnap_contents_when_skip_bootsnap_is_given
app_root = File.join(destination_root, "myapp")
run_generator [app_root, "--skip-bootsnap"]
- FileUtils.cd(app_root) do
- quietly { system("bin/rails app:update") }
- end
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_bootsnap: true }, { destination_root: app_root, shell: @shell }
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/boot.rb" do |content|
- assert_no_match(/require 'bootsnap\/setup'/, content)
+ assert_file "#{app_root}/config/boot.rb" do |content|
+ assert_no_match(/require 'bootsnap\/setup'/, content)
+ end
end
end
@@ -396,46 +396,50 @@ class AppGeneratorTest < Rails::Generators::TestCase
app_root = File.join(destination_root, "myapp")
run_generator [app_root, "--skip-active-storage"]
- FileUtils.cd(app_root) do
- quietly { system("bin/rails app:update") }
- end
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_active_storage: true }, { destination_root: app_root, shell: @shell }
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/environments/development.rb" do |content|
- assert_no_match(/config\.active_storage/, content)
- end
+ assert_file "#{app_root}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
- assert_file "#{app_root}/config/environments/production.rb" do |content|
- assert_no_match(/config\.active_storage/, content)
- end
+ assert_file "#{app_root}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
- assert_file "#{app_root}/config/environments/test.rb" do |content|
- assert_no_match(/config\.active_storage/, content)
- end
+ assert_file "#{app_root}/config/environments/test.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
- assert_no_file "#{app_root}/config/storage.yml"
+ assert_no_file "#{app_root}/config/storage.yml"
+ end
end
def test_app_update_does_not_generate_active_storage_contents_when_skip_active_record_is_given
app_root = File.join(destination_root, "myapp")
run_generator [app_root, "--skip-active-record"]
- FileUtils.cd(app_root) do
- quietly { system("bin/rails app:update") }
- end
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_active_record: true }, { destination_root: app_root, shell: @shell }
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/environments/development.rb" do |content|
- assert_no_match(/config\.active_storage/, content)
- end
+ assert_file "#{app_root}/config/environments/development.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
- assert_file "#{app_root}/config/environments/production.rb" do |content|
- assert_no_match(/config\.active_storage/, content)
- end
+ assert_file "#{app_root}/config/environments/production.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
- assert_file "#{app_root}/config/environments/test.rb" do |content|
- assert_no_match(/config\.active_storage/, content)
- end
+ assert_file "#{app_root}/config/environments/test.rb" do |content|
+ assert_no_match(/config\.active_storage/, content)
+ end
- assert_no_file "#{app_root}/config/storage.yml"
+ assert_no_file "#{app_root}/config/storage.yml"
+ end
end
def test_generator_skips_action_mailbox_when_skip_action_mailbox_is_given
@@ -469,16 +473,17 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_app_update_does_not_change_config_target_version
- run_generator
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root, "--skip-spring"]
- FileUtils.cd(destination_root) do
+ FileUtils.cd(app_root) do
config = "config/application.rb"
content = File.read(config)
File.write(config, content.gsub(/config\.load_defaults #{Rails::VERSION::STRING.to_f}/, "config.load_defaults 5.1"))
quietly { system("bin/rails app:update") }
end
- assert_file "config/application.rb", /\s+config\.load_defaults 5\.1/
+ assert_file "#{app_root}/config/application.rb", /\s+config\.load_defaults 5\.1/
end
def test_app_update_does_not_change_app_name_when_app_name_is_hyphenated_name
@@ -495,13 +500,15 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_match(/hyphenated-app/, content)
end
- FileUtils.cd(app_root) do
- quietly { system("bin/rails app:update") }
- end
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { update: true }, { destination_root: app_root, shell: @shell }
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/cable.yml" do |content|
- assert_match(/hyphenated_app/, content)
- assert_no_match(/hyphenated-app/, content)
+ assert_file "#{app_root}/config/cable.yml" do |content|
+ assert_match(/hyphenated_app/, content)
+ assert_no_match(/hyphenated-app/, content)
+ end
end
end
@@ -526,7 +533,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcsqlite3-adapter"
else
- assert_gem "sqlite3", "'~> 1.3', '>= 1.3.6'"
+ assert_gem "sqlite3", "'~> 1.4'"
end
end
@@ -678,6 +685,21 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_inclusion_of_listen_related_configuration_on_other_rubies
+ ruby_engine = Object.send(:remove_const, :RUBY_ENGINE)
+ Object.const_set(:RUBY_ENGINE, "MyRuby")
+
+ run_generator
+ if RbConfig::CONFIG["host_os"] =~ /darwin|linux/
+ assert_listen_related_configuration
+ else
+ assert_no_listen_related_configuration
+ end
+ ensure
+ Object.send(:remove_const, :RUBY_ENGINE)
+ Object.const_set(:RUBY_ENGINE, ruby_engine)
+ end
+
def test_non_inclusion_of_listen_related_configuration_if_skip_listen
run_generator [destination_root, "--skip-listen"]
assert_no_listen_related_configuration
@@ -812,6 +834,9 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_spring
run_generator
assert_gem "spring"
+ assert_file("config/environments/test.rb") do |contents|
+ assert_match("config.cache_classes = false", contents)
+ end
end
def test_bundler_binstub
@@ -830,7 +855,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_spring_no_fork
jruby_skip "spring doesn't run on JRuby"
- assert_called_with(Process, :respond_to?, [[:fork], [:fork]], returns: false) do
+ assert_called_with(Process, :respond_to?, [[:fork], [:fork], [:fork]], returns: false) do
run_generator
assert_no_gem "spring"
@@ -842,6 +867,9 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_file "config/spring.rb"
assert_no_gem "spring"
+ assert_file("config/environments/test.rb") do |contents|
+ assert_match("config.cache_classes = true", contents)
+ end
end
def test_spring_with_dev_option
@@ -968,6 +996,8 @@ class AppGeneratorTest < Rails::Generators::TestCase
else
assert_match(/#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}/, content)
end
+
+ assert content.end_with?("\n"), "expected .ruby-version to end with newline"
end
end
diff --git a/railties/test/generators/db_system_change_generator_test.rb b/railties/test/generators/db_system_change_generator_test.rb
index d3d27b616a..607db96906 100644
--- a/railties/test/generators/db_system_change_generator_test.rb
+++ b/railties/test/generators/db_system_change_generator_test.rb
@@ -68,7 +68,7 @@ module Rails
assert_file("Gemfile") do |content|
assert_match "# Use sqlite3 as the database for Active Record", content
- assert_match "gem 'sqlite3', '~> 1.3', '>= 1.3.6'", content
+ assert_match "gem 'sqlite3', '~> 1.4'", content
end
end
diff --git a/railties/test/generators/generated_attribute_test.rb b/railties/test/generators/generated_attribute_test.rb
index 772b4f6f0d..82d550a124 100644
--- a/railties/test/generators/generated_attribute_test.rb
+++ b/railties/test/generators/generated_attribute_test.rb
@@ -6,6 +6,15 @@ require "rails/generators/generated_attribute"
class GeneratedAttributeTest < Rails::Generators::TestCase
include GeneratorsTestHelper
+ def setup
+ @old_belongs_to_required_by_default = Rails.application.config.active_record.belongs_to_required_by_default
+ Rails.application.config.active_record.belongs_to_required_by_default = true
+ end
+
+ def teardown
+ Rails.application.config.active_record.belongs_to_required_by_default = @old_belongs_to_required_by_default
+ end
+
def test_field_type_returns_number_field
assert_field_type :integer, :number_field
end
@@ -38,6 +47,16 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
assert_field_type :boolean, :check_box
end
+ def test_field_type_returns_rich_text_area
+ assert_field_type :rich_text, :rich_text_area
+ end
+
+ def test_field_type_returns_file_field
+ %w(attachment attachments).each do |attribute_type|
+ assert_field_type attribute_type, :file_field
+ end
+ end
+
def test_field_type_with_unknown_type_returns_text_field
%w(foo bar baz).each do |attribute_type|
assert_field_type attribute_type, :text_field
@@ -84,7 +103,7 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
end
def test_default_value_is_nil
- %w(references belongs_to).each do |attribute_type|
+ %w(references belongs_to rich_text attachment attachments).each do |attribute_type|
assert_field_default_value attribute_type, nil
end
end
@@ -145,10 +164,16 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
end
def test_parse_required_attribute_with_index
- att = Rails::Generators::GeneratedAttribute.parse("supplier:references{required}:index")
+ att = Rails::Generators::GeneratedAttribute.parse("supplier:references:index")
assert_equal "supplier", att.name
assert_equal :references, att.type
assert_predicate att, :has_index?
assert_predicate att, :required?
end
+
+ def test_parse_required_attribute_with_index_false_when_belongs_to_required_by_default_global_config_is_false
+ Rails.application.config.active_record.belongs_to_required_by_default = false
+ att = Rails::Generators::GeneratedAttribute.parse("supplier:references:index")
+ assert_not_predicate att, :required?
+ end
end
diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb
index acc5fc3b25..e6b614a935 100644
--- a/railties/test/generators/migration_generator_test.rb
+++ b/railties/test/generators/migration_generator_test.rb
@@ -6,6 +6,16 @@ require "rails/generators/rails/migration/migration_generator"
class MigrationGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
+ def setup
+ @old_belongs_to_required_by_default = Rails.application.config.active_record.belongs_to_required_by_default
+
+ Rails.application.config.active_record.belongs_to_required_by_default = true
+ end
+
+ def teardown
+ Rails.application.config.active_record.belongs_to_required_by_default = @old_belongs_to_required_by_default
+ end
+
def test_migration
migration = "change_title_body_from_posts"
run_generator [migration]
@@ -196,9 +206,9 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
end
end
- def test_add_migration_with_required_references
+ def test_add_migration_with_references_adds_null_false_by_default
migration = "add_references_to_books"
- run_generator [migration, "author:belongs_to{required}", "distributor:references{polymorphic,required}"]
+ run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"]
assert_migration "db/migrate/#{migration}.rb" do |content|
assert_method :change, content do |change|
@@ -208,6 +218,21 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_add_migration_with_references_does_not_add_belongs_to_when_required_by_default_global_config_is_false
+ Rails.application.config.active_record.belongs_to_required_by_default = false
+
+ migration = "add_references_to_books"
+
+ run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/add_reference :books, :author/, change)
+ assert_match(/add_reference :books, :distributor, polymorphic: true/, change)
+ end
+ end
+ end
+
def test_add_migration_with_references_adds_foreign_keys
migration = "add_references_to_books"
run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"]
@@ -366,6 +391,44 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
Rails.application.config.paths["db/migrate"] = old_paths
end
+ def test_add_migration_ignores_virtual_attributes
+ migration = "add_rich_text_content_to_messages"
+ run_generator [migration, "content:rich_text", "video:attachment", "photos:attachments"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_no_match(/add_column :messages, :content, :rich_text/, change)
+ assert_no_match(/add_column :messages, :video, :attachment/, change)
+ assert_no_match(/add_column :messages, :photos, :attachments/, change)
+ end
+ end
+ end
+
+ def test_create_table_migration_ignores_virtual_attributes
+ run_generator ["create_messages", "content:rich_text", "video:attachment", "photos:attachments"]
+ assert_migration "db/migrate/create_messages.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_table :messages/, change)
+ assert_no_match(/ t\.rich_text :content/, change)
+ assert_no_match(/ t\.attachment :video/, change)
+ assert_no_match(/ t\.attachments :photos/, change)
+ end
+ end
+ end
+
+ def test_remove_migration_with_virtual_attributes
+ migration = "remove_content_from_messages"
+ run_generator [migration, "content:rich_text", "video:attachment", "photos:attachments"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_no_match(/remove_column :messages, :content, :rich_text/, change)
+ assert_no_match(/remove_column :messages, :video, :attachment/, change)
+ assert_no_match(/remove_column :messages, :photos, :attachments/, change)
+ end
+ end
+ end
+
private
def with_singular_table_name
diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb
index bdb430369e..d6abaf7a6f 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -10,6 +10,14 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def setup
super
Rails::Generators::ModelHelpers.skip_warn = false
+ @old_belongs_to_required_by_default = Rails.application.config.active_record.belongs_to_required_by_default
+
+ Rails.application.config.active_record.belongs_to_required_by_default = true
+ end
+
+
+ def teardown
+ Rails.application.config.active_record.belongs_to_required_by_default = @old_belongs_to_required_by_default
end
def test_help_shows_invoked_generators_options
@@ -414,45 +422,57 @@ class ModelGeneratorTest < Rails::Generators::TestCase
end
end
- def test_required_belongs_to_adds_required_association
- run_generator ["account", "supplier:references{required}"]
+ def test_polymorphic_belongs_to_generates_correct_model
+ run_generator ["account", "supplier:references{polymorphic}"]
expected_file = <<~FILE
class Account < ApplicationRecord
- belongs_to :supplier, required: true
+ belongs_to :supplier, polymorphic: true
end
FILE
assert_file "app/models/account.rb", expected_file
end
- def test_required_polymorphic_belongs_to_generages_correct_model
- run_generator ["account", "supplier:references{required,polymorphic}"]
+ def test_passing_required_to_model_generator_is_deprecated
+ assert_deprecated do
+ run_generator ["account", "supplier:references{required}"]
+ end
- expected_file = <<~FILE
- class Account < ApplicationRecord
- belongs_to :supplier, polymorphic: true, required: true
+ assert_migration "db/migrate/create_accounts.rb" do |m|
+ assert_method :change, m do |up|
+ assert_match(/t\.references :supplier,.*\snull: false/, up)
end
- FILE
- assert_file "app/models/account.rb", expected_file
+ end
end
- def test_required_and_polymorphic_are_order_independent
- run_generator ["account", "supplier:references{polymorphic.required}"]
+ def test_null_false_is_added_for_references_by_default
+ run_generator ["account", "user:references"]
- expected_file = <<~FILE
- class Account < ApplicationRecord
- belongs_to :supplier, polymorphic: true, required: true
+ assert_migration "db/migrate/create_accounts.rb" do |m|
+ assert_method :change, m do |up|
+ assert_match(/t\.references :user,.*\snull: false/, up)
end
- FILE
- assert_file "app/models/account.rb", expected_file
+ end
end
- def test_required_adds_null_false_to_column
- run_generator ["account", "supplier:references{required}"]
+ def test_null_false_is_added_for_belongs_to_by_default
+ run_generator ["account", "user:belongs_to"]
assert_migration "db/migrate/create_accounts.rb" do |m|
assert_method :change, m do |up|
- assert_match(/t\.references :supplier,.*\snull: false/, up)
+ assert_match(/t\.belongs_to :user,.*\snull: false/, up)
+ end
+ end
+ end
+
+ def test_null_false_is_not_added_when_belongs_to_required_by_default_global_config_is_false
+ Rails.application.config.active_record.belongs_to_required_by_default = false
+
+ run_generator ["account", "user:belongs_to"]
+
+ assert_migration "db/migrate/create_accounts.rb" do |m|
+ assert_method :change, m do |up|
+ assert_match(/t\.belongs_to :user/, up)
end
end
end
@@ -499,6 +519,43 @@ class ModelGeneratorTest < Rails::Generators::TestCase
assert_file "app/models/user.rb", expected_file
end
+ def test_model_with_rich_text_attribute_adds_has_rich_text
+ run_generator ["message", "content:rich_text"]
+ expected_file = <<~FILE
+ class Message < ApplicationRecord
+ has_rich_text :content
+ end
+ FILE
+ assert_file "app/models/message.rb", expected_file
+ end
+
+ def test_model_with_attachment_attribute_adds_has_one_attached
+ run_generator ["message", "video:attachment"]
+ expected_file = <<~FILE
+ class Message < ApplicationRecord
+ has_one_attached :video
+ end
+ FILE
+ assert_file "app/models/message.rb", expected_file
+ end
+
+ def test_model_with_attachments_attribute_adds_has_many_attached
+ run_generator ["message", "photos:attachments"]
+ expected_file = <<~FILE
+ class Message < ApplicationRecord
+ has_many_attached :photos
+ end
+ FILE
+ assert_file "app/models/message.rb", expected_file
+ end
+
+ def test_skip_virtual_fields_in_fixtures
+ run_generator ["message", "content:rich_text", "video:attachment", "photos:attachments"]
+
+ assert_generated_fixture("test/fixtures/messages.yml",
+ "one" => nil, "two" => nil)
+ end
+
private
def assert_generated_fixture(path, parsed_contents)
fixture_file = File.new File.expand_path(path, destination_root)
diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb
index fd5aa817b4..1348744b0b 100644
--- a/railties/test/generators/scaffold_controller_generator_test.rb
+++ b/railties/test/generators/scaffold_controller_generator_test.rb
@@ -80,6 +80,15 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_controller_permit_attachment_attributes
+ run_generator ["Message", "video:attachment", "photos:attachments"]
+
+ assert_file "app/controllers/messages_controller.rb" do |content|
+ assert_match(/def message_params/, content)
+ assert_match(/params\.require\(:message\)\.permit\(:video, photos: \[\]\)/, content)
+ end
+ end
+
def test_helper_are_invoked_with_a_pluralized_name
run_generator
assert_file "app/helpers/users_helper.rb", /module UsersHelper/
@@ -276,4 +285,13 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
assert_no_match(/assert_redirected_to/, content)
end
end
+
+ def test_api_only_generates_params_for_attachments
+ run_generator ["Message", "video:attachment", "photos:attachments", "--api"]
+
+ assert_file "app/controllers/messages_controller.rb" do |content|
+ assert_match(/def message_params/, content)
+ assert_match(/params\.require\(:message\)\.permit\(:video, photos: \[\]\)/, content)
+ end
+ end
end
diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb
index 715ad938f4..bfa52a1beb 100644
--- a/railties/test/generators/scaffold_generator_test.rb
+++ b/railties/test/generators/scaffold_generator_test.rb
@@ -471,6 +471,24 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_scaffold_generator_attachments
+ run_generator ["message", "video:attachment", "photos:attachments", "images:attachments"]
+
+ assert_file "app/models/message.rb", /has_one_attached :video/
+ assert_file "app/models/message.rb", /has_many_attached :photos/
+
+ assert_file "app/controllers/messages_controller.rb" do |content|
+ assert_instance_method :message_params, content do |m|
+ assert_match(/permit\(:video, photos: \[\], images: \[\]\)/, m)
+ end
+ end
+
+ assert_file "app/views/messages/_form.html.erb" do |content|
+ assert_match(/^\W{4}<%= form\.file_field :video %>/, content)
+ assert_match(/^\W{4}<%= form\.file_field :photos, multiple: true %>/, content)
+ end
+ end
+
def test_scaffold_generator_database
with_secondary_database_configuration do
run_generator ["posts", "--database=secondary"]
diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb
index 26ce487c5f..3e8ce1c018 100644
--- a/railties/test/generators/shared_generator_tests.rb
+++ b/railties/test/generators/shared_generator_tests.rb
@@ -191,10 +191,7 @@ module SharedGeneratorTests
assert_no_match(/fixtures :all/, helper_content)
end
assert_file "#{application_path}/bin/setup" do |setup_content|
- assert_no_match(/db:setup/, setup_content)
- end
- assert_file "#{application_path}/bin/update" do |update_content|
- assert_no_match(/db:migrate/, update_content)
+ assert_no_match(/db:prepare/, setup_content)
end
assert_file ".gitignore" do |content|
assert_no_match(/sqlite/i, content)
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index 3fcfaa9623..fab704944b 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -18,7 +18,9 @@ require "active_support/testing/method_call_assertions"
require "active_support/test_case"
require "minitest/retry"
-Minitest::Retry.use!(verbose: false, retry_count: 1)
+if ENV["BUILDKITE"]
+ Minitest::Retry.use!(verbose: false, retry_count: 1)
+end
RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__)
diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb
index 69f6e34d58..fe5c62c07d 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -879,7 +879,7 @@ YAML
assert Bukkits::Engine.config.bukkits_seeds_loaded
end
- test "jobs are ran inline while loading seeds" do
+ test "jobs are ran inline while loading seeds with async adapter configured" do
app_file "db/seeds.rb", <<-RUBY
Rails.application.config.seed_queue_adapter = ActiveJob::Base.queue_adapter
RUBY
@@ -891,6 +891,19 @@ YAML
assert_instance_of ActiveJob::QueueAdapters::AsyncAdapter, ActiveJob::Base.queue_adapter
end
+ test "jobs are ran with original adapter while loading seeds with custom adapter configured" do
+ app_file "db/seeds.rb", <<-RUBY
+ Rails.application.config.seed_queue_adapter = ActiveJob::Base.queue_adapter
+ RUBY
+
+ boot_rails
+ Rails.application.config.active_job.queue_adapter = :delayed_job
+ Rails.application.load_seed
+
+ assert_instance_of ActiveJob::QueueAdapters::DelayedJobAdapter, Rails.application.config.seed_queue_adapter
+ assert_instance_of ActiveJob::QueueAdapters::DelayedJobAdapter, ActiveJob::Base.queue_adapter
+ end
+
test "skips nonexistent seed data" do
FileUtils.rm "#{app_path}/db/seeds.rb"
boot_rails
diff --git a/tools/test_common.rb b/tools/test_common.rb
new file mode 100644
index 0000000000..9959f55ded
--- /dev/null
+++ b/tools/test_common.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+if ENV["BUILDKITE"]
+ require "minitest/reporters"
+ require "fileutils"
+
+ module Minitest
+ def self.plugin_rails_ci_junit_format_test_report_for_buildkite_init(*)
+ dir = File.join(__dir__, "../test-reports/#{ENV['BUILDKITE_JOB_ID']}")
+ reporter << Minitest::Reporters::JUnitReporter.new(dir, false)
+ FileUtils.mkdir_p(dir)
+ end
+ end
+
+ Minitest.load_plugins
+ Minitest.extensions.unshift "rails_ci_junit_format_test_report_for_buildkite"
+end
diff --git a/yarn.lock b/yarn.lock
index fdf508b9a3..02d1463723 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5692,11 +5692,6 @@ trim-right@^1.0.1:
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
-trix@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/trix/-/trix-1.0.0.tgz#e9cc98cf6030c908f8d54e317b5b072f927b0c6b"
- integrity sha512-feli9QVXe6gzZOCUfpPGpNDURW9jMciIRVQ5gkDudOctcA1oMtI5K/qEbsL2rFCoGl1rSoeRt+HPhIFGyQscKg==
-
tslib@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"