diff options
28 files changed, 725 insertions, 198 deletions
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 26787c9b5e..1676c23856 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -4,6 +4,7 @@ require 'action_mailer/collector' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/anonymous' +require 'active_support/queueing' require 'action_mailer/log_subscriber' module ActionMailer #:nodoc: @@ -393,6 +394,7 @@ module ActionMailer #:nodoc: }.freeze class_attribute :queue + self.queue = ActiveSupport::SynchronousQueue.new class << self # Register one or more Observers which will be notified when mail is delivered. diff --git a/actionmailer/lib/action_mailer/queued_message.rb b/actionmailer/lib/action_mailer/queued_message.rb index e5868ab43b..8d200617c4 100644 --- a/actionmailer/lib/action_mailer/queued_message.rb +++ b/actionmailer/lib/action_mailer/queued_message.rb @@ -5,23 +5,33 @@ module ActionMailer attr_reader :queue def initialize(queue, mailer_class, method_name, *args) - @queue = queue - @mailer_class = mailer_class - @method_name = method_name - @args = args + @queue = queue + @job = DeliveryJob.new(mailer_class, method_name, args) end def __getobj__ - @actual_message ||= @mailer_class.send(:new, @method_name, *@args).message + @job.message end - def run - __getobj__.deliver + # Queues the message for delivery. + def deliver + tap { @queue.push @job } end - # Will push the message onto the Queue to be processed - def deliver - @queue << self + class DeliveryJob + def initialize(mailer_class, method_name, args) + @mailer_class = mailer_class + @method_name = method_name + @args = args + end + + def message + @message ||= @mailer_class.send(:new, @method_name, *@args).message + end + + def run + message.deliver + end end end end diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 0b418c4ea1..4b38d4bd31 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -27,7 +27,6 @@ ActionView::Template.register_template_handler :bak, lambda { |template| "Lame b FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__)) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH -ActionMailer::Base.queue = ActiveSupport::SynchronousQueue.new class MockSMTP def self.deliveries diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 6a06cec041..4f2af50fdd 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -412,7 +412,7 @@ class BaseTest < ActiveSupport::TestCase BaseMailer.deliveries.clear BaseMailer.expects(:deliver_mail).once mail = BaseMailer.welcome.deliver - assert_instance_of Mail::Message, mail + assert_equal 'The first email on new API!', mail.subject end test "calling deliver on the action should increment the deliveries collection if using the test mailer" do @@ -422,24 +422,15 @@ class BaseTest < ActiveSupport::TestCase assert_equal(1, BaseMailer.deliveries.length) end - def stub_queue(klass, queue) - Class.new(klass) { - extend Module.new { - define_method :queue do - queue - end - } - } - end - test "delivering message asynchronously" do - testing_queue = ActiveSupport::TestQueue.new AsyncMailer.delivery_method = :test AsyncMailer.deliveries.clear - stub_queue(AsyncMailer, testing_queue).welcome.deliver - assert_equal(0, AsyncMailer.deliveries.length) - testing_queue.drain - assert_equal(1, AsyncMailer.deliveries.length) + + AsyncMailer.welcome.deliver + assert_equal 0, AsyncMailer.deliveries.length + + AsyncMailer.queue.drain + assert_equal 1, AsyncMailer.deliveries.length end test "calling deliver, ActionMailer should yield back to mail to let it call :do_delivery on itself" do diff --git a/actionmailer/test/mailers/async_mailer.rb b/actionmailer/test/mailers/async_mailer.rb index 8a87e2e1cf..c21a464f38 100644 --- a/actionmailer/test/mailers/async_mailer.rb +++ b/actionmailer/test/mailers/async_mailer.rb @@ -1,2 +1,3 @@ class AsyncMailer < BaseMailer + self.queue = ActiveSupport::TestQueue.new end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 239e4445d3..3b71af9af7 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,18 @@ ## Rails 4.0.0 (unreleased) ## +* Sprockets integration has been extracted from Action Pack and the `sprockets-rails` + gem should be added to Gemfile (under the assets group) in order to use Rails asset + pipeline in future versions of Rails. + + *Guillermo Iguaran* + +* `ActionDispatch::Session::MemCacheStore` now uses `dalli` instead of the deprecated + `memcache-client` gem. As side effect the autoloading of unloaded classes objects + saved as values in session isn't supported anymore when mem_cache session store is + used, this can have an impact in apps only when config.cache_classes is false. + + *Arun Agrawal + Guillermo Iguaran* + * Support multiple etags in If-None-Match header. *Travis Warlick* * Allow to configure how unverified request will be handled using `:with` diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index ef059e5d3c..b344d5c804 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -19,7 +19,7 @@ self.primary_key = :title end - Post.find_in_batches(:start => 'My First Post') do |batch| + Post.find_in_batches(start: 'My First Post') do |batch| batch.each { |post| post.author.greeting } end @@ -31,15 +31,15 @@ *Matt Jones* -* Accept belongs_to (including polymorphic) association keys in queries +* Accept belongs_to (including polymorphic) association keys in queries. The following queries are now equivalent: - Post.where(:author => author) - Post.where(:author_id => author) + Post.where(author: author) + Post.where(author_id: author) - PriceEstimate.where(:estimate_of => treasure) - PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure) + PriceEstimate.where(estimate_of: treasure) + PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure) *Peter Brown* @@ -48,9 +48,41 @@ *kennyj* +* PostgreSQL inet and cidr types are converted to `IPAddr` objects. + + *Dan McClain* + +* PostgreSQL array type support. Any datatype can be used to create an + array column, with full migration and schema dumper support. + + To declare an array column, use the following syntax: + + create_table :table_with_arrays do |t| + t.integer :int_array, array: true + # integer[] + t.integer :int_array, array: true, :length => 2 + # smallint[] + t.string :string_array, array: true, length: 30 + # char varying(30)[] + end + + This respects any other migration detail (limits, defaults, etc). + ActiveRecord will serialize and deserialize the array columns on + their way to and from the database. + + One thing to note: PostgreSQL does not enforce any limits on the + number of elements, and any array can be multi-dimensional. Any + array that is multi-dimensional must be rectangular (each sub array + must have the same number of elements as its siblings). + + If the `pg_array_parser` gem is available, it will be used when + parsing PostgreSQL's array representation. + + *Dan McClain* + * Attribute predicate methods, such as `article.title?`, will now raise `ActiveModel::MissingAttributeError` if the attribute being queried for - truthiness was not read from the database, instead of just returning false. + truthiness was not read from the database, instead of just returning `false`. *Ernie Miller* @@ -59,9 +91,13 @@ *Konstantin Shabanov* -* Map interval with precision to string datatype in PostgreSQL. Fixes #7518. *Yves Senn* +* Map interval with precision to string datatype in PostgreSQL. Fixes #7518. -* Fix eagerly loading associations without primary keys. Fixes #4976. *Kelley Reynolds* + *Yves Senn* + +* Fix eagerly loading associations without primary keys. Fixes #4976. + + *Kelley Reynolds* * Rails now raise an exception when you're trying to run a migration that has an invalid file name. Only lower case letters, numbers, and '_' are allowed in migration's file name. @@ -88,16 +124,18 @@ *Dickson S. Guedes* -* Fix time column type casting for invalid time string values to correctly return nil. +* Fix time column type casting for invalid time string values to correctly return `nil`. *Adam Meehan* -* Allow to pass Symbol or Proc into :limit option of #accepts_nested_attributes_for. +* Allow to pass Symbol or Proc into `:limit` option of #accepts_nested_attributes_for. *Mikhail Dieterle* * ActiveRecord::SessionStore has been extracted from Active Record as `activerecord-session_store` - gem. Please read the `README.md` file on the gem for the usage. *Prem Sichanugrist* + gem. Please read the `README.md` file on the gem for the usage. + + *Prem Sichanugrist* * Fix `reset_counters` when there are multiple `belongs_to` association with the same foreign key and one of them have a counter cache. @@ -233,6 +271,7 @@ * Add `add_reference` and `remove_reference` schema statements. Aliases, `add_belongs_to` and `remove_belongs_to` are acceptable. References are reversible. + Examples: # Create a user_id column @@ -254,10 +293,10 @@ * `ActiveRecord::Relation#inspect` now makes it clear that you are dealing with a `Relation` object rather than an array:. - User.where(:age => 30).inspect + User.where(age: 30).inspect # => <ActiveRecord::Relation [#<User ...>, #<User ...>, ...]> - User.where(:age => 30).to_a.inspect + User.where(age: 30).to_a.inspect # => [#<User ...>, #<User ...>] The number of records displayed will be limited to 10. @@ -368,10 +407,14 @@ *kennyj* -* Add uuid datatype support to PostgreSQL adapter. *Konstantin Shabanov* +* Add uuid datatype support to PostgreSQL adapter. + + *Konstantin Shabanov* * Added `ActiveRecord::Migration.check_pending!` that raises an error if - migrations are pending. *Richard Schneeman* + migrations are pending. + + *Richard Schneeman* * Added `#destroy!` which acts like `#destroy` but will raise an `ActiveRecord::RecordNotDestroyed` exception instead of returning `false`. @@ -421,7 +464,7 @@ methods which previously accepted "finder options" no longer do. For example this: - Post.find(:all, :conditions => { :comments_count => 10 }, :limit => 5) + Post.find(:all, conditions: { comments_count: 10 }, limit: 5) Should be rewritten in the new style which has existed since Rails 3: @@ -429,7 +472,7 @@ Note that as an interim step, it is possible to rewrite the above as: - Post.all.merge(:where => { :comments_count => 10 }, :limit => 5) + Post.all.merge(where: { comments_count: 10 }, limit: 5) This could save you a lot of work if there is a lot of old-style finder usage in your application. @@ -439,9 +482,9 @@ finder method. These are mostly identical to the old-style finder option names, except in the following cases: - * `:conditions` becomes `:where` - * `:include` becomes `:includes` - * `:extend` becomes `:extending` + * `:conditions` becomes `:where`. + * `:include` becomes `:includes`. + * `:extend` becomes `:extending`. The code to implement the deprecated features has been moved out to the `activerecord-deprecated_finders` gem. This gem is a dependency @@ -456,7 +499,7 @@ *Johannes Barre* -* Added ability to ActiveRecord::Relation#from to accept other ActiveRecord::Relation objects +* Added ability to ActiveRecord::Relation#from to accept other ActiveRecord::Relation objects. Record.from(subquery) Record.from(subquery, :a) @@ -482,7 +525,7 @@ *Marcelo Silveira* -* Added an :index option to automatically create indexes for references +* Added an `:index` option to automatically create indexes for references and belongs_to statements in migrations. The `references` and `belongs_to` methods now support an `index` @@ -490,7 +533,7 @@ that is identical to options available to the add_index method: create_table :messages do |t| - t.references :person, :index => true + t.references :person, index: true end Is the same as: @@ -502,7 +545,7 @@ Generators have also been updated to use the new syntax. - [Joshua Wood] + *Joshua Wood* * Added bang methods for mutating `ActiveRecord::Relation` objects. For example, while `foo.where(:bar)` will return a new object @@ -591,12 +634,12 @@ *kennyj* -* Added support for partial indices to PostgreSQL adapter +* Added support for partial indices to PostgreSQL adapter. The `add_index` method now supports a `where` option that receives a string with the partial index criteria. - add_index(:accounts, :code, :where => "active") + add_index(:accounts, :code, where: 'active') Generates @@ -604,7 +647,7 @@ *Marcelo Silveira* -* Implemented ActiveRecord::Relation#none method +* Implemented ActiveRecord::Relation#none method. The `none` method returns a chainable relation with zero records (an instance of the NullRelation class). @@ -615,9 +658,11 @@ *Juanjo Bazán* * Added the `ActiveRecord::NullRelation` class implementing the null - object pattern for the Relation class. *Juanjo Bazán* + object pattern for the Relation class. + + *Juanjo Bazán* -* Added new `:dependent => :restrict_with_error` option. This will add +* Added new `dependent: :restrict_with_error` option. This will add an error to the model, rather than raising an exception. The `:restrict` option is renamed to `:restrict_with_exception` to @@ -625,20 +670,22 @@ *Manoj Kumar & Jon Leighton* -* Added `create_join_table` migration helper to create HABTM join tables +* Added `create_join_table` migration helper to create HABTM join tables. create_join_table :products, :categories # => - # create_table :categories_products, :id => false do |td| - # td.integer :product_id, :null => false - # td.integer :category_id, :null => false + # create_table :categories_products, id: false do |td| + # td.integer :product_id, null: false + # td.integer :category_id, null: false # end *Rafael Mendonça França* -* The primary key is always initialized in the @attributes hash to nil (unless +* The primary key is always initialized in the @attributes hash to `nil` (unless another value has been specified). + *Aaron Paterson* + * In previous releases, the following would generate a single query with an `OUTER JOIN comments`, rather than two separate queries: @@ -669,14 +716,18 @@ loading. Basically, don't worry unless you see a deprecation warning or (in future releases) an SQL error due to a missing JOIN. - [Jon Leighton] + *Jon Leighton* -* Support for the `schema_info` table has been dropped. Please +* Support for the `schema_info` table has been dropped. Please switch to `schema_migrations`. -* Connections *must* be closed at the end of a thread. If not, your + *Aaron Patterson* + +* Connections *must* be closed at the end of a thread. If not, your connection pool can fill and an exception will be raised. + *Aaron Patterson* + * Added the `ActiveRecord::Model` module which can be included in a class as an alternative to inheriting from `ActiveRecord::Base`: @@ -707,6 +758,10 @@ * PostgreSQL hstore records can be created. + *Aaron Patterson* + * PostgreSQL hstore types are automatically deserialized from the database. + *Aaron Patterson* + Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index b15df4f308..424c1a5b79 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -373,7 +373,7 @@ module ActiveRecord # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */ interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do count_with = $2.to_s - count_with = '*' if count_with.blank? || count_with =~ /,/ + count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/ "SELECT #{$1}COUNT(#{count_with}) FROM" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb new file mode 100644 index 0000000000..9d6111b51e --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -0,0 +1,56 @@ +module ActiveRecord + module ConnectionAdapters # :nodoc: + # The goal of this module is to move Adapter specific column + # definitions to the Adapter instead of having it in the schema + # dumper itself. This code represents the normal case. + # We can then redefine how certain data types may be handled in the schema dumper on the + # Adapter level by over-writing this code inside the database spececific adapters + module ColumnDumper + def column_spec(column, types) + spec = prepare_column_options(column, types) + (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")} + spec + end + + # This can be overridden on a Adapter level basis to support other + # extended datatypes (Example: Adding an array option in the + # PostgreSQLAdapter) + def prepare_column_options(column, types) + spec = {} + spec[:name] = column.name.inspect + + # AR has an optimization which handles zero-scale decimals as integers. This + # code ensures that the dumper still dumps the column as a decimal. + spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type + 'decimal' + else + column.type.to_s + end + spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal' + spec[:precision] = column.precision.inspect if column.precision + spec[:scale] = column.scale.inspect if column.scale + spec[:null] = 'false' unless column.null + spec[:default] = default_string(column.default) if column.has_default? + spec + end + + # Lists the valid migration options + def migration_keys + [:name, :limit, :precision, :scale, :default, :null] + end + + private + + def default_string(value) + case value + when BigDecimal + value.to_s + when Date, DateTime, Time + "'#{value.to_s(:db)}'" + else + value.inspect + 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 c37c9b1ae1..3a8fbcf93f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -3,6 +3,7 @@ require 'bigdecimal' require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' +require 'active_record/connection_adapters/abstract/schema_dumper' require 'monitor' require 'active_support/deprecation' @@ -59,6 +60,7 @@ module ActiveRecord include QueryCache include ActiveSupport::Callbacks include MonitorMixin + include ColumnDumper define_callbacks :checkout, :checkin diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb new file mode 100644 index 0000000000..b7d24f2bb3 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -0,0 +1,97 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLColumn < Column + module ArrayParser + private + # Loads pg_array_parser if available. String parsing can be + # performed quicker by a native extension, which will not create + # a large amount of Ruby objects that will need to be garbage + # collected. pg_array_parser has a C and Java extension + begin + require 'pg_array_parser' + include PgArrayParser + rescue LoadError + def parse_pg_array(string) + parse_data(string, 0) + end + end + + def parse_data(string, index) + local_index = index + array = [] + while(local_index < string.length) + case string[local_index] + when '{' + local_index,array = parse_array_contents(array, string, local_index + 1) + when '}' + return array + end + local_index += 1 + end + + array + end + + def parse_array_contents(array, string, index) + is_escaping = false + is_quoted = false + was_quoted = false + current_item = '' + + local_index = index + while local_index + token = string[local_index] + if is_escaping + current_item << token + is_escaping = false + else + if is_quoted + case token + when '"' + is_quoted = false + was_quoted = true + when "\\" + is_escaping = true + else + current_item << token + end + else + case token + when "\\" + is_escaping = true + when ',' + add_item_to_array(array, current_item, was_quoted) + current_item = '' + was_quoted = false + when '"' + is_quoted = true + when '{' + internal_items = [] + local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) + array.push(internal_items) + when '}' + add_item_to_array(array, current_item, was_quoted) + return local_index,array + else + current_item << token + end + end + end + + local_index += 1 + end + return local_index,array + end + + def add_item_to_array(array, current_item, quoted) + if current_item.length == 0 + elsif !quoted && current_item == 'NULL' + array.push nil + else + array.push current_item + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index b59195f98a..62d091357d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -45,6 +45,21 @@ module ActiveRecord end end + def array_to_string(value, column, adapter, should_be_quoted = false) + casted_values = value.map do |val| + if String === val + if val == "NULL" + "\"#{val}\"" + else + quote_and_escape(adapter.type_cast(val, column, true)) + end + else + adapter.type_cast(val, column, true) + end + end + "{#{casted_values.join(',')}}" + end + def string_to_json(string) if String === string ActiveSupport::JSON.decode(string) @@ -71,6 +86,10 @@ module ActiveRecord end end + def string_to_array(string, oid) + parse_pg_array(string).map{|val| oid.type_cast val} + end + private HstorePair = begin @@ -90,6 +109,15 @@ module ActiveRecord end end end + + def quote_and_escape(value) + case value + when "NULL" + value + else + "\"#{value.gsub(/"/,"\\\"")}\"" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index b8e7687b21..52344f61c0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -63,6 +63,21 @@ module ActiveRecord end end + class Array < Type + attr_reader :subtype + def initialize(subtype) + @subtype = subtype + end + + def type_cast(value) + if String === value + ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype + else + value + end + end + end + class Integer < Type def type_cast(value) return if value.nil? diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 85721601a9..37d43d891d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -19,6 +19,12 @@ module ActiveRecord return super unless column case value + when Array + if column.array + "'#{PostgreSQLColumn.array_to_string(value, column, self)}'" + else + super + end when Hash case column.sql_type when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) @@ -59,24 +65,35 @@ module ActiveRecord end end - def type_cast(value, column) - return super unless column + def type_cast(value, column, array_member = false) + return super(value, column) unless column case value + when NilClass + if column.array && array_member + 'NULL' + elsif column.array + value + else + super(value, column) + end + when Array + return super(value, column) unless column.array + PostgreSQLColumn.array_to_string(value, column, self) when String - return super unless 'bytea' == column.sql_type + return super(value, column) unless 'bytea' == column.sql_type { :value => value, :format => 1 } when Hash case column.sql_type when 'hstore' then PostgreSQLColumn.hstore_to_string(value) when 'json' then PostgreSQLColumn.json_to_string(value) - else super + else super(value, column) end when IPAddr - return super unless ['inet','cidr'].includes? column.sql_type + return super(value, column) unless ['inet','cidr'].includes? column.sql_type PostgreSQLColumn.cidr_to_string(value) else - super + super(value, column) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e85e63d607..761052a788 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -2,6 +2,7 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' require 'active_record/connection_adapters/postgresql/oid' require 'active_record/connection_adapters/postgresql/cast' +require 'active_record/connection_adapters/postgresql/array_parser' require 'active_record/connection_adapters/postgresql/quoting' require 'active_record/connection_adapters/postgresql/schema_statements' require 'active_record/connection_adapters/postgresql/database_statements' @@ -41,16 +42,23 @@ module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: + attr_accessor :array # Instantiates a new PostgreSQL column definition in a table. def initialize(name, default, oid_type, sql_type = nil, null = true) @oid_type = oid_type - super(name, self.class.extract_value_from_default(default), sql_type, null) + if sql_type =~ /\[\]$/ + @array = true + super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null) + else + @array = false + super(name, self.class.extract_value_from_default(default), sql_type, null) + end end # :stopdoc: class << self include ConnectionAdapters::PostgreSQLColumn::Cast - + include ConnectionAdapters::PostgreSQLColumn::ArrayParser attr_accessor :money_precision end # :startdoc: @@ -243,6 +251,10 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :array + end + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition def xml(*args) options = args.extract_options! @@ -277,6 +289,23 @@ module ActiveRecord def json(name, options = {}) column(name, 'json', options) end + + def column(name, type = nil, options = {}) + super + column = self[name] + column.array = options[:array] + + self + end + + private + + def new_column_definition(base, name, type) + definition = ColumnDefinition.new base, name, type + @columns << definition + @columns_hash[name] = definition + definition + end end ADAPTER_NAME = 'PostgreSQL' @@ -314,6 +343,19 @@ module ActiveRecord ADAPTER_NAME end + # Adds `:array` option to the default set provided by the + # AbstractAdapter + def prepare_column_options(column, types) + spec = super + spec[:array] = 'true' if column.respond_to?(:array) && column.array + spec + end + + # Adds `:array` as a valid migration key + def migration_keys + super + [:array] + end + # Returns +true+, since this connection adapter supports prepared statement # caching. def supports_statement_cache? @@ -493,6 +535,13 @@ module ActiveRecord @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i end + def add_column_options!(sql, options) + if options[:array] || options[:column].try(:array) + sql << '[]' + end + super + end + # Set the authorized user for this session def session_auth=(user) clear_cache! @@ -547,7 +596,7 @@ module ActiveRecord private def initialize_type_map - result = execute('SELECT oid, typname, typelem, typdelim FROM pg_type', 'SCHEMA') + result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA') leaves, nodes = result.partition { |row| row['typelem'] == '0' } # populate the leaf nodes @@ -555,11 +604,19 @@ module ActiveRecord OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] end + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + # populate composite types nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] OID::TYPE_MAP[row['oid'].to_i] = vector end + + # populate array types + arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| + array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i] + OID::TYPE_MAP[row['oid'].to_i] = array + end end FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: @@ -702,12 +759,12 @@ module ActiveRecord # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: exec_query(<<-end_sql, 'SCHEMA').rows - SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod - FROM pg_attribute a LEFT JOIN pg_attrdef d - ON a.attrelid = d.adrelid AND a.attnum = d.adnum - WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass - AND a.attnum > 0 AND NOT a.attisdropped - ORDER BY a.attnum + SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod + FROM pg_attribute a LEFT JOIN pg_attrdef d + ON a.attrelid = d.adrelid AND a.attnum = d.adnum + WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass + AND a.attnum > 0 AND NOT a.attisdropped + ORDER BY a.attnum end_sql end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 310b4c1459..36bde44e7c 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -107,27 +107,11 @@ HEADER column_specs = columns.map do |column| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil? next if column.name == pk - spec = {} - spec[:name] = column.name.inspect - - # AR has an optimization which handles zero-scale decimals as integers. This - # code ensures that the dumper still dumps the column as a decimal. - spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type - 'decimal' - else - column.type.to_s - end - spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal' - spec[:precision] = column.precision.inspect if column.precision - spec[:scale] = column.scale.inspect if column.scale - spec[:null] = 'false' unless column.null - spec[:default] = default_string(column.default) if column.has_default? - (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")} - spec + @connection.column_spec(column, @types) end.compact # find all migration keys used in this table - keys = [:name, :limit, :precision, :scale, :default, :null] + keys = @connection.migration_keys # figure out the lengths for each column based on above keys lengths = keys.map { |key| @@ -170,17 +154,6 @@ HEADER stream end - def default_string(value) - case value - when BigDecimal - value.to_s - when Date, DateTime, Time - "'#{value.to_s(:db)}'" - else - value.inspect - end - end - def indexes(table, stream) if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb new file mode 100644 index 0000000000..8774bf626f --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -0,0 +1,98 @@ +# encoding: utf-8 +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlArrayTest < ActiveRecord::TestCase + class PgArray < ActiveRecord::Base + self.table_name = 'pg_arrays' + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.create_table('pg_arrays') do |t| + t.string 'tags', :array => true + end + end + @column = PgArray.columns.find { |c| c.name == 'tags' } + end + + def teardown + @connection.execute 'drop table if exists pg_arrays' + end + + def test_column + assert_equal :string, @column.type + assert @column.array + end + + def test_type_cast_array + assert @column + + data = '{1,2,3}' + oid_type = @column.instance_variable_get('@oid_type').subtype + # we are getting the instance variable in this test, but in the + # normal use of string_to_array, it's called from the OID::Array + # class and will have the OID instance that will provide the type + # casting + array = @column.class.string_to_array data, oid_type + assert_equal(['1', '2', '3'], array) + assert_equal(['1', '2', '3'], @column.type_cast(data)) + + assert_equal([], @column.type_cast('{}')) + assert_equal([nil], @column.type_cast('{NULL}')) + end + + def test_rewrite + @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" + x = PgArray.first + x.tags = ['1','2','3','4'] + assert x.save! + end + + def test_select + @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" + x = PgArray.first + assert_equal(['1','2','3'], x.tags) + end + + def test_multi_dimensional + assert_cycle([['1','2'],['2','3']]) + end + + def test_strings_with_quotes + assert_cycle(['this has','some "s that need to be escaped"']) + end + + def test_strings_with_commas + assert_cycle(['this,has','many,values']) + end + + def test_strings_with_array_delimiters + assert_cycle(['{','}']) + end + + def test_strings_with_null_strings + assert_cycle(['NULL','NULL']) + end + + def test_contains_nils + assert_cycle(['1',nil,nil]) + end + + private + def assert_cycle array + # test creation + x = PgArray.create!(:tags => array) + x.reload + assert_equal(array, x.tags) + + # test updating + x = PgArray.create!(:tags => []) + x.tags = array + x.save! + x.reload + assert_equal(array, x.tags) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index a7f6d9c580..c7ce43d71e 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -70,8 +70,8 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_data_type_of_array_types - assert_equal :string, @first_array.column_for_attribute(:commission_by_quarter).type - assert_equal :string, @first_array.column_for_attribute(:nicknames).type + assert_equal :integer, @first_array.column_for_attribute(:commission_by_quarter).type + assert_equal :text, @first_array.column_for_attribute(:nicknames).type end def test_data_type_of_tsvector_types @@ -112,8 +112,8 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_array_values - assert_equal '{35000,21000,18000,17000}', @first_array.commission_by_quarter - assert_equal '{foo,bar,baz}', @first_array.nicknames + assert_equal [35000,21000,18000,17000], @first_array.commission_by_quarter + assert_equal ['foo','bar','baz'], @first_array.nicknames end def test_tsvector_values @@ -170,7 +170,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_update_integer_array - new_value = '{32800,95000,29350,17000}' + new_value = [32800,95000,29350,17000] assert @first_array.commission_by_quarter = new_value assert @first_array.save assert @first_array.reload @@ -182,7 +182,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_update_text_array - new_value = '{robby,robert,rob,robbie}' + new_value = ['robby','robert','rob','robbie'] assert @first_array.nicknames = new_value assert @first_array.save assert @first_array.reload diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 04714f42e9..4405f34355 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -46,10 +46,13 @@ class HasManyAssociationsTestForCountWithCountSql < ActiveRecord::TestCase end end -class HasManyAssociationsTestForCountDistinctWithFinderSql < ActiveRecord::TestCase +class HasManyAssociationsTestForCountWithVariousFinderSqls < ActiveRecord::TestCase class Invoice < ActiveRecord::Base ActiveSupport::Deprecation.silence do has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT DISTINCT line_items.amount from line_items" + has_many :custom_full_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.invoice_id, line_items.amount from line_items" + has_many :custom_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT * from line_items" + has_many :custom_qualified_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items" end end @@ -61,6 +64,33 @@ class HasManyAssociationsTestForCountDistinctWithFinderSql < ActiveRecord::TestC assert_equal 1, invoice.custom_line_items.count end + + def test_should_count_results_with_multiple_fields + invoice = Invoice.new + invoice.custom_full_line_items << LineItem.new(:amount => 0) + invoice.custom_full_line_items << LineItem.new(:amount => 0) + invoice.save! + + assert_equal 2, invoice.custom_full_line_items.count + end + + def test_should_count_results_with_star + invoice = Invoice.new + invoice.custom_star_line_items << LineItem.new(:amount => 0) + invoice.custom_star_line_items << LineItem.new(:amount => 0) + invoice.save! + + assert_equal 2, invoice.custom_star_line_items.count + end + + def test_should_count_results_with_qualified_star + invoice = Invoice.new + invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0) + invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0) + invoice.save! + + assert_equal 2, invoice.custom_qualified_star_line_items.count + end end class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 80d2670f94..80f46c6b08 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -79,9 +79,9 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_arguments_line_up column_definition_lines.each do |column_set| - assert_line_up(column_set, /:default => /) - assert_line_up(column_set, /:limit => /) - assert_line_up(column_set, /:null => /) + assert_line_up(column_set, /default: /) + assert_line_up(column_set, /limit: /) + assert_line_up(column_set, /null: /) end end @@ -278,6 +278,14 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dump_includes_arrays_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_arrays"} =~ output + assert_match %r[t.text\s+"nicknames",\s+array: true], output + assert_match %r[t.integer\s+"commission_by_quarter",\s+array: true], output + end + end + def test_schema_dump_includes_tsvector_shorthand_definition output = standard_dump if %r{create_table "postgresql_tsvectors"} =~ output diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index c10a0b390b..59e112e3f9 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,18 @@ ## Rails 4.0.0 (unreleased) ## +* An optional block can be passed to `config_accessor` to set its default value + + class User + include ActiveSupport::Configurable + config_accessor :hair_colors do + [:brown, :black, :blonde, :red] + end + end + + User.hair_colors # => [:brown, :black, :blonde, :red] + + *Larry Lv* + * ActiveSupport::Benchmarkable#silence has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1. *Steve Klabnik* diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb index 307ae40398..15a5b98d56 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -39,7 +39,7 @@ module ActiveSupport # Allows you to add shortcut so that you don't have to refer to attribute # through config. Also look at the example for config to contrast. - # + # # Defines both class and instance config accessors. # # class User @@ -47,16 +47,16 @@ module ActiveSupport # config_accessor :allowed_access # end # - # User.allowed_access # => nil + # User.allowed_access # => nil # User.allowed_access = false - # User.allowed_access # => false - # + # User.allowed_access # => false + # # user = User.new # user.allowed_access # => false # user.allowed_access = true # user.allowed_access # => true # - # User.allowed_access # => false + # User.allowed_access # => false # # The attribute name must be a valid method name in Ruby. # @@ -91,7 +91,18 @@ module ActiveSupport # User.allowed_access # => false # # User.new.allowed_access = true # => NoMethodError - # User.new.allowed_access # => NoMethodError + # User.new.allowed_access # => NoMethodError + # + # Also you can pass a block to set up the attribute with a default value. + # + # class User + # include ActiveSupport::Configurable + # config_accessor :hair_colors do + # [:brown, :black, :blonde, :red] + # end + # end + # + # User.hair_colors # => [:brown, :black, :blonde, :red] def config_accessor(*names) options = names.extract_options! @@ -108,6 +119,7 @@ module ActiveSupport class_eval reader, __FILE__, reader_line unless options[:instance_reader] == false class_eval writer, __FILE__, writer_line unless options[:instance_writer] == false end + send("#{name}=", yield) if block_given? end end end diff --git a/activesupport/lib/active_support/queueing.rb b/activesupport/lib/active_support/queueing.rb index f397e1c0c5..d36b5c17a8 100644 --- a/activesupport/lib/active_support/queueing.rb +++ b/activesupport/lib/active_support/queueing.rb @@ -2,17 +2,33 @@ require 'delegate' require 'thread' module ActiveSupport - # A Queue that simply inherits from STDLIB's Queue. Everytime this - # queue is used, Rails automatically sets up a ThreadedConsumer - # to consume it. + # A Queue that simply inherits from STDLIB's Queue. When this + # queue is used, Rails automatically starts a job runner in a + # background thread. class Queue < ::Queue + attr_writer :consumer + + def initialize(consumer_options = {}) + super() + @consumer_options = consumer_options + end + + def consumer + @consumer ||= ThreadedQueueConsumer.new(self, @consumer_options) + end + + # Drain the queue, running all jobs in a different thread. This method + # may not be available on production queues. + def drain + # run the jobs in a separate thread so assumptions of synchronous + # jobs are caught in test mode. + consumer.drain + end end - class SynchronousQueue < ::Queue + class SynchronousQueue < Queue def push(job) - result = nil - Thread.new { result = job.run }.join - result + super.tap { drain } end alias << push alias enq push @@ -25,7 +41,7 @@ module ActiveSupport # # Jobs are run in a separate thread to catch mistakes where code # assumes that the job is run in the same thread. - class TestQueue < ::Queue + class TestQueue < Queue # Get a list of the jobs off this queue. This method may not be # available on production queues. def jobs @@ -38,14 +54,6 @@ module ActiveSupport def push(job) super Marshal.load(Marshal.dump(job)) end - - # Drain the queue, running all jobs in a different thread. This method - # may not be available on production queues. - def drain - # run the jobs in a separate thread so assumptions of synchronous - # jobs are caught in test mode. - Thread.new { pop.run until empty? }.join - end end # A container for multiple queues. This class delegates to a default Queue @@ -82,25 +90,17 @@ module ActiveSupport # queue and joins the thread, which will ensure that all jobs # are executed before the process finally dies. class ThreadedQueueConsumer - def self.start(queue, logger=nil) - new(queue, logger).start + def self.start(*args) + new(*args).start end - def initialize(queue, logger=nil) - @queue = queue - @logger = logger + def initialize(queue, options = {}) + @queue = queue + @logger = options[:logger] end def start - @thread = Thread.new do - while job = @queue.pop - begin - job.run - rescue Exception => e - handle_exception e - end - end - end + @thread = Thread.new { consume } self end @@ -109,8 +109,25 @@ module ActiveSupport @thread.join end - def handle_exception(e) - @logger.error "Job Error: #{e.message}\n#{e.backtrace.join("\n")}" if @logger + def drain + Thread.new { run(@queue.pop) until @queue.empty? }.join + end + + def consume + while job = @queue.pop + run job + end + end + + def run(job) + job.run + rescue Exception => exception + handle_exception job, exception + end + + def handle_exception(job, exception) + raise unless @logger + @logger.error "Job Error: #{exception.message}\n#{exception.backtrace.join("\n")}" end end end diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb index da7729d066..215a6e06b0 100644 --- a/activesupport/test/configurable_test.rb +++ b/activesupport/test/configurable_test.rb @@ -48,6 +48,18 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase assert !instance.respond_to?(:baz=) end + test "configuration accessors can take a default value" do + parent = Class.new do + include ActiveSupport::Configurable + config_accessor :hair_colors, :tshirt_colors do + [:black, :blue, :white] + end + end + + assert_equal [:black, :blue, :white], parent.hair_colors + assert_equal [:black, :blue, :white], parent.tshirt_colors + end + test "configuration hash is available on instance" do instance = Parent.new assert_equal :bar, instance.config.foo @@ -78,7 +90,7 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase test "should raise name error if attribute name is invalid" do assert_raises NameError do - Class.new do + Class.new do include ActiveSupport::Configurable config_accessor "invalid attribute name" end @@ -94,4 +106,4 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase methods = object.public_methods.map(&:to_s) assert !methods.include?(method.to_s), "Expected #{methods.inspect} to not include #{method.to_s.inspect}" end -end
\ No newline at end of file +end diff --git a/activesupport/test/queueing/synchronous_queue_test.rb b/activesupport/test/queueing/synchronous_queue_test.rb new file mode 100644 index 0000000000..86c39d0f6c --- /dev/null +++ b/activesupport/test/queueing/synchronous_queue_test.rb @@ -0,0 +1,27 @@ +require 'abstract_unit' +require 'active_support/queueing' + +class SynchronousQueueTest < ActiveSupport::TestCase + class Job + attr_reader :ran + def run; @ran = true end + end + + class ExceptionRaisingJob + def run; raise end + end + + def setup + @queue = ActiveSupport::SynchronousQueue.new + end + + def test_runs_jobs_immediately + job = Job.new + @queue.push job + assert job.ran + + assert_raises RuntimeError do + @queue.push ExceptionRaisingJob.new + end + end +end diff --git a/activesupport/test/queueing/test_queue_test.rb b/activesupport/test/queueing/test_queue_test.rb index 4c08314366..9e74bc64ee 100644 --- a/activesupport/test/queueing/test_queue_test.rb +++ b/activesupport/test/queueing/test_queue_test.rb @@ -12,7 +12,7 @@ class TestQueueTest < ActiveSupport::TestCase end end - def test_drain_raises + def test_drain_raises_exceptions_from_running_jobs @queue.push ExceptionRaisingJob.new assert_raises(RuntimeError) { @queue.drain } end @@ -41,8 +41,8 @@ class TestQueueTest < ActiveSupport::TestCase end def test_contents - assert @queue.empty? job = EquivalentJob.new + assert @queue.empty? @queue.push job refute @queue.empty? assert_equal job, @queue.pop diff --git a/activesupport/test/queueing/threaded_consumer_test.rb b/activesupport/test/queueing/threaded_consumer_test.rb index 20a1cc4e8e..fc43cb555a 100644 --- a/activesupport/test/queueing/threaded_consumer_test.rb +++ b/activesupport/test/queueing/threaded_consumer_test.rb @@ -5,7 +5,7 @@ require "active_support/log_subscriber/test_helper" class TestThreadConsumer < ActiveSupport::TestCase class Job attr_reader :id - def initialize(id, &block) + def initialize(id = 1, &block) @id = id @block = block end @@ -16,83 +16,77 @@ class TestThreadConsumer < ActiveSupport::TestCase end def setup - @queue = ActiveSupport::Queue.new @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new - @consumer = ActiveSupport::ThreadedQueueConsumer.start(@queue, @logger) + @queue = ActiveSupport::Queue.new(logger: @logger) end def teardown - @queue.push nil + @queue.drain end test "the jobs are executed" do ran = false - - job = Job.new(1) do - ran = true - end + job = Job.new { ran = true } @queue.push job - sleep 0.1 + @queue.drain + assert_equal true, ran end test "the jobs are not executed synchronously" do - ran = false - - job = Job.new(1) do - sleep 0.1 - ran = true - end + run, ran = Queue.new, Queue.new + job = Job.new { ran.push run.pop } + @queue.consumer.start @queue.push job - assert_equal false, ran + assert ran.empty? + + run.push true + assert_equal true, ran.pop end test "shutting down the queue synchronously drains the jobs" do ran = false - - job = Job.new(1) do + job = Job.new do sleep 0.1 ran = true end + @queue.consumer.start @queue.push job assert_equal false, ran - @consumer.shutdown - + @queue.consumer.shutdown assert_equal true, ran end test "log job that raises an exception" do - job = Job.new(1) do - raise "RuntimeError: Error!" - end + job = Job.new { raise "RuntimeError: Error!" } @queue.push job - sleep 0.1 + @queue.drain assert_equal 1, @logger.logged(:error).size - assert_match(/Job Error: RuntimeError: Error!/, @logger.logged(:error).last) + assert_match 'Job Error: RuntimeError: Error!', @logger.logged(:error).last end test "test overriding exception handling" do - @consumer.shutdown - @consumer = Class.new(ActiveSupport::ThreadedQueueConsumer) do - attr_reader :last_error - def handle_exception(e) - @last_error = e.message + @queue.consumer.instance_eval do + def handle_exception(job, exception) + @last_error = exception.message end - end.start(@queue) - job = Job.new(1) do - raise "RuntimeError: Error!" + def last_error + @last_error + end end + job = Job.new { raise "RuntimeError: Error!" } + @queue.push job - sleep 0.1 + @queue.drain - assert_equal "RuntimeError: Error!", @consumer.last_error + assert_equal "RuntimeError: Error!", @queue.consumer.last_error end end diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index f9a3c00946..d2a402aa51 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -98,7 +98,7 @@ module Rails initializer :activate_queue_consumer do |app| if config.queue == ActiveSupport::Queue - app.queue_consumer = config.queue_consumer.start(app.queue, Rails.logger) + app.queue_consumer = config.queue_consumer.start(app.queue, {logger: Rails.logger}) at_exit { app.queue_consumer.shutdown } end end |