aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionmailer/lib/action_mailer/log_subscriber.rb1
-rw-r--r--actionmailer/lib/action_mailer/mail_helper.rb4
-rw-r--r--actionpack/lib/abstract_controller/base.rb2
-rw-r--r--actionpack/lib/action_controller/metal/live.rb2
-rw-r--r--actionpack/lib/action_controller/test_case.rb1
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb3
-rw-r--r--actionpack/test/controller/new_base/bare_metal_test.rb2
-rw-r--r--actionpack/test/controller/render_test.rb6
-rw-r--r--actionpack/test/controller/send_file_test.rb1
-rw-r--r--actionview/CHANGELOG.md9
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb20
-rw-r--r--actionview/lib/action_view/helpers/debug_helper.rb12
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb10
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb33
-rw-r--r--actionview/test/template/compiled_templates_test.rb9
-rw-r--r--actionview/test/template/debug_helper_test.rb2
-rw-r--r--actionview/test/template/text_helper_test.rb27
-rw-r--r--activemodel/CHANGELOG.md4
-rw-r--r--activemodel/lib/active_model/naming.rb6
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb14
-rw-r--r--activemodel/test/cases/naming_test.rb6
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb1
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb15
-rw-r--r--activerecord/CHANGELOG.md97
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb13
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb34
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb19
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb8
-rw-r--r--activerecord/lib/active_record/attribute.rb73
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb14
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb18
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb30
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb7
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb17
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb24
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb9
-rw-r--r--activerecord/lib/active_record/attribute_set.rb69
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb32
-rw-r--r--activerecord/lib/active_record/attributes.rb13
-rw-r--r--activerecord/lib/active_record/autosave_association.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb115
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb60
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb36
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb13
-rw-r--r--activerecord/lib/active_record/core.rb16
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb23
-rw-r--r--activerecord/lib/active_record/migration.rb4
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb19
-rw-r--r--activerecord/lib/active_record/model_schema.rb20
-rw-r--r--activerecord/lib/active_record/persistence.rb10
-rw-r--r--activerecord/lib/active_record/querying.rb9
-rw-r--r--activerecord/lib/active_record/railties/databases.rake58
-rw-r--r--activerecord/lib/active_record/reflection.rb4
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb31
-rw-r--r--activerecord/lib/active_record/result.rb23
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb40
-rw-r--r--activerecord/lib/active_record/schema_migration.rb4
-rw-r--r--activerecord/lib/active_record/store.rb10
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb6
-rw-r--r--activerecord/lib/active_record/type/binary.rb2
-rw-r--r--activerecord/lib/active_record/type/mutable.rb4
-rw-r--r--activerecord/lib/active_record/type/numeric.rb18
-rw-r--r--activerecord/lib/active_record/type/value.rb64
-rw-r--r--activerecord/lib/active_record/validations.rb18
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb23
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb3
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb3
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb2
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb4
-rw-r--r--activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb26
-rw-r--r--activerecord/test/cases/associations/eager_test.rb29
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb11
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb38
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb37
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb25
-rw-r--r--activerecord/test/cases/attribute_decorators_test.rb12
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb40
-rw-r--r--activerecord/test/cases/attribute_set_test.rb165
-rw-r--r--activerecord/test/cases/attribute_test.rb59
-rw-r--r--activerecord/test/cases/calculations_test.rb5
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb4
-rw-r--r--activerecord/test/cases/defaults_test.rb2
-rw-r--r--activerecord/test/cases/finder_test.rb4
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb14
-rw-r--r--activerecord/test/cases/migration/columns_test.rb17
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb25
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb242
-rw-r--r--activerecord/test/cases/migration_test.rb2
-rw-r--r--activerecord/test/cases/persistence_test.rb7
-rw-r--r--activerecord/test/cases/primary_keys_test.rb12
-rw-r--r--activerecord/test/cases/quoting_test.rb25
-rw-r--r--activerecord/test/cases/reflection_test.rb22
-rw-r--r--activerecord/test/cases/relation_test.rb28
-rw-r--r--activerecord/test/cases/result_test.rb44
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb19
-rw-r--r--activerecord/test/cases/types_test.rb18
-rw-r--r--activerecord/test/cases/validations_test.rb14
-rw-r--r--activerecord/test/fixtures/fk_test_has_pk.yml2
-rw-r--r--activerecord/test/fixtures/posts.yml2
-rw-r--r--activerecord/test/models/club.rb7
-rw-r--r--activerecord/test/models/contact.rb1
-rw-r--r--activerecord/test/models/post.rb2
-rw-r--r--activerecord/test/models/publisher/article.rb1
-rw-r--r--activerecord/test/models/tagging.rb2
-rw-r--r--activerecord/test/schema/schema.rb12
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb6
-rw-r--r--activerecord/test/support/ddl_helper.rb4
-rw-r--r--activesupport/CHANGELOG.md16
-rw-r--r--activesupport/lib/active_support/backtrace_cleaner.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/securerandom.rb8
-rw-r--r--activesupport/lib/active_support/dependencies.rb8
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb2
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb22
-rw-r--r--activesupport/test/caching_test.rb27
-rw-r--r--activesupport/test/core_ext/date_ext_test.rb18
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb10
-rw-r--r--activesupport/test/core_ext/duration_test.rb11
-rw-r--r--activesupport/test/core_ext/numeric_ext_test.rb8
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb12
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb11
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb62
-rw-r--r--activesupport/test/dependencies_test.rb5
-rw-r--r--activesupport/test/inflector_test.rb8
-rw-r--r--activesupport/test/json/decoding_test.rb30
-rw-r--r--activesupport/test/json/encoding_test.rb41
-rw-r--r--activesupport/test/time_zone_test.rb11
-rw-r--r--activesupport/test/time_zone_test_helpers.rb16
-rw-r--r--guides/assets/javascripts/guides.js6
-rw-r--r--guides/source/4_2_release_notes.md81
-rw-r--r--guides/source/action_controller_overview.md57
-rw-r--r--guides/source/active_record_migrations.md142
-rw-r--r--guides/source/asset_pipeline.md8
-rw-r--r--guides/source/command_line.md25
-rw-r--r--guides/source/debugging_rails_applications.md7
-rw-r--r--guides/source/engines.md26
-rw-r--r--guides/source/getting_started.md2
-rw-r--r--guides/source/layouts_and_rendering.md2
-rw-r--r--guides/source/testing.md2
-rw-r--r--guides/source/upgrading_ruby_on_rails.md29
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb2
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/index.html.erb2
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/new.html.erb2
-rw-r--r--railties/lib/rails/generators/rails/controller/controller_generator.rb8
-rw-r--r--railties/lib/rails/generators/testing/assertions.rb2
-rw-r--r--railties/lib/rails/test_unit/sub_test_task.rb4
-rw-r--r--railties/test/application/rake/migrations_test.rb15
-rw-r--r--railties/test/generators/controller_generator_test.rb7
190 files changed, 2457 insertions, 849 deletions
diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb
index eb6fb11d81..e4aaab34b1 100644
--- a/actionmailer/lib/action_mailer/log_subscriber.rb
+++ b/actionmailer/lib/action_mailer/log_subscriber.rb
@@ -21,6 +21,7 @@ module ActionMailer
# An email was generated.
def process(event)
+ return unless logger.debug?
mailer = event.payload[:mailer]
action = event.payload[:action]
debug("\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms")
diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb
index 54ad9f066f..483277af04 100644
--- a/actionmailer/lib/action_mailer/mail_helper.rb
+++ b/actionmailer/lib/action_mailer/mail_helper.rb
@@ -11,8 +11,8 @@ module ActionMailer
}.join("\n\n")
# Make list points stand on their own line
- formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" }
- formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" }
+ formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { " #{$1} #{$2.strip}\n" }
+ formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { " #{$1} #{$2.strip}\n" }
formatted
end
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb
index acdfb33efa..15faabf977 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -255,7 +255,7 @@ module AbstractController
# Checks if the action name is valid and returns false otherwise.
def _valid_action_name?(action_name)
- action_name !~ Regexp.new(File::SEPARATOR)
+ !action_name.to_s.include? File::SEPARATOR
end
end
end
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index 67875141cb..706ce04062 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -205,7 +205,7 @@ module ActionController
end
class Response < ActionDispatch::Response #:nodoc: all
- class Header < DelegateClass(Hash)
+ class Header < DelegateClass(Hash) # :nodoc:
def initialize(response, header)
@response = response
super(header)
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 849286a4a9..b117170514 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -593,7 +593,6 @@ module ActionController
unless @controller.respond_to?(:recycle!)
@controller.extend(Testing::Functional)
- @controller.class.class_eval { include Testing }
end
@request.recycle!
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index a5858758c6..3997c6ee98 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -36,13 +36,13 @@ module ActionDispatch
path = options[:script_name].to_s.chomp("/")
path << options[:path].to_s
- add_trailing_slash(path) if options[:trailing_slash]
+ path = add_trailing_slash(path) if options[:trailing_slash]
- result = path
-
- unless options[:only_path]
- result.prepend build_host_url(options)
- end
+ result = if options[:only_path]
+ path
+ else
+ build_host_url(options).concat path
+ end
if options.key? :params
params = options[:params].is_a?(Hash) ?
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index fe3fc0a9fa..21817b374c 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -105,7 +105,8 @@ module ActionDispatch
routes.concat get_routes_as_head(routes)
end
- routes.sort_by!(&:precedence).select! { |r| r.matches?(req) }
+ routes.select! { |r| r.matches?(req) }
+ routes.sort_by!(&:precedence)
routes.map! { |r|
match_data = r.path.match(req.path_info)
diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb
index 2ddc07ef72..246ba099af 100644
--- a/actionpack/test/controller/new_base/bare_metal_test.rb
+++ b/actionpack/test/controller/new_base/bare_metal_test.rb
@@ -2,6 +2,8 @@ require "abstract_unit"
module BareMetalTest
class BareController < ActionController::Metal
+ include ActionController::RackDelegation
+
def index
self.response_body = "Hello world"
end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index 26806fb03f..9926130c02 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -242,6 +242,8 @@ class MetalTestController < ActionController::Metal
include AbstractController::Rendering
include ActionView::Rendering
include ActionController::Rendering
+ include ActionController::RackDelegation
+
def accessing_logger_in_template
render :inline => "<%= logger.class %>"
@@ -387,10 +389,6 @@ end
class EtagRenderTest < ActionController::TestCase
tests TestControllerWithExtraEtags
- def setup
- super
- end
-
def test_multiple_etags
@request.if_none_match = etag(["123", 'ab', :cde, [:f]])
get :fresh
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
index 73ed33ce2d..c002cf4d8f 100644
--- a/actionpack/test/controller/send_file_test.rb
+++ b/actionpack/test/controller/send_file_test.rb
@@ -9,6 +9,7 @@ end
class SendFileController < ActionController::Base
include TestFileUtils
+ include ActionController::Testing
layout "layouts/standard" # to make sure layouts don't interfere
attr_writer :options
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index d825d3b627..03ac155848 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,12 @@
+* The `highlight` helper now accepts a block to be used instead of the `highlighter`
+ option.
+
+ *Lucas Mazza*
+
+* The `except` and `highlight` helpers now accept regular expressions.
+
+ *Jan Szumiec*
+
* Flatten the array parameter in `safe_join`, so it behaves consistently with
`Array#join`.
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
index 006b15be91..4a682ce4e2 100644
--- a/actionview/lib/action_view/helpers/asset_url_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -7,10 +7,10 @@ module ActionView
# urls.
#
# image_path("rails.png")
- # # => "/images/rails.png"
+ # # => "/assets/rails.png"
#
# image_url("rails.png")
- # # => "http://www.example.com/images/rails.png"
+ # # => "http://www.example.com/assets/rails.png"
#
# === Using asset hosts
#
@@ -155,7 +155,7 @@ module ActionView
#
# All other options provided are forwarded to +asset_path+ call.
#
- # asset_url "application.js" # => http://example.com/application.js
+ # asset_url "application.js" # => http://example.com/assets/application.js
# asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js
#
def asset_url(source, options = {})
@@ -236,8 +236,8 @@ module ActionView
# Full paths from the document root will be passed through.
# Used internally by +javascript_include_tag+ to build the script path.
#
- # javascript_path "xmlhr" # => /javascripts/xmlhr.js
- # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js
+ # javascript_path "xmlhr" # => /assets/xmlhr.js
+ # javascript_path "dir/xmlhr.js" # => /assets/dir/xmlhr.js
# javascript_path "/dir/xmlhr" # => /dir/xmlhr.js
# javascript_path "http://www.example.com/js/xmlhr" # => http://www.example.com/js/xmlhr
# javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
@@ -258,8 +258,8 @@ module ActionView
# Full paths from the document root will be passed through.
# Used internally by +stylesheet_link_tag+ to build the stylesheet path.
#
- # stylesheet_path "style" # => /stylesheets/style.css
- # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css
+ # stylesheet_path "style" # => /assets/style.css
+ # stylesheet_path "dir/style.css" # => /assets/dir/style.css
# stylesheet_path "/dir/style.css" # => /dir/style.css
# stylesheet_path "http://www.example.com/css/style" # => http://www.example.com/css/style
# stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css
@@ -279,9 +279,9 @@ module ActionView
# Full paths from the document root will be passed through.
# Used internally by +image_tag+ to build the image path:
#
- # image_path("edit") # => "/images/edit"
- # image_path("edit.png") # => "/images/edit.png"
- # image_path("icons/edit.png") # => "/images/icons/edit.png"
+ # image_path("edit") # => "/assets/edit"
+ # image_path("edit.png") # => "/assets/edit.png"
+ # image_path("icons/edit.png") # => "/assets/icons/edit.png"
# image_path("/icons/edit.png") # => "/icons/edit.png"
# image_path("http://www.example.com/img/edit.png") # => "http://www.example.com/img/edit.png"
#
diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb
index 16cddec339..ba47eee9ba 100644
--- a/actionview/lib/action_view/helpers/debug_helper.rb
+++ b/actionview/lib/action_view/helpers/debug_helper.rb
@@ -16,15 +16,15 @@ module ActionView
# # =>
# <pre class='debug_dump'>--- !ruby/object:User
# attributes:
- # &nbsp; updated_at:
- # &nbsp; username: testing
- # &nbsp; age: 42
- # &nbsp; password: xyz
- # &nbsp; created_at:
+ # updated_at:
+ # username: testing
+ # age: 42
+ # password: xyz
+ # created_at:
# </pre>
def debug(object)
Marshal::dump(object)
- object = ERB::Util.html_escape(object.to_yaml).gsub(" ", "&nbsp; ").html_safe
+ object = ERB::Util.html_escape(object.to_yaml)
content_tag(:pre, object, :class => "debug_dump")
rescue Exception # errors from Marshal or YAML
# Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index 9c0c43d096..b18f578183 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -508,19 +508,19 @@ module ActionView
#
# ==== Examples
# image_submit_tag("login.png")
- # # => <input alt="Login" src="/images/login.png" type="image" />
+ # # => <input alt="Login" src="/assets/login.png" type="image" />
#
# image_submit_tag("purchase.png", disabled: true)
- # # => <input alt="Purchase" disabled="disabled" src="/images/purchase.png" type="image" />
+ # # => <input alt="Purchase" disabled="disabled" src="/assets/purchase.png" type="image" />
#
# image_submit_tag("search.png", class: 'search_button', alt: 'Find')
- # # => <input alt="Find" class="search_button" src="/images/search.png" type="image" />
+ # # => <input alt="Find" class="search_button" src="/assets/search.png" type="image" />
#
# image_submit_tag("agree.png", disabled: true, class: "agree_disagree_button")
- # # => <input alt="Agree" class="agree_disagree_button" disabled="disabled" src="/images/agree.png" type="image" />
+ # # => <input alt="Agree" class="agree_disagree_button" disabled="disabled" src="/assets/agree.png" type="image" />
#
# image_submit_tag("save.png", data: { confirm: "Are you sure?" })
- # # => <input alt="Save" src="/images/save.png" data-confirm="Are you sure?" type="image" />
+ # # => <input alt="Save" src="/assets/save.png" data-confirm="Are you sure?" type="image" />
def image_submit_tag(source, options = {})
options = options.stringify_keys
tag :input, { "alt" => image_alt(source), "type" => "image", "src" => path_to_image(source) }.update(options)
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
index 7cfbca5b6f..b859653bc9 100644
--- a/actionview/lib/action_view/helpers/text_helper.rb
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -103,11 +103,14 @@ module ActionView
# Highlights one or more +phrases+ everywhere in +text+ by inserting it into
# a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
# as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to
- # '<mark>\1</mark>')
+ # '<mark>\1</mark>') or passing a block that receives each matched term.
#
# highlight('You searched for: rails', 'rails')
# # => You searched for: <mark>rails</mark>
#
+ # highlight('You searched for: rails', /for|rails/)
+ # # => You searched <mark>for</mark>: <mark>rails</mark>
+ #
# highlight('You searched for: ruby, rails, dhh', 'actionpack')
# # => You searched for: ruby, rails, dhh
#
@@ -116,15 +119,25 @@ module ActionView
#
# highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>')
# # => You searched for: <a href="search?q=rails">rails</a>
+ #
+ # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) }
+ # # => You searched for: <a href="search?q=rails">rails</a>
def highlight(text, phrases, options = {})
text = sanitize(text) if options.fetch(:sanitize, true)
if text.blank? || phrases.blank?
text
else
- highlighter = options.fetch(:highlighter, '<mark>\1</mark>')
- match = Array(phrases).map { |p| Regexp.escape(p) }.join('|')
- text.gsub(/(#{match})(?![^<]*?>)/i, highlighter)
+ match = Array(phrases).map do |p|
+ Regexp === p ? p.to_s : Regexp.escape(p)
+ end.join('|')
+
+ if block_given?
+ text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found }
+ else
+ highlighter = options.fetch(:highlighter, '<mark>\1</mark>')
+ text.gsub(/(#{match})(?![^<]*?>)/i, highlighter)
+ end
end.html_safe
end
@@ -155,9 +168,13 @@ module ActionView
def excerpt(text, phrase, options = {})
return unless text && phrase
- separator = options[:separator] || ''
- phrase = Regexp.escape(phrase)
- regex = /#{phrase}/i
+ separator = options.fetch(:separator, nil) || ""
+ case phrase
+ when Regexp
+ regex = phrase
+ else
+ regex = /#{Regexp.escape(phrase)}/i
+ end
return unless matches = text.match(regex)
phrase = matches[0]
@@ -171,7 +188,7 @@ module ActionView
end
end
- first_part, second_part = text.split(regex, 2)
+ first_part, second_part = text.split(phrase, 2)
prefix, first_part = cut_excerpt_part(:first, first_part, separator, options)
postfix, second_part = cut_excerpt_part(:second, second_part, separator, options)
diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb
index 2336321f3e..b84aca6746 100644
--- a/actionview/test/template/compiled_templates_test.rb
+++ b/actionview/test/template/compiled_templates_test.rb
@@ -1,15 +1,8 @@
require 'abstract_unit'
class CompiledTemplatesTest < ActiveSupport::TestCase
- def setup
- # Clean up any details key cached to expose failures
- # that otherwise would appear just on isolated tests
+ teardown do
ActionView::LookupContext::DetailsKey.clear
-
- @compiled_templates = ActionView::CompiledTemplates
- @compiled_templates.instance_methods.each do |m|
- @compiled_templates.send(:remove_method, m) if m =~ /^_render_template_/
- end
end
def test_template_gets_recompiled_when_using_different_keys_in_local_assigns
diff --git a/actionview/test/template/debug_helper_test.rb b/actionview/test/template/debug_helper_test.rb
index 42d06bd9ff..5609694cd5 100644
--- a/actionview/test/template/debug_helper_test.rb
+++ b/actionview/test/template/debug_helper_test.rb
@@ -3,6 +3,6 @@ require 'active_record_unit'
class DebugHelperTest < ActionView::TestCase
def test_debug
company = Company.new(name: "firebase")
- assert_match "&nbsp; name: firebase", debug(company)
+ assert_match "name: firebase", debug(company)
end
end
diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb
index a514bba83d..db416a8de4 100644
--- a/actionview/test/template/text_helper_test.rb
+++ b/actionview/test/template/text_helper_test.rb
@@ -222,6 +222,11 @@ class TextHelperTest < ActionView::TestCase
)
end
+ def test_highlight_accepts_regexp
+ assert_equal("This day was challenging for judge <mark>Allen</mark> and his colleagues.",
+ highlight("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i))
+ end
+
def test_highlight_with_multiple_phrases_in_one_pass
assert_equal %(<em>wow</em> <em>em</em>), highlight('wow em', %w(wow em), :highlighter => '<em>\1</em>')
end
@@ -260,6 +265,13 @@ class TextHelperTest < ActionView::TestCase
assert_equal options, passed_options
end
+ def test_highlight_with_block
+ assert_equal(
+ "<b>one</b> <b>two</b> <b>three</b>",
+ highlight("one two three", ["one", "two", "three"]) { |word| "<b>#{word}</b>" }
+ )
+ end
+
def test_excerpt
assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", :radius => 5))
assert_equal("This is a...", excerpt("This is a beautiful morning", "this", :radius => 5))
@@ -267,6 +279,16 @@ class TextHelperTest < ActionView::TestCase
assert_nil excerpt("This is a beautiful morning", "day")
end
+ def test_excerpt_with_regex
+ assert_equal('...is a beautiful! mor...', excerpt('This is a beautiful! morning', 'beautiful', :radius => 5))
+ assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', 'beautiful', :radius => 5))
+ assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', /\bbeau\w*\b/i, :radius => 5))
+ assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', /\b(beau\w*)\b/i, :radius => 5))
+ assert_equal("...udge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, :radius => 5))
+ assert_equal("...judge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, :radius => 1, :separator => ' '))
+ assert_equal("...was challenging for...", excerpt("This day was challenging for judge Allen and his colleagues.", /\b(\w*allen\w*)\b/i, :radius => 5))
+ end
+
def test_excerpt_should_not_be_html_safe
assert !excerpt('This is a beautiful! morning', 'beautiful', :radius => 5).html_safe?
end
@@ -288,11 +310,6 @@ class TextHelperTest < ActionView::TestCase
assert_equal("...abc...", excerpt("z abc d", "b", :radius => 1))
end
- def test_excerpt_with_regex
- assert_equal('...is a beautiful! mor...', excerpt('This is a beautiful! morning', 'beautiful', :radius => 5))
- assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', 'beautiful', :radius => 5))
- end
-
def test_excerpt_with_omission
assert_equal("[...]is a beautiful morn[...]", excerpt("This is a beautiful morning", "beautiful", :omission => "[...]",:radius => 5))
assert_equal(
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 4d9186017f..890e99415f 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Allow proc and symbol as values for `only_integer` of `NumericalityValidator`
+
+ *Robin Mehner*
+
* `has_secure_password` now verifies that the given password is less than 72
characters if validations are enabled.
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 5219de2606..8cf1a191f4 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -214,6 +214,12 @@ module ActiveModel
# is required to pass the Active Model Lint test. So either extending the
# provided method below, or rolling your own is required.
module Naming
+ def self.extended(base) #:nodoc:
+ base.class_eval do
+ delegate :model_name, to: :class
+ end
+ end
+
# Returns an ActiveModel::Name object for module. It can be
# used to retrieve all kinds of naming-related information
# (See ActiveModel::Name for more information).
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index a9fb9804d4..5bd162433d 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -30,7 +30,7 @@ module ActiveModel
return
end
- if options[:only_integer]
+ if allow_only_integer?(record)
unless value = parse_raw_value_as_an_integer(raw_value)
record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value))
return
@@ -75,6 +75,17 @@ module ActiveModel
filtered[:value] = value
filtered
end
+
+ def allow_only_integer?(record)
+ case options[:only_integer]
+ when Symbol
+ record.send(options[:only_integer])
+ when Proc
+ options[:only_integer].call(record)
+ else
+ options[:only_integer]
+ end
+ end
end
module HelperMethods
@@ -121,6 +132,7 @@ module ActiveModel
# * <tt>:equal_to</tt>
# * <tt>:less_than</tt>
# * <tt>:less_than_or_equal_to</tt>
+ # * <tt>:only_integer</tt>
#
# For example:
#
diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb
index aa683f4152..7b8287edbf 100644
--- a/activemodel/test/cases/naming_test.rb
+++ b/activemodel/test/cases/naming_test.rb
@@ -272,3 +272,9 @@ class NameWithAnonymousClassTest < ActiveModel::TestCase
assert_equal "Anonymous", model_name
end
end
+
+class NamingMethodDelegationTest < ActiveModel::TestCase
+ def test_model_name
+ assert_equal Blog::Post.model_name, Blog::Post.new.model_name
+ end
+end
diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb
index 60414a6570..734656b749 100644
--- a/activemodel/test/cases/serializers/json_serialization_test.rb
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -1,6 +1,5 @@
require 'cases/helper'
require 'models/contact'
-require 'models/automobile'
require 'active_support/core_ext/object/instance_variables'
class Contact
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index e1657407cf..3834d327ea 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -50,6 +50,21 @@ class NumericalityValidationTest < ActiveModel::TestCase
valid!(NIL + INTEGERS)
end
+ def test_validates_numericality_of_with_integer_only_and_symbol_as_value
+ Topic.validates_numericality_of :approved, only_integer: :condition_is_true_but_its_not
+
+ invalid!(NIL + BLANK + JUNK)
+ valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ end
+
+ def test_validates_numericality_of_with_integer_only_and_proc_as_value
+ Topic.send(:define_method, :allow_only_integers?, lambda { false })
+ Topic.validates_numericality_of :approved, only_integer: Proc.new {|topic| topic.allow_only_integers? }
+
+ invalid!(NIL + BLANK + JUNK)
+ valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ end
+
def test_validates_numericality_with_greater_than
Topic.validates_numericality_of :approved, greater_than: 10
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 823597fc92..4af510603e 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,94 @@
+* Detect in-place modifications on String attributes.
+
+ Before this change user have to mark the attribute as changed to it be persisted
+ in the database. Now it is not required anymore.
+
+ Before:
+
+ user = User.first
+ user.name << ' Griffin'
+ user.name_will_change!
+ user.save
+ user.reload.name # => "Sean Griffin"
+
+ After:
+
+ user = User.first
+ user.name << ' Griffin'
+ user.save
+ user.reload.name # => "Sean Griffin"
+
+ *Sean Griffin*
+
+* Add `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the record
+ is invalid.
+
+ *Bogdan Gusiev*, *Marc Schütz*
+
+* Support for adding and removing foreign keys. Foreign keys are now
+ a part of `schema.rb`. This is supported by Mysql2Adapter, MysqlAdapter
+ and PostgreSQLAdapter.
+
+ Many thanks to *Matthew Higgins* for laying the foundation with his work on
+ [foreigner](https://github.com/matthuhiggins/foreigner).
+
+ Example:
+
+ # within your migrations:
+ add_foreign_key :articles, :authors
+ remove_foreign_key :articles, :authors
+
+ *Yves Senn*
+
+* Fix subtle bugs regarding attribute assignment on models with no primary
+ key. `'id'` will no longer be part of the attributes hash.
+
+ *Sean Griffin*
+
+* Deprecate automatic counter caches on `has_many :through`. The behavior was
+ broken and inconsistent.
+
+ *Sean Griffin*
+
+* `preload` preserves readonly flag for associations.
+
+ See #15853.
+
+ *Yves Senn*
+
+* Assume numeric types have changed if they were assigned to a value that
+ would fail numericality validation, regardless of the old value. Previously
+ this would only occur if the old value was 0.
+
+ Example:
+
+ model = Model.create!(number: 5)
+ model.number = '5wibble'
+ model.number_changed? # => true
+
+ Fixes #14731.
+
+ *Sean Griffin*
+
+* `reload` no longer merges with the existing attributes.
+ The attribute hash is fully replaced. The record is put into the same state
+ as it would be with `Model.find(model.id)`.
+
+ *Sean Griffin*
+
+* The object returned from `select_all` must respond to `column_types`.
+ If this is not the case a `NoMethodError` is raised.
+
+ *Sean Griffin*
+
+* `has_many :through` associations will no longer save the through record
+ twice when added in an `after_create` callback defined before the
+ associations.
+
+ Fixes #3798.
+
+ *Sean Griffin*
+
* Detect in-place modifications of PG array types
*Sean Griffin*
@@ -6,8 +97,7 @@
*Yves Senn*
-* Deprecate `serialized_attributes` without replacement. You can access its
- behavior by going through the column's type object.
+* Deprecate `serialized_attributes` without replacement.
*Sean Griffin*
@@ -98,7 +188,8 @@
*Lauro Caetano*, *Carlos Antonio da Silva*
-* Return a null column from `column_for_attribute` when no column exists.
+* Deprecate returning `nil` from `column_for_attribute` when no column exists.
+ It will return a null object in Rails 5.0
*Sean Griffin*
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 53fa132219..17b00bbaea 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -27,6 +27,7 @@ require 'active_model'
require 'arel'
require 'active_record/version'
+require 'active_record/attribute_set'
module ActiveRecord
extend ActiveSupport::Autoload
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index 0ad5206980..34a555dfd4 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -15,7 +15,10 @@ module ActiveRecord::Associations::Builder
end
private
- def klass; @rhs_class_name.constantize; end
+
+ def klass
+ @lhs_class.send(:compute_type, @rhs_class_name)
+ end
end
def self.build(lhs_class, name, options)
@@ -23,13 +26,7 @@ module ActiveRecord::Associations::Builder
KnownTable.new options[:join_table].to_s
else
class_name = options.fetch(:class_name) {
- model_name = name.to_s.camelize.singularize
-
- if lhs_class.parent_name
- model_name.prepend("#{lhs_class.parent_name}::")
- end
-
- model_name
+ name.to_s.camelize.singularize
}
KnownClass.new lhs_class, class_name
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 306588ac66..065a2cff01 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -55,9 +55,9 @@ module ActiveRecord
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
- pk_column = reflection.primary_key_column
+ pk_type = reflection.primary_key_type
ids = Array(ids).reject { |id| id.blank? }
- ids.map! { |i| pk_column.type_cast_from_user(i) }
+ ids.map! { |i| pk_type.type_cast_from_user(i) }
replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 2727e23870..453615ba87 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -80,9 +80,20 @@ module ActiveRecord
end
def update_counter(difference, reflection = reflection())
+ update_counter_in_database(difference, reflection)
+ update_counter_in_memory(difference, reflection)
+ end
+
+ def update_counter_in_database(difference, reflection = reflection())
if has_cached_counter?(reflection)
counter = cached_counter_attribute_name(reflection)
owner.class.update_counters(owner.id, counter => difference)
+ end
+ end
+
+ def update_counter_in_memory(difference, reflection = reflection())
+ if has_cached_counter?(reflection)
+ counter = cached_counter_attribute_name(reflection)
owner[counter] += difference
owner.changed_attributes.delete(counter) # eww
end
@@ -100,6 +111,10 @@ module ActiveRecord
# Hence this method.
def inverse_updates_counter_cache?(reflection = reflection())
counter_name = cached_counter_attribute_name(reflection)
+ inverse_updates_counter_named?(counter_name, reflection)
+ end
+
+ def inverse_updates_counter_named?(counter_name, reflection = reflection())
reflection.klass._reflections.values.any? { |inverse_reflection|
:belongs_to == inverse_reflection.macro &&
inverse_reflection.counter_cache_column == counter_name
@@ -137,6 +152,25 @@ module ActiveRecord
false
end
end
+
+ def concat_records(records, *)
+ update_counter_if_success(super, records.length)
+ end
+
+ def _create_record(attributes, *)
+ if attributes.is_a?(Array)
+ super
+ else
+ update_counter_if_success(super, 1)
+ end
+ end
+
+ def update_counter_if_success(saved_successfully, difference)
+ if saved_successfully
+ update_counter_in_memory(difference)
+ end
+ saved_successfully
+ 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 175019a72b..007e3bc555 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -63,7 +63,15 @@ module ActiveRecord
end
save_through_record(record)
- update_counter(1)
+ if has_cached_counter? && !through_reflection_updates_counter_cache?
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ Automatic updating of counter caches on through associations has been
+ deprecated, and will be removed in Rails 5.0. Instead, please set the
+ appropriate counter_cache options on the has_many and belongs_to for
+ your associations to #{through_reflection.name}.
+ MESSAGE
+ update_counter_in_database(1)
+ end
record
end
@@ -93,7 +101,9 @@ module ActiveRecord
end
def through_scope_attributes
- scope.where_values_hash(through_association.reflection.name.to_s).except!(through_association.reflection.foreign_key)
+ scope.where_values_hash(through_association.reflection.name.to_s).
+ except!(through_association.reflection.foreign_key,
+ through_association.reflection.klass.inheritance_column)
end
def save_through_record(record)
@@ -216,6 +226,11 @@ module ActiveRecord
def invertible_for?(record)
false
end
+
+ def through_reflection_updates_counter_cache?
+ counter_name = cached_counter_attribute_name
+ inverse_updates_counter_named?(counter_name, through_reflection)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index fbb4551b22..ec5c189cd3 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -131,7 +131,6 @@ module ActiveRecord
def instantiate(result_set, aliases)
primary_key = aliases.column_alias(join_root, join_root.primary_key)
- type_caster = result_set.column_type primary_key
seen = Hash.new { |h,parent_klass|
h[parent_klass] = Hash.new { |i,parent_id|
@@ -144,8 +143,7 @@ module ActiveRecord
column_aliases = aliases.column_aliases join_root
result_set.each { |row_hash|
- primary_id = type_caster.type_cast_from_database row_hash[primary_key]
- parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases)
+ parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases)
construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
}
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 33c8619359..c0639742be 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -104,11 +104,11 @@ module ActiveRecord
end
def association_key_type
- @klass.column_for_attribute(association_key_name).type
+ @klass.type_for_attribute(association_key_name.to_s).type
end
def owner_key_type
- @model.column_for_attribute(owner_key_name).type
+ @model.type_for_attribute(owner_key_name.to_s).type
end
def load_slices(slices)
@@ -151,6 +151,10 @@ module ActiveRecord
end
end
+ if preload_values[:readonly] || values[:readonly]
+ scope.readonly!
+ end
+
if options[:as]
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
index 8604ccb90d..6d38224830 100644
--- a/activerecord/lib/active_record/attribute.rb
+++ b/activerecord/lib/active_record/attribute.rb
@@ -1,20 +1,29 @@
module ActiveRecord
class Attribute # :nodoc:
class << self
- def from_database(value, type)
- FromDatabase.new(value, type)
+ def from_database(name, value, type)
+ FromDatabase.new(name, value, type)
end
- def from_user(value, type)
- FromUser.new(value, type)
+ def from_user(name, value, type)
+ FromUser.new(name, value, type)
+ end
+
+ def null(name)
+ Null.new(name)
+ end
+
+ def uninitialized(name, type)
+ Uninitialized.new(name, type)
end
end
- attr_reader :value_before_type_cast, :type
+ attr_reader :name, :value_before_type_cast, :type
# This method should not be called directly.
# Use #from_database or #from_user
- def initialize(value_before_type_cast, type)
+ def initialize(name, value_before_type_cast, type)
+ @name = name
@value_before_type_cast = value_before_type_cast
@type = type
end
@@ -37,10 +46,22 @@ module ActiveRecord
type.changed_in_place?(old_value, value)
end
+ def with_value_from_user(value)
+ self.class.from_user(name, value, type)
+ end
+
+ def with_value_from_database(value)
+ self.class.from_database(name, value, type)
+ end
+
def type_cast
raise NotImplementedError
end
+ def initialized?
+ true
+ end
+
protected
def initialize_dup(other)
@@ -49,27 +70,51 @@ module ActiveRecord
end
end
- class FromDatabase < Attribute
+ class FromDatabase < Attribute # :nodoc:
def type_cast(value)
type.type_cast_from_database(value)
end
end
- class FromUser < Attribute
+ class FromUser < Attribute # :nodoc:
def type_cast(value)
type.type_cast_from_user(value)
end
end
- class Null
- class << self
- attr_reader :value, :value_before_type_cast, :value_for_database
+ class Null < Attribute # :nodoc:
+ def initialize(name)
+ super(name, nil, Type::Value.new)
+ end
+
+ def value
+ nil
+ end
+
+ def with_value_from_database(value)
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
+ end
+ alias_method :with_value_from_user, :with_value_from_database
+ end
+
+ class Uninitialized < Attribute # :nodoc:
+ def initialize(name, type)
+ super(name, nil, type)
+ end
- def changed_from?(*)
- false
+ def value
+ if block_given?
+ yield name
end
- alias changed_in_place_from? changed_from?
+ end
+
+ def value_for_database
+ end
+
+ def initialized?
+ false
end
end
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized
end
end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 40e2918777..2887db3bf7 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -115,7 +115,7 @@ module ActiveRecord
end
class MultiparameterAttribute #:nodoc:
- attr_reader :object, :name, :values, :column
+ attr_reader :object, :name, :values, :cast_type
def initialize(object, name, values)
@object = object
@@ -126,22 +126,22 @@ module ActiveRecord
def read_value
return if values.values.compact.empty?
- @column = object.column_for_attribute(name)
- klass = column ? column.klass : nil
+ @cast_type = object.type_for_attribute(name)
+ klass = cast_type.klass
if klass == Time
read_time
elsif klass == Date
read_date
else
- read_other(klass)
+ read_other
end
end
private
def instantiate_time_object(set_values)
- if object.class.send(:create_time_zone_conversion_attribute?, name, column)
+ if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type)
Time.zone.local(*set_values)
else
Time.send(object.class.default_timezone, *set_values)
@@ -151,7 +151,7 @@ module ActiveRecord
def read_time
# If column is a :time (and not :date or :datetime) there is no need to validate if
# there are year/month/day fields
- if column.type == :time
+ if cast_type.type == :time
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
{ 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
values[key] ||= value
@@ -181,7 +181,7 @@ module ActiveRecord
end
end
- def read_other(klass)
+ def read_other
max_position = extract_max_param
positions = (1..max_position)
validate_required_parameters!(positions)
diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
index 92627f8d5d..5b96623b6e 100644
--- a/activerecord/lib/active_record/attribute_decorators.rb
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -7,7 +7,7 @@ module ActiveRecord
self.attribute_type_decorations = TypeDecorator.new
end
- module ClassMethods
+ module ClassMethods # :nodoc:
def decorate_attribute_type(column_name, decorator_name, &block)
matcher = ->(name, _) { name == column_name.to_s }
key = "_#{column_name}_#{decorator_name}"
@@ -26,13 +26,13 @@ module ActiveRecord
def add_user_provided_columns(*)
super.map do |column|
- decorated_type = attribute_type_decorations.apply(self, column.name, column.cast_type)
+ decorated_type = attribute_type_decorations.apply(column.name, column.cast_type)
column.with_type(decorated_type)
end
end
end
- class TypeDecorator
+ class TypeDecorator # :nodoc:
delegate :clear, to: :@decorations
def initialize(decorations = {})
@@ -43,8 +43,8 @@ module ActiveRecord
TypeDecorator.new(@decorations.merge(*args))
end
- def apply(context, name, type)
- decorations = decorators_for(context, name, type)
+ def apply(name, type)
+ decorations = decorators_for(name, type)
decorations.inject(type) do |new_type, block|
block.call(new_type)
end
@@ -52,13 +52,13 @@ module ActiveRecord
private
- def decorators_for(context, name, type)
- matching(context, name, type).map(&:last)
+ def decorators_for(name, type)
+ matching(name, type).map(&:last)
end
- def matching(context, name, type)
+ def matching(name, type)
@decorations.values.select do |(matcher, _)|
- context.instance_exec(name, type, &matcher)
+ matcher.call(name, type)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index b4d75d6556..51f6a009db 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -186,8 +186,7 @@ module ActiveRecord
end
# Returns the column object for the named attribute.
- # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the
- # named attribute does not exist.
+ # Returns nil if the named attribute does not exist.
#
# class Person < ActiveRecord::Base
# end
@@ -197,12 +196,17 @@ module ActiveRecord
# # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
#
# person.column_for_attribute(:nothing)
- # # => #<ActiveRecord::ConnectionAdapters::NullColumn:0xXXX @name=nil, @sql_type=nil, @cast_type=#<Type::Value>, ...>
+ # # => nil
def column_for_attribute(name)
- name = name.to_s
- columns_hash.fetch(name) do
- ConnectionAdapters::NullColumn.new(name)
+ column = columns_hash[name.to_s]
+ if column.nil?
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ `column_for_attribute` will return a null object for non-existent columns
+ in Rails 5.0. If you would like to continue to receive `nil`, you should
+ instead call `model.class.columns_hash[name]`
+ MESSAGE
end
+ column
end
end
@@ -230,7 +234,7 @@ module ActiveRecord
# For queries selecting a subset of columns, return false for unselected columns.
# We check defined?(@attributes) not to issue warnings if called on objects that
# have been allocated but not yet initialized.
- if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name)
+ if defined?(@attributes) && self.class.column_names.include?(name)
return has_attribute?(name)
end
@@ -247,7 +251,7 @@ module ActiveRecord
# person.has_attribute?('age') # => true
# person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
- @attributes.has_key?(attr_name.to_s)
+ @attributes.include?(attr_name.to_s)
end
# Returns an array of names for the attributes available on this object.
@@ -271,9 +275,7 @@ module ActiveRecord
# person.attributes
# # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
def attributes
- attribute_names.each_with_object({}) { |name, attrs|
- attrs[name] = read_attribute(name)
- }
+ @attributes.to_hash
end
# Returns an <tt>#inspect</tt>-like string for the value of the
@@ -367,12 +369,6 @@ module ActiveRecord
protected
- def clone_attributes # :nodoc:
- @attributes.each_with_object({}) do |(name, attr), h|
- h[name] = attr.dup
- end
- end
-
def clone_attribute_value(reader_method, attribute_name) # :nodoc:
value = send(reader_method, attribute_name)
value.duplicable? ? value.clone : value
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 d057f0941a..fd61febd57 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -43,9 +43,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)
- if attr = @attributes[attr_name.to_s]
- attr.value_before_type_cast
- end
+ @attributes[attr_name.to_s].value_before_type_cast
end
# Returns a hash of attributes before typecasting and deserialization.
@@ -59,7 +57,7 @@ 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
- @attributes.each_with_object({}) { |(k, v), h| h[k] = v.value_before_type_cast }
+ @attributes.values_before_type_cast
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index ca71834641..e1a86fd3aa 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -129,7 +129,7 @@ module ActiveRecord
end
def _field_changed?(attr, old_value)
- attribute_named(attr).changed_from?(old_value)
+ @attributes[attr].changed_from?(old_value)
end
def changed_in_place
@@ -140,7 +140,7 @@ module ActiveRecord
def changed_in_place?(attr_name)
old_value = original_raw_attribute(attr_name)
- attribute_named(attr_name).changed_in_place_from?(old_value)
+ @attributes[attr_name].changed_in_place_from?(old_value)
end
def original_raw_attribute(attr_name)
@@ -154,7 +154,7 @@ module ActiveRecord
end
def store_original_raw_attribute(attr_name)
- original_raw_attributes[attr_name] = attribute_named(attr_name).value_for_database
+ original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database
end
def store_original_raw_attributes
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 1c81a5b71b..cadad60ddd 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -83,12 +83,9 @@ module ActiveRecord
end
def get_primary_key(base_name) #:nodoc:
- return 'id' if base_name.blank?
-
- case primary_key_prefix_type
- when :table_name
+ if base_name && primary_key_prefix_type == :table_name
base_name.foreign_key(false)
- when :table_name_with_underscore
+ elsif base_name && primary_key_prefix_type == :table_name_with_underscore
base_name.foreign_key
else
if ActiveRecord::Base != self && table_exists?
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 525c46970a..10869dfc1e 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -81,17 +81,10 @@ module ActiveRecord
# Returns the value of the attribute identified by <tt>attr_name</tt> after
# it has been typecast (for example, "2004-12-12" in a date column is cast
# to a date object, like Date.new(2004, 12, 12)).
- def read_attribute(attr_name)
+ def read_attribute(attr_name, &block)
name = attr_name.to_s
- @attributes.fetch(name) {
- if name == 'id'
- return read_attribute(self.class.primary_key)
- elsif block_given? && self.class.columns_hash.key?(name)
- return yield(name)
- else
- return nil
- end
- }.value
+ name = self.class.primary_key if name == 'id'
+ @attributes.fetch_value(name, &block)
end
private
@@ -99,10 +92,6 @@ module ActiveRecord
def attribute(attribute_name)
read_attribute(attribute_name)
end
-
- def attribute_named(attribute_name)
- @attributes.fetch(attribute_name, Attribute::Null)
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 188d5d2aab..f439bd1ffe 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -33,19 +33,29 @@ module ActiveRecord
class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false
self.skip_time_zone_conversion_for_attributes = []
-
- matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
- decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type|
- TimeZoneConverter.new(type)
- end
end
module ClassMethods
private
- def create_time_zone_conversion_attribute?(name, column)
+
+ def inherited(subclass)
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or
+ # `skip_time_zone_conversion_for_attributes` would not be picked up.
+ subclass.class_eval do
+ matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
+ decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type|
+ TimeZoneConverter.new(type)
+ end
+ end
+ super
+ end
+
+ def create_time_zone_conversion_attribute?(name, cast_type)
time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- (:datetime == column.type)
+ (:datetime == cast_type.type)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 246a2cd8ba..b3c8209a74 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -69,16 +69,11 @@ module ActiveRecord
def write_attribute_with_type_cast(attr_name, value, should_type_cast)
attr_name = attr_name.to_s
attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
- type = type_for_attribute(attr_name)
-
- unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name)
- raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
- end
if should_type_cast
- @attributes[attr_name] = Attribute.from_user(value, type)
+ @attributes.write_from_user(attr_name, value)
else
- @attributes[attr_name] = Attribute.from_database(value, type)
+ @attributes.write_from_database(attr_name, value)
end
value
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
new file mode 100644
index 0000000000..5be11e6ab9
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -0,0 +1,69 @@
+require 'active_record/attribute_set/builder'
+
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ delegate :keys, to: :initialized_attributes
+
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def [](name)
+ attributes[name] || Attribute.null(name)
+ end
+
+ def values_before_type_cast
+ attributes.each_with_object({}) { |(k, v), h| h[k] = v.value_before_type_cast }
+ end
+
+ def to_hash
+ initialized_attributes.each_with_object({}) { |(k, v), h| h[k] = v.value }
+ end
+ alias_method :to_h, :to_hash
+
+ def include?(name)
+ attributes.include?(name) && self[name].initialized?
+ end
+
+ def fetch_value(name, &block)
+ self[name].value(&block)
+ end
+
+ def write_from_database(name, value)
+ attributes[name] = self[name].with_value_from_database(value)
+ end
+
+ def write_from_user(name, value)
+ attributes[name] = self[name].with_value_from_user(value)
+ end
+
+ def freeze
+ @attributes.freeze
+ super
+ end
+
+ def initialize_dup(_)
+ @attributes = attributes.dup
+ attributes.each do |key, attr|
+ attributes[key] = attr.dup
+ end
+
+ super
+ end
+
+ def initialize_clone(_)
+ @attributes = attributes.clone
+ super
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def initialized_attributes
+ attributes.select { |_, attr| attr.initialized? }
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
new file mode 100644
index 0000000000..1e146a07da
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ class Builder # :nodoc:
+ attr_reader :types
+
+ def initialize(types)
+ @types = types
+ end
+
+ def build_from_database(values = {}, additional_types = {})
+ attributes = build_attributes_from_values(values, additional_types)
+ add_uninitialized_attributes(attributes)
+ AttributeSet.new(attributes)
+ end
+
+ private
+
+ def build_attributes_from_values(values, additional_types)
+ values.each_with_object({}) do |(name, value), hash|
+ type = additional_types.fetch(name, types[name])
+ hash[name] = Attribute.from_database(name, value, type)
+ end
+ end
+
+ def add_uninitialized_attributes(attributes)
+ types.except(*attributes.keys).each do |name, type|
+ attributes[name] = Attribute.uninitialized(name, type)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index 9c80121d70..492d8f3560 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -9,7 +9,7 @@ module ActiveRecord
self.user_provided_columns = {}
end
- module ClassMethods
+ module ClassMethods # :nodoc:
# Defines or overrides a attribute on this model. This allows customization of
# Active Record's type casting behavior, as well as adding support for user defined
# types.
@@ -27,7 +27,7 @@ module ActiveRecord
#
# ==== Examples
#
- # The type detected by Active Record can be overriden.
+ # The type detected by Active Record can be overridden.
#
# # db/schema.rb
# create_table :store_listings, force: true do |t|
@@ -109,13 +109,14 @@ module ActiveRecord
end
def clear_caches_calculated_from_columns
- @columns = nil
- @columns_hash = nil
- @column_types = nil
+ @attributes_builder = nil
@column_defaults = nil
- @raw_column_defaults = nil
@column_names = nil
+ @column_types = nil
+ @columns = nil
+ @columns_hash = nil
@content_columns = nil
+ @raw_column_defaults = nil
end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index b3c3e26c9f..dd92e29199 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -184,9 +184,7 @@ module ActiveRecord
before_save :before_save_collection_association
define_non_cyclic_method(save_method) { save_collection_association(reflection) }
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create save_method
- after_update save_method
+ after_save save_method
elsif reflection.has_one?
define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method)
# Configures two callbacks instead of a single after_save so that
@@ -364,6 +362,7 @@ module ActiveRecord
raise ActiveRecord::Rollback unless saved
end
+ @new_record_before_save = false
end
# reconstruct the scope now that we know the owner's id
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 db80c0faee..cb75070e3a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -364,7 +364,7 @@ module ActiveRecord
conn.expire
end
- release conn, owner
+ release owner
@available.add conn
end
@@ -377,7 +377,7 @@ module ActiveRecord
@connections.delete conn
@available.delete conn
- release conn, conn.owner
+ release conn.owner
@available.add checkout_new_connection if @available.any_waiting?
end
@@ -425,7 +425,7 @@ module ActiveRecord
end
end
- def release(conn, owner)
+ def release(owner)
thread_id = owner.object_id
@reserved_connections.delete thread_id
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 04ae67234f..ff92375820 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -14,8 +14,8 @@ module ActiveRecord
# value. Is this really the only case? Are we missing tests for other types?
# We should have a real column object passed (or nil) here, and check for that
# instead
- if column.respond_to?(:type_cast_for_database)
- value = column.type_cast_for_database(value)
+ if column.respond_to?(:cast_type)
+ value = column.cast_type.type_cast_for_database(value)
end
_quote(value)
@@ -34,8 +34,8 @@ module ActiveRecord
# value. Is this really the only case? Are we missing tests for other types?
# We should have a real column object passed (or nil) here, and check for that
# instead
- if column.respond_to?(:type_cast_for_database)
- value = column.type_cast_for_database(value)
+ if column.respond_to?(:cast_type)
+ value = column.cast_type.type_cast_for_database(value)
end
_type_cast(value)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 47fe501752..c1379f6bec 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -23,6 +23,8 @@ module ActiveRecord
def visit_AlterTable(o)
sql = "ALTER TABLE #{quote_table_name(o.name)} "
sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ')
+ sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ')
end
def visit_ColumnDefinition(o)
@@ -41,6 +43,21 @@ module ActiveRecord
create_sql
end
+ def visit_AddForeignKey(o)
+ sql = <<-SQL.strip_heredoc
+ ADD CONSTRAINT #{quote_column_name(o.name)}
+ FOREIGN KEY (#{quote_column_name(o.column)})
+ REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
+ SQL
+ sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete
+ sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update
+ sql
+ end
+
+ def visit_DropForeignKey(name)
+ "DROP CONSTRAINT #{quote_column_name(name)}"
+ end
+
def column_options(o)
column_options = {}
column_options[:null] = o.null unless o.null.nil?
@@ -84,6 +101,19 @@ module ActiveRecord
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
+
+ def action_sql(action, dependency)
+ case dependency
+ when :nullify then "ON #{action} SET NULL"
+ when :cascade then "ON #{action} CASCADE"
+ when :restrict then "ON #{action} RESTRICT"
+ else
+ raise ArgumentError, <<-MSG.strip_heredoc
+ '#{dependency}' is not supported for :on_update or :on_delete.
+ Supported values are: :nullify, :cascade, :restrict
+ MSG
+ end
+ end
end
end
end
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 a9b3e9cfb9..33b522f391 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -25,6 +25,37 @@ module ActiveRecord
class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc:
end
+ class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
+ def name
+ options[:name]
+ end
+
+ def column
+ options[:column]
+ end
+
+ def primary_key
+ options[:primary_key] || default_primary_key
+ end
+
+ def on_delete
+ options[:on_delete]
+ end
+
+ def on_update
+ options[:on_update]
+ end
+
+ def custom_primary_key?
+ options[:primary_key] != default_primary_key
+ end
+
+ private
+ def default_primary_key
+ "id"
+ end
+ end
+
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
@@ -303,14 +334,26 @@ module ActiveRecord
class AlterTable # :nodoc:
attr_reader :adds
+ attr_reader :foreign_key_adds
+ attr_reader :foreign_key_drops
def initialize(td)
@td = td
@adds = []
+ @foreign_key_adds = []
+ @foreign_key_drops = []
end
def name; @td.name; end
+ def add_foreign_key(to_table, options)
+ @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options)
+ end
+
+ def drop_foreign_key(name)
+ @foreign_key_drops << name
+ end
+
def add_column(name, type, options)
name = name.to_s
type = type.to_sym
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index 5a0efe49c7..9bd0401e40 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -23,7 +23,8 @@ module ActiveRecord
spec[:precision] = column.precision.inspect if column.precision
spec[:scale] = column.scale.inspect if column.scale
spec[:null] = 'false' unless column.null
- spec[:default] = column.type_cast_for_schema(column.default) if column.has_default?
+ spec[:default] = schema_default(column) if column.has_default?
+ spec.delete(:default) if spec[:default].nil?
spec
end
@@ -31,6 +32,15 @@ module ActiveRecord
def migration_keys
[:name, :limit, :precision, :scale, :default, :null]
end
+
+ private
+
+ def schema_default(column)
+ default = column.type_cast_from_database(column.default)
+ unless default.nil?
+ column.type_cast_for_schema(default)
+ end
+ end
end
end
end
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 22823a8c58..5814c2b711 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -642,6 +642,115 @@ module ActiveRecord
end
alias :remove_belongs_to :remove_reference
+ # Returns an array of foreign keys for the given table.
+ # The foreign keys are represented as +ForeignKeyDefinition+ objects.
+ def foreign_keys(table_name)
+ raise NotImplementedError, "foreign_keys is not implemented"
+ end
+
+ # Adds a new foreign key. +from_table+ is the table with the key column,
+ # +to_table+ contains the referenced primary key.
+ #
+ # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>.
+ # +identifier+ is a 10 character long random string. A custom name can be specified with
+ # the <tt>:name</tt> option.
+ #
+ # ====== Creating a simple foreign key
+ #
+ # add_foreign_key :articles, :authors
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
+ #
+ # ====== Creating a foreign key on a specific column
+ #
+ # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id")
+ #
+ # ====== Creating a cascading foreign key
+ #
+ # add_foreign_key :articles, :authors, on_delete: :cascade
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
+ #
+ # The +options+ hash can include the following keys:
+ # [<tt>:column</tt>]
+ # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt>
+ # [<tt>:primary_key</tt>]
+ # The primary key column name on +to_table+. Defaults to +id+.
+ # [<tt>:name</tt>]
+ # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>.
+ # [<tt>:on_delete</tt>]
+ # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ # [<tt>:on_update</tt>]
+ # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ def add_foreign_key(from_table, to_table, options = {})
+ return unless supports_foreign_keys?
+
+ options[:column] ||= foreign_key_column_for(to_table)
+
+ options = {
+ column: options[:column],
+ primary_key: options[:primary_key],
+ name: foreign_key_name(from_table, options),
+ on_delete: options[:on_delete],
+ on_update: options[:on_update]
+ }
+ at = create_alter_table from_table
+ at.add_foreign_key to_table, options
+
+ execute schema_creation.accept(at)
+ end
+
+ # Removes the given foreign key from the table.
+ #
+ # Removes the foreign key on +accounts.branch_id+.
+ #
+ # remove_foreign_key :accounts, :branches
+ #
+ # Removes the foreign key on +accounts.owner_id+.
+ #
+ # remove_foreign_key :accounts, column: :owner_id
+ #
+ # Removes the foreign key named +special_fk_name+ on the +accounts+ table.
+ #
+ # remove_foreign_key :accounts, name: :special_fk_name
+ #
+ def remove_foreign_key(from_table, options_or_to_table = {})
+ return unless supports_foreign_keys?
+
+ if options_or_to_table.is_a?(Hash)
+ options = options_or_to_table
+ else
+ options = { column: foreign_key_column_for(options_or_to_table) }
+ end
+
+ fk_name_to_delete = options.fetch(:name) do
+ fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] }
+
+ if fk_to_delete
+ fk_to_delete.name
+ else
+ raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'"
+ end
+ end
+
+ at = create_alter_table from_table
+ at.drop_foreign_key fk_name_to_delete
+
+ execute schema_creation.accept(at)
+ end
+
+ def foreign_key_column_for(table_name) # :nodoc:
+ "#{table_name.to_s.singularize}_id"
+ end
+
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
@@ -852,6 +961,12 @@ module ActiveRecord
def create_alter_table(name)
AlterTable.new create_table_definition(name, false, {})
end
+
+ def foreign_key_name(table_name, options) # :nodoc:
+ options.fetch(:name) do
+ "fk_rails_#{SecureRandom.hex(5)}"
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index cc494a7f40..294ed6d7bf 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -233,6 +233,11 @@ module ActiveRecord
false
end
+ # Does this adapter support creating foreign key constraints?
+ def supports_foreign_keys?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
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 a9b97d5919..e4a6f43225 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -12,6 +12,10 @@ module ActiveRecord
private
+ def visit_DropForeignKey(name)
+ "DROP FOREIGN KEY #{name}"
+ end
+
def visit_TableDefinition(o)
name = o.name
create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
@@ -62,15 +66,14 @@ module ActiveRecord
@extra = extra
super(name, default, cast_type, sql_type, null)
assert_valid_default(default)
+ extract_default
end
- def default
- @default ||= if blob_or_text_column?
- null || strict ? nil : ''
- elsif missing_default_forged_as_empty_string?(@original_default)
- nil
- else
- super
+ def extract_default
+ if blob_or_text_column?
+ @default = null || strict ? nil : ''
+ elsif missing_default_forged_as_empty_string?(@default)
+ @default = nil
end
end
@@ -193,6 +196,10 @@ module ActiveRecord
true
end
+ def supports_foreign_keys?
+ true
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -382,7 +389,7 @@ module ActiveRecord
end
def table_exists?(name)
- return false unless name
+ return false unless name.present?
return true if tables(nil, nil, name).any?
name = name.to_s
@@ -502,6 +509,34 @@ module ActiveRecord
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT fk.referenced_table_name as 'to_table'
+ ,fk.referenced_column_name as 'primary_key'
+ ,fk.column_name as 'column'
+ ,fk.constraint_name as 'name'
+ FROM information_schema.key_column_usage fk
+ WHERE fk.referenced_column_name is not null
+ AND fk.table_schema = '#{@config[:database]}'
+ AND fk.table_name = '#{table_name}'
+ SQL
+
+ create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE")
+ options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE")
+
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
# Maps logical Rails types to MySQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
case type.to_s
@@ -780,6 +815,15 @@ module ActiveRecord
# ...and send them all in one query
@connection.query "SET #{encoding} #{variable_assignments}"
end
+
+ def extract_foreign_key_action(structure, name, action) # :nodoc:
+ if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/
+ case $1
+ when 'CASCADE'; :cascade
+ when 'SET NULL'; :nullify
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index d629fca911..1f1e2c46f4 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -13,7 +13,7 @@ module ActiveRecord
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
end
- attr_reader :name, :cast_type, :null, :sql_type, :default_function
+ attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function
delegate :type, :precision, :scale, :limit, :klass, :accessor,
:text?, :number?, :binary?, :changed?,
@@ -35,7 +35,7 @@ module ActiveRecord
@cast_type = cast_type
@sql_type = sql_type
@null = null
- @original_default = default
+ @default = default
@default_function = nil
end
@@ -51,23 +51,12 @@ module ActiveRecord
Base.human_attribute_name(@name)
end
- def default
- @default ||= type_cast_from_database(@original_default)
- end
-
def with_type(type)
dup.tap do |clone|
- clone.instance_variable_set('@default', nil)
clone.instance_variable_set('@cast_type', type)
end
end
end
-
- class NullColumn < Column
- def initialize(name)
- super name, nil, Type::Value.new
- end
- end
end
# :startdoc:
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 252b82643d..ad07a46e51 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -263,7 +263,7 @@ module ActiveRecord
end
module Fields # :nodoc:
- class DateTime < Type::DateTime
+ class DateTime < Type::DateTime # :nodoc:
def cast_value(value)
if Mysql::Time === value
new_time(
@@ -280,7 +280,7 @@ module ActiveRecord
end
end
- class Time < Type::Time
+ class Time < Type::Time # :nodoc:
def cast_value(value)
if Mysql::Time === value
new_time(
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
index d322c56acc..cd5efe2bb8 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Array < Type::Value
+ class Array < Type::Value # :nodoc:
include Type::Mutable
# Loads pg_array_parser if available. String parsing can be
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
index 3073f8ff30..243ecd13cf 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Bit < Type::Value
+ class Bit < Type::Value # :nodoc:
def type
:bit
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
index 054af285bb..4c21097d48 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class BitVarying < OID::Bit
+ class BitVarying < OID::Bit # :nodoc:
def type
:bit_varying
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
index 89b203d2b1..997613d7be 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Bytea < Type::Binary
+ class Bytea < Type::Binary # :nodoc:
def type_cast_from_database(value)
return if value.nil?
PGconn.unescape_bytea(super)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
index 534961a414..a53b4ee8e2 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Cidr < Type::Value
+ class Cidr < Type::Value # :nodoc:
def type
:cidr
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
index 3c30ad5fec..1d8d264530 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Date < Type::Date
+ class Date < Type::Date # :nodoc:
include Infinity
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
index 34e2276dd1..b9e7894e5c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class DateTime < Type::DateTime
+ class DateTime < Type::DateTime # :nodoc:
include Infinity
def cast_value(value)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
index ed4b8911d9..43d22c8daf 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Decimal < Type::Decimal
+ class Decimal < Type::Decimal # :nodoc:
def infinity(options = {})
BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
index 5fed8b0f89..77d5038efd 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Enum < Type::Value
+ class Enum < Type::Value # :nodoc:
def type
:enum
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
index 77dd97e140..26c5d3d78f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Float < Type::Float
+ class Float < Type::Float # :nodoc:
include Infinity
def cast_value(value)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
index 0dd4b65333..57b20477df 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Hstore < Type::Value
+ class Hstore < Type::Value # :nodoc:
include Type::Mutable
def type
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
index 7ed8f5f031..96486fa65b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Inet < Cidr
+ class Inet < Cidr # :nodoc:
def type
:inet
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
index d438ffa140..e47780399a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- module Infinity
+ module Infinity # :nodoc:
def infinity(options = {})
options[:negative] ? -::Float::INFINITY : ::Float::INFINITY
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
index 388d3dd9ed..59abdc0009 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Integer < Type::Integer
+ class Integer < Type::Integer # :nodoc:
include Infinity
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
index d1347c7bb5..ab1165f301 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Json < Type::Value
+ class Json < Type::Value # :nodoc:
include Type::Mutable
def type
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
index d25eb256c2..df890c2ed6 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Money < Type::Decimal
+ class Money < Type::Decimal # :nodoc:
include Infinity
class_attribute :precision
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
index 86277c5542..9b6494867f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Point < Type::Value
+ class Point < Type::Value # :nodoc:
include Type::Mutable
def type
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
index c289ba8980..991cdd0913 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Range < Type::Value
+ class Range < Type::Value # :nodoc:
attr_reader :subtype, :type
def initialize(subtype, type)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
index 7b1ca16bc4..b2a42e9ebb 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class SpecializedString < Type::String
+ class SpecializedString < Type::String # :nodoc:
attr_reader :type
def initialize(type)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
index ea1f599b0f..8f0246eddb 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Time < Type::Time
+ class Time < Type::Time # :nodoc:
include Infinity
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
index 0ed5491887..89728b0fe2 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Uuid < Type::Value
+ class Uuid < Type::Value # :nodoc:
def type
:uuid
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
index 2f7d1be197..de4187b028 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
- class Vector < Type::Value
+ class Vector < Type::Value # :nodoc:
attr_reader :delim, :subtype
# +delim+ corresponds to the `typdelim` column in the pg_types
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 17fabe5af6..4caed77952 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -179,14 +179,14 @@ module ActiveRecord
end
def array_column(column)
- if column.array && !column.respond_to?(:type_cast_for_database)
- OID::Array.new(AdapterProxyType.new(column, self))
+ if column.array && !column.respond_to?(:cast_type)
+ Column.new('', nil, OID::Array.new(AdapterProxyType.new(column, self)))
else
column
end
end
- class AdapterProxyType < SimpleDelegator
+ class AdapterProxyType < SimpleDelegator # :nodoc:
def initialize(column, adapter)
@column = column
@adapter = adapter
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 b2aeb3a058..30df98be1b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -448,6 +448,42 @@ module ActiveRecord
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
+ FROM pg_constraint c
+ JOIN pg_class t1 ON c.conrelid = t1.oid
+ JOIN pg_class t2 ON c.confrelid = t2.oid
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
+ WHERE c.contype = 'f'
+ AND t1.relname = #{quote(table_name)}
+ AND t3.nspname = ANY (current_schemas(false))
+ ORDER BY c.conname
+ SQL
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
+ def extract_foreign_key_action(specifier) # :nodoc:
+ case specifier
+ when 'c'; :cascade
+ when 'n'; :nullify
+ when 'r'; :restrict
+ end
+ end
+
def index_name_length
63
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 71b05cdbae..34262cf91d 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -159,6 +159,10 @@ module ActiveRecord
true
end
+ def supports_foreign_keys?
+ true
+ end
+
def index_algorithms
{ concurrently: 'CONCURRENTLY' }
end
@@ -306,10 +310,6 @@ module ActiveRecord
self.client_min_messages = old
end
- def supports_insert_with_returning?
- true
- end
-
def supports_ddl_transactions?
true
end
@@ -348,14 +348,13 @@ module ActiveRecord
if supports_extensions?
res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled",
'SCHEMA'
- res.column_types['enabled'].type_cast_from_database res.rows.first.first
+ res.cast_values.first
end
end
def extensions
if supports_extensions?
- res = exec_query "SELECT extname from pg_extension", "SCHEMA"
- res.rows.map { |r| res.column_types['extname'].type_cast_from_database r.first }
+ exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values
else
super
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index d39e5fddfe..3321e268d5 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -16,7 +16,6 @@ module ActiveRecord
mattr_accessor :logger, instance_writer: false
##
- # :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
# as a Hash.
#
@@ -251,12 +250,10 @@ module ActiveRecord
def initialize(attributes = nil, options = {})
defaults = {}
self.class.raw_column_defaults.each do |k, v|
- default = v.duplicable? ? v.dup : v
- defaults[k] = Attribute.from_database(default, type_for_attribute(k))
+ defaults[k] = v.duplicable? ? v.dup : v
end
- @attributes = defaults
- @column_types = self.class.column_types
+ @attributes = self.class.attributes_builder.build_from_database(defaults)
init_internals
initialize_internals_callback
@@ -282,7 +279,6 @@ module ActiveRecord
# post.title # => 'hello world'
def init_with(coder)
@attributes = coder['attributes']
- @column_types = self.class.column_types
init_internals
@@ -325,8 +321,8 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
pk = self.class.primary_key
- @attributes = other.clone_attributes
- @attributes[pk] = Attribute.from_database(nil, type_for_attribute(pk))
+ @attributes = @attributes.dup
+ @attributes.write_from_database(pk, nil)
run_callbacks(:initialize) unless _initialize_callbacks.empty?
@@ -525,7 +521,9 @@ module ActiveRecord
def init_internals
pk = self.class.primary_key
- @attributes[pk] ||= Attribute.from_database(nil, type_for_attribute(pk))
+ if pk && !@attributes.include?(pk)
+ @attributes.write_from_database(pk, nil)
+ end
@aggregation_cache = {}
@association_cache = {}
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 0a764fb7ad..52eeb8ae1f 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -53,11 +53,6 @@ module ActiveRecord
included do
class_attribute :lock_optimistically, instance_writer: false
self.lock_optimistically = true
-
- is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
- decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type|
- LockingType.new(type)
- end
end
def locking_enabled? #:nodoc:
@@ -167,10 +162,26 @@ module ActiveRecord
counters = counters.merge(locking_column => 1) if locking_enabled?
super
end
+
+ private
+
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `lock_optimistically`, or
+ # `locking_column` would not be picked up.
+ def inherited(subclass)
+ subclass.class_eval do
+ is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
+ decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type|
+ LockingType.new(type)
+ end
+ end
+ super
+ end
end
end
- class LockingType < SimpleDelegator
+ class LockingType < SimpleDelegator # :nodoc:
def type_cast_from_database(value)
# `nil` *should* be changed to 0
super.to_i
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 01c001e692..12eaf36156 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -645,7 +645,9 @@ module ActiveRecord
unless @connection.respond_to? :revert
unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
arguments[0] = proper_table_name(arguments.first, table_name_options)
- arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table
+ if [:rename_table, :add_foreign_key].include?(method)
+ arguments[1] = proper_table_name(arguments.second, table_name_options)
+ end
end
end
return super unless connection.respond_to?(method)
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index c44d8c1665..f833caaab6 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -74,7 +74,9 @@ module ActiveRecord
:rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
:change_column_default, :add_reference, :remove_reference, :transaction,
:drop_join_table, :drop_table, :execute_block, :enable_extension,
- :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too
+ :change_column, :execute, :remove_columns, :change_column_null,
+ :add_foreign_key, :remove_foreign_key
+ # irreversible methods need to be here too
].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def create_table(*args, &block)
@@ -167,6 +169,21 @@ module ActiveRecord
[:change_column_null, args]
end
+ def invert_add_foreign_key(args)
+ from_table, to_table, add_options = args
+ add_options ||= {}
+
+ if add_options[:name]
+ options = { name: add_options[:name] }
+ elsif add_options[:column]
+ options = { column: add_options[:column] }
+ else
+ options = to_table
+ end
+
+ [:remove_foreign_key, [from_table, options]]
+ end
+
# Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
if @delegate.respond_to?(method)
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index b9558b0752..099042cab2 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -219,25 +219,33 @@ module ActiveRecord
connection.schema_cache.table_exists?(table_name)
end
+ def attributes_builder # :nodoc:
+ @attributes_builder ||= AttributeSet::Builder.new(column_types)
+ end
+
def column_types # :nodoc:
- @column_types ||= Hash[columns.map { |column| [column.name, column.cast_type] }]
+ @column_types ||= Hash.new(Type::Value.new).tap do |column_types|
+ columns.each { |column| column_types[column.name] = column.cast_type }
+ end
end
def type_for_attribute(attr_name) # :nodoc:
- column_types.fetch(attr_name) { Type::Value.new }
+ column_types[attr_name]
end
# Returns a hash where the keys are column names and the values are
# default values when instantiating the AR object for this table.
def column_defaults
- @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }]
+ @column_defaults ||= Hash[raw_column_defaults.map { |name, default|
+ [name, type_for_attribute(name).type_cast_from_database(default)]
+ }]
end
# Returns a hash where the keys are the column names and the values
# are the default values suitable for use in `@raw_attriubtes`
def raw_column_defaults # :nodoc:
- @raw_column_defauts ||= Hash[column_defaults.map { |name, default|
- [name, columns_hash[name].type_cast_for_database(default)]
+ @raw_column_defaults ||= Hash[columns_hash.map { |name, column|
+ [name, column.default]
}]
end
@@ -285,7 +293,7 @@ module ActiveRecord
@arel_engine = nil
@column_defaults = nil
- @raw_column_defauts = nil
+ @raw_column_defaults = nil
@column_names = nil
@column_types = nil
@content_columns = nil
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 5c744762d9..ee634d7bb3 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -48,11 +48,7 @@ module ActiveRecord
# how this "single-table" inheritance mapping is implemented.
def instantiate(attributes, column_types = {})
klass = discriminate_class_for_record(attributes)
-
- attributes = attributes.each_with_object({}) do |(name, value), h|
- type = column_types.fetch(name) { klass.type_for_attribute(name) }
- h[name] = Attribute.from_database(value, type)
- end
+ attributes = klass.attributes_builder.build_from_database(attributes, column_types)
klass.allocate.init_with('attributes' => attributes, 'new_record' => false)
end
@@ -399,9 +395,7 @@ module ActiveRecord
self.class.unscoped { self.class.find(id) }
end
- @attributes.update(fresh_object.instance_variable_get('@attributes'))
-
- @column_types = self.class.column_types
+ @attributes = fresh_object.instance_variable_get('@attributes')
self
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 39817703cd..a9ddd9141f 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -37,14 +37,7 @@ module ActiveRecord
# Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
def find_by_sql(sql, binds = [])
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
- column_types = {}
-
- if result_set.respond_to? :column_types
- column_types = result_set.column_types.except(*columns_hash.keys)
- else
- ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`"
- end
-
+ column_types = result_set.column_types.except(*columns_hash.keys)
result_set.map { |record| instantiate(record, column_types) }
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index ecf5afc4f4..fa94df7a52 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -93,22 +93,21 @@ db_namespace = namespace :db do
desc 'Display status of migrations'
task :status => [:environment, :load_config] do
- unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
+ unless ActiveRecord::SchemaMigration.table_exists?
abort 'Schema migrations table does not exist yet.'
end
- db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
- db_list.map! { |version| ActiveRecord::SchemaMigration.normalize_migration_number(version) }
- file_list = []
- ActiveRecord::Migrator.migrations_paths.each do |path|
- Dir.foreach(path) do |file|
- # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
- if match_data = /^(\d{3,})_(.+)\.rb$/.match(file)
- version = ActiveRecord::SchemaMigration.normalize_migration_number(match_data[1])
- status = db_list.delete(version) ? 'up' : 'down'
- file_list << [status, version, match_data[2].humanize]
+ db_list = ActiveRecord::SchemaMigration.normalized_versions
+
+ file_list =
+ ActiveRecord::Migrator.migrations_paths.flat_map do |path|
+ # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
+ Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do
+ version = ActiveRecord::SchemaMigration.normalize_migration_number($1)
+ status = db_list.delete(version) ? 'up' : 'down'
+ [status, version, $2.humanize]
+ end
end
- end
- end
+
db_list.map! do |version|
['up', version, '********** NO FILE **********']
end
@@ -116,8 +115,8 @@ db_namespace = namespace :db do
puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
puts "-" * 50
- (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration|
- puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}"
+ (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name|
+ puts "#{status.center(8)} #{version.ljust(14)} #{name}"
end
puts
end
@@ -189,17 +188,21 @@ db_namespace = namespace :db do
task :load => [:environment, :load_config] do
require 'active_record/fixtures'
- base_dir = if ENV['FIXTURES_PATH']
- File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
- else
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- end
+ base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
+ fixtures_dir = if ENV['FIXTURES_DIR']
+ File.join base_dir, ENV['FIXTURES_DIR']
+ else
+ base_dir
+ end
- (ENV['FIXTURES'] ? ENV['FIXTURES'].split(',') : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
- ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file)
- end
+ fixture_files = if ENV['FIXTURES']
+ ENV['FIXTURES'].split(',')
+ else
+ Pathname.glob("#{fixtures_dir}/**/*.yml").map {|f| f.basename.sub_ext('').to_s }
+ end
+
+ ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)
end
# desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
@@ -211,12 +214,7 @@ db_namespace = namespace :db do
puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label
- base_dir = if ENV['FIXTURES_PATH']
- File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
- else
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- end
-
+ base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
Dir["#{base_dir}/**/*.yml"].each do |file|
if data = YAML::load(ERB.new(IO.read(file)).result)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 51c96373ee..28c39bdd5c 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -290,8 +290,8 @@ module ActiveRecord
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
- def primary_key_column
- klass.columns_hash[klass.primary_key]
+ def primary_key_type
+ klass.type_for_attribute(klass.primary_key)
end
def association_foreign_key
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 316a59e0bb..b4ae204813 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -178,16 +178,7 @@ module ActiveRecord
columns_hash.key?(cn) ? arel_table[cn] : cn
}
result = klass.connection.select_all(relation.arel, nil, bind_values)
- columns = result.columns.map do |key|
- klass.column_types.fetch(key) {
- result.column_types.fetch(key) { result.identity_type }
- }
- end
-
- result = result.rows.map do |values|
- columns.zip(values).map { |column, value| column.type_cast_from_database value }
- end
- columns.one? ? result.map!(&:first) : result
+ result.cast_values(klass.column_types)
end
end
@@ -273,7 +264,7 @@ module ActiveRecord
row = result.first
value = row && row.values.first
column = result.column_types.fetch(column_alias) do
- column_for(column_name)
+ type_for(column_name)
end
type_cast_calculated_value(value, column, operation)
@@ -336,14 +327,14 @@ module ActiveRecord
Hash[calculated_data.map do |row|
key = group_columns.map { |aliaz, col_name|
column = calculated_data.column_types.fetch(aliaz) do
- column_for(col_name)
+ type_for(col_name)
end
type_cast_calculated_value(row[aliaz], column)
}
key = key.first if key.size == 1
key = key_records[key] if associated
- column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) }
+ column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
[key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
end]
end
@@ -370,24 +361,20 @@ module ActiveRecord
@klass.connection.table_alias_for(table_name)
end
- def column_for(field)
+ def type_for(field)
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
- @klass.columns_hash[field_name]
+ @klass.type_for_attribute(field_name)
end
- def type_cast_calculated_value(value, column, operation = nil)
+ def type_cast_calculated_value(value, type, operation = nil)
case operation
when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || 0, column)
+ when 'sum' then type.type_cast_from_database(value || 0)
when 'average' then value.respond_to?(:to_d) ? value.to_d : value
- else type_cast_using_column(value, column)
+ else type.type_cast_from_database(value)
end
end
- def type_cast_using_column(value, column)
- column ? column.type_cast_from_database(value) : value
- end
-
# TODO: refactor to allow non-string `select_values` (eg. Arel nodes).
def select_for_count
if select_values.present?
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 8c29c69a9b..8405fdaeb9 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -42,14 +42,6 @@ module ActiveRecord
@column_types = column_types
end
- def identity_type # :nodoc:
- IDENTITY_TYPE
- end
-
- def column_type(name)
- @column_types[name] || identity_type
- end
-
def each
if block_given?
hash_rows.each { |row| yield row }
@@ -82,6 +74,15 @@ module ActiveRecord
hash_rows.last
end
+ def cast_values(type_overrides = {}) # :nodoc:
+ types = columns.map { |name| column_type(name, type_overrides) }
+ result = rows.map do |values|
+ types.zip(values).map { |type, value| type.type_cast_from_database(value) }
+ end
+
+ columns.one? ? result.map!(&:first) : result
+ end
+
def initialize_copy(other)
@columns = columns.dup
@rows = rows.dup
@@ -91,6 +92,12 @@ module ActiveRecord
private
+ def column_type(name, type_overrides = {})
+ type_overrides.fetch(name) do
+ column_types.fetch(name, IDENTITY_TYPE)
+ end
+ end
+
def hash_rows
@hash_rows ||=
begin
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index e055d571ab..64bc68eefd 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -91,7 +91,8 @@ HEADER
end
def tables(stream)
- @connection.tables.sort.each do |tbl|
+ sorted_tables = @connection.tables.sort
+ sorted_tables.each do |tbl|
next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
case ignored
when String; remove_prefix_and_suffix(tbl) == ignored
@@ -102,6 +103,13 @@ HEADER
end
table(tbl, stream)
end
+
+ # dump foreign keys at the end to make sure all dependent tables exist.
+ if @connection.supports_foreign_keys?
+ sorted_tables.each do |tbl|
+ foreign_keys(tbl, stream)
+ end
+ end
end
def table(table, stream)
@@ -212,6 +220,36 @@ HEADER
end
end
+ def foreign_keys(table, stream)
+ if (foreign_keys = @connection.foreign_keys(table)).any?
+ add_foreign_key_statements = foreign_keys.map do |foreign_key|
+ parts = [
+ 'add_foreign_key ' + remove_prefix_and_suffix(foreign_key.from_table).inspect,
+ remove_prefix_and_suffix(foreign_key.to_table).inspect,
+ ]
+
+ if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table)
+ parts << ('column: ' + foreign_key.column.inspect)
+ end
+
+ if foreign_key.custom_primary_key?
+ parts << ('primary_key: ' + foreign_key.primary_key.inspect)
+ end
+
+ if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
+ parts << ('name: ' + foreign_key.name.inspect)
+ end
+
+ parts << ('on_update: ' + foreign_key.on_update.inspect) if foreign_key.on_update
+ parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete
+
+ ' ' + parts.join(', ')
+ end
+
+ stream.puts add_foreign_key_statements.sort.join("\n")
+ end
+ end
+
def remove_prefix_and_suffix(table)
table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2")
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 3a004d58c9..b5038104ac 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -43,6 +43,10 @@ module ActiveRecord
def normalize_migration_number(number)
"%.3d" % number.to_i
end
+
+ def normalized_versions
+ pluck(:version).map { |v| normalize_migration_number v }
+ end
end
def version
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 7014bc6d45..3c291f28e3 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -99,7 +99,7 @@ module ActiveRecord
self.local_stored_attributes[store_attribute] |= keys
end
- def _store_accessors_module
+ def _store_accessors_module # :nodoc:
@_store_accessors_module ||= begin
mod = Module.new
include mod
@@ -129,10 +129,10 @@ module ActiveRecord
private
def store_accessor_for(store_attribute)
- @column_types[store_attribute.to_s].accessor
+ type_for_attribute(store_attribute.to_s).accessor
end
- class HashAccessor
+ class HashAccessor # :nodoc:
def self.read(object, attribute, key)
prepare(object, attribute)
object.public_send(attribute)[key]
@@ -151,7 +151,7 @@ module ActiveRecord
end
end
- class StringKeyedHashAccessor < HashAccessor
+ class StringKeyedHashAccessor < HashAccessor # :nodoc:
def self.read(object, attribute, key)
super object, attribute, key.to_s
end
@@ -161,7 +161,7 @@ module ActiveRecord
end
end
- class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor
+ class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc:
def self.prepare(object, store_attribute)
attribute = object.send(store_attribute)
unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 7712a14b79..4727469420 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -58,7 +58,11 @@ module ActiveRecord
end
def fixtures_path
- @fixtures_path ||= File.join(root, 'test', 'fixtures')
+ @fixtures_path ||= if ENV['FIXTURES_PATH']
+ File.join(root, ENV['FIXTURES_PATH'])
+ else
+ File.join(root, 'test', 'fixtures')
+ end
end
def root
diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb
index 3bf29b5026..7416c554c7 100644
--- a/activerecord/lib/active_record/type/binary.rb
+++ b/activerecord/lib/active_record/type/binary.rb
@@ -22,7 +22,7 @@ module ActiveRecord
Data.new(super)
end
- class Data
+ class Data # :nodoc:
def initialize(value)
@value = value
end
diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb
index 64cf4b9b93..066617ea59 100644
--- a/activerecord/lib/active_record/type/mutable.rb
+++ b/activerecord/lib/active_record/type/mutable.rb
@@ -1,6 +1,6 @@
module ActiveRecord
module Type
- module Mutable
+ module Mutable # :nodoc:
def type_cast_from_user(value)
type_cast_from_database(type_cast_for_database(value))
end
@@ -8,7 +8,7 @@ module ActiveRecord
# +raw_old_value+ will be the `_before_type_cast` version of the
# value (likely a string). +new_value+ will be the current, type
# cast value.
- def changed_in_place?(raw_old_value, new_value) # :nodoc:
+ def changed_in_place?(raw_old_value, new_value)
raw_old_value != type_cast_for_database(new_value)
end
end
diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb
index 137c9e4c99..fa43266504 100644
--- a/activerecord/lib/active_record/type/numeric.rb
+++ b/activerecord/lib/active_record/type/numeric.rb
@@ -16,26 +16,20 @@ module ActiveRecord
end
def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
- # 0 => 'wibble' should mark as changed so numericality validations run
- if nil_or_zero?(old_value) && non_numeric_string?(new_value_before_type_cast)
- # nil => '' should not mark as changed
- old_value != new_value_before_type_cast.presence
- else
- super
- end
+ super || number_to_non_number?(old_value, new_value_before_type_cast)
end
private
+ def number_to_non_number?(old_value, new_value_before_type_cast)
+ old_value != nil && non_numeric_string?(new_value_before_type_cast)
+ end
+
def non_numeric_string?(value)
# 'wibble'.to_i will give zero, we want to make sure
# that we aren't marking int zero to string zero as
# changed.
- value !~ /\A\d+\.?\d*\z/
- end
-
- def nil_or_zero?(value)
- value.nil? || value == 0
+ value.to_s !~ /\A\d+\.?\d*\z/
end
end
end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
index 875fb98c4b..081da7547e 100644
--- a/activerecord/lib/active_record/type/value.rb
+++ b/activerecord/lib/active_record/type/value.rb
@@ -3,8 +3,8 @@ module ActiveRecord
class Value # :nodoc:
attr_reader :precision, :scale, :limit
- # Valid options are +precision+, +scale+, and +limit+.
- # They are only used when dumping schema.
+ # Valid options are +precision+, +scale+, and +limit+. They are only
+ # used when dumping schema.
def initialize(options = {})
options.assert_valid_keys(:precision, :scale, :limit)
@precision = options[:precision]
@@ -12,65 +12,85 @@ module ActiveRecord
@limit = options[:limit]
end
- # The simplified type that this object represents. Subclasses
- # must override this method.
+ # The simplified type that this object represents. Returns a symbol such
+ # as +:string+ or +:integer+
def type; end
+ # Type casts a string from the database into the appropriate ruby type.
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +cast_value+ instead.
def type_cast_from_database(value)
type_cast(value)
end
+ # Type casts a value from user input (e.g. from a setter). This value may
+ # be a string from the form builder, or an already type cast value
+ # provided manually to a setter.
+ #
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +type_cast+ or +cast_value+
+ # instead.
def type_cast_from_user(value)
type_cast(value)
end
+ # Cast a value from the ruby type to a type that the database knows how
+ # to understand. The returned value from this method should be a
+ # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
+ # +nil+
def type_cast_for_database(value)
value
end
- def type_cast_for_schema(value)
+ # Type cast a value for schema dumping. This method is private, as we are
+ # hoping to remove it entirely.
+ def type_cast_for_schema(value) # :nodoc:
value.inspect
end
- def text?
+ # These predicates are not documented, as I need to look further into
+ # their use, and see if they can be removed entirely.
+ def text? # :nodoc:
false
end
- def number?
+ def number? # :nodoc:
false
end
- def binary?
+ def binary? # :nodoc:
false
end
def klass # :nodoc:
end
- # +old_value+ will always be type-cast.
- # +new_value+ will come straight from the database
- # or from assignment, so it could be anything. Types
- # which cannot typecast arbitrary values should override
- # this method.
- def changed?(old_value, new_value, _new_value_before_type_cast) # :nodoc:
+ # Determines whether a value has changed for dirty checking. +old_value+
+ # and +new_value+ will always be type-cast. Types should not need to
+ # override this method.
+ def changed?(old_value, new_value, _new_value_before_type_cast)
old_value != new_value
end
- def changed_in_place?(*) # :nodoc:
+ # Determines whether the mutable value has been modified since it was
+ # read. Returns +false+ by default. This method should not need to be
+ # overriden directly. Types which return a mutable value should include
+ # +Type::Mutable+, which will define this method.
+ def changed_in_place?(*)
false
end
private
- # Takes an input from the database, or from attribute setters,
- # and casts it to a type appropriate for this object. This method
- # should not be overriden by subclasses. Instead, override `cast_value`.
- def type_cast(value) # :api: public
+
+ def type_cast(value)
cast_value(value) unless value.nil?
end
- # Responsible for casting values from external sources to the appropriate
- # type. Called by `type_cast` for all values except `nil`.
- def cast_value(value) # :api: public
+ # Convenience method for types which do not need separate type casting
+ # behavior for user and database inputs. Called by
+ # `type_cast_from_database` and `type_cast_from_user` for all values
+ # except `nil`.
+ def cast_value(value) # :doc:
value
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 9999624fcf..b4b33804de 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -54,7 +54,7 @@ module ActiveRecord
# Attempts to save the record just like Base#save but will raise a +RecordInvalid+
# exception instead of returning +false+ if the record is not valid.
def save!(options={})
- perform_validations(options) ? super : raise(RecordInvalid.new(self))
+ perform_validations(options) ? super : raise_record_invalid
end
# Runs all the validations within the specified context. Returns +true+ if
@@ -75,8 +75,24 @@ module ActiveRecord
alias_method :validate, :valid?
+ # Runs all the validations within the specified context. Returns +true+ if
+ # no errors are found, raises +RecordInvalid+ otherwise.
+ #
+ # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
+ # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
+ #
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
+ # some <tt>:on</tt> option will only run in the specified context.
+ def validate!(context = nil)
+ valid?(context) || raise_record_invalid
+ end
+
protected
+ def raise_record_invalid
+ raise(RecordInvalid.new(self))
+ end
+
def perform_validations(options={}) # :nodoc:
options[:validate] == false || valid?(options[:context])
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 2e7b1d7206..04e28a0cfe 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -48,7 +48,7 @@ module ActiveRecord
def build_relation(klass, table, attribute, value) #:nodoc:
if reflection = klass._reflect_on_association(attribute)
attribute = reflection.foreign_key
- value = value.attributes[reflection.primary_key_column.name] unless value.nil?
+ value = value.attributes[reflection.klass.primary_key] unless value.nil?
end
attribute_name = attribute.to_s
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 4775b53eac..70585e0148 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -17,6 +17,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
@connection.commit_db_transaction
end
+ @connection.reconnect!
+
@connection.transaction do
@connection.create_table('pg_arrays') do |t|
t.string 'tags', array: true
@@ -49,9 +51,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def test_default
@connection.add_column 'pg_arrays', 'score', :integer, array: true, default: [4, 4, 2]
PgArray.reset_column_information
- column = PgArray.columns_hash["score"]
- assert_equal([4, 4, 2], column.default)
+ assert_equal([4, 4, 2], PgArray.column_defaults['score'])
assert_equal([4, 4, 2], PgArray.new.score)
ensure
PgArray.reset_column_information
@@ -60,9 +61,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def test_default_strings
@connection.add_column 'pg_arrays', 'names', :string, array: true, default: ["foo", "bar"]
PgArray.reset_column_information
- column = PgArray.columns_hash["names"]
- assert_equal(["foo", "bar"], column.default)
+ assert_equal(["foo", "bar"], PgArray.column_defaults['names'])
assert_equal(["foo", "bar"], PgArray.new.names)
ensure
PgArray.reset_column_information
@@ -76,7 +76,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
column = PgArray.columns_hash['snippets']
assert_equal :text, column.type
- assert_equal [], column.default
+ assert_equal [], PgArray.column_defaults['snippets']
assert column.array
end
@@ -93,8 +93,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
@connection.change_column_default :pg_arrays, :tags, []
PgArray.reset_column_information
- column = PgArray.columns_hash['tags']
- assert_equal [], column.default
+ assert_equal [], PgArray.column_defaults['tags']
end
def test_type_cast_array
@@ -193,16 +192,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings))
end
- def test_update_all
- pg_array = PgArray.create! tags: ["one", "two", "three"]
-
- PgArray.update_all tags: ["four", "five"]
- assert_equal ["four", "five"], pg_array.reload.tags
-
- PgArray.update_all tags: []
- assert_equal [], pg_array.reload.tags
- end
-
def test_escaping
unknown = 'foo\\",bar,baz,\\'
tags = ["hello_#{unknown}"]
diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
index 3a9397bc26..9ee3610afd 100644
--- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
@@ -43,12 +43,10 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase
end
def test_default
- column = PostgresqlBitString.columns_hash["a_bit"]
- assert_equal "00000011", column.default
+ assert_equal "00000011", PostgresqlBitString.column_defaults['a_bit']
assert_equal "00000011", PostgresqlBitString.new.a_bit
- column = PostgresqlBitString.columns_hash["a_bit_varying"]
- assert_equal "0011", column.default
+ assert_equal "0011", PostgresqlBitString.column_defaults['a_bit_varying']
assert_equal "0011", PostgresqlBitString.new.a_bit_varying
end
diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb
index b809f1a79c..0e97f37a6c 100644
--- a/activerecord/test/cases/adapters/postgresql/enum_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -42,9 +42,8 @@ class PostgresqlEnumTest < ActiveRecord::TestCase
def test_enum_defaults
@connection.add_column 'postgresql_enums', 'good_mood', :mood, default: 'happy'
PostgresqlEnum.reset_column_information
- column = PostgresqlEnum.columns_hash["good_mood"]
- assert_equal "happy", column.default
+ assert_equal "happy", PostgresqlEnum.column_defaults['good_mood']
assert_equal "happy", PostgresqlEnum.new.good_mood
ensure
PostgresqlEnum.reset_column_information
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
index aa737617c4..faf195783d 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -35,12 +35,10 @@ class PostgresqlPointTest < ActiveRecord::TestCase
end
def test_default
- column = PostgresqlPoint.columns_hash["y"]
- assert_equal [12.2, 13.3], column.default
+ assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y']
assert_equal [12.2, 13.3], PostgresqlPoint.new.y
- column = PostgresqlPoint.columns_hash["z"]
- assert_equal [14.4, 15.5], column.default
+ assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z']
assert_equal [14.4, 15.5], PostgresqlPoint.new.z
end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index c4e2917251..8f78dbecf5 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -64,9 +64,8 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
def test_default
@connection.add_column 'hstores', 'permissions', :hstore, default: '"users"=>"read", "articles"=>"write"'
Hstore.reset_column_information
- column = Hstore.columns_hash["permissions"]
- assert_equal({"users"=>"read", "articles"=>"write"}, column.default)
+ assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.column_defaults['permissions'])
assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions)
ensure
Hstore.reset_column_information
@@ -296,16 +295,6 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_cycle("a\nb" => "c\nd")
end
- def test_update_all
- hstore = Hstore.create! tags: { "one" => "two" }
-
- Hstore.update_all tags: { "three" => "four" }
- assert_equal({ "three" => "four" }, hstore.reload.tags)
-
- Hstore.update_all tags: { }
- assert_equal({ }, hstore.reload.tags)
- end
-
class TagCollection
def initialize(hash); @hash = hash end
def to_hash; @hash end
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index 3c07209472..e44afea33a 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -43,9 +43,8 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
def test_default
@connection.add_column 'json_data_type', 'permissions', :json, default: '{"users": "read", "posts": ["read", "write"]}'
JsonDataType.reset_column_information
- column = JsonDataType.columns_hash["permissions"]
- assert_equal({"users"=>"read", "posts"=>["read", "write"]}, column.default)
+ assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions'])
assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions)
ensure
JsonDataType.reset_column_information
@@ -156,16 +155,6 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
assert_equal "320×480", y.resolution
end
- def test_update_all
- json = JsonDataType.create! payload: { "one" => "two" }
-
- JsonDataType.update_all payload: { "three" => "four" }
- assert_equal({ "three" => "four" }, json.reload.payload)
-
- JsonDataType.update_all payload: { }
- assert_equal({ }, json.reload.payload)
- end
-
def test_changes_in_place
json = JsonDataType.new
assert_not json.changed?
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
index bdfeedafab..cf2a4ab6ea 100644
--- a/activerecord/test/cases/adapters/postgresql/money_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -32,8 +32,7 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase
end
def test_default
- column = PostgresqlMoney.columns_hash["depth"]
- assert_equal BigDecimal.new("150.55"), column.default
+ assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults['depth']
assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index e55525177f..b89caa3d55 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -339,7 +339,7 @@ module ActiveRecord
column = @conn.columns('ex').find { |x|
x.name == 'number'
}
- assert_equal 10, column.default
+ assert_equal '10', column.default
end
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 9c92dc1141..d8d8bbf75e 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -787,8 +787,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
comment = comments(:greetings)
- assert_difference lambda { post.reload.taggings_count }, -1 do
- assert_difference 'comment.reload.taggings_count', +1 do
+ assert_difference lambda { post.reload.tags_count }, -1 do
+ assert_difference 'comment.reload.tags_count', +1 do
tagging.taggable = comment
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 71c0609df5..51d8e0523e 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -35,9 +35,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
assert_nothing_raised do
- Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a
+ Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
end
- authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a
+ authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
assert_equal 1, assert_no_queries { authors.size }
assert_equal 10, assert_no_queries { authors[0].comments.size }
end
diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
new file mode 100644
index 0000000000..48f7ddbe83
--- /dev/null
+++ b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
@@ -0,0 +1,26 @@
+require "cases/helper"
+
+class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase
+ class Post < ActiveRecord::Base
+ has_many :taggings, as: :taggable
+ has_many :tags, through: :taggings
+ end
+
+ class Tagging < ActiveRecord::Base
+ belongs_to :taggable, polymorphic: true
+ belongs_to :tag
+ end
+
+ class Tag < ActiveRecord::Base
+ end
+
+ test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do
+ post = Post.create!(title: 'Hello', body: 'World!')
+ assert_deprecated { post.tags << Tag.create!(name: 'whatever') }
+
+ assert_equal 1, post.tags.size
+ assert_equal 1, post.tags_count
+ assert_equal 1, post.reload.tags.size
+ assert_equal 1, post.reload.tags_count
+ end
+end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 910067666a..21912fdf0f 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -1261,4 +1261,33 @@ class EagerAssociationTest < ActiveRecord::TestCase
Author.eager_load(:posts_with_signature).to_a
end
end
+
+ test "preloading readonly association" do
+ # has-one
+ firm = Firm.where(id: "1").preload(:readonly_account).first!
+ assert firm.readonly_account.readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").preload(:readonly_developers).first!
+ assert project.readonly_developers.first.readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").preload(:readonly_comments).first!
+ assert david.readonly_comments.first.readonly?
+ end
+
+ test "eager-loading readonly association" do
+ skip "eager_load does not yet preserve readonly associations"
+ # has-one
+ firm = Firm.where(id: "1").eager_load(:readonly_account).first!
+ assert firm.readonly_account.readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").eager_load(:readonly_developers).first!
+ assert project.readonly_developers.first.readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").eager_load(:readonly_comments).first!
+ assert david.readonly_comments.first.readonly?
+ end
end
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index 080c499444..cc58a4a1a2 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -860,7 +860,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 'edges', Vertex.reflect_on_association(:sources).join_table
end
- def test_namespaced_habtm
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_namespaced_model
magazine = Publisher::Magazine.create
article = Publisher::Article.create
magazine.articles << article
@@ -869,6 +869,15 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_includes magazine.articles, article
end
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_non_namespaced_model
+ article = Publisher::Article.create
+ tag = Tag.create
+ article.tags << tag
+ article.save
+
+ assert_includes article.tags, tag
+ end
+
def test_redefine_habtm
child = SubDeveloper.new("name" => "Aredridel")
child.special_projects << SpecialProject.new("name" => "Special Project")
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 5f01352ab4..3e5b7e655b 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -36,7 +36,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa
author = authors(:david)
# this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression
# if the reorder clauses are not correctly handled
- assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.taggings_count DESC').last
+ assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.tags_count DESC').last
end
end
@@ -772,6 +772,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal topic.replies.to_a.size, topic.replies_count
end
+ def test_counter_cache_updates_in_memory_after_concat
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies << Reply.create(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create_with_array
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!([
+ { title: "re: zoom", content: "speedy quick!" },
+ { title: "re: zoom 2", content: "OMG lol!" },
+ ])
+ assert_equal 2, topic.replies_count
+ assert_equal 2, topic.replies.size
+ assert_equal 2, topic.reload.replies.size
+ end
+
def test_pushing_association_updates_counter_cache
topic = Topic.order("id ASC").first
reply = Reply.create!
@@ -784,14 +814,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_without_dependent_option
post = posts(:welcome)
- assert_difference "post.reload.taggings_count", -1 do
+ assert_difference "post.reload.tags_count", -1 do
post.taggings.delete(post.taggings.first)
end
end
def test_deleting_updates_counter_cache_with_dependent_delete_all
post = posts(:welcome)
- post.update_columns(taggings_with_delete_all_count: post.taggings_count)
+ post.update_columns(taggings_with_delete_all_count: post.tags_count)
assert_difference "post.reload.taggings_with_delete_all_count", -1 do
post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first)
@@ -800,7 +830,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_with_dependent_destroy
post = posts(:welcome)
- post.update_columns(taggings_with_destroy_count: post.taggings_count)
+ post.update_columns(taggings_with_destroy_count: post.tags_count)
assert_difference "post.reload.taggings_with_destroy_count", -1 do
post.taggings_with_destroy.delete(post.taggings_with_destroy.first)
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 8641584c0c..a85e020f0c 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -489,7 +489,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
tag = post.tags.create!(:name => 'doomed')
- assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do
+ assert_difference ['post.reload.tags_count'], -1 do
posts(:welcome).tags.delete(tag)
end
end
@@ -499,7 +499,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag = post.tags.create!(:name => 'doomed')
post.update_columns(tags_with_destroy_count: post.tags.count)
- assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do
+ assert_difference ['post.reload.tags_with_destroy_count'], -1 do
posts(:welcome).tags_with_destroy.delete(tag)
end
end
@@ -509,7 +509,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag = post.tags.create!(:name => 'doomed')
post.update_columns(tags_with_nullify_count: post.tags.count)
- assert_no_difference 'post.reload.taggings_count' do
+ assert_no_difference 'post.reload.tags_count' do
assert_difference 'post.reload.tags_with_nullify_count', -1 do
posts(:welcome).tags_with_nullify.delete(tag)
end
@@ -524,14 +524,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag.tagged_posts = []
post.reload
- assert_equal(post.taggings.count, post.taggings_count)
+ assert_equal(post.taggings.count, post.tags_count)
end
def test_update_counter_caches_on_destroy
post = posts(:welcome)
tag = post.tags.create!(name: 'doomed')
- assert_difference 'post.reload.taggings_count', -1 do
+ assert_difference 'post.reload.tags_count', -1 do
tag.tagged_posts.destroy(post)
end
end
@@ -1139,4 +1139,31 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size
assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size
end
+
+ def test_has_many_through_add_with_sti_middle_relation
+ club = SuperClub.create!(name: 'Fight Club')
+ member = Member.create!(name: 'Tyler Durden')
+
+ club.members << member
+ assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count
+ end
+
+ class ClubWithCallbacks < ActiveRecord::Base
+ self.table_name = 'clubs'
+ after_create :add_a_member
+
+ has_many :memberships, inverse_of: :club, foreign_key: :club_id
+ has_many :members, through: :memberships
+
+ def add_a_member
+ members << Member.last
+ end
+ end
+
+ def test_has_many_with_callback_before_association
+ Member.create!
+ club = ClubWithCallbacks.create!
+
+ assert_equal 1, club.reload.memberships.count
+ end
end
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index aabeea025f..cace7ba142 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -326,11 +326,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_belongs_to_polymorphic_with_counter_cache
- assert_equal 1, posts(:welcome)[:taggings_count]
+ assert_equal 1, posts(:welcome)[:tags_count]
tagging = posts(:welcome).taggings.create(:tag => tags(:general))
- assert_equal 2, posts(:welcome, :reload)[:taggings_count]
+ assert_equal 2, posts(:welcome, :reload)[:tags_count]
tagging.destroy
- assert_equal 1, posts(:welcome, :reload)[:taggings_count]
+ assert_equal 1, posts(:welcome, :reload)[:tags_count]
end
def test_unavailable_through_reflection
@@ -489,7 +489,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 1, post_thinking.tags.size)
+ assert_equal(count + 1, post_thinking.reload.tags.size)
assert_equal(count + 1, post_thinking.tags(true).size)
assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo')
@@ -497,7 +497,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 2, post_thinking.tags.size)
+ assert_equal(count + 2, post_thinking.reload.tags.size)
assert_equal(count + 2, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) }
@@ -505,7 +505,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 4, post_thinking.tags.size)
+ assert_equal(count + 4, post_thinking.reload.tags.size)
assert_equal(count + 4, post_thinking.tags(true).size)
# Raises if the wrong reflection name is used to set the Edge belongs_to
@@ -554,34 +554,35 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_associate_when_deleting_from_has_many_through
count = posts(:thinking).tags.count
- tags_before = posts(:thinking).tags
+ tags_before = posts(:thinking).tags.sort
tag = Tag.create!(:name => 'doomed')
post_thinking = posts(:thinking)
post_thinking.tags << tag
assert_equal(count + 1, post_thinking.taggings(true).size)
- assert_equal(count + 1, post_thinking.tags(true).size)
+ assert_equal(count + 1, post_thinking.reload.tags(true).size)
+ assert_not_equal(tags_before, post_thinking.tags.sort)
assert_nothing_raised { post_thinking.tags.delete(tag) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
assert_equal(count, post_thinking.taggings(true).size)
- assert_equal(tags_before.sort, post_thinking.tags.sort)
+ assert_equal(tags_before, post_thinking.tags.sort)
end
def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags
count = posts(:thinking).tags.count
- tags_before = posts(:thinking).tags
+ tags_before = posts(:thinking).tags.sort
doomed = Tag.create!(:name => 'doomed')
doomed2 = Tag.create!(:name => 'doomed2')
quaked = Tag.create!(:name => 'quaked')
post_thinking = posts(:thinking)
post_thinking.tags << doomed << doomed2
- assert_equal(count + 2, post_thinking.tags(true).size)
+ assert_equal(count + 2, post_thinking.reload.tags(true).size)
assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
- assert_equal(tags_before.sort, post_thinking.tags.sort)
+ assert_equal(tags_before, post_thinking.tags.sort)
end
def test_deleting_junk_from_has_many_through_should_raise_type_mismatch
diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb
index bc3e9a8cf5..b352d1a6c2 100644
--- a/activerecord/test/cases/attribute_decorators_test.rb
+++ b/activerecord/test/cases/attribute_decorators_test.rb
@@ -98,17 +98,7 @@ module ActiveRecord
assert_equal 'Hello! decorated!', model.a_string
assert_equal 'whatever', model.another_string
assert_equal 'Hello! decorated! decorated!', child.a_string
- # We are round tripping the default, and we don't undo our decoration
- assert_equal 'whatever decorated! decorated!', child.another_string
- end
-
- test "defaults are decorated on the column" do
- Model.attribute :a_string, Type::String.new, default: 'whatever'
- Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
-
- column = Model.columns_hash['a_string']
-
- assert_equal 'whatever decorated!', column.default
+ assert_equal 'whatever decorated!', child.another_string
end
class Multiplier < SimpleDelegator
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index f832ca3451..2048e0d5ad 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -253,6 +253,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes
end
+ def test_attributes_without_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers_projects'
+ end
+
+ assert_equal klass.column_names, klass.new.attributes.keys
+ assert_not klass.new.attributes.key?('id')
+ end
+
def test_hashes_not_mangled
new_topic = { :title => "New Topic" }
new_topic_values = { :title => "AnotherTopic" }
@@ -831,6 +840,37 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
+ def test_attribute_method?
+ assert @target.attribute_method?(:title)
+ assert @target.attribute_method?(:title=)
+ assert_not @target.attribute_method?(:wibble)
+ end
+
+ def test_attribute_method_returns_false_if_table_does_not_exist
+ @target.table_name = 'wibble'
+ assert_not @target.attribute_method?(:title)
+ end
+
+ def test_attribute_names_on_new_record
+ model = @target.new
+
+ assert_equal @target.column_names, model.attribute_names
+ end
+
+ def test_attribute_names_on_queried_record
+ model = @target.last!
+
+ assert_equal @target.column_names, model.attribute_names
+ end
+
+ def test_attribute_names_with_custom_select
+ model = @target.select('id').last!
+
+ assert_equal ['id'], model.attribute_names
+ # Sanity check, make sure other columns exist
+ assert_not_equal ['id'], @target.column_names
+ end
+
private
def new_topic_like_ar_class(&block)
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
new file mode 100644
index 0000000000..cdbb11fa32
--- /dev/null
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -0,0 +1,165 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class AttributeSetTest < ActiveRecord::TestCase
+ test "building a new set from raw attributes" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2.2, attributes[:bar].value
+ assert_equal :foo, attributes[:foo].name
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "building with custom types" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database({ foo: '3.3', bar: '4.4' }, { bar: Type::Integer.new })
+
+ assert_equal 3.3, attributes[:foo].value
+ assert_equal 4, attributes[:bar].value
+ end
+
+ test "[] returns a null object" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database(foo: '3.3')
+
+ assert_equal '3.3', attributes[:foo].value_before_type_cast
+ assert_equal nil, attributes[:bar].value_before_type_cast
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "duping creates a new hash and dups each attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: 'foo')
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << 'bar'
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
+ assert_equal 'foo', attributes[:bar].value
+ assert_equal 'foobar', duped[:bar].value
+ end
+
+ test "freezing cloned set does not freeze original" do
+ attributes = AttributeSet.new({})
+ clone = attributes.clone
+
+ clone.freeze
+
+ assert clone.frozen?
+ assert_not attributes.frozen?
+ end
+
+ test "to_hash returns a hash of the type cast values" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
+ end
+
+ test "values_before_type_cast" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast)
+ end
+
+ test "known columns are built with uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes[:foo].initialized?
+ assert_not attributes[:bar].initialized?
+ end
+
+ test "uninitialized attributes are not included in the attributes hash" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal({ foo: 1 }, attributes.to_hash)
+ end
+
+ test "uninitialized attributes are not included in keys" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal [:foo], attributes.keys
+ end
+
+ test "uninitialized attributes return false for include?" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes.include?(:foo)
+ assert_not attributes.include?(:bar)
+ end
+
+ test "unknown attributes return false for include?" do
+ attributes = attributes_with_uninitialized_key
+ assert_not attributes.include?(:wibble)
+ end
+
+ test "fetch_value returns the value for the given initialized attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal 1, attributes.fetch_value(:foo)
+ assert_equal 2.2, attributes.fetch_value(:bar)
+ end
+
+ test "fetch_value returns nil for unknown attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:wibble)
+ end
+
+ test "fetch_value uses the given block for uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ value = attributes.fetch_value(:bar) { |n| n.to_s + '!' }
+ assert_equal 'bar!', value
+ end
+
+ test "fetch_value returns nil for uninitialized attributes if no block is given" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:bar)
+ end
+
+ class MyType
+ def type_cast_from_user(value)
+ return if value.nil?
+ value + " from user"
+ end
+
+ def type_cast_from_database(value)
+ return if value.nil?
+ value + " from database"
+ end
+ end
+
+ test "write_from_database sets the attribute with database typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_database(:foo, "value")
+
+ assert_equal "value from database", attributes.fetch_value(:foo)
+ end
+
+ test "write_from_user sets the attribute with user typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_user(:foo, "value")
+
+ assert_equal "value from user", attributes.fetch_value(:foo)
+ end
+
+ def attributes_with_uninitialized_key
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ builder.build_from_database(foo: '1.1')
+ end
+ end
+end
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
index 57dd2e9a5e..24452fdec2 100644
--- a/activerecord/test/cases/attribute_test.rb
+++ b/activerecord/test/cases/attribute_test.rb
@@ -13,7 +13,7 @@ module ActiveRecord
test "from_database + read type casts from database" do
@type.expect(:type_cast_from_database, 'type cast from database', ['a value'])
- attribute = Attribute.from_database('a value', @type)
+ attribute = Attribute.from_database(nil, 'a value', @type)
type_cast_value = attribute.value
@@ -22,7 +22,7 @@ module ActiveRecord
test "from_user + read type casts from user" do
@type.expect(:type_cast_from_user, 'type cast from user', ['a value'])
- attribute = Attribute.from_user('a value', @type)
+ attribute = Attribute.from_user(nil, 'a value', @type)
type_cast_value = attribute.value
@@ -31,7 +31,7 @@ module ActiveRecord
test "reading memoizes the value" do
@type.expect(:type_cast_from_database, 'from the database', ['whatever'])
- attribute = Attribute.from_database('whatever', @type)
+ attribute = Attribute.from_database(nil, 'whatever', @type)
type_cast_value = attribute.value
second_read = attribute.value
@@ -42,14 +42,14 @@ module ActiveRecord
test "reading memoizes falsy values" do
@type.expect(:type_cast_from_database, false, ['whatever'])
- attribute = Attribute.from_database('whatever', @type)
+ attribute = Attribute.from_database(nil, 'whatever', @type)
attribute.value
attribute.value
end
test "read_before_typecast returns the given value" do
- attribute = Attribute.from_database('raw value', @type)
+ attribute = Attribute.from_database(nil, 'raw value', @type)
raw_value = attribute.value_before_type_cast
@@ -59,7 +59,7 @@ module ActiveRecord
test "from_database + read_for_database type casts to and from database" do
@type.expect(:type_cast_from_database, 'read from database', ['whatever'])
@type.expect(:type_cast_for_database, 'ready for database', ['read from database'])
- attribute = Attribute.from_database('whatever', @type)
+ attribute = Attribute.from_database(nil, 'whatever', @type)
type_cast_for_database = attribute.value_for_database
@@ -69,7 +69,7 @@ module ActiveRecord
test "from_user + read_for_database type casts from the user to the database" do
@type.expect(:type_cast_from_user, 'read from user', ['whatever'])
@type.expect(:type_cast_for_database, 'ready for database', ['read from user'])
- attribute = Attribute.from_user('whatever', @type)
+ attribute = Attribute.from_user(nil, 'whatever', @type)
type_cast_for_database = attribute.value_for_database
@@ -78,7 +78,7 @@ module ActiveRecord
test "duping dups the value" do
@type.expect(:type_cast_from_database, 'type cast', ['a value'])
- attribute = Attribute.from_database('a value', @type)
+ attribute = Attribute.from_database(nil, 'a value', @type)
value_from_orig = attribute.value
value_from_clone = attribute.dup.value
@@ -90,14 +90,53 @@ module ActiveRecord
test "duping does not dup the value if it is not dupable" do
@type.expect(:type_cast_from_database, false, ['a value'])
- attribute = Attribute.from_database('a value', @type)
+ attribute = Attribute.from_database(nil, 'a value', @type)
assert_same attribute.value, attribute.dup.value
end
test "duping does not eagerly type cast if we have not yet type cast" do
- attribute = Attribute.from_database('a value', @type)
+ attribute = Attribute.from_database(nil, 'a value', @type)
attribute.dup
end
+
+ class MyType
+ def type_cast_from_user(value)
+ value + " from user"
+ end
+
+ def type_cast_from_database(value)
+ value + " from database"
+ end
+ end
+
+ test "with_value_from_user returns a new attribute with the value from the user" do
+ old = Attribute.from_database(nil, "old", MyType.new)
+ new = old.with_value_from_user("new")
+
+ assert_equal "old from database", old.value
+ assert_equal "new from user", new.value
+ end
+
+ test "with_value_from_database returns a new attribute with the value from the database" do
+ old = Attribute.from_user(nil, "old", MyType.new)
+ new = old.with_value_from_database("new")
+
+ assert_equal "old from user", old.value
+ assert_equal "new from database", new.value
+ end
+
+ test "uninitialized attributes yield their name if a block is given to value" do
+ block = proc { |name| name.to_s + "!" }
+ foo = Attribute.uninitialized(:foo, nil)
+ bar = Attribute.uninitialized(:bar, nil)
+
+ assert_equal "foo!", foo.value(&block)
+ assert_equal "bar!", bar.value(&block)
+ end
+
+ test "uninitialized attributes have no value" do
+ assert_nil Attribute.uninitialized(:foo, nil).value
+ end
end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 222b1505a8..319ea9260a 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -53,11 +53,6 @@ class CalculationsTest < ActiveRecord::TestCase
assert_nil NumericData.average(:bank_balance)
end
- def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal
- assert_equal 0, NumericData.all.send(:type_cast_calculated_value, 0, nil, 'avg')
- assert_equal 53.0, NumericData.all.send(:type_cast_calculated_value, 53, nil, 'avg')
- end
-
def test_should_get_maximum_of_field
assert_equal 60, Account.maximum(:credit_limit)
end
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index ecad7c942f..c7531f5418 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -45,8 +45,8 @@ module ActiveRecord
@cache = Marshal.load(Marshal.dump(@cache))
- assert_equal 12, @cache.columns('posts').size
- assert_equal 12, @cache.columns_hash('posts').size
+ assert_equal 11, @cache.columns('posts').size
+ assert_equal 11, @cache.columns_hash('posts').size
assert @cache.tables('posts')
assert_equal 'id', @cache.primary_keys('posts')
end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 92144bc802..c089e63128 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -154,7 +154,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
t.column :omit, :integer, :null => false
end
- assert_equal 0, klass.columns_hash['zero'].default
+ assert_equal '0', klass.columns_hash['zero'].default
assert !klass.columns_hash['zero'].null
# 0 in MySQL 4, nil in 5.
assert [0, nil].include?(klass.columns_hash['omit'].default)
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index bd77c412f6..40e51a0cdc 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -144,8 +144,8 @@ class FinderTest < ActiveRecord::TestCase
def test_exists_with_distinct_association_includes_limit_and_order
author = Author.first
- assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists?
- assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists?
+ assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(0).exists?
+ assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(1).exists?
end
def test_exists_with_empty_table_and_no_args_given
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 9b26c30d14..c66eaf1ee1 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -68,9 +68,9 @@ module ActiveRecord
five = columns.detect { |c| c.name == "five" } unless mysql
assert_equal "hello", one.default
- assert_equal true, two.default
- assert_equal false, three.default
- assert_equal 1, four.default
+ assert_equal true, two.type_cast_from_database(two.default)
+ assert_equal false, three.type_cast_from_database(three.default)
+ assert_equal '1', four.default
assert_equal "hello", five.default unless mysql
end
@@ -275,7 +275,7 @@ module ActiveRecord
person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99
person_klass.reset_column_information
- assert_equal 99, person_klass.columns_hash["wealth"].default
+ assert_equal 99, person_klass.column_defaults["wealth"]
assert_equal false, person_klass.columns_hash["wealth"].null
# Oracle needs primary key value from sequence
if current_adapter?(:OracleAdapter)
@@ -287,20 +287,20 @@ module ActiveRecord
# change column default to see that column doesn't lose its not null definition
person_klass.connection.change_column_default "testings", "wealth", 100
person_klass.reset_column_information
- assert_equal 100, person_klass.columns_hash["wealth"].default
+ assert_equal 100, person_klass.column_defaults["wealth"]
assert_equal false, person_klass.columns_hash["wealth"].null
# rename column to see that column doesn't lose its not null and/or default definition
person_klass.connection.rename_column "testings", "wealth", "money"
person_klass.reset_column_information
assert_nil person_klass.columns_hash["wealth"]
- assert_equal 100, person_klass.columns_hash["money"].default
+ assert_equal 100, person_klass.column_defaults["money"]
assert_equal false, person_klass.columns_hash["money"].null
# change column
person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000
person_klass.reset_column_information
- assert_equal 1000, person_klass.columns_hash["money"].default
+ assert_equal 1000, person_klass.column_defaults["money"]
assert_equal false, person_klass.columns_hash["money"].null
# change column, make it nullable and clear default
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index a7c287515d..4e6d7963aa 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -53,13 +53,13 @@ module ActiveRecord
add_column 'test_models', 'salary', :integer, :default => 70000
default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default
- assert_equal 70000, default_before
+ assert_equal '70000', default_before
rename_column "test_models", "salary", "annual_salary"
assert TestModel.column_names.include?("annual_salary")
default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default
- assert_equal 70000, default_after
+ assert_equal '70000', default_after
end
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
@@ -193,14 +193,21 @@ module ActiveRecord
old_columns = connection.columns(TestModel.table_name)
assert old_columns.find { |c|
- c.name == 'approved' && c.type == :boolean && c.default == true
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' && c.type == :boolean && default == true
}
change_column :test_models, :approved, :boolean, :default => false
new_columns = connection.columns(TestModel.table_name)
- assert_not new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true }
- assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false }
+ assert_not new_columns.find { |c|
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' and c.type == :boolean and default == true
+ }
+ assert new_columns.find { |c|
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' and c.type == :boolean and default == false
+ }
change_column :test_models, :approved, :boolean, :default => true
end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index 1c0134843b..e955beae1a 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -270,6 +270,31 @@ module ActiveRecord
enable = @recorder.inverse_of :disable_extension, ['uuid-ossp']
assert_equal [:enable_extension, ['uuid-ossp'], nil], enable
end
+
+ def test_invert_add_foreign_key
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people]
+ assert_equal [:remove_foreign_key, [:dogs, :people]], enable
+ end
+
+ def test_invert_add_foreign_key_with_column
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"]
+ assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable
+ end
+
+ def test_invert_add_foreign_key_with_column_and_name
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]
+ assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable
+ end
+
+ def test_remove_foreign_key_is_irreversible
+ assert_raises ActiveRecord::IrreversibleMigration do
+ @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"]
+ end
+
+ assert_raises ActiveRecord::IrreversibleMigration do
+ @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"]
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
new file mode 100644
index 0000000000..c985092b4c
--- /dev/null
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -0,0 +1,242 @@
+require 'cases/helper'
+require 'support/ddl_helper'
+require 'support/schema_dumping_helper'
+
+if ActiveRecord::Base.connection.supports_foreign_keys?
+module ActiveRecord
+ class Migration
+ class ForeignKeyTest < ActiveRecord::TestCase
+ include DdlHelper
+ include SchemaDumpingHelper
+
+ class Rocket < ActiveRecord::Base
+ end
+
+ class Astronaut < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "rockets" do |t|
+ t.string :name
+ end
+
+ @connection.create_table "astronauts" do |t|
+ t.string :name
+ t.references :rocket
+ end
+ end
+
+ teardown do
+ if defined?(@connection)
+ @connection.execute "DROP TABLE IF EXISTS astronauts"
+ @connection.execute "DROP TABLE IF EXISTS rockets"
+ end
+ end
+
+ def test_foreign_keys
+ foreign_keys = @connection.foreign_keys("fk_test_has_fk")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "fk_test_has_fk", fk.from_table
+ assert_equal "fk_test_has_pk", fk.to_table
+ assert_equal "fk_id", fk.column
+ assert_equal "pk_id", fk.primary_key
+ assert_equal "fk_name", fk.name
+ end
+
+ def test_add_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_match(/^fk_rails_.{10}$/, fk.name)
+ end
+
+ def test_add_foreign_key_with_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_match(/^fk_rails_.{10}$/, fk.name)
+ end
+
+ def test_add_foreign_key_with_non_standard_primary_key
+ with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do
+ @connection.add_foreign_key(:astronauts, :space_shuttles,
+ column: "rocket_id", primary_key: "pk", name: "custom_pk")
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "space_shuttles", fk.to_table
+ assert_equal "pk", fk.primary_key
+
+ @connection.remove_foreign_key :astronauts, name: "custom_pk"
+ end
+ end
+
+ def test_add_on_delete_restrict_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ # ON DELETE RESTRICT is the default on MySQL
+ assert_equal nil, fk.on_delete
+ else
+ assert_equal :restrict, fk.on_delete
+ end
+ end
+
+ def test_add_on_delete_cascade_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :cascade, fk.on_delete
+ end
+
+ def test_add_on_delete_nullify_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_delete
+ end
+
+ def test_on_update_and_on_delete_raises_with_invalid_values
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid
+ end
+
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid
+ end
+ end
+
+ def test_add_foreign_key_with_on_update
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_update
+ end
+
+ def test_remove_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, :rockets
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_key_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, column: "rocket_id"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_key_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, name: "fancy_named_fk"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_non_existing_foreign_key_raises
+ assert_raises ArgumentError do
+ @connection.remove_foreign_key :astronauts, :rockets
+ end
+ end
+
+ def test_schema_dumping
+ @connection.add_foreign_key :astronauts, :rockets
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output
+ end
+
+ def test_schema_dumping_with_options
+ output = dump_table_schema "fk_test_has_fk"
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
+ end
+
+ def test_schema_dumping_on_delete_and_on_update_options
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade
+
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output
+ end
+
+ class CreateCitiesAndHousesMigration < ActiveRecord::Migration
+ def change
+ create_table("cities") { |t| }
+
+ create_table("houses") do |t|
+ t.column :city_id, :integer
+ end
+ add_foreign_key :houses, :cities, column: "city_id"
+ end
+ end
+
+ def test_add_foreign_key_is_reversible
+ migration = CreateCitiesAndHousesMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("houses").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ end
+ end
+ end
+end
+else
+module ActiveRecord
+ class Migration
+ class NoForeignKeySupportTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_add_foreign_key_should_be_noop
+ @connection.add_foreign_key :clubs, :categories
+ end
+
+ def test_remove_foreign_key_should_be_noop
+ @connection.remove_foreign_key :clubs, :categories
+ end
+
+ def test_foreign_keys_should_raise_not_implemented
+ assert_raises NotImplementedError do
+ @connection.foreign_keys("clubs")
+ end
+ end
+ end
+ end
+end
+end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 9855835e27..6b840e16bb 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -567,7 +567,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert_equal 8, columns.size
[:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type }
- assert_equal 0, column(:age).default
+ assert_equal '0', column(:age).default
end
def test_removing_columns
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index 28341d0b42..1192ecd6b4 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -858,4 +858,11 @@ class PersistenceTest < ActiveRecord::TestCase
post.body
end
end
+
+ def test_reload_removes_custom_selects
+ post = Post.select('posts.*, 1 as wibble').last!
+
+ assert_equal 1, post[:wibble]
+ assert_nil post.reload[:wibble]
+ end
end
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index f43483d291..b04df7ce43 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -134,14 +134,22 @@ class PrimaryKeysTest < ActiveRecord::TestCase
end
def test_primary_key_returns_value_if_it_exists
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers'
+ end
+
if ActiveRecord::Base.connection.supports_primary_key?
- assert_equal 'id', ActiveRecord::Base.connection.primary_key('developers')
+ assert_equal 'id', klass.primary_key
end
end
def test_primary_key_returns_nil_if_it_does_not_exist
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers_projects'
+ end
+
if ActiveRecord::Base.connection.supports_primary_key?
- assert_nil ActiveRecord::Base.connection.primary_key('developers_projects')
+ assert_nil klass.primary_key
end
end
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index bbd5298da1..70d9b9dbf5 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -93,12 +93,10 @@ module ActiveRecord
def test_quote_true
assert_equal @quoter.quoted_true, @quoter.quote(true, nil)
- assert_equal '1', @quoter.quote(true, Type::Integer.new)
end
def test_quote_false
assert_equal @quoter.quoted_false, @quoter.quote(false, nil)
- assert_equal '0', @quoter.quote(false, Type::Integer.new)
end
def test_quote_float
@@ -157,25 +155,6 @@ module ActiveRecord
assert_equal "'lo\\\\l'", @quoter.quote(string, nil)
end
- def test_quote_string_int_column
- assert_equal "1", @quoter.quote('1', Type::Integer.new)
- assert_equal "1", @quoter.quote('1.2', Type::Integer.new)
- end
-
- def test_quote_string_float_column
- assert_equal "1.0", @quoter.quote('1', Type::Float.new)
- assert_equal "1.2", @quoter.quote('1.2', Type::Float.new)
- end
-
- def test_quote_as_mb_chars_binary_column
- string = ActiveSupport::Multibyte::Chars.new('lo\l')
- assert_equal "'lo\\\\l'", @quoter.quote(string, Type::Binary.new)
- end
-
- def test_quote_binary_without_string_to_binary
- assert_equal "'lo\\\\l'", @quoter.quote('lo\l', Type::Binary.new)
- end
-
def test_string_with_crazy_column
assert_equal "'lo\\\\l'", @quoter.quote('lo\l')
end
@@ -183,10 +162,6 @@ module ActiveRecord
def test_quote_duration
assert_equal "1800", @quoter.quote(30.minutes)
end
-
- def test_quote_duration_int_column
- assert_equal "7200", @quoter.quote(2.hours, Type::Integer.new)
- end
end
end
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index c6e73f78cc..acbd065649 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -80,24 +80,10 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal :integer, @first.column_for_attribute("id").type
end
- def test_non_existent_columns_return_null_object
- column = @first.column_for_attribute("attribute_that_doesnt_exist")
- assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column
- assert_equal "attribute_that_doesnt_exist", column.name
- assert_equal nil, column.sql_type
- assert_equal nil, column.type
- assert_not column.number?
- assert_not column.text?
- assert_not column.binary?
- end
-
- def test_non_existent_columns_are_identity_types
- column = @first.column_for_attribute("attribute_that_doesnt_exist")
- object = Object.new
-
- assert_equal object, column.type_cast_from_database(object)
- assert_equal object, column.type_cast_from_user(object)
- assert_equal object, column.type_cast_for_database(object)
+ def test_non_existent_columns_return_nil
+ assert_deprecated do
+ assert_nil @first.column_for_attribute("attribute_that_doesnt_exist")
+ end
end
def test_reflection_klass_for_nested_class_name
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index fb0b906c07..3280945d09 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -235,5 +235,33 @@ module ActiveRecord
posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings)
assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length
end
+
+ class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
+ def type
+ :string
+ end
+
+ def type_cast_from_database(value)
+ raise value unless value == "type cast for database"
+ "type cast from database"
+ end
+
+ def type_cast_for_database(value)
+ raise value unless value == "value from user"
+ "type cast for database"
+ end
+ end
+
+ class UpdateAllTestModel < ActiveRecord::Base
+ self.table_name = 'posts'
+
+ attribute :body, EnsureRoundTripTypeCasting.new
+ end
+
+ def test_update_all_goes_through_normal_type_casting
+ UpdateAllTestModel.update_all(body: "value from user", type: nil) # No STI
+
+ assert_equal "type cast from database", UpdateAllTestModel.first.body
+ end
end
end
diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb
index 2131b32a0c..d6decafad9 100644
--- a/activerecord/test/cases/result_test.rb
+++ b/activerecord/test/cases/result_test.rb
@@ -10,7 +10,7 @@ module ActiveRecord
])
end
- def test_to_hash_returns_row_hashes
+ test "to_hash returns row_hashes" do
assert_equal [
{'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'},
{'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'},
@@ -18,13 +18,13 @@ module ActiveRecord
], result.to_hash
end
- def test_each_with_block_returns_row_hashes
+ test "each with block returns row hashes" do
result.each do |row|
assert_equal ['col_1', 'col_2'], row.keys
end
end
- def test_each_without_block_returns_an_enumerator
+ test "each without block returns an enumerator" do
result.each.with_index do |row, index|
assert_equal ['col_1', 'col_2'], row.keys
assert_kind_of Integer, index
@@ -32,9 +32,45 @@ module ActiveRecord
end
if Enumerator.method_defined? :size
- def test_each_without_block_returns_a_sized_enumerator
+ test "each without block returns a sized enumerator" do
assert_equal 3, result.each.size
end
end
+
+ test "cast_values returns rows after type casting" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new, "col2" => Type::Float.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1, 2.2], [3, 4.4]], result.cast_values
+ end
+
+ test "cast_values uses identity type for unknown types" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1, "2.2"], [3, "4.4"]], result.cast_values
+ end
+
+ test "cast_values returns single dimensional array if single column" do
+ values = [["1.1"], ["3.3"]]
+ columns = ["col1"]
+ types = { "col1" => Type::Integer.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [1, 3], result.cast_values
+ end
+
+ test "cast_values can receive types to use instead" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new, "col2" => Type::Float.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1.1, 2.2], [3.3, 4.4]], result.cast_values("col1" => Type::Float.new)
+ end
end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 5f02d39e32..4e71d04bc0 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -372,15 +372,28 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{create_table "subscribers", id: false}, output
end
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ def test_foreign_keys_are_dumped_at_the_bottom_to_circumvent_dependency_issues
+ output = standard_dump
+ assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output)
+ end
+ end
+
class CreateDogMigration < ActiveRecord::Migration
def up
+ create_table("dog_owners") do |t|
+ end
+
create_table("dogs") do |t|
t.column :name, :string
+ t.column :owner_id, :integer
end
add_index "dogs", [:name]
+ add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys?
end
def down
drop_table("dogs")
+ drop_table("dog_owners")
end
end
@@ -396,13 +409,17 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_no_match %r{create_table "foo_.+_bar"}, output
assert_no_match %r{add_index "foo_.+_bar"}, output
assert_no_match %r{create_table "schema_migrations"}, output
+
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ assert_no_match %r{add_foreign_key "foo_.+_bar"}, output
+ assert_no_match %r{add_foreign_key "[^"]+", "foo_.+_bar"}, output
+ end
ensure
migration.migrate(:down)
ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = ''
$stdout = original
end
-
end
class SchemaDumperDefaultsTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb
index c62e22c902..47cf775cb6 100644
--- a/activerecord/test/cases/types_test.rb
+++ b/activerecord/test/cases/types_test.rb
@@ -72,11 +72,29 @@ module ActiveRecord
assert_nil type.type_cast_from_user(1.0/0.0)
end
+ def test_changing_integers
+ type = Type::Integer.new
+
+ assert type.changed?(5, 5, '5wibble')
+ assert_not type.changed?(5, 5, '5')
+ assert_not type.changed?(5, 5, '5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
def test_type_cast_float
type = Type::Float.new
assert_equal 1.0, type.type_cast_from_user("1")
end
+ def test_changing_float
+ type = Type::Float.new
+
+ assert type.changed?(5.0, 5.0, '5wibble')
+ assert_not type.changed?(5.0, 5.0, '5')
+ assert_not type.changed?(5.0, 5.0, '5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
def test_type_cast_decimal
type = Type::Decimal.new
assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0"))
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index a6e1dc72e5..55804f9576 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -78,6 +78,20 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal r, invalid.record
end
+ def test_validate_with_bang
+ assert_raise(ActiveRecord::RecordInvalid) do
+ WrongReply.new.validate!
+ end
+ end
+
+ def test_validate_with_bang_and_context
+ assert_raise(ActiveRecord::RecordInvalid) do
+ WrongReply.new.validate!(:special_case)
+ end
+ r = WrongReply.new(:title => "Valid title", :author_name => "secret", :content => "Good")
+ assert r.validate!(:special_case)
+ end
+
def test_exception_on_create_bang_many
assert_raise(ActiveRecord::RecordInvalid) do
WrongReply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
diff --git a/activerecord/test/fixtures/fk_test_has_pk.yml b/activerecord/test/fixtures/fk_test_has_pk.yml
index c93952180b..73882bac41 100644
--- a/activerecord/test/fixtures/fk_test_has_pk.yml
+++ b/activerecord/test/fixtures/fk_test_has_pk.yml
@@ -1,2 +1,2 @@
first:
- id: 1 \ No newline at end of file
+ pk_id: 1 \ No newline at end of file
diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml
index 7298096025..86d46f753a 100644
--- a/activerecord/test/fixtures/posts.yml
+++ b/activerecord/test/fixtures/posts.yml
@@ -4,7 +4,6 @@ welcome:
title: Welcome to the weblog
body: Such a lovely day
comments_count: 2
- taggings_count: 1
tags_count: 1
type: Post
@@ -14,7 +13,6 @@ thinking:
title: So I was thinking
body: Like I hopefully always am
comments_count: 1
- taggings_count: 1
tags_count: 1
type: SpecialPost
diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb
index a762ad4bb5..6ceafe5858 100644
--- a/activerecord/test/models/club.rb
+++ b/activerecord/test/models/club.rb
@@ -14,3 +14,10 @@ class Club < ActiveRecord::Base
"I'm sorry sir, this is a *private* club, not a *pirate* club"
end
end
+
+class SuperClub < ActiveRecord::Base
+ self.table_name = "clubs"
+
+ has_many :memberships, class_name: 'SuperMembership', foreign_key: 'club_id'
+ has_many :members, through: :memberships
+end
diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb
index a1cb8d62b6..3ea17c3abf 100644
--- a/activerecord/test/models/contact.rb
+++ b/activerecord/test/models/contact.rb
@@ -8,6 +8,7 @@ module ContactFakeColumns
table_name => 'id'
}
+ column :id, :integer
column :name, :string
column :age, :integer
column :avatar, :binary
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 5f01ab0a82..a29858213b 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -88,7 +88,7 @@ class Post < ActiveRecord::Base
has_and_belongs_to_many :categories
has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
- has_many :taggings, :as => :taggable
+ has_many :taggings, :as => :taggable, :counter_cache => :tags_count
has_many :tags, :through => :taggings do
def add_joins_and_select
select('tags.*, authors.id as author_id')
diff --git a/activerecord/test/models/publisher/article.rb b/activerecord/test/models/publisher/article.rb
index 03a277bbdd..d73a8eb936 100644
--- a/activerecord/test/models/publisher/article.rb
+++ b/activerecord/test/models/publisher/article.rb
@@ -1,3 +1,4 @@
class Publisher::Article < ActiveRecord::Base
has_and_belongs_to_many :magazines
+ has_and_belongs_to_many :tags
end
diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb
index f91f2ad2e9..a6c05da26a 100644
--- a/activerecord/test/models/tagging.rb
+++ b/activerecord/test/models/tagging.rb
@@ -8,6 +8,6 @@ class Tagging < ActiveRecord::Base
belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id'
belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id
belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key
- belongs_to :taggable, :polymorphic => true, :counter_cache => true
+ belongs_to :taggable, :polymorphic => true, :counter_cache => :tags_count
has_many :things, :through => :taggable
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index a7c7fc70bc..03d33c151a 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -62,6 +62,11 @@ ActiveRecord::Schema.define do
t.references :magazine
end
+ create_table :articles_tags, force: true do |t|
+ t.references :article
+ t.references :tag
+ end
+
create_table :audit_logs, force: true do |t|
t.column :message, :string, null: false
t.column :developer_id, :integer, null: false
@@ -185,7 +190,7 @@ ActiveRecord::Schema.define do
t.text :body, null: false
end
t.string :type
- t.integer :taggings_count, default: 0
+ t.integer :tags_count, default: 0
t.integer :children_count, default: 0
t.integer :parent_id
t.references :author, polymorphic: true
@@ -564,7 +569,6 @@ ActiveRecord::Schema.define do
end
t.string :type
t.integer :comments_count, default: 0
- t.integer :taggings_count, default: 0
t.integer :taggings_with_delete_all_count, default: 0
t.integer :taggings_with_destroy_count, default: 0
t.integer :tags_count, default: 0
@@ -851,10 +855,10 @@ ActiveRecord::Schema.define do
t.integer :fk_id, null: false
end
- create_table :fk_test_has_pk, force: true do |t|
+ create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t|
end
- execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})"
+ execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'pk_id'})"
execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})"
end
diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb
index b7aff4f47d..b5552c2755 100644
--- a/activerecord/test/schema/sqlite_specific_schema.rb
+++ b/activerecord/test/schema/sqlite_specific_schema.rb
@@ -7,7 +7,7 @@ ActiveRecord::Schema.define do
execute "DROP TABLE fk_test_has_pk" rescue nil
execute <<_SQL
CREATE TABLE 'fk_test_has_pk' (
- 'id' INTEGER NOT NULL PRIMARY KEY
+ 'pk_id' INTEGER NOT NULL PRIMARY KEY
);
_SQL
@@ -16,7 +16,7 @@ _SQL
'id' INTEGER NOT NULL PRIMARY KEY,
'fk_id' INTEGER NOT NULL,
- FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id')
+ FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id')
);
_SQL
-end \ No newline at end of file
+end
diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb
index 0107babaaf..43cb235e01 100644
--- a/activerecord/test/support/ddl_helper.rb
+++ b/activerecord/test/support/ddl_helper.rb
@@ -1,8 +1,8 @@
module DdlHelper
def with_example_table(connection, table_name, definition = nil)
- connection.exec_query("CREATE TABLE #{table_name}(#{definition})")
+ connection.execute("CREATE TABLE #{table_name}(#{definition})")
yield
ensure
- connection.exec_query("DROP TABLE #{table_name}")
+ connection.execute("DROP TABLE #{table_name}")
end
end
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index d513874aee..f8e8544e72 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,5 +1,13 @@
+* Make the `apply_inflections` method case-insensitive when checking
+ whether a word is uncountable or not.
+
+ *Robin Dupret*
+
+* Make Dependencies pass a name to NameError error.
+ *arthurnn*
+
* Fixed `ActiveSupport::Cache::FileStore` exploding with long paths.
- *Adam Panzer / Michael Grosser*
+ *Adam Panzer / Michael Grosser*
* Fixed `ActiveSupport::TimeWithZone#-` so precision is not unnecessarily lost
when working with objects with a nanosecond component.
@@ -24,12 +32,12 @@
* Fixed precision error in NumberHelper when using Rationals.
Before:
-
+
ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2
#=> "330.00"
-
+
After:
-
+
ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2
#=> "333.33"
diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb
index 1fec1bea0d..d06f22ad5c 100644
--- a/activesupport/lib/active_support/backtrace_cleaner.rb
+++ b/activesupport/lib/active_support/backtrace_cleaner.rb
@@ -13,7 +13,7 @@ module ActiveSupport
# can focus on the rest.
#
# bc = BacktraceCleaner.new
- # bc.add_filter { |line| line.gsub(Rails.root, '') } # strip the Rails.root prefix
+ # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
# bc.add_silencer { |line| line =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems
# bc.clean(exception.backtrace) # perform the cleanup
#
diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb
index fec8f7c0ec..ff1eb52843 100644
--- a/activesupport/lib/active_support/core_ext/securerandom.rb
+++ b/activesupport/lib/active_support/core_ext/securerandom.rb
@@ -1,4 +1,4 @@
-module SecureRandom
+module SecureRandom #:nodoc:
UUID_DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
UUID_URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
UUID_OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
@@ -10,7 +10,7 @@ module SecureRandom
# ::uuid_from_hash always generates the same UUID for a given name and namespace combination.
#
# See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt
- def self.uuid_from_hash(hash_class, uuid_namespace, name)
+ def self.uuid_from_hash(hash_class, uuid_namespace, name) #:nodoc:
if hash_class == Digest::MD5
version = 3
elsif hash_class == Digest::SHA1
@@ -31,12 +31,12 @@ module SecureRandom
end
# Convenience method for ::uuid_from_hash using Digest::MD5.
- def self.uuid_v3(uuid_namespace, name)
+ def self.uuid_v3(uuid_namespace, name) #:nodoc:
self.uuid_from_hash(Digest::MD5, uuid_namespace, name)
end
# Convenience method for ::uuid_from_hash using Digest::SHA1.
- def self.uuid_v5(uuid_namespace, name)
+ def self.uuid_v5(uuid_namespace, name) #:nodoc:
self.uuid_from_hash(Digest::SHA1, uuid_namespace, name)
end
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index 8a545e4386..93a11d4586 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -187,7 +187,7 @@ module ActiveSupport #:nodoc:
# top-level constant.
def guess_for_anonymous(const_name)
if Object.const_defined?(const_name)
- raise NameError, "#{const_name} cannot be autoloaded from an anonymous class or module"
+ raise NameError.new "#{const_name} cannot be autoloaded from an anonymous class or module", const_name
else
Object
end
@@ -516,9 +516,9 @@ module ActiveSupport #:nodoc:
end
end
- raise NameError,
- "uninitialized constant #{qualified_name}",
- caller.reject { |l| l.starts_with? __FILE__ }
+ name_error = NameError.new("uninitialized constant #{qualified_name}", const_name)
+ name_error.set_backtrace(caller.reject {|l| l.starts_with? __FILE__ })
+ raise name_error
end
# Remove the constants that have been autoloaded, and those that have been
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index eda0edff28..97401ccec7 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -160,7 +160,7 @@ module ActiveSupport
# uncountable 'money', 'information'
# uncountable %w( money information rice )
def uncountable(*words)
- (@uncountables << words).flatten!
+ @uncountables += words.flatten.map(&:downcase)
end
# Specifies a humanized form of a string by a regular expression rule or
diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
index 908af176be..68bda35980 100644
--- a/activesupport/lib/active_support/testing/isolation.rb
+++ b/activesupport/lib/active_support/testing/isolation.rb
@@ -70,14 +70,24 @@ module ActiveSupport
exit!
else
Tempfile.open("isolation") do |tmpfile|
- ENV["ISOLATION_TEST"] = self.class.name
- ENV["ISOLATION_OUTPUT"] = tmpfile.path
+ env = {
+ ISOLATION_TEST: self.class.name,
+ ISOLATION_OUTPUT: tmpfile.path
+ }
load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ")
- `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")}`
-
- ENV.delete("ISOLATION_TEST")
- ENV.delete("ISOLATION_OUTPUT")
+ orig_args = ORIG_ARGV.join(" ")
+ test_opts = "-n#{self.class.name}##{self.name}"
+ command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}"
+
+ # IO.popen lets us pass env in a cross-platform way
+ child = IO.popen([env, command])
+
+ begin
+ Process.wait(child.pid)
+ rescue Errno::ECHILD # The child process may exit before we wait
+ nil
+ end
return tmpfile.read.unpack("m")[0]
end
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index d55cc5d3b0..8287e62f4c 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -60,36 +60,25 @@ class CacheKeyTest < ActiveSupport::TestCase
end
def test_expand_cache_key_with_rails_cache_id
- begin
- ENV['RAILS_CACHE_ID'] = 'c99'
+ with_env('RAILS_CACHE_ID' => 'c99') do
assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo)
assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key([:foo])
assert_equal 'c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar])
assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key(:foo, :nm)
assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key([:foo], :nm)
assert_equal 'nm/c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm)
- ensure
- ENV['RAILS_CACHE_ID'] = nil
end
end
def test_expand_cache_key_with_rails_app_version
- begin
- ENV['RAILS_APP_VERSION'] = 'rails3'
+ with_env('RAILS_APP_VERSION' => 'rails3') do
assert_equal 'rails3/foo', ActiveSupport::Cache.expand_cache_key(:foo)
- ensure
- ENV['RAILS_APP_VERSION'] = nil
end
end
def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version
- begin
- ENV['RAILS_CACHE_ID'] = 'c99'
- ENV['RAILS_APP_VERSION'] = 'rails3'
+ with_env('RAILS_CACHE_ID' => 'c99', 'RAILS_APP_VERSION' => 'rails3') do
assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo)
- ensure
- ENV['RAILS_CACHE_ID'] = nil
- ENV['RAILS_APP_VERSION'] = nil
end
end
@@ -124,6 +113,16 @@ class CacheKeyTest < ActiveSupport::TestCase
def test_expand_cache_key_of_array_like_object
assert_equal 'foo/bar/baz', ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum)
end
+
+ private
+
+ def with_env(kv)
+ old_values = {}
+ kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value }
+ yield
+ ensure
+ old_values.each { |key, value| ENV[key] = value}
+ end
end
class CacheStoreSettingTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb
index 5d0af035cc..e89be25b53 100644
--- a/activesupport/test/core_ext/date_ext_test.rb
+++ b/activesupport/test/core_ext/date_ext_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'active_support/time'
require 'core_ext/date_and_time_behavior'
+require 'time_zone_test_helpers'
class DateExtCalculationsTest < ActiveSupport::TestCase
def date_time_init(year,month,day,*args)
@@ -8,6 +9,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
include DateAndTimeBehavior
+ include TimeZoneTestHelpers
def test_yesterday_in_calendar_reform
assert_equal Date.new(1582,10,4), Date.new(1582,10,15).yesterday
@@ -349,22 +351,6 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
Date.new(2005,2,28).advance(options)
assert_equal({ :years => 3, :months => 11, :days => 2 }, options)
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
-
- def with_tz_default(tz = nil)
- old_tz = Time.zone
- Time.zone = tz
- yield
- ensure
- Time.zone = old_tz
- end
end
class DateExtBehaviorTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index 224172e39f..2c08b46791 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'active_support/time'
require 'core_ext/date_and_time_behavior'
+require 'time_zone_test_helpers'
class DateTimeExtCalculationsTest < ActiveSupport::TestCase
def date_time_init(year,month,day,hour,minute,second,*args)
@@ -8,6 +9,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
include DateAndTimeBehavior
+ include TimeZoneTestHelpers
def test_to_s
datetime = DateTime.new(2005, 2, 21, 14, 30, 0, 0)
@@ -352,12 +354,4 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal 0, DateTime.civil(2000).nsec
assert_equal 500000000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).nsec
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index c8f17f4618..328521bdb5 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -2,8 +2,11 @@ require 'abstract_unit'
require 'active_support/inflector'
require 'active_support/time'
require 'active_support/json'
+require 'time_zone_test_helpers'
class DurationTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def test_is_a
d = 1.day
assert d.is_a?(ActiveSupport::Duration)
@@ -167,12 +170,4 @@ class DurationTest < ActiveSupport::TestCase
cased = case 1.day when 1.day then "ok" end
assert_equal cased, "ok"
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb
index dbc3ffd319..b82448458d 100644
--- a/activesupport/test/core_ext/numeric_ext_test.rb
+++ b/activesupport/test/core_ext/numeric_ext_test.rb
@@ -71,14 +71,6 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
assert_equal Time.utc(2005,2,28,15,15,10), Time.utc(2004,2,29,15,15,10) + 1.year
assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10) + 1.year
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
class NumericExtDateTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index 95df173880..515144245a 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -10,10 +10,12 @@ require 'active_support/time'
require 'active_support/core_ext/string/strip'
require 'active_support/core_ext/string/output_safety'
require 'active_support/core_ext/string/indent'
+require 'time_zone_test_helpers'
class StringInflectionsTest < ActiveSupport::TestCase
include InflectorTestCases
include ConstantizeTestCases
+ include TimeZoneTestHelpers
def test_strip_heredoc_on_an_empty_string
assert_equal '', ''.strip_heredoc
@@ -354,6 +356,8 @@ class StringAccessTest < ActiveSupport::TestCase
end
class StringConversionsTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def test_string_to_time
with_env_tz "Europe/Moscow" do
assert_equal Time.utc(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time(:utc)
@@ -523,14 +527,6 @@ class StringConversionsTest < ActiveSupport::TestCase
assert_nil "".to_date
assert_equal Date.new(Date.today.year, 2, 3), "Feb 3rd".to_date
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
class StringBehaviourTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index e0a4b1be3e..c8283cddc5 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'active_support/time'
require 'core_ext/date_and_time_behavior'
+require 'time_zone_test_helpers'
class TimeExtCalculationsTest < ActiveSupport::TestCase
def date_time_init(year,month,day,hour,minute,second,usec=0)
@@ -8,6 +9,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
include DateAndTimeBehavior
+ include TimeZoneTestHelpers
def test_seconds_since_midnight
assert_equal 1,Time.local(2005,1,1,0,0,1).seconds_since_midnight
@@ -847,15 +849,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
def test_all_year
assert_equal Time.local(2011,1,1,0,0,0)..Time.local(2011,12,31,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_year
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
-
end
class TimeExtMarshalingTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index 6d779bf3d5..75599a71c3 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -1,7 +1,9 @@
require 'abstract_unit'
require 'active_support/time'
+require 'time_zone_test_helpers'
class TimeWithZoneTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
def setup
@utc = Time.utc(2000, 1, 1, 0)
@@ -810,23 +812,17 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "undefined method `this_method_does_not_exist' for Fri, 31 Dec 1999 19:00:00 EST -05:00:Time", e.message
assert_no_match "rescue", e.backtrace.first
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def setup
- @t, @dt = Time.utc(2000), DateTime.civil(2000)
+ @t, @dt, @zone = Time.utc(2000), DateTime.civil(2000), Time.zone
end
def teardown
- Time.zone = nil
+ Time.zone = @zone
end
def test_in_time_zone
@@ -882,8 +878,6 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
def test_localtime
Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
assert_equal @dt.in_time_zone.localtime, @dt.in_time_zone.utc.to_time.getlocal
- ensure
- Time.zone = nil
end
def test_use_zone
@@ -922,6 +916,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
end
def test_time_zone_getter_and_setter_with_zone_default_set
+ old_zone_default = Time.zone_default
Time.zone_default = ActiveSupport::TimeZone['Alaska']
assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
Time.zone = ActiveSupport::TimeZone['Hawaii']
@@ -929,8 +924,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
Time.zone = nil
assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
ensure
- Time.zone = nil
- Time.zone_default = nil
+ Time.zone_default = old_zone_default
end
def test_time_zone_setter_is_thread_safe
@@ -1002,8 +996,6 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
assert_equal 'Eastern Time (US & Canada)', Time.current.time_zone.name
assert_equal Time.utc(2000), Time.current.time
end
- ensure
- Time.zone = nil
end
def test_time_in_time_zone_doesnt_affect_receiver
@@ -1014,25 +1006,15 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
assert_not time.utc?, 'time expected to be local, but is UTC'
end
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
class TimeWithZoneMethodsForDate < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def setup
@d = Date.civil(2000)
end
- def teardown
- Time.zone = nil
- end
-
def test_in_time_zone
with_tz_default 'Alaska' do
assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @d.in_time_zone.inspect
@@ -1065,28 +1047,17 @@ class TimeWithZoneMethodsForDate < ActiveSupport::TestCase
assert_raise(ArgumentError) { @d.in_time_zone(-15.hours) }
assert_raise(ArgumentError) { @d.in_time_zone(Object.new) }
end
-
- protected
- def with_tz_default(tz = nil)
- old_tz = Time.zone
- Time.zone = tz
- yield
- ensure
- Time.zone = old_tz
- end
end
class TimeWithZoneMethodsForString < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def setup
@s = "Sat, 01 Jan 2000 00:00:00"
@u = "Sat, 01 Jan 2000 00:00:00 UTC +00:00"
@z = "Fri, 31 Dec 1999 19:00:00 EST -05:00"
end
- def teardown
- Time.zone = nil
- end
-
def test_in_time_zone
with_tz_default 'Alaska' do
assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @s.in_time_zone.inspect
@@ -1141,13 +1112,4 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase
assert_raise(ArgumentError) { @u.in_time_zone(Object.new) }
assert_raise(ArgumentError) { @z.in_time_zone(Object.new) }
end
-
- protected
- def with_tz_default(tz = nil)
- old_tz = Time.zone
- Time.zone = tz
- yield
- ensure
- Time.zone = old_tz
- end
end
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index 4ca63b3417..a013aadd67 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -367,9 +367,11 @@ class DependenciesTest < ActiveSupport::TestCase
with_autoloading_fixtures do
e = assert_raise(NameError) { A::DoesNotExist.nil? }
assert_equal "uninitialized constant A::DoesNotExist", e.message
+ assert_equal :DoesNotExist, e.name
e = assert_raise(NameError) { A::B::DoesNotExist.nil? }
assert_equal "uninitialized constant A::B::DoesNotExist", e.message
+ assert_equal :DoesNotExist, e.name
end
end
@@ -537,6 +539,7 @@ class DependenciesTest < ActiveSupport::TestCase
mod = Module.new
e = assert_raise(NameError) { mod::E }
assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message
+ assert_equal :E, e.name
end
end
@@ -954,7 +957,7 @@ class DependenciesTest < ActiveSupport::TestCase
assert_kind_of Class, A::B # Necessary to load A::B for the test
ActiveSupport::Dependencies.mark_for_unload(A::B)
ActiveSupport::Dependencies.remove_unloadable_constants!
-
+
A::B # Make sure no circular dependency error
end
end
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index eb8b0d878e..58fdea0972 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -509,6 +509,14 @@ class InflectorTest < ActiveSupport::TestCase
end
end
+ def test_inflections_with_uncountable_words
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.uncountable "HTTP"
+ end
+
+ assert_equal "HTTP", ActiveSupport::Inflector.pluralize("HTTP")
+ end
+
# Dups the singleton and yields, restoring the original inflections later.
# Use this in tests what modify the state of the singleton.
#
diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb
index 07d7e530ca..80bf255080 100644
--- a/activesupport/test/json/decoding_test.rb
+++ b/activesupport/test/json/decoding_test.rb
@@ -73,22 +73,20 @@ class TestJSONDecoding < ActiveSupport::TestCase
TESTS.each_with_index do |(json, expected), index|
test "json decodes #{index}" do
- prev = ActiveSupport.parse_json_times
- ActiveSupport.parse_json_times = true
- silence_warnings do
- assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \
- failed for #{json}"
+ with_parse_json_times(true) do
+ silence_warnings do
+ assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \
+ failed for #{json}"
+ end
end
- ActiveSupport.parse_json_times = prev
end
end
test "json decodes time json with time parsing disabled" do
- prev = ActiveSupport.parse_json_times
- ActiveSupport.parse_json_times = false
- expected = {"a" => "2007-01-01 01:12:34 Z"}
- assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
- ActiveSupport.parse_json_times = prev
+ with_parse_json_times(false) do
+ expected = {"a" => "2007-01-01 01:12:34 Z"}
+ assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
+ end
end
def test_failed_json_decoding
@@ -101,5 +99,15 @@ class TestJSONDecoding < ActiveSupport::TestCase
def test_cannot_pass_unsupported_options
assert_raise(ArgumentError) { ActiveSupport::JSON.decode("", create_additions: true) }
end
+
+ private
+
+ def with_parse_json_times(value)
+ old_value = ActiveSupport.parse_json_times
+ ActiveSupport.parse_json_times = value
+ yield
+ ensure
+ ActiveSupport.parse_json_times = old_value
+ end
end
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index f22d7b8b02..ad358ad21d 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -4,8 +4,11 @@ require 'abstract_unit'
require 'active_support/core_ext/string/inflections'
require 'active_support/json'
require 'active_support/time'
+require 'time_zone_test_helpers'
class TestJSONEncoding < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
class Foo
def initialize(a, b)
@a, @b = a, b
@@ -491,31 +494,28 @@ EXPECTED
def test_twz_to_json_with_custom_time_precision
with_standard_json_time_format(true) do
- ActiveSupport::JSON::Encoding.time_precision = 0
- zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
- assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time)
+ with_time_precision(0) do
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
+ assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time)
+ end
end
- ensure
- ActiveSupport::JSON::Encoding.time_precision = 3
end
def test_time_to_json_with_custom_time_precision
with_standard_json_time_format(true) do
- ActiveSupport::JSON::Encoding.time_precision = 0
- assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000))
+ with_time_precision(0) do
+ assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000))
+ end
end
- ensure
- ActiveSupport::JSON::Encoding.time_precision = 3
end
def test_datetime_to_json_with_custom_time_precision
with_standard_json_time_format(true) do
- ActiveSupport::JSON::Encoding.time_precision = 0
- assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000))
+ with_time_precision(0) do
+ assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000))
+ end
end
- ensure
- ActiveSupport::JSON::Encoding.time_precision = 3
end
def test_twz_to_json_when_wrapping_a_date_time
@@ -530,17 +530,18 @@ EXPECTED
json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort
end
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
+ def with_standard_json_time_format(boolean = true)
+ old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean
yield
ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
+ ActiveSupport.use_standard_json_time_format = old
end
- def with_standard_json_time_format(boolean = true)
- old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean
+ def with_time_precision(value)
+ old_value = ActiveSupport::JSON::Encoding.time_precision
+ ActiveSupport::JSON::Encoding.time_precision = value
yield
ensure
- ActiveSupport.use_standard_json_time_format = old
+ ActiveSupport::JSON::Encoding.time_precision = old_value
end
end
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index 127bcc2b4d..b7a89ed332 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -1,7 +1,10 @@
require 'abstract_unit'
require 'active_support/time'
+require 'time_zone_test_helpers'
class TimeZoneTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def test_utc_to_local
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
assert_equal Time.utc(1999, 12, 31, 19), zone.utc_to_local(Time.utc(2000, 1)) # standard offset -0500
@@ -416,12 +419,4 @@ class TimeZoneTest < ActiveSupport::TestCase
assert ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Hawaii"])
assert !ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Kuala Lumpur"])
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb
new file mode 100644
index 0000000000..9632b89d09
--- /dev/null
+++ b/activesupport/test/time_zone_test_helpers.rb
@@ -0,0 +1,16 @@
+module TimeZoneTestHelpers
+ def with_tz_default(tz = nil)
+ old_tz = Time.zone
+ Time.zone = tz
+ yield
+ ensure
+ Time.zone = old_tz
+ end
+
+ def with_env_tz(new_tz = 'US/Eastern')
+ old_tz, ENV['TZ'] = ENV['TZ'], new_tz
+ yield
+ ensure
+ old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
+ end
+end
diff --git a/guides/assets/javascripts/guides.js b/guides/assets/javascripts/guides.js
index e4d25dfb21..a9c7f0d016 100644
--- a/guides/assets/javascripts/guides.js
+++ b/guides/assets/javascripts/guides.js
@@ -51,3 +51,9 @@ var guidesIndex = {
window.location = url;
}
};
+
+// Disable autolink inside example code blocks of guides.
+$(document).ready(function() {
+ SyntaxHighlighter.defaults['auto-links'] = false;
+ SyntaxHighlighter.all();
+});
diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md
index be007f93a7..ef294f55d7 100644
--- a/guides/source/4_2_release_notes.md
+++ b/guides/source/4_2_release_notes.md
@@ -25,6 +25,31 @@ guide.
Major Features
--------------
+### Foreign key support
+
+The migration DSL now supports adding and removing foreign keys. They are dumped
+to `schema.rb` as well. At this time, only the `mysql`, `mysql2` and `postgresql`
+adapters support foreign keys.
+
+```ruby
+# add a foreign key to `articles.author_id` referencing `authors.id`
+add_foreign_key :articles, :authors
+
+# add a foreign key to `articles.author_id` referencing `users.lng_id`
+add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+
+# remove the foreign key on `accounts.branch_id`
+remove_foreign_key :accounts, :branches
+
+# remove the foreign key on `accounts.owner_id`
+remove_foreign_key :accounts, column: :owner_id
+```
+
+See the API documentation on
+[add_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key)
+and
+[remove_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_foreign_key)
+for a full description.
Railties
@@ -115,6 +140,27 @@ for detailed changes.
([Pull Request](https://github.com/rails/rails/pull/14280))
+Action View
+-------------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-2-stable/actionview/CHANGELOG.md)
+for detailed changes.
+
+### Deprecations
+
+* Deprecated `AbstractController::Base.parent_prefixes`.
+ Override `AbstractController::Base.local_prefixes` when you want to change
+ where to find views.
+ ([Pull Request](https://github.com/rails/rails/pull/15026))
+
+* Deprecated `ActionView::Digestor#digest(name, format, finder, options = {})`,
+ arguments should be passed as a hash instead.
+ ([Pull Request](https://github.com/rails/rails/pull/14243))
+
+### Notable changes
+
+
Action Mailer
-------------
@@ -132,8 +178,32 @@ Please refer to the
[Changelog](https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md)
for detailed changes.
+### Removals
+
+* Removed deprecated method `ActiveRecord::Base.quoted_locking_column`.
+ ([Pull Request](https://github.com/rails/rails/pull/15612))
+
+* Removed deprecated `ActiveRecord::Migrator.proper_table_name`. Use the
+ `proper_table_name` instance method on `ActiveRecord::Migration` instead.
+ ([Pull Request](https://github.com/rails/rails/pull/15512))
+
+* Removed `cache_attributes` and friends. All attributes are cached.
+ ([Pull Request](https://github.com/rails/rails/pull/15429))
+
+* Removed unused `:timestamp` type. Transparently alias it to `:datetime`
+ in all cases. Fixes inconsistencies when column types are sent outside of
+ `ActiveRecord`, such as for XML Serialization.
+ ([Pull Request](https://github.com/rails/rails/pull/15184))
+
### Deprecations
+* Deprecated returning `nil` from `column_for_attribute` when no column exists.
+ It will return a null object in Rails 5.0
+ ([Pull Request](https://github.com/rails/rails/pull/15878))
+
+* Deprecated `serialized_attributes` without replacement.
+ ([Pull Request](https://github.com/rails/rails/pull/15704))
+
* Deprecated using `.joins`, `.preload` and `.eager_load` with associations that
depends on the instance state (i.e. those defined with a scope that takes an
argument) without replacement.
@@ -156,6 +226,12 @@ for detailed changes.
([Commit](https://github.com/rails/rails/commit/91949e48cf41af9f3e4ffba3e5eecf9b0a08bfc3))
+* Deprecated broken support for automatic detection of counter caches on
+ `has_many :through` associations. You should instead manually specify the
+ counter cache on the `has_many` and `belongs_to` associations for the through
+ records.
+ ([Pull Request](https://github.com/rails/rails/pull/15754))
+
### Notable changes
* Added support for `#pretty_print` in `ActiveRecord::Base` objects.
@@ -193,6 +269,11 @@ Please refer to the
[Changelog](https://github.com/rails/rails/blob/4-2-stable/activemodel/CHANGELOG.md)
for detailed changes.
+### Removals
+
+* Removed deprecated `Validator#setup` without replacement.
+ ([Pull Request](https://github.com/rails/rails/pull/15617))
+
### Notable changes
* Introduced `#validate` as an alias for `#valid?`.
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index 3d15319ca4..4c04a06dbb 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -1164,8 +1164,65 @@ class ClientsController < ApplicationController
end
```
+WARNING: You shouldn't do `rescue_from Exception` or `rescue_from StandardError` unless you have a particular reason as it will cause serious side-effects (e.g. you won't be able to see exception details and tracebacks during development). If you would like to dynamically generate error pages, see [Custom errors page](#custom-errors-page).
+
NOTE: Certain exceptions are only rescuable from the `ApplicationController` class, as they are raised before the controller gets initialized and the action gets executed. See Pratik Naik's [article](http://m.onkey.org/2008/7/20/rescue-from-dispatching) on the subject for more information.
+
+### Custom errors page
+
+You can customize the layout of your error handling using controllers and views.
+First define your app own routes to display the errors page.
+
+* `config/application.rb`
+
+ ```ruby
+ config.exceptions_app = self.routes
+ ```
+
+* `config/routes.rb`
+
+ ```ruby
+ get '/404', to: 'errors#not_found'
+ get '/422', to: 'errors#unprocessable_entity'
+ get '/500', to: 'errors#server_error'
+ ```
+
+Create the controller and views.
+
+* `app/controllers/errors_controller.rb`
+
+ ```ruby
+ class ErrorsController < ActionController::Base
+ layout 'error'
+
+ def not_found
+ render status: :not_found
+ end
+
+ def unprocessable_entity
+ render status: :unprocessable_entity
+ end
+
+ def server_error
+ render status: :server_error
+ end
+ end
+ ```
+
+* `app/views`
+
+ ```
+ errors/
+ not_found.html.erb
+ unprocessable_entity.html.erb
+ server_error.html.erb
+ layouts/
+ error.html.erb
+ ```
+
+Do not forget to set the correct status code on the controller as shown before. You should avoid using the database or any complex operations because the user is already on the error page. Generating another error while on an error page could cause issues.
+
Force HTTPS protocol
--------------------
diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md
index 5a550d9e55..229c6ee458 100644
--- a/guides/source/active_record_migrations.md
+++ b/guides/source/active_record_migrations.md
@@ -434,21 +434,62 @@ change_column_default :products, :approved, false
This sets `:name` field on products to a `NOT NULL` column and the default
value of the `:approved` field to false.
+TIP: Unlike `change_column` (and `change_column_default`), `change_column_null`
+is reversible.
+
### Column Modifiers
Column modifiers can be applied when creating or changing a column:
* `limit` Sets the maximum size of the `string/text/binary/integer` fields.
-* `precision` Defines the precision for the `decimal` fields, representing the total number of digits in the number.
-* `scale` Defines the scale for the `decimal` fields, representing the number of digits after the decimal point.
+* `precision` Defines the precision for the `decimal` fields, representing the
+total number of digits in the number.
+* `scale` Defines the scale for the `decimal` fields, representing the
+number of digits after the decimal point.
* `polymorphic` Adds a `type` column for `belongs_to` associations.
* `null` Allows or disallows `NULL` values in the column.
-* `default` Allows to set a default value on the column. NOTE: If using a dynamic value (such as date), the default will only be calculated the first time (e.g. on the date the migration is applied.)
+* `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.
Some adapters may support additional options; see the adapter specific API docs
for further information.
+### Foreign Keys
+
+While it's not required you might want to add foreign key constraints to
+[guarantee referential integrity](#active-record-and-referential-integrity).
+
+```ruby
+add_foreign_key :articles, :authors
+```
+
+This adds a new foreign key to the `author_id` column of the `articles`
+table. The key references the `id` column of the `articles` table. If the
+column names can not be derived from the table names, you can use the
+`:column` and `:primary_key` options.
+
+Rails will generate a name for every foreign key starting with
+`fk_rails_` followed by 10 random characters.
+There is a `:name` option to specify a different name if needed.
+
+NOTE: Active Record only supports single column foreign keys. `execute` and
+`structure.sql` are required to use composite foreign keys.
+
+Removing a foreign key is easy as well:
+
+```ruby
+# let Active Record figure out the column name
+remove_foreign_key :accounts, :branches
+
+# remove foreign key for a specific column
+remove_foreign_key :accounts, column: :owner_id
+
+# remove foreign key by name
+remove_foreign_key :accounts, name: :special_fk_name
+```
+
### When Helpers aren't Enough
If the helpers provided by Active Record aren't enough you can use the `execute`
@@ -479,6 +520,7 @@ definitions:
* `add_index`
* `add_reference`
* `add_timestamps`
+* `add_foreign_key`
* `create_table`
* `create_join_table`
* `drop_table` (must supply a block)
@@ -504,24 +546,23 @@ migration what else to do when reverting it. For example:
```ruby
class ExampleMigration < ActiveRecord::Migration
def change
- create_table :products do |t|
- t.references :category
+ create_table :distributors do |t|
+ t.string :zipcode
end
reversible do |dir|
dir.up do
- #add a foreign key
+ # add a CHECK constraint
execute <<-SQL
- ALTER TABLE products
- ADD CONSTRAINT fk_products_categories
- FOREIGN KEY (category_id)
- REFERENCES categories(id)
+ ALTER TABLE distributors
+ ADD CONSTRAINT zipchk
+ CHECK (char_length(zipcode) = 5) NO INHERIT;
SQL
end
dir.down do
execute <<-SQL
- ALTER TABLE products
- DROP FOREIGN KEY fk_products_categories
+ ALTER TABLE distributors
+ DROP CONSTRAINT zipchk
SQL
end
end
@@ -535,7 +576,7 @@ end
Using `reversible` will ensure that the instructions are executed in the
right order too. If the previous example migration is reverted,
the `down` block will be run after the `home_page_url` column is removed and
-right before the table `products` is dropped.
+right before the table `distributors` is dropped.
Sometimes your migration will do something which is just plain irreversible; for
example, it might destroy some data. In such cases, you can raise
@@ -558,16 +599,15 @@ made in the `up` method. The example in the `reversible` section is equivalent t
```ruby
class ExampleMigration < ActiveRecord::Migration
def up
- create_table :products do |t|
- t.references :category
+ create_table :distributors do |t|
+ t.string :zipcode
end
- # add a foreign key
+ # add a CHECK constraint
execute <<-SQL
- ALTER TABLE products
- ADD CONSTRAINT fk_products_categories
- FOREIGN KEY (category_id)
- REFERENCES categories(id)
+ ALTER TABLE distributors
+ ADD CONSTRAINT zipchk
+ CHECK (char_length(zipcode) = 5);
SQL
add_column :users, :home_page_url, :string
@@ -579,11 +619,11 @@ class ExampleMigration < ActiveRecord::Migration
remove_column :users, :home_page_url
execute <<-SQL
- ALTER TABLE products
- DROP FOREIGN KEY fk_products_categories
+ ALTER TABLE distributors
+ DROP CONSTRAINT zipchk
SQL
- drop_table :products
+ drop_table :distributors
end
end
```
@@ -614,43 +654,27 @@ end
The `revert` method also accepts a block of instructions to reverse.
This could be useful to revert selected parts of previous migrations.
For example, let's imagine that `ExampleMigration` is committed and it
-is later decided it would be best to serialize the product list instead.
-One could write:
+is later decided it would be best to use Active Record validations,
+in place of the `CHECK` constraint, to verify the zipcode.
```ruby
-class SerializeProductListMigration < ActiveRecord::Migration
+class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration
def change
- add_column :categories, :product_list
-
- reversible do |dir|
- dir.up do
- # transfer data from Products to Category#product_list
- end
- dir.down do
- # create Products from Category#product_list
- end
- end
-
revert do
# copy-pasted code from ExampleMigration
- create_table :products do |t|
- t.references :category
- end
-
reversible do |dir|
dir.up do
- #add a foreign key
+ # add a CHECK constraint
execute <<-SQL
- ALTER TABLE products
- ADD CONSTRAINT fk_products_categories
- FOREIGN KEY (category_id)
- REFERENCES categories(id)
+ ALTER TABLE distributors
+ ADD CONSTRAINT zipchk
+ CHECK (char_length(zipcode) = 5);
SQL
end
dir.down do
execute <<-SQL
- ALTER TABLE products
- DROP FOREIGN KEY fk_products_categories
+ ALTER TABLE distributors
+ DROP CONSTRAINT zipchk
SQL
end
end
@@ -915,10 +939,10 @@ that Active Record supports. This could be very useful if you were to
distribute an application that is able to run against multiple databases.
There is however a trade-off: `db/schema.rb` cannot express database specific
-items such as foreign key constraints, triggers, or stored procedures. While in
-a migration you can execute custom SQL statements, the schema dumper cannot
-reconstitute those statements from the database. If you are using features like
-this, then you should set the schema format to `:sql`.
+items such as triggers, or stored procedures. While in a migration you can
+execute custom SQL statements, the schema dumper cannot reconstitute those
+statements from the database. If you are using features like this, then you
+should set the schema format to `:sql`.
Instead of using Active Record's schema dumper, the database's structure will
be dumped using a tool specific to the database (via the `db:structure:dump`
@@ -945,7 +969,7 @@ Active Record and Referential Integrity
---------------------------------------
The Active Record way claims that intelligence belongs in your models, not in
-the database. As such, features such as triggers or foreign key constraints,
+the database. As such, features such as triggers or constraints,
which push some of that intelligence back into the database, are not heavily
used.
@@ -954,14 +978,10 @@ which models can enforce data integrity. The `:dependent` option on
associations allows models to automatically destroy child objects when the
parent is destroyed. Like anything which operates at the application level,
these cannot guarantee referential integrity and so some people augment them
-with foreign key constraints in the database.
-
-Although Active Record does not provide any tools for working directly with
-such features, the `execute` method can be used to execute arbitrary SQL. You
-can also use a gem like
-[foreigner](https://github.com/matthuhiggins/foreigner) which adds foreign key
-support to Active Record (including support for dumping foreign keys in
-`db/schema.rb`).
+with [foreign key constraints](#foreign-keys) in the database.
+
+Although Active Record does not provide all the tools for working directly with
+such features, the `execute` method can be used to execute arbitrary SQL.
Migrations and Seed Data
------------------------
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index 2d1548f252..709f9583ec 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -494,10 +494,10 @@ file (if any) at the precise location of the `require_self` call. If
`require_self` is called more than once, only the last call is respected.
NOTE. If you want to use multiple Sass files, you should generally use the [Sass `@import` rule](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import)
-instead of these Sprockets directives. Using Sprockets directives all Sass files exist within
+instead of these Sprockets directives. When using Sprockets directives, Sass files exist within
their own scope, making variables or mixins only available within the document they were defined in.
-You can do file globbing as well using `@import "*"`, and `@import "**/*"` to add the whole tree
-equivalent to how `require_tree` works. Check the [sass-rails documentation](https://github.com/rails/sass-rails#features) for more info and important caveats.
+
+You can do file globbing as well using `@import "*"`, and `@import "**/*"` to add the whole tree which is equivalent to how `require_tree` works. Check the [sass-rails documentation](https://github.com/rails/sass-rails#features) for more info and important caveats.
You can have as many manifest files as you need. For example, the `admin.css`
and `admin.js` manifest could contain the JS and CSS files that are used for the
@@ -760,7 +760,7 @@ typical manifest file looks like:
"digest":"12b3c7dd74d2e9df37e7cbb1efa76a6d"},"application-1c5752789588ac18d7e1a50b1f0fd4c2.css":{"logical_path":"application.css","mtime":"2013-07-26T22:56:17-07:00","size":1591,
"digest":"1c5752789588ac18d7e1a50b1f0fd4c2"},"favicon-a9c641bf2b81f0476e876f7c5e375969.ico":{"logical_path":"favicon.ico","mtime":"2013-07-26T23:00:10-07:00","size":1406,
"digest":"a9c641bf2b81f0476e876f7c5e375969"},"my_image-231a680f23887d9dd70710ea5efd3c62.png":{"logical_path":"my_image.png","mtime":"2013-07-26T23:00:27-07:00","size":6646,
-"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets"{"application.js":
+"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets":{"application.js":
"application-723d1be6cc741a3aabb1cec24276d681.js","application.css":
"application-1c5752789588ac18d7e1a50b1f0fd4c2.css",
"favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png":
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index 7e60f929a1..e283bcd0ef 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -294,6 +294,31 @@ Any modifications you make will be rolled back on exit
irb(main):001:0>
```
+#### The app and helper objects
+
+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.
+
+```bash
+>> app.root_path
+=> "/"
+
+>> app.get _
+Started GET "/" for 127.0.0.1 at 2014-06-19 10:41:57 -0300
+...
+```
+
+With the `helper` method it is possible to access Rails and your application's helpers.
+
+```bash
+>> helper.time_ago_in_words 30.days.ago
+=> "about 1 month"
+
+>> helper.my_custom_helper
+=> "my custom helper"
+```
+
### `rails dbconsole`
`rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL, PostgreSQL, SQLite and SQLite3.
diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md
index 5f738b76af..c1f26c2a5e 100644
--- a/guides/source/debugging_rails_applications.md
+++ b/guides/source/debugging_rails_applications.md
@@ -159,10 +159,10 @@ class ArticlesController < ApplicationController
def create
@article = Article.new(params[:article])
logger.debug "New article: #{@article.attributes.inspect}"
- logger.debug Article should be valid: #{@article.valid?}"
+ logger.debug "Article should be valid: #{@article.valid?}"
if @article.save
- flash[:notice] = Article was successfully created.'
+ flash[:notice] = 'Article was successfully created.'
logger.debug "The article was saved and now the user is going to be redirected..."
redirect_to(@article)
else
@@ -184,7 +184,8 @@ vbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=--b18cd92fba90eacf8137e
"body"=>"I'm learning how to print in logs!!!", "published"=>"0"},
"authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"articles"}
New article: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!",
- "published"=>false, "created_at"=>nil} Article should be valid: true
+ "published"=>false, "created_at"=>nil}
+Article should be valid: true
Article Create (0.000443) INSERT INTO "articles" ("updated_at", "title", "body", "published",
"created_at") VALUES('2008-09-08 14:52:54', 'Debugging Rails',
'I''m learning how to print in logs!!!', 'f', '2008-09-08 14:52:54')
diff --git a/guides/source/engines.md b/guides/source/engines.md
index e7f024f1fc..e630e3d93b 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -82,8 +82,11 @@ The full list of options for the plugin generator may be seen by typing:
$ bin/rails plugin --help
```
-The `--full` option tells the generator that you want to create an engine,
-including a skeleton structure that provides the following:
+The `--mountable` option tells the generator that you want to create a
+"mountable" and namespace-isolated engine. This generator will provide the same
+skeleton structure as would the `--full` option. The `--full` option tells the
+generator that you want to create an engine, including a skeleton structure
+that provides the following:
* An `app` directory tree
* A `config/routes.rb` file:
@@ -94,7 +97,7 @@ including a skeleton structure that provides the following:
```
* A file at `lib/blorgh/engine.rb`, which is identical in function to a
- * standard Rails application's `config/application.rb` file:
+ standard Rails application's `config/application.rb` file:
```ruby
module Blorgh
@@ -103,9 +106,7 @@ including a skeleton structure that provides the following:
end
```
-The `--mountable` option tells the generator that you want to create a
-"mountable" and namespace-isolated engine. This generator will provide the same
-skeleton structure as would the `--full` option, and will add:
+The `--mountable` option will add to the `--full` option:
* Asset manifest files (`application.js` and `application.css`)
* A namespaced `ApplicationController` stub
@@ -393,7 +394,7 @@ end
```
This helps prevent conflicts with any other engine or application that may have
-a article resource as well.
+an article resource as well.
Finally, the assets for this resource are generated in two files:
`app/assets/javascripts/blorgh/articles.js` and
@@ -505,8 +506,8 @@ NOTE: Because the `has_many` is defined inside a class that is inside the
model for these objects, so there's no need to specify that using the
`:class_name` option here.
-Next, there needs to be a form so that comments can be created on a article. To add
-this, put this line underneath the call to `render @article.comments` in
+Next, there needs to be a form so that comments can be created on an article. To
+add this, put this line underneath the call to `render @article.comments` in
`app/views/blorgh/articles/show.html.erb`:
```erb
@@ -738,9 +739,10 @@ the application. In the case of the `blorgh` engine, making articles and comment
have authors would make a lot of sense.
A typical application might have a `User` class that would be used to represent
-authors for a article or a comment. But there could be a case where the application
-calls this class something different, such as `Person`. For this reason, the
-engine should not hardcode associations specifically for a `User` class.
+authors for an article or a comment. But there could be a case where the
+application calls this class something different, such as `Person`. For this
+reason, the engine should not hardcode associations specifically for a `User`
+class.
To keep it simple in this case, the application will have a class called `User`
that represents the users of the application. It can be generated using this
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index ea6c8cdd55..4a0600d02a 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -1873,7 +1873,7 @@ Then you make the `app/views/articles/show.html.erb` look like the following:
<%= render @article.comments %>
<h2>Add a comment:</h2>
-<%= render "comments/form" %>
+<%= render 'comments/form' %>
<%= link_to 'Edit Article', edit_article_path(@article) %> |
<%= link_to 'Back to Articles', articles_path %>
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 5b75540c05..f00f7bca1b 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -308,7 +308,7 @@ TIP: This option should be used only if you don't care about the content type of
the response. Using `:plain` or `:html` might be more appropriate in most of the
time.
-NOTE: Unless overriden, your response returned from this render option will be
+NOTE: Unless overridden, your response returned from this render option will be
`text/html`, as that is the default content type of Action Dispatch response.
#### Options for `render`
diff --git a/guides/source/testing.md b/guides/source/testing.md
index c01b2e575a..a55466341a 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -585,7 +585,7 @@ Here's another example that uses `flash`, `assert_redirected_to`, and `assert_di
```ruby
test "should create article" do
- assert_difference('article.count') do
+ assert_difference('Article.count') do
post :create, article: {title: 'Hi', body: 'This is my first article.'}
end
assert_redirected_to article_path(assigns(:article))
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index 6800e71a3c..d1d24eac66 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -22,6 +22,29 @@ Rails generally stays close to the latest released Ruby version when it's releas
TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump straight to 1.9.3 for smooth sailing.
+### The Rake Task
+
+Rails provides the `rails:update` rake task. After updating the Rails version
+in the Gemfile, run this rake task.
+This will help you with the creation of new files and changes of old files in a
+interactive session.
+
+```bash
+$ rake rails:update
+ identical config/boot.rb
+ exist config
+ conflict config/routes.rb
+Overwrite /myapp/config/routes.rb? (enter "h" for help) [Ynaqdh]
+ force config/routes.rb
+ conflict config/application.rb
+Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh]
+ force config/application.rb
+ conflict config/environment.rb
+...
+```
+
+Don't forget to review the difference, to see if there were any unexpected changes.
+
Upgrading from Rails 4.1 to Rails 4.2
-------------------------------------
@@ -146,7 +169,7 @@ If you use the cookie session store, this would apply to the `session` and
Flash message keys are
[normalized to strings](https://github.com/rails/rails/commit/a668beffd64106a1e1fedb71cc25eaaa11baf0c1). They
-can still be accessed using either symbols or strings. Lopping through the flash
+can still be accessed using either symbols or strings. Looping through the flash
will always yield string keys:
```ruby
@@ -315,10 +338,10 @@ authors.compact!
### Changes on Default Scopes
-Default scopes are no longer overriden by chained conditions.
+Default scopes are no longer overridden by chained conditions.
In previous versions when you defined a `default_scope` in a model
-it was overriden by chained conditions in the same field. Now it
+it was overridden by chained conditions in the same field. Now it
is merged like any other scope.
Before:
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb
index e58b9fbd08..5620fcc850 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb
@@ -1,4 +1,4 @@
-<h1>Editing <%= singular_table_name %></h1>
+<h1>Editing <%= singular_table_name.titleize %></h1>
<%%= render 'form' %>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
index 814d6fdb0e..025b1d8699 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
@@ -1,4 +1,4 @@
-<h1>Listing <%= plural_table_name %></h1>
+<h1>Listing <%= plural_table_name.titleize %></h1>
<table>
<thead>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb
index 02ae4d015e..db13a5d870 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb
@@ -1,4 +1,4 @@
-<h1>New <%= singular_table_name %></h1>
+<h1>New <%= singular_table_name.titleize %></h1>
<%%= render 'form' %>
diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb
index 7588a558e7..fbecab1823 100644
--- a/railties/lib/rails/generators/rails/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb
@@ -2,6 +2,8 @@ module Rails
module Generators
class ControllerGenerator < NamedBase # :nodoc:
argument :actions, type: :array, default: [], banner: "action action"
+ class_option :skip_routes, type: :boolean, desc: "Dont' add routes to config/routes.rb."
+
check_class_collision suffix: "Controller"
def create_controller_files
@@ -9,8 +11,10 @@ module Rails
end
def add_routes
- actions.reverse.each do |action|
- route generate_routing_code(action)
+ unless options[:skip_routes]
+ actions.reverse.each do |action|
+ route generate_routing_code(action)
+ end
end
end
diff --git a/railties/lib/rails/generators/testing/assertions.rb b/railties/lib/rails/generators/testing/assertions.rb
index 2e877f8762..bd069e4bd0 100644
--- a/railties/lib/rails/generators/testing/assertions.rb
+++ b/railties/lib/rails/generators/testing/assertions.rb
@@ -1,3 +1,5 @@
+require 'shellwords'
+
module Rails
module Generators
module Testing
diff --git a/railties/lib/rails/test_unit/sub_test_task.rb b/railties/lib/rails/test_unit/sub_test_task.rb
index d9bffba4d7..6fa96d2ced 100644
--- a/railties/lib/rails/test_unit/sub_test_task.rb
+++ b/railties/lib/rails/test_unit/sub_test_task.rb
@@ -7,7 +7,7 @@ module Rails
#
# This class takes a TestInfo class and defines the appropriate rake task
# based on the information, then invokes it.
- class TestCreator
+ class TestCreator # :nodoc:
def initialize(info)
@info = info
end
@@ -41,7 +41,7 @@ module Rails
# to test files (or can be transformed into test files). Calling <tt>files</tt>
# provides the set of test files and is used when initializing tests after
# a call to <tt>rake test</tt>.
- class TestInfo
+ class TestInfo # :nodoc:
def initialize(tasks)
@tasks = tasks
@files = nil
diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb
index a6900a57c4..a3819b93b2 100644
--- a/railties/test/application/rake/migrations_test.rb
+++ b/railties/test/application/rake/migrations_test.rb
@@ -184,6 +184,21 @@ module ApplicationTests
assert_match(/create_table "books"/, structure_dump)
end
end
+
+ test 'test migration status migrated file is deleted' do
+ Dir.chdir(app_path) do
+ `rails generate model user username:string password:string;
+ rails generate migration add_email_to_users email:string;
+ rake db:migrate
+ rm db/migrate/*email*.rb`
+
+ output = `rake db:migrate:status`
+ File.write('test.txt', output)
+
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output)
+ end
+ end
end
end
end
diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb
index 4b2f8539d0..c906d56b8f 100644
--- a/railties/test/generators/controller_generator_test.rb
+++ b/railties/test/generators/controller_generator_test.rb
@@ -70,6 +70,13 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/
end
+ def test_skip_routes
+ run_generator ["account", "foo", "--skip-routes"]
+ assert_file "config/routes.rb" do |routes|
+ assert_no_match /get 'account\/foo'/, routes
+ end
+ end
+
def test_invokes_default_template_engine_even_with_no_action
run_generator ["account"]
assert_file "app/views/account"