diff options
48 files changed, 552 insertions, 322 deletions
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index e6021939ff..a822412090 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,7 @@ +## Rails 4.0.0 (unreleased) ## + +* Raise an `ActionView::MissingTemplate` exception when no implicit template could be found. *Damien Mathieu* + ## Rails 3.2.5 (Jun 1, 2012) ## * No changes. diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 4f0cff0612..1899b13e71 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -184,6 +184,16 @@ module ActionMailer #:nodoc: # and the second being a <tt>application/pdf</tt> with a Base64 encoded copy of the file.pdf book # with the filename +free_book.pdf+. # + # If you need to send attachments with no content, you need to create an empty view for it, + # or add an empty body parameter like this: + # + # class ApplicationMailer < ActionMailer::Base + # def welcome(recipient) + # attachments['free_book.pdf'] = File.read('path/to/file.pdf') + # mail(:to => recipient, :subject => "New account information", :body => "") + # end + # end + # # = Inline Attachments # # You can also specify that a file should be displayed inline with other HTML. This is useful @@ -598,8 +608,10 @@ module ActionMailer #:nodoc: # end # end # - # Will look for all templates at "app/views/notifier" with name "welcome". However, those - # can be customized: + # Will look for all templates at "app/views/notifier" with name "welcome". + # If no welcome template exists, it will raise an ActionView::MissingTemplate error. + # + # However, those can be customized: # # mail(:template_path => 'notifications', :template_name => 'another') # @@ -733,7 +745,11 @@ module ActionMailer #:nodoc: def each_template(paths, name, &block) #:nodoc: templates = lookup_context.find_all(name, Array(paths)) - templates.uniq { |t| t.formats }.each(&block) + if templates.empty? + raise ActionView::MissingTemplate.new([paths], name, [paths], false, 'mailer') + else + templates.uniq { |t| t.formats }.each(&block) + end end def create_parts_from_responses(m, responses) #:nodoc: diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 1d747ed18a..1b2e39b3f7 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -433,6 +433,13 @@ class BaseTest < ActiveSupport::TestCase assert_equal("TEXT Implicit Multipart", mail.text_part.body.decoded) end + test "should raise if missing template in implicit render" do + assert_raises ActionView::MissingTemplate do + BaseMailer.implicit_different_template('missing_template').deliver + end + assert_equal(0, BaseMailer.deliveries.length) + end + test "you can specify a different template for explicit render" do mail = BaseMailer.explicit_different_template('explicit_multipart_templates').deliver assert_equal("HTML Explicit Multipart Templates", mail.html_part.body.decoded) diff --git a/actionmailer/test/fixtures/base_mailer/attachment_with_hash.html.erb b/actionmailer/test/fixtures/base_mailer/attachment_with_hash.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionmailer/test/fixtures/base_mailer/attachment_with_hash.html.erb diff --git a/actionmailer/test/fixtures/base_mailer/attachment_with_hash_default_encoding.html.erb b/actionmailer/test/fixtures/base_mailer/attachment_with_hash_default_encoding.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionmailer/test/fixtures/base_mailer/attachment_with_hash_default_encoding.html.erb diff --git a/actionmailer/test/fixtures/base_mailer/welcome_with_headers.html.erb b/actionmailer/test/fixtures/base_mailer/welcome_with_headers.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionmailer/test/fixtures/base_mailer/welcome_with_headers.html.erb diff --git a/actionmailer/test/fixtures/base_test/after_filter_mailer/welcome.html.erb b/actionmailer/test/fixtures/base_test/after_filter_mailer/welcome.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionmailer/test/fixtures/base_test/after_filter_mailer/welcome.html.erb diff --git a/actionmailer/test/fixtures/base_test/before_filter_mailer/welcome.html.erb b/actionmailer/test/fixtures/base_test/before_filter_mailer/welcome.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionmailer/test/fixtures/base_test/before_filter_mailer/welcome.html.erb diff --git a/actionmailer/test/fixtures/base_test/default_inline_attachment_mailer/welcome.html.erb b/actionmailer/test/fixtures/base_test/default_inline_attachment_mailer/welcome.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionmailer/test/fixtures/base_test/default_inline_attachment_mailer/welcome.html.erb diff --git a/actionmailer/test/fixtures/mail_delivery_test/delivery_mailer/welcome.html.erb b/actionmailer/test/fixtures/mail_delivery_test/delivery_mailer/welcome.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionmailer/test/fixtures/mail_delivery_test/delivery_mailer/welcome.html.erb diff --git a/actionmailer/test/fixtures/proc_mailer/welcome.html.erb b/actionmailer/test/fixtures/proc_mailer/welcome.html.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionmailer/test/fixtures/proc_mailer/welcome.html.erb diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index fa2038f78d..2bb656fac9 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -2,12 +2,14 @@ require "pathname" require "active_support/core_ext/class" require "active_support/core_ext/class/attribute_accessors" require "action_view/template" +require "thread" +require "mutex_m" module ActionView # = Action View Resolver class Resolver # Keeps all information about view path and builds virtual path. - class Path < String + class Path attr_reader :name, :prefix, :partial, :virtual alias_method :partial?, :partial @@ -19,8 +21,77 @@ module ActionView end def initialize(name, prefix, partial, virtual) - @name, @prefix, @partial = name, prefix, partial - super(virtual) + @name = name + @prefix = prefix + @partial = partial + @virtual = virtual + end + + def to_str + @virtual + end + alias :to_s :to_str + end + + # Threadsafe template cache + class Cache #:nodoc: + class CacheEntry + include Mutex_m + + attr_accessor :templates + end + + def initialize + @data = Hash.new { |h1,k1| h1[k1] = Hash.new { |h2,k2| + h2[k2] = Hash.new { |h3,k3| h3[k3] = Hash.new { |h4,k4| h4[k4] = {} } } } } + @mutex = Mutex.new + end + + # Cache the templates returned by the block + def cache(key, name, prefix, partial, locals) + cache_entry = nil + + # first obtain a lock on the main data structure to create the cache entry + @mutex.synchronize do + cache_entry = @data[key][name][prefix][partial][locals] ||= CacheEntry.new + end + + # then to avoid a long lasting global lock, obtain a more granular lock + # on the CacheEntry itself + cache_entry.synchronize do + if Resolver.caching? + cache_entry.templates ||= yield + else + fresh_templates = yield + + if templates_have_changed?(cache_entry.templates, fresh_templates) + cache_entry.templates = fresh_templates + else + cache_entry.templates ||= [] + end + end + end + end + + def clear + @mutex.synchronize do + @data.clear + end + end + + private + + def templates_have_changed?(cached_templates, fresh_templates) + # if either the old or new template list is empty, we don't need to (and can't) + # compare modification times, and instead just check whether the lists are different + if cached_templates.blank? || fresh_templates.blank? + return fresh_templates.blank? != cached_templates.blank? + end + + cached_templates_max_updated_at = cached_templates.map(&:updated_at).max + + # if a template has changed, it will be now be newer than all the cached templates + fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at } end end @@ -32,12 +103,11 @@ module ActionView end def initialize - @cached = Hash.new { |h1,k1| h1[k1] = Hash.new { |h2,k2| - h2[k2] = Hash.new { |h3,k3| h3[k3] = Hash.new { |h4,k4| h4[k4] = {} } } } } + @cache = Cache.new end def clear_cache - @cached.clear + @cache.clear end # Normalizes the arguments and passes it on to find_template. @@ -65,27 +135,18 @@ module ActionView # Handles templates caching. If a key is given and caching is on # always check the cache before hitting the resolver. Otherwise, - # it always hits the resolver but check if the resolver is fresher - # before returning it. + # it always hits the resolver but if the key is present, check if the + # resolver is fresher before returning it. def cached(key, path_info, details, locals) #:nodoc: name, prefix, partial = path_info locals = locals.map { |x| x.to_s }.sort! - if key && caching? - @cached[key][name][prefix][partial][locals] ||= decorate(yield, path_info, details, locals) - else - fresh = decorate(yield, path_info, details, locals) - return fresh unless key - - scope = @cached[key][name][prefix][partial] - cache = scope[locals] - mtime = cache && cache.map(&:updated_at).max - - if !mtime || fresh.empty? || fresh.any? { |t| t.updated_at > mtime } - scope[locals] = fresh - else - cache + if key + @cache.cache(key, name, prefix, partial, locals) do + decorate(yield, path_info, details, locals) end + else + decorate(yield, path_info, details, locals) end end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 4a795de5a2..6bebe7e1ed 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -1526,10 +1526,10 @@ class ExpiresInRenderTest < ActionController::TestCase assert_equal "no-cache", @response.headers["Cache-Control"] end - def test_expires_now + def test_expires_now_with_cache_control_headers get :conditional_hello_with_cache_control_headers - assert_match /no-cache/, @response.headers["Cache-Control"] - assert_match /no-transform/, @response.headers["Cache-Control"] + assert_match(/no-cache/, @response.headers["Cache-Control"]) + assert_match(/no-transform/, @response.headers["Cache-Control"]) end def test_date_header_when_expires_in diff --git a/actionpack/test/template/lookup_context_test.rb b/actionpack/test/template/lookup_context_test.rb index 96b14a0acd..ef9c5ce10c 100644 --- a/actionpack/test/template/lookup_context_test.rb +++ b/actionpack/test/template/lookup_context_test.rb @@ -169,7 +169,7 @@ class LookupContextTest < ActiveSupport::TestCase assert_not_equal template, old_template end - + test "responds to #prefixes" do assert_equal [], @lookup_context.prefixes @lookup_context.prefixes = ["foo"] @@ -180,7 +180,7 @@ end class LookupContextWithFalseCaching < ActiveSupport::TestCase def setup @resolver = ActionView::FixtureResolver.new("test/_foo.erb" => ["Foo", Time.utc(2000)]) - @resolver.stubs(:caching?).returns(false) + ActionView::Resolver.stubs(:caching?).returns(false) @lookup_context = ActionView::LookupContext.new(@resolver, {}) end @@ -247,6 +247,6 @@ class TestMissingTemplate < ActiveSupport::TestCase @lookup_context.view_paths.find("foo", "parent", true, details) end assert_match %r{Missing partial parent/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message - end - + end + end diff --git a/actionpack/test/template/testing/null_resolver_test.rb b/actionpack/test/template/testing/null_resolver_test.rb index 535ad3ab14..55ec36e753 100644 --- a/actionpack/test/template/testing/null_resolver_test.rb +++ b/actionpack/test/template/testing/null_resolver_test.rb @@ -6,7 +6,7 @@ class NullResolverTest < ActiveSupport::TestCase templates = resolver.find_all("path.erb", "arbitrary", false, {:locale => [], :formats => [:html], :handlers => []}) assert_equal 1, templates.size, "expected one template" assert_equal "Template generated by Null Resolver", templates.first.source - assert_equal "arbitrary/path.erb", templates.first.virtual_path + assert_equal "arbitrary/path.erb", templates.first.virtual_path.to_s assert_equal [:html], templates.first.formats end end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 30f51e041f..16fcf14009 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -260,7 +260,7 @@ module ActiveModel end protected - def instance_method_already_implemented?(method_name) + def instance_method_already_implemented?(method_name) #:nodoc: generated_attribute_methods.method_defined?(method_name) end @@ -316,7 +316,7 @@ module ActiveModel RUBY end - class AttributeMethodMatcher + class AttributeMethodMatcher #:nodoc: attr_reader :prefix, :suffix, :method_missing_target AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name) diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 7eb48abc3c..a93323a3a8 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -104,7 +104,7 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<createdAt}, @xml end - test "should use serialiable hash" do + test "should use serializable hash" do @contact = SerializableContact.new @contact.name = 'aaron stack' @contact.age = 25 diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 10bfe1945a..ed5f29cfe4 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,53 @@ ## Rails 4.0.0 (unreleased) ## +* Allow ActiveRecord::Relation#pluck to accept multiple columns. Returns an + array of arrays containing the typecasted values: + + Person.pluck(:id, :name) + # SELECT people.id, people.name FROM people + # [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] + + *Jeroen van Ingen & Carlos Antonio da Silva* + +* Improve the derivation of HABTM join table name to take account of nesting. + It now takes the table names of the two models, sorts them lexically and + then joins them, stripping any common prefix from the second table name. + + Some examples: + + Top level models (Category <=> Product) + Old: categories_products + New: categories_products + + Top level models with a global table_name_prefix (Category <=> Product) + Old: site_categories_products + New: site_categories_products + + Nested models in a module without a table_name_prefix method (Admin::Category <=> Admin::Product) + Old: categories_products + New: categories_products + + Nested models in a module with a table_name_prefix method (Admin::Category <=> Admin::Product) + Old: categories_products + New: admin_categories_products + + Nested models in a parent model (Catalog::Category <=> Catalog::Product) + Old: categories_products + New: catalog_categories_products + + Nested models in different parent models (Catalog::Category <=> Content::Page) + Old: categories_pages + New: catalog_categories_content_pages + + *Andrew White* + +* Move HABTM validity checks to ActiveRecord::Reflection. One side effect of + this is to move when the exceptions are raised from the point of declaration + to when the association is built. This is consistant with other association + validity checks. + + *Andrew White* + * Added `stored_attributes` hash which contains the attributes stored using ActiveRecord::Store. This allows you to retrieve the list of attributes you've defined. 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 30fc44b4c2..f7656ecd47 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 @@ -6,7 +6,6 @@ module ActiveRecord::Associations::Builder def build reflection = super - check_validity(reflection) define_destroy_hook reflection end @@ -24,34 +23,5 @@ module ActiveRecord::Associations::Builder RUBY }) end - - # TODO: These checks should probably be moved into the Reflection, and we should not be - # redefining the options[:join_table] value - instead we should define a - # reflection.join_table method. - def check_validity(reflection) - if reflection.association_foreign_key == reflection.foreign_key - raise ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) - end - - reflection.options[:join_table] ||= join_table_name( - model.send(:undecorated_table_name, model.to_s), - model.send(:undecorated_table_name, reflection.class_name) - ) - end - - # Generates a join table name from two provided table names. - # The names in the join table names end up in lexicographic order. - # - # join_table_name("members", "clubs") # => "clubs_members" - # join_table_name("members", "special_clubs") # => "members_special_clubs" - def join_table_name(first_table_name, second_table_name) - if first_table_name < second_table_name - join_table = "#{first_table_name}_#{second_table_name}" - else - join_table = "#{second_table_name}_#{first_table_name}" - end - - model.table_name_prefix + join_table + model.table_name_suffix - end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 2447914640..2f6ddfeeb3 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -186,7 +186,7 @@ module ActiveRecord if options[:uniq] # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. column_name ||= reflection.klass.primary_key - count_options.merge!(:distinct => true) + count_options[:distinct] = true end value = scoped.count(column_name, count_options) diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 58d041ec1d..93618721bb 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -5,7 +5,7 @@ module ActiveRecord attr_reader :join_table def initialize(owner, reflection) - @join_table = Arel::Table.new(reflection.options[:join_table]) + @join_table = Arel::Table.new(reflection.join_table) super end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index cea6ad6944..5a41b40c8f 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -19,7 +19,7 @@ module ActiveRecord if reflection.source_macro == :has_and_belongs_to_many tables << alias_tracker.aliased_table_for( - (reflection.source_reflection || reflection).options[:join_table], + (reflection.source_reflection || reflection).join_table, table_alias_for(reflection, true) ) end diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb index b77b667219..8e8925f0a9 100644 --- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb @@ -6,7 +6,7 @@ module ActiveRecord def initialize(klass, records, reflection, preload_options) super - @join_table = Arel::Table.new(options[:join_table]).alias('t0') + @join_table = Arel::Table.new(reflection.join_table).alias('t0') end # Unlike the other associations, we want to get a raw array of rows so that we can diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index a85b6b3b82..a24b4b7839 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -78,7 +78,7 @@ module ActiveRecord def _field_changed?(attr, old, value) if column = column_for_attribute(attr) if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || - changes_from_zero_to_string?(column, old, value)) + changes_from_zero_to_string?(old, value)) value = nil else value = column.type_cast(value) @@ -96,7 +96,7 @@ module ActiveRecord column.null && (old.nil? || old == 0) && value.blank? end - def changes_from_zero_to_string?(column, old, value) + def changes_from_zero_to_string?(old, value) # For columns with old 0 and value non-empty string old == 0 && value.present? && value != '0' end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 9b88bb8178..858b667e22 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -196,4 +196,7 @@ module ActiveRecord "Unknown primary key for table #{model.table_name} in model #{model}." end end + + class ImmutableRelation < ActiveRecordError + end end diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 1f8c4fc203..d5ba343b4c 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -2,9 +2,12 @@ require 'active_support/notifications' module ActiveRecord class ExplainSubscriber # :nodoc: - def call(*args) + def start(name, id, payload) + # unused + end + + def finish(name, id, payload) if queries = Thread.current[:available_queries_for_explain] - payload = args.last queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload) end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 7e6512501c..96d24b72b3 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -594,7 +594,7 @@ module ActiveRecord when :has_and_belongs_to_many if (targets = row.delete(association.name.to_s)) targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - table_name = association.options[:join_table] + table_name = association.join_table rows[table_name].concat targets.map { |target| { association.foreign_key => row[primary_key_name], association.association_foreign_key => ActiveRecord::Fixtures.identify(target) } diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index e9a8e90538..0d9534acd6 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -161,6 +161,10 @@ module ActiveRecord @quoted_table_name ||= klass.quoted_table_name end + def join_table + @join_table ||= options[:join_table] || derive_join_table + end + def foreign_key @foreign_key ||= options[:foreign_key] || derive_foreign_key end @@ -208,6 +212,10 @@ module ActiveRecord def check_validity! check_validity_of_inverse! + + if has_and_belongs_to_many? && association_foreign_key == foreign_key + raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self) + end end def check_validity_of_inverse! @@ -290,6 +298,10 @@ module ActiveRecord macro == :belongs_to end + def has_and_belongs_to_many? + macro == :has_and_belongs_to_many + end + def association_class case macro when :belongs_to @@ -332,6 +344,10 @@ module ActiveRecord end end + def derive_join_table + [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + end + def primary_key(klass) klass.primary_key || raise(UnknownPrimaryKey.new(klass)) end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 22c3e6a324..86eb8f35b5 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -107,7 +107,6 @@ module ActiveRecord relation = with_default_scope if relation.equal?(self) - if has_include?(column_name) construct_relation_for_association_calculations.calculate(operation, column_name, options) else @@ -139,6 +138,10 @@ module ActiveRecord # # SELECT people.id FROM people # # => [1, 2, 3] # + # Person.pluck(:id, :name) + # # SELECT people.id, people.name FROM people + # # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] + # # Person.uniq.pluck(:role) # # SELECT DISTINCT role FROM people # # => ['admin', 'member', 'guest'] @@ -151,30 +154,35 @@ module ActiveRecord # # SELECT DATEDIFF(updated_at, created_at) FROM people # # => ['0', '27761', '173'] # - def pluck(column_name) - if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s) - column_name = "#{table_name}.#{column_name}" + def pluck(*column_names) + column_names.map! do |column_name| + if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s) + "#{table_name}.#{column_name}" + else + column_name + end end - if has_include?(column_name) - construct_relation_for_association_calculations.pluck(column_name) + if has_include?(column_names.first) + construct_relation_for_association_calculations.pluck(*column_names) else - result = klass.connection.select_all(select(column_name).arel, nil, bind_values) - - key = result.columns.first - column = klass.column_types.fetch(key) { - result.column_types.fetch(key) { - Class.new { def type_cast(v); v; end }.new + result = klass.connection.select_all(select(column_names).arel, nil, bind_values) + columns = result.columns.map do |key| + klass.column_types.fetch(key) { + result.column_types.fetch(key) { + Class.new { def type_cast(v); v; end }.new + } } - } - - result.map do |attributes| - raise ArgumentError, "Pluck expects to select just one attribute: #{attributes.inspect}" unless attributes.one? + end - value = klass.initialize_attributes(attributes).values.first + result = result.map do |attributes| + values = klass.initialize_attributes(attributes).values - column.type_cast(value) + columns.zip(values).map do |column, value| + column.type_cast(value) + end end + columns.one? ? result.map!(&:first) : result end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 529ddb5e31..e394acfaf2 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -7,34 +7,36 @@ module ActiveRecord Relation::MULTI_VALUE_METHODS.each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_values # def select_values - @values[:#{name}] || [] # @values[:select] || [] - end # end - # - def #{name}_values=(values) # def select_values=(values) - @values[:#{name}] = values # @values[:select] = values - end # end + def #{name}_values # def select_values + @values[:#{name}] || [] # @values[:select] || [] + end # end + # + def #{name}_values=(values) # def select_values=(values) + raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + @values[:#{name}] = values # @values[:select] = values + end # end CODE end (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_value # def readonly_value - @values[:#{name}] # @values[:readonly] - end # end - # - def #{name}_value=(value) # def readonly_value=(value) - @values[:#{name}] = value # @values[:readonly] = value - end # end + def #{name}_value # def readonly_value + @values[:#{name}] # @values[:readonly] + end # end CODE end - def create_with_value - @values[:create_with] || {} + Relation::SINGLE_VALUE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_value=(value) # def readonly_value=(value) + raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + @values[:#{name}] = value # @values[:readonly] = value + end # end + CODE end - def create_with_value=(value) - @values[:create_with] = value + def create_with_value + @values[:create_with] || {} end alias extensions extending_values diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index b833af64fe..834d01a1e8 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -177,11 +177,6 @@ module ActiveRecord #:nodoc: end class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: - def initialize(*args) - super - options[:except] = Array(options[:except]) | Array(@serializable.class.inheritance_column) - end - class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 6faceaf7c0..8e3eeac81e 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -34,11 +34,11 @@ class MysqlReservedWordTest < ActiveRecord::TestCase 'select'=>'id int auto_increment primary key', 'values'=>'id int auto_increment primary key, group_id int', 'distinct'=>'id int auto_increment primary key', - 'distincts_selects'=>'distinct_id int, select_id int' + 'distinct_select'=>'distinct_id int, select_id int' end def teardown - drop_tables_directly ['group', 'select', 'values', 'distinct', 'distincts_selects', 'order'] + drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order'] end # create tables with reserved-word names and columns @@ -80,7 +80,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #activerecord model class with reserved-word table name def test_activerecord_model - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select x = nil assert_nothing_raised { x = Group.new } x.order = 'x' @@ -94,7 +94,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_one association with reserved-word table name def test_has_one_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select v = nil assert_nothing_raised { v = Group.find(1).values } assert_equal 2, v.id @@ -102,7 +102,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # belongs_to association with reserved-word table name def test_belongs_to_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select gs = nil assert_nothing_raised { gs = Select.find(2).groups } assert_equal gs.length, 2 @@ -111,7 +111,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_and_belongs_to_many with reserved-word table name def test_has_and_belongs_to_many - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select s = nil assert_nothing_raised { s = Distinct.find(1).selects } assert_equal s.length, 2 diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 32d4282623..5c2c113c78 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -34,11 +34,11 @@ class MysqlReservedWordTest < ActiveRecord::TestCase 'select'=>'id int auto_increment primary key', 'values'=>'id int auto_increment primary key, group_id int', 'distinct'=>'id int auto_increment primary key', - 'distincts_selects'=>'distinct_id int, select_id int' + 'distinct_select'=>'distinct_id int, select_id int' end def teardown - drop_tables_directly ['group', 'select', 'values', 'distinct', 'distincts_selects', 'order'] + drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order'] end # create tables with reserved-word names and columns @@ -80,7 +80,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #activerecord model class with reserved-word table name def test_activerecord_model - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select x = nil assert_nothing_raised { x = Group.new } x.order = 'x' @@ -94,7 +94,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_one association with reserved-word table name def test_has_one_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select v = nil assert_nothing_raised { v = Group.find(1).values } assert_equal 2, v.id @@ -102,7 +102,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # belongs_to association with reserved-word table name def test_belongs_to_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select gs = nil assert_nothing_raised { gs = Select.find(2).groups } assert_equal gs.length, 2 @@ -111,7 +111,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_and_belongs_to_many with reserved-word table name def test_has_and_belongs_to_many - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select s = nil assert_nothing_raised { s = Distinct.find(1).selects } assert_equal s.length, 2 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 9d693bae0c..24bb4adf0a 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 @@ -773,9 +773,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_self_referential_habtm_without_foreign_key_set_should_raise_exception assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) { - Member.class_eval do - has_and_belongs_to_many :friends, :class_name => "Member", :join_table => "member_friends" - end + SelfMember.new.friends } end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index f748b897ee..4df613488a 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -532,10 +532,6 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [50 + 53 + 55 + 60], Account.pluck('SUM(DISTINCT(credit_limit)) as credit_limit') end - def test_pluck_expects_a_single_selection - assert_raise(ArgumentError) { Account.pluck 'id, credit_limit' } - end - def test_plucks_with_ids assert_equal Company.all.map(&:id).sort, Company.ids.sort end @@ -546,4 +542,29 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal Company.count, ids.length assert_equal [7], ids.compact end + + def test_pluck_multiple_columns + assert_equal [ + [1, "The First Topic"], [2, "The Second Topic of the day"], + [3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"] + ], Topic.order(:id).pluck(:id, :title) + assert_equal [ + [1, "The First Topic", "David"], [2, "The Second Topic of the day", "Mary"], + [3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"] + ], Topic.order(:id).pluck(:id, :title, :author_name) + end + + def test_pluck_with_multiple_columns_and_selection_clause + assert_equal [[1, 50], [2, 50], [3, 50], [4, 60], [5, 55], [6, 53]], + Account.pluck('id, credit_limit') + end + + def test_pluck_with_multiple_columns_and_includes + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + companies_and_developers = Company.order('companies.id').includes(:contracts).pluck(:name, :developer_id) + + assert_equal Company.count, companies_and_developers.length + assert_equal ["37signals", nil], companies_and_developers.first + assert_equal ["test", 7], companies_and_developers.last + end end diff --git a/activerecord/test/cases/database_tasks_test.rb b/activerecord/test/cases/database_tasks_test.rb index bf38abc337..5f36b2c841 100644 --- a/activerecord/test/cases/database_tasks_test.rb +++ b/activerecord/test/cases/database_tasks_test.rb @@ -10,31 +10,21 @@ module ActiveRecord end end + ADAPTERS_TASKS = { + :mysql => :mysql_tasks, + :mysql2 => :mysql_tasks, + :postgresql => :postgresql_tasks, + :sqlite3 => :sqlite_tasks + } + class DatabaseTasksCreateTest < ActiveRecord::TestCase include DatabaseTasksSetupper - def test_mysql_create - @mysql_tasks.expects(:create) - - ActiveRecord::Tasks::DatabaseTasks.create 'adapter' => 'mysql' - end - - def test_mysql2_create - @mysql_tasks.expects(:create) - - ActiveRecord::Tasks::DatabaseTasks.create 'adapter' => 'mysql2' - end - - def test_postgresql_create - @postgresql_tasks.expects(:create) - - ActiveRecord::Tasks::DatabaseTasks.create 'adapter' => 'postgresql' - end - - def test_sqlite_create - @sqlite_tasks.expects(:create) - - ActiveRecord::Tasks::DatabaseTasks.create 'adapter' => 'sqlite3' + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_create") do + eval("@#{v}").expects(:create) + ActiveRecord::Tasks::DatabaseTasks.create 'adapter' => k + end end end @@ -141,28 +131,11 @@ module ActiveRecord class DatabaseTasksDropTest < ActiveRecord::TestCase include DatabaseTasksSetupper - def test_mysql_create - @mysql_tasks.expects(:drop) - - ActiveRecord::Tasks::DatabaseTasks.drop 'adapter' => 'mysql' - end - - def test_mysql2_create - @mysql_tasks.expects(:drop) - - ActiveRecord::Tasks::DatabaseTasks.drop 'adapter' => 'mysql2' - end - - def test_postgresql_create - @postgresql_tasks.expects(:drop) - - ActiveRecord::Tasks::DatabaseTasks.drop 'adapter' => 'postgresql' - end - - def test_sqlite_create - @sqlite_tasks.expects(:drop) - - ActiveRecord::Tasks::DatabaseTasks.drop 'adapter' => 'sqlite3' + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_drop") do + eval("@#{v}").expects(:drop) + ActiveRecord::Tasks::DatabaseTasks.drop 'adapter' => k + end end end @@ -255,115 +228,48 @@ module ActiveRecord end end + class DatabaseTasksPurgeTest < ActiveRecord::TestCase include DatabaseTasksSetupper - def test_mysql_create - @mysql_tasks.expects(:purge) - - ActiveRecord::Tasks::DatabaseTasks.purge 'adapter' => 'mysql' - end - - def test_mysql2_create - @mysql_tasks.expects(:purge) - - ActiveRecord::Tasks::DatabaseTasks.purge 'adapter' => 'mysql2' - end - - def test_postgresql_create - @postgresql_tasks.expects(:purge) - - ActiveRecord::Tasks::DatabaseTasks.purge 'adapter' => 'postgresql' - end - - def test_sqlite_create - @sqlite_tasks.expects(:purge) - - ActiveRecord::Tasks::DatabaseTasks.purge 'adapter' => 'sqlite3' + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_purge") do + eval("@#{v}").expects(:purge) + ActiveRecord::Tasks::DatabaseTasks.purge 'adapter' => k + end end end class DatabaseTasksCharsetTest < ActiveRecord::TestCase include DatabaseTasksSetupper - def test_mysql_charset - @mysql_tasks.expects(:charset) - - ActiveRecord::Tasks::DatabaseTasks.charset 'adapter' => 'mysql' - end - - def test_mysql2_charset - @mysql_tasks.expects(:charset) - - ActiveRecord::Tasks::DatabaseTasks.charset 'adapter' => 'mysql2' - end - - def test_postgresql_charset - @postgresql_tasks.expects(:charset) - - ActiveRecord::Tasks::DatabaseTasks.charset 'adapter' => 'postgresql' - end - - def test_sqlite_charset - @sqlite_tasks.expects(:charset) - - ActiveRecord::Tasks::DatabaseTasks.charset 'adapter' => 'sqlite3' + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_charset") do + eval("@#{v}").expects(:charset) + ActiveRecord::Tasks::DatabaseTasks.charset 'adapter' => k + end end end class DatabaseTasksStructureDumpTest < ActiveRecord::TestCase include DatabaseTasksSetupper - def test_mysql_structure_dump - @mysql_tasks.expects(:structure_dump).with("awesome-file.sql") - - ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => 'mysql'}, "awesome-file.sql") - end - - def test_mysql2_structure_dump - @mysql_tasks.expects(:structure_dump).with("awesome-file.sql") - - ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => 'mysql2'}, "awesome-file.sql") - end - - def test_postgresql_structure_dump - @postgresql_tasks.expects(:structure_dump).with("awesome-file.sql") - - ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => 'postgresql'}, "awesome-file.sql") - end - - def test_sqlite_structure_dump - @sqlite_tasks.expects(:structure_dump).with("awesome-file.sql") - - ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => 'sqlite3'}, "awesome-file.sql") + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_structure_dump") do + eval("@#{v}").expects(:structure_dump).with("awesome-file.sql") + ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => k}, "awesome-file.sql") + end end end class DatabaseTasksStructureLoadTest < ActiveRecord::TestCase include DatabaseTasksSetupper - def test_mysql_structure_load - @mysql_tasks.expects(:structure_load).with("awesome-file.sql") - - ActiveRecord::Tasks::DatabaseTasks.structure_load({'adapter' => 'mysql'}, "awesome-file.sql") - end - - def test_mysql2_structure_load - @mysql_tasks.expects(:structure_load).with("awesome-file.sql") - - ActiveRecord::Tasks::DatabaseTasks.structure_load({'adapter' => 'mysql2'}, "awesome-file.sql") - end - - def test_postgresql_structure_load - @postgresql_tasks.expects(:structure_load).with("awesome-file.sql") - - ActiveRecord::Tasks::DatabaseTasks.structure_load({'adapter' => 'postgresql'}, "awesome-file.sql") - end - - def test_sqlite_structure_load - @sqlite_tasks.expects(:structure_load).with("awesome-file.sql") - - ActiveRecord::Tasks::DatabaseTasks.structure_load({'adapter' => 'sqlite3'}, "awesome-file.sql") + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_structure_load") do + eval("@#{v}").expects(:structure_load).with("awesome-file.sql") + ActiveRecord::Tasks::DatabaseTasks.structure_load({'adapter' => k}, "awesome-file.sql") + end end end end diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index e118add44c..91e1df91cd 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -6,14 +6,14 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_if_available_queries_for_explain_is_nil with_queries(nil) do - SUBSCRIBER.call + SUBSCRIBER.finish(nil, nil, {}) assert_nil Thread.current[:available_queries_for_explain] end end def test_collects_nothing_if_the_payload_has_an_exception with_queries([]) do |queries| - SUBSCRIBER.call(:exception => Exception.new) + SUBSCRIBER.finish(nil, nil, :exception => Exception.new) assert queries.empty? end end @@ -21,7 +21,7 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_for_ignored_payloads with_queries([]) do |queries| ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip| - SUBSCRIBER.call(:name => ip) + SUBSCRIBER.finish(nil, nil, :name => ip) end assert queries.empty? end @@ -31,7 +31,7 @@ if ActiveRecord::Base.connection.supports_explain? sql = 'select 1 from users' binds = [1, 2] with_queries([]) do |queries| - SUBSCRIBER.call(:name => 'SQL', :sql => sql, :binds => binds) + SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => sql, :binds => binds) assert_equal 1, queries.size assert_equal sql, queries[0][0] assert_equal binds, queries[0][1] @@ -45,4 +45,4 @@ if ActiveRecord::Base.connection.supports_explain? Thread.current[:available_queries_for_explain] = nil end end -end
\ No newline at end of file +end diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index d9e350abc0..a86b165c78 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -83,6 +83,53 @@ class JsonSerializationTest < ActiveRecord::TestCase assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json end + def test_uses_serializable_hash_with_only_option + def @contact.serializable_hash(options=nil) + super(only: %w(name)) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{awesome}, json + assert_no_match %r{age}, json + end + + def test_uses_serializable_hash_with_except_option + def @contact.serializable_hash(options=nil) + super(except: %w(age)) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"awesome":true}, json + assert_no_match %r{age}, json + end + + def test_does_not_include_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{type}, json + assert_no_match %r{ContactSti}, json + end + + def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + def @contact.serializable_hash(options={}) + super({ except: %w(age) }.merge!(options)) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{age}, json + assert_no_match %r{type}, json + assert_no_match %r{ContactSti}, json + end + def test_serializable_hash_should_not_modify_options_in_argument options = { :only => :name } @contact.serializable_hash(options) diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 6c5bee7382..8544d36aa8 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1284,4 +1284,31 @@ class RelationTest < ActiveRecord::TestCase Post.scoped.find_by!("1 = 0") end end + + test "loaded relations cannot be mutated by multi value methods" do + relation = Post.scoped + relation.to_a + + assert_raises(ActiveRecord::ImmutableRelation) do + relation.where! 'foo' + end + end + + test "loaded relations cannot be mutated by single value methods" do + relation = Post.scoped + relation.to_a + + assert_raises(ActiveRecord::ImmutableRelation) do + relation.limit! 5 + end + end + + test "loaded relations cannot be mutated by merge!" do + relation = Post.scoped + relation.to_a + + assert_raises(ActiveRecord::ImmutableRelation) do + relation.merge! where: 'foo' + end + end end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 12373333b0..7249ae9e4d 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -74,33 +74,88 @@ end class DefaultXmlSerializationTest < ActiveRecord::TestCase def setup - @xml = Contact.new(:name => 'aaron stack', :age => 25, :avatar => 'binarydata', :created_at => Time.utc(2006, 8, 1), :awesome => false, :preferences => { :gem => 'ruby' }).to_xml + @contact = Contact.new( + :name => 'aaron stack', + :age => 25, + :avatar => 'binarydata', + :created_at => Time.utc(2006, 8, 1), + :awesome => false, + :preferences => { :gem => 'ruby' } + ) end def test_should_serialize_string - assert_match %r{<name>aaron stack</name>}, @xml + assert_match %r{<name>aaron stack</name>}, @contact.to_xml end def test_should_serialize_integer - assert_match %r{<age type="integer">25</age>}, @xml + assert_match %r{<age type="integer">25</age>}, @contact.to_xml end def test_should_serialize_binary - assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, @xml - assert_match %r{<avatar(.*)(type="binary")}, @xml - assert_match %r{<avatar(.*)(encoding="base64")}, @xml + xml = @contact.to_xml + assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml + assert_match %r{<avatar(.*)(type="binary")}, xml + assert_match %r{<avatar(.*)(encoding="base64")}, xml end def test_should_serialize_datetime - assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @xml + assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml end def test_should_serialize_boolean - assert_match %r{<awesome type=\"boolean\">false</awesome>}, @xml + assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml end def test_should_serialize_hash - assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @xml + assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml + end + + def test_uses_serializable_hash_with_only_option + def @contact.serializable_hash(options=nil) + super(only: %w(name)) + end + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_no_match %r{age}, xml + assert_no_match %r{awesome}, xml + end + + def test_uses_serializable_hash_with_except_option + def @contact.serializable_hash(options=nil) + super(except: %w(age)) + end + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml + assert_no_match %r{age}, xml + end + + def test_does_not_include_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_no_match %r{<type}, xml + assert_no_match %r{ContactSti}, xml + end + + def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + def @contact.serializable_hash(options={}) + super({ except: %w(age) }.merge!(options)) + end + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_no_match %r{age}, xml + assert_no_match %r{<type}, xml + assert_no_match %r{ContactSti}, xml end end diff --git a/activerecord/test/fixtures/reserved_words/distincts_selects.yml b/activerecord/test/fixtures/reserved_words/distinct_select.yml index 90e8c95fef..d96779ade4 100644 --- a/activerecord/test/fixtures/reserved_words/distincts_selects.yml +++ b/activerecord/test/fixtures/reserved_words/distinct_select.yml @@ -1,11 +1,11 @@ -distincts_selects1: +distinct_select1: distinct_id: 1 select_id: 1 -distincts_selects2: +distinct_select2: distinct_id: 1 select_id: 2 -distincts_selects3: +distinct_select3: distinct_id: 2 select_id: 3 diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index 3d15c7fbed..a1cb8d62b6 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -1,25 +1,40 @@ -class Contact < ActiveRecord::Base - establish_connection(:adapter => 'fake') +module ContactFakeColumns + def self.extended(base) + base.class_eval do + establish_connection(:adapter => 'fake') + + connection.tables = [table_name] + connection.primary_keys = { + table_name => 'id' + } + + column :name, :string + column :age, :integer + column :avatar, :binary + column :created_at, :datetime + column :awesome, :boolean + column :preferences, :string + column :alternative_id, :integer + + serialize :preferences - connection.tables = ['contacts'] - connection.primary_keys = { - 'contacts' => 'id' - } + belongs_to :alternative, :class_name => 'Contact' + end + end # mock out self.columns so no pesky db is needed for these tests - def self.column(name, sql_type = nil, options = {}) - connection.merge_column('contacts', name, sql_type, options) + def column(name, sql_type = nil, options = {}) + connection.merge_column(table_name, name, sql_type, options) end +end - column :name, :string - column :age, :integer - column :avatar, :binary - column :created_at, :datetime - column :awesome, :boolean - column :preferences, :string - column :alternative_id, :integer +class Contact < ActiveRecord::Base + extend ContactFakeColumns +end - serialize :preferences +class ContactSti < ActiveRecord::Base + extend ContactFakeColumns + column :type, :string - belongs_to :alternative, :class_name => 'Contact' + def type; 'ContactSti' end end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 11a0f4ff63..1f719b0858 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -30,3 +30,8 @@ class Member < ActiveRecord::Base has_many :current_memberships, :conditions => { :favourite => true } has_many :clubs, :through => :current_memberships end + +class SelfMember < ActiveRecord::Base + self.table_name = "members" + has_and_belongs_to_many :friends, :class_name => "SelfMember", :join_table => "member_friends" +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 5bcb9652cd..ba01ef35f0 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -361,6 +361,11 @@ ActiveRecord::Schema.define do t.string :extra_data end + create_table :member_friends, :force => true, :id => false do |t| + t.integer :member_id + t.integer :friend_id + end + create_table :memberships, :force => true do |t| t.datetime :joined_on t.integer :club_id, :member_id diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index 8c3921c423..e5b4ca2738 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -58,7 +58,6 @@ module ActiveSupport def attach_to(namespace, log_subscriber=new, notifier=ActiveSupport::Notifications) log_subscribers << log_subscriber - @@flushable_loggers = nil log_subscriber.public_methods(false).each do |event| next if %w{ start finish }.include?(event.to_s) @@ -71,23 +70,14 @@ module ActiveSupport @@log_subscribers ||= [] end - def flushable_loggers - @@flushable_loggers ||= begin - loggers = log_subscribers.map(&:logger) - loggers.uniq! - loggers.select! { |l| l.respond_to?(:flush) } - loggers - end - end - # Flush all log_subscribers' logger. def flush_all! - flushable_loggers.each { |log| log.flush } + logger.flush if logger.respond_to?(:flush) end end def initialize - @event_stack = [] + @queue_key = [self.class.name, object_id].join "-" super end @@ -99,17 +89,17 @@ module ActiveSupport return unless logger e = ActiveSupport::Notifications::Event.new(name, Time.now, nil, id, payload) - parent = @event_stack.last + parent = event_stack.last parent << e if parent - @event_stack.push e + event_stack.push e end def finish(name, id, payload) return unless logger finished = Time.now - event = @event_stack.pop + event = event_stack.pop event.end = finished event.payload.merge!(payload) @@ -142,5 +132,11 @@ module ActiveSupport bold = bold ? BOLD : "" "#{bold}#{color}#{text}#{CLEAR}" end + + private + + def event_stack + Thread.current[@queue_key] ||= [] + end end end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 91ae6bc189..1f32e4ff92 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -413,6 +413,16 @@ class InflectorTest < ActiveSupport::TestCase end end + Irregularities.each do |irregularity| + singular, plural = *irregularity + ActiveSupport::Inflector.inflections do |inflect| + define_method("test_singularize_of_irregularity_#{singular}_should_be_the_same") do + inflect.irregular(singular, plural) + assert_equal singular, ActiveSupport::Inflector.singularize(singular) + end + end + end + [ :all, [] ].each do |scope| ActiveSupport::Inflector.inflections do |inflect| define_method("test_clear_inflections_with_#{scope.kind_of?(Array) ? "no_arguments" : scope}") do diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index 9fa1f417e4..ca4efd2e59 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -308,5 +308,7 @@ module InflectorTestCases 'child' => 'children', 'sex' => 'sexes', 'move' => 'moves', + 'cow' => 'kine', + 'zombie' => 'zombies', } end diff --git a/guides/source/active_record_querying.textile b/guides/source/active_record_querying.textile index 4b14671efc..101988c59e 100644 --- a/guides/source/active_record_querying.textile +++ b/guides/source/active_record_querying.textile @@ -609,8 +609,8 @@ And this will give you a single +Order+ object for each date where there are ord The SQL that would be executed would be something like this: <sql> -SELECT date(created_at) as ordered_date, sum(price) as total_price -FROM orders +SELECT date(created_at) as ordered_date, sum(price) as total_price +FROM orders GROUP BY date(created_at) </sql> @@ -627,9 +627,9 @@ Order.select("date(created_at) as ordered_date, sum(price) as total_price").grou The SQL that would be executed would be something like this: <sql> -SELECT date(created_at) as ordered_date, sum(price) as total_price -FROM orders -GROUP BY date(created_at) +SELECT date(created_at) as ordered_date, sum(price) as total_price +FROM orders +GROUP BY date(created_at) HAVING sum(price) > 100 </sql> @@ -1286,26 +1286,36 @@ Client.connection.select_all("SELECT * FROM clients WHERE id = '1'") h3. +pluck+ -<tt>pluck</tt> can be used to query a single column from the underlying table of a model. It accepts a column name as argument and returns an array of values of the specified column with the corresponding data type. +<tt>pluck</tt> can be used to query a single or multiple columns from the underlying table of a model. It accepts a list of column names as argument and returns an array of values of the specified columns with the corresponding data type. <ruby> Client.where(:active => true).pluck(:id) # SELECT id FROM clients WHERE active = 1 +# => [1, 2, 3] Client.uniq.pluck(:role) # SELECT DISTINCT role FROM clients +# => ['admin', 'member', 'guest'] + +Client.pluck(:id, :name) +# SELECT clients.id, clients.name FROM clients +# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] </ruby> +pluck+ makes it possible to replace code like <ruby> Client.select(:id).map { |c| c.id } +# or +Client.select(:id).map { |c| [c.id, c.name] } </ruby> with <ruby> Client.pluck(:id) +# or +Client.pluck(:id, :name) </ruby> h3. +ids+ |