diff options
author | Neeraj Singh <neerajdotname@gmail.com> | 2010-11-22 17:32:27 -0500 |
---|---|---|
committer | Neeraj Singh <neerajdotname@gmail.com> | 2010-11-22 17:32:27 -0500 |
commit | 5debc65c356c619bce20dcc10c0befc7c4204ef5 (patch) | |
tree | dc32bab1eccd11bacdaff70f25696689a2b9dc66 /activerecord/lib | |
parent | 1c68e55ae5e90b7c072a1f6030ea3dd4becd8a07 (diff) | |
parent | 818b366c3e3645b3375ee0b1d1023621dbeaede2 (diff) | |
download | rails-5debc65c356c619bce20dcc10c0befc7c4204ef5.tar.gz rails-5debc65c356c619bce20dcc10c0befc7c4204ef5.tar.bz2 rails-5debc65c356c619bce20dcc10c0befc7c4204ef5.zip |
Merge branch 'master' of github.com:lifo/docrails
Diffstat (limited to 'activerecord/lib')
22 files changed, 438 insertions, 225 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 911a5155fd..9743b1b4a7 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -193,13 +193,17 @@ module ActiveRecord conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" conditions << append_conditions(reflection, preload_options) - associated_records = reflection.klass.unscoped.where([conditions, ids]). + associated_records_proxy = reflection.klass.unscoped. includes(options[:include]). joins("INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}"). select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id"). - order(options[:order]).to_a + order(options[:order]) - set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id') + all_associated_records = associated_records(ids) do |some_ids| + associated_records_proxy.where([conditions, ids]).to_a + end + + set_association_collection_records(id_to_record_map, reflection.name, all_associated_records, 'the_parent_record_id') end def preload_has_one_association(records, reflection, preload_options={}) @@ -358,13 +362,14 @@ module ActiveRecord find_options = { :select => preload_options[:select] || options[:select] || Arel::SqlLiteral.new("#{table_name}.*"), :include => preload_options[:include] || options[:include], - :conditions => [conditions, ids], :joins => options[:joins], :group => preload_options[:group] || options[:group], :order => preload_options[:order] || options[:order] } - reflection.klass.scoped.apply_finder_options(find_options).to_a + associated_records(ids) do |some_ids| + reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => [conditions, some_ids])).to_a + end end @@ -382,6 +387,17 @@ module ActiveRecord def in_or_equals_for_ids(ids) ids.size > 1 ? "IN (?)" : "= ?" end + + # Some databases impose a limit on the number of ids in a list (in Oracle its 1000) + # Make several smaller queries if necessary or make one query if the adapter supports it + def associated_records(ids) + max_ids_in_a_list = connection.ids_in_list_limit || ids.size + records = [] + ids.each_slice(max_ids_in_a_list) do |some_ids| + records += yield(some_ids) + end + records + end end end end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 08a4ebfd7e..7da38cd03f 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/conversions' require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/class/attribute' module ActiveRecord class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: @@ -1810,12 +1811,12 @@ module ActiveRecord callbacks.each do |callback_name| full_callback_name = "#{callback_name}_for_#{association_name}" defined_callbacks = options[callback_name.to_sym] - if options.has_key?(callback_name.to_sym) - class_inheritable_reader full_callback_name.to_sym - write_inheritable_attribute(full_callback_name.to_sym, [defined_callbacks].flatten) - else - write_inheritable_attribute(full_callback_name.to_sym, []) - end + + full_callback_value = options.has_key?(callback_name.to_sym) ? [defined_callbacks].flatten : [] + + # TODO : why do i need method_defined? I think its because of the inheritance chain + class_attribute full_callback_name.to_sym unless method_defined?(full_callback_name) + self.send("#{full_callback_name}=", full_callback_value) end end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 5b34ac907c..774103342a 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -182,7 +182,7 @@ module ActiveRecord unless options.blank? raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" end - + @reflection.klass.count_by_sql(custom_counter_sql) else @@ -332,13 +332,10 @@ module ActiveRecord end def uniq(collection = self) - seen = Set.new - collection.map do |record| - unless seen.include?(record.id) - seen << record.id - record - end - end.compact + seen = {} + collection.find_all do |record| + seen[record.id] = true unless seen.key?(record.id) + end end # Replace this collection with +other_array+ @@ -375,14 +372,16 @@ module ActiveRecord def load_target if @owner.persisted? || foreign_key_present begin - if !loaded? + unless loaded? if @target.is_a?(Array) && @target.any? @target = find_target.map do |f| i = @target.index(f) if i @target.delete_at(i).tap do |t| keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) - t.attributes = f.attributes.except(*keys) + f.attributes.except(*keys).each do |k,v| + t.send("#{k}=", v) + end end else f @@ -409,11 +408,7 @@ module ActiveRecord end if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) - if block_given? - super { |*block_args| yield(*block_args) } - else - super - end + super elsif @reflection.klass.scopes[method] @_named_scopes_cache ||= {} @_named_scopes_cache[method] ||= {} @@ -436,10 +431,10 @@ module ActiveRecord # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ counter_sql = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } end - + interpolate_sql(counter_sql) end - + def custom_finder_sql interpolate_sql(@reflection.options[:finder_sql]) end @@ -535,7 +530,7 @@ module ActiveRecord def callbacks_for(callback_name) full_callback_name = "#{callback_name}_for_#{@reflection.name}" - @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || [] + @owner.class.send(full_callback_name.to_sym) || [] end def ensure_owner_is_not_new diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 09bba213ce..7cd04a1ad5 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -211,12 +211,12 @@ module ActiveRecord :create => construct_create_scope } end - + # Implemented by subclasses def construct_find_scope raise NotImplementedError end - + # Implemented by (some) subclasses def construct_create_scope {} diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 439880c1fa..c19a33faa8 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/object/blank' module ActiveRecord @@ -88,7 +89,7 @@ module ActiveRecord end def clone_with_time_zone_conversion_attribute?(attr, old) - old.class.name == "Time" && time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(attr.to_sym) + old.class.name == "Time" && time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(attr.to_sym) end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index d640b26b74..dc2785b6bf 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/class/attribute' + module ActiveRecord module AttributeMethods module TimeZoneConversion @@ -7,7 +9,7 @@ module ActiveRecord cattr_accessor :time_zone_aware_attributes, :instance_writer => false self.time_zone_aware_attributes = false - class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false + class_attribute :skip_time_zone_conversion_for_attributes, :instance_writer => false self.skip_time_zone_conversion_for_attributes = [] end @@ -54,7 +56,7 @@ module ActiveRecord private def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) + time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f588475bbf..aee6f3a7eb 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -7,7 +7,7 @@ require 'active_support/time' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/delegating_attributes' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/indifferent_access' @@ -412,7 +412,7 @@ module ActiveRecord #:nodoc: self.store_full_sti_class = true # Stores the default scope for the class - class_inheritable_accessor :default_scoping, :instance_writer => false + class_attribute :default_scoping, :instance_writer => false self.default_scoping = [] # Returns a hash of all the attributes that have been specified for serialization as @@ -420,6 +420,9 @@ module ActiveRecord #:nodoc: class_attribute :serialized_attributes self.serialized_attributes = {} + class_attribute :_attr_readonly, :instance_writer => false + self._attr_readonly = [] + class << self # Class methods delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped delegate :find_each, :find_in_batches, :to => :scoped @@ -504,12 +507,12 @@ module ActiveRecord #:nodoc: # Attributes listed as readonly will be used to create a new record but update operations will # ignore these fields. def attr_readonly(*attributes) - write_inheritable_attribute(:attr_readonly, Set.new(attributes.map { |a| a.to_s }) + (readonly_attributes || [])) + self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || []) end # Returns an array of all the attributes that have been specified as readonly. def readonly_attributes - read_inheritable_attribute(:attr_readonly) || [] + self._attr_readonly end # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, @@ -724,10 +727,6 @@ module ActiveRecord #:nodoc: @arel_engine = @relation = @arel_table = nil end - def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc: - descendants.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } - end - def attribute_method?(attribute) super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) end @@ -736,15 +735,12 @@ module ActiveRecord #:nodoc: def lookup_ancestors #:nodoc: klass = self classes = [klass] + return classes if klass == ActiveRecord::Base + while klass != klass.base_class classes << klass = klass.superclass end classes - rescue - # OPTIMIZE this rescue is to fix this test: ./test/cases/reflection_test.rb:56:in `test_human_name_for_column' - # Apparently the method base_class causes some trouble. - # It now works for sure. - [self] end # Set the i18n scope to overwrite ActiveModel. @@ -1129,7 +1125,8 @@ MSG # Article.create.published # => true def default_scope(options = {}) reset_scoped_methods - self.default_scoping << construct_finder_arel(options, default_scoping.pop) + default_scoping = self.default_scoping.dup + self.default_scoping = default_scoping << construct_finder_arel(options, default_scoping.pop) end def current_scoped_methods #:nodoc: @@ -1286,7 +1283,7 @@ MSG # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" def sanitize_sql_array(ary) statement, *values = ary - if values.first.is_a?(Hash) and statement =~ /:\w+/ + if values.first.is_a?(Hash) && statement =~ /:\w+/ replace_named_bind_variables(statement, values.first) elsif statement.include?('?') replace_bind_variables(statement, values) @@ -1582,12 +1579,20 @@ MSG self.class.columns_hash[name.to_s] end - # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id. + # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ + # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+. + # + # Note that new records are different from any other record by definition, unless the + # other record is the receiver itself. Besides, if you fetch existing records with + # +select+ and leave the ID out, you're on your own, this predicate will return false. + # + # Note also that destroying a record preserves its ID in the model instance, so deleted + # models are still comparable. def ==(comparison_object) comparison_object.equal?(self) || - persisted? && - (comparison_object.instance_of?(self.class) && - comparison_object.id == id) + comparison_object.instance_of?(self.class) && + id.present? && + comparison_object.id == id end # Delegates to == diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index f3fba9a3a9..ada1560ce2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -91,6 +91,11 @@ module ActiveRecord false end + # Does this adapter restrict the number of ids you can use in a list. Oracle has a limit of 1000. + def ids_in_list_limit + nil + end + # QUOTING ================================================== # Override to return the quoted table name. Defaults to column quoting. diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index a4c09b654a..f6321f1499 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,6 +1,3 @@ -require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/module/aliasing' - module ActiveRecord # Exception that can be raised to stop migrations from going backwards. class IrreversibleMigration < ActiveRecordError @@ -43,11 +40,11 @@ module ActiveRecord # Example of a simple migration: # # class AddSsl < ActiveRecord::Migration - # def self.up + # def up # add_column :accounts, :ssl_enabled, :boolean, :default => 1 # end # - # def self.down + # def down # remove_column :accounts, :ssl_enabled # end # end @@ -63,7 +60,7 @@ module ActiveRecord # Example of a more complex migration that also needs to initialize data: # # class AddSystemSettings < ActiveRecord::Migration - # def self.up + # def up # create_table :system_settings do |t| # t.string :name # t.string :label @@ -77,7 +74,7 @@ module ActiveRecord # :value => 1 # end # - # def self.down + # def down # drop_table :system_settings # end # end @@ -138,7 +135,7 @@ module ActiveRecord # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the # UTC formatted date and time that the migration was generated. # - # You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of + # You may then edit the <tt>up</tt> and <tt>down</tt> methods of # MyNewMigration. # # There is a special syntactic shortcut to generate migrations that add fields to a table. @@ -147,11 +144,11 @@ module ActiveRecord # # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: # class AddFieldnameToTablename < ActiveRecord::Migration - # def self.up + # def up # add_column :tablenames, :fieldname, :string # end # - # def self.down + # def down # remove_column :tablenames, :fieldname # end # end @@ -179,11 +176,11 @@ module ActiveRecord # Not all migrations change the schema. Some just fix the data: # # class RemoveEmptyTags < ActiveRecord::Migration - # def self.up + # def up # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? } # end # - # def self.down + # def down # # not much we can do to restore deleted data # raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags" # end @@ -192,12 +189,12 @@ module ActiveRecord # Others remove columns when they migrate up instead of down: # # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration - # def self.up + # def up # remove_column :items, :incomplete_items_count # remove_column :items, :completed_items_count # end # - # def self.down + # def down # add_column :items, :incomplete_items_count # add_column :items, :completed_items_count # end @@ -206,11 +203,11 @@ module ActiveRecord # And sometimes you need to do something in SQL not abstracted directly by migrations: # # class MakeJoinUnique < ActiveRecord::Migration - # def self.up + # def up # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)" # end # - # def self.down + # def down # execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`" # end # end @@ -223,7 +220,7 @@ module ActiveRecord # latest column data from after the new column was added. Example: # # class AddPeopleSalary < ActiveRecord::Migration - # def self.up + # def up # add_column :people, :salary, :integer # Person.reset_column_information # Person.find(:all).each do |p| @@ -243,7 +240,7 @@ module ActiveRecord # You can also insert your own messages and benchmarks by using the +say_with_time+ # method: # - # def self.up + # def up # ... # say_with_time "Updating salaries..." do # Person.find(:all).each do |p| @@ -286,143 +283,201 @@ module ActiveRecord # # In application.rb. # + # == Reversible Migrations + # + # Starting with Rails 3.1, you will be able to define reversible migrations. + # Reversible migrations are migrations that know how to go +down+ for you. + # You simply supply the +up+ logic, and the Migration system will figure out + # how to execute the down commands for you. + # + # To define a reversible migration, define the +change+ method in your + # migration like this: + # + # class TenderloveMigration < ActiveRecord::Migration + # def change + # create_table(:horses) do + # t.column :content, :text + # t.column :remind_at, :datetime + # end + # end + # end + # + # This migration will create the horses table for you on the way up, and + # automatically figure out how to drop the table on the way down. + # + # Some commands like +remove_column+ cannot be reversed. If you care to + # define how to move up and down in these cases, you should define the +up+ + # and +down+ methods as before. + # + # If a command cannot be reversed, an + # <tt>ActiveRecord::IrreversibleMigration</tt> exception will be raised when + # the migration is moving down. + # + # For a list of commands that are reversible, please see + # <tt>ActiveRecord::Migration::CommandRecorder</tt>. class Migration - @@verbose = true - cattr_accessor :verbose + autoload :CommandRecorder, 'active_record/migration/command_recorder' class << self - def up_with_benchmarks #:nodoc: - migrate(:up) - end + attr_accessor :delegate # :nodoc: + end - def down_with_benchmarks #:nodoc: - migrate(:down) - end + def self.method_missing(name, *args, &block) # :nodoc: + (delegate || superclass.delegate).send(name, *args, &block) + end - # Execute this migration in the named direction - def migrate(direction) - return unless respond_to?(direction) + cattr_accessor :verbose - case direction - when :up then announce "migrating" - when :down then announce "reverting" - end + attr_accessor :name, :version - result = nil - time = Benchmark.measure { result = send("#{direction}_without_benchmarks") } + def initialize + @name = self.class.name + @version = nil + @connection = nil + end - case direction - when :up then announce "migrated (%.4fs)" % time.real; write - when :down then announce "reverted (%.4fs)" % time.real; write - end + # instantiate the delegate object after initialize is defined + self.verbose = true + self.delegate = new - result - end + def up + self.class.delegate = self + return unless self.class.respond_to?(:up) + self.class.up + end + + def down + self.class.delegate = self + return unless self.class.respond_to?(:down) + self.class.down + end - # Because the method added may do an alias_method, it can be invoked - # recursively. We use @ignore_new_methods as a guard to indicate whether - # it is safe for the call to proceed. - def singleton_method_added(sym) #:nodoc: - return if defined?(@ignore_new_methods) && @ignore_new_methods + # Execute this migration in the named direction + def migrate(direction) + return unless respond_to?(direction) - begin - @ignore_new_methods = true + case direction + when :up then announce "migrating" + when :down then announce "reverting" + end - case sym - when :up, :down - singleton_class.send(:alias_method_chain, sym, "benchmarks") + time = nil + ActiveRecord::Base.connection_pool.with_connection do |conn| + @connection = conn + if respond_to?(:change) + if direction == :down + recorder = CommandRecorder.new(@connection) + suppress_messages do + @connection = recorder + change + end + @connection = conn + time = Benchmark.measure { + recorder.inverse.each do |cmd, args| + send(cmd, *args) + end + } + else + time = Benchmark.measure { change } end - ensure - @ignore_new_methods = false + else + time = Benchmark.measure { send(direction) } end + @connection = nil end - def write(text="") - puts(text) if verbose + case direction + when :up then announce "migrated (%.4fs)" % time.real; write + when :down then announce "reverted (%.4fs)" % time.real; write end + end - def announce(message) - version = defined?(@version) ? @version : nil + def write(text="") + puts(text) if verbose + end - text = "#{version} #{name}: #{message}" - length = [0, 75 - text.length].max - write "== %s %s" % [text, "=" * length] - end + def announce(message) + text = "#{version} #{name}: #{message}" + length = [0, 75 - text.length].max + write "== %s %s" % [text, "=" * length] + end - def say(message, subitem=false) - write "#{subitem ? " ->" : "--"} #{message}" - end + def say(message, subitem=false) + write "#{subitem ? " ->" : "--"} #{message}" + end - def say_with_time(message) - say(message) - result = nil - time = Benchmark.measure { result = yield } - say "%.4fs" % time.real, :subitem - say("#{result} rows", :subitem) if result.is_a?(Integer) - result - end + def say_with_time(message) + say(message) + result = nil + time = Benchmark.measure { result = yield } + say "%.4fs" % time.real, :subitem + say("#{result} rows", :subitem) if result.is_a?(Integer) + result + end - def suppress_messages - save, self.verbose = verbose, false - yield - ensure - self.verbose = save - end + def suppress_messages + save, self.verbose = verbose, false + yield + ensure + self.verbose = save + end - def connection - ActiveRecord::Base.connection - end + def connection + @connection || ActiveRecord::Base.connection + end - def method_missing(method, *arguments, &block) - arg_list = arguments.map{ |a| a.inspect } * ', ' + def method_missing(method, *arguments, &block) + arg_list = arguments.map{ |a| a.inspect } * ', ' - say_with_time "#{method}(#{arg_list})" do - unless arguments.empty? || method == :execute - arguments[0] = Migrator.proper_table_name(arguments.first) - end - connection.send(method, *arguments, &block) + say_with_time "#{method}(#{arg_list})" do + unless arguments.empty? || method == :execute + arguments[0] = Migrator.proper_table_name(arguments.first) end + return super unless connection.respond_to?(method) + connection.send(method, *arguments, &block) end + end - def copy(destination, sources, options = {}) - copied = [] - - destination_migrations = ActiveRecord::Migrator.migrations(destination) - last = destination_migrations.last - sources.each do |name, path| - source_migrations = ActiveRecord::Migrator.migrations(path) + def copy(destination, sources, options = {}) + copied = [] - source_migrations.each do |migration| - source = File.read(migration.filename) - source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}" + FileUtils.mkdir_p(destination) unless File.exists?(destination) - if duplicate = destination_migrations.detect { |m| m.name == migration.name } - options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip] - next - end + destination_migrations = ActiveRecord::Migrator.migrations(destination) + last = destination_migrations.last + sources.each do |name, path| + source_migrations = ActiveRecord::Migrator.migrations(path) - migration.version = next_migration_number(last ? last.version + 1 : 0).to_i - new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb") - old_path, migration.filename = migration.filename, new_path - last = migration + source_migrations.each do |migration| + source = File.read(migration.filename) + source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}" - FileUtils.cp(old_path, migration.filename) - copied << migration - options[:on_copy].call(name, migration, old_path) if options[:on_copy] - destination_migrations << migration + if duplicate = destination_migrations.detect { |m| m.name == migration.name } + options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip] + next end - end - copied - end + migration.version = next_migration_number(last ? last.version + 1 : 0).to_i + new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb") + old_path, migration.filename = migration.filename, new_path + last = migration - def next_migration_number(number) - if ActiveRecord::Base.timestamped_migrations - [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max - else - "%.3d" % number + FileUtils.cp(old_path, migration.filename) + copied << migration + options[:on_copy].call(name, migration, old_path) if options[:on_copy] + destination_migrations << migration end end + + copied + end + + def next_migration_number(number) + if ActiveRecord::Base.timestamped_migrations + [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max + else + "%.3d" % number + end end end @@ -449,7 +504,7 @@ module ActiveRecord def load_migration require(File.expand_path(filename)) - name.constantize + name.constantize.new end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb new file mode 100644 index 0000000000..d7e481905a --- /dev/null +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -0,0 +1,91 @@ +module ActiveRecord + class Migration + # ActiveRecord::Migration::CommandRecorder records commands done during + # a migration and knows how to reverse those commands. The CommandRecorder + # knows how to invert the following commands: + # + # * add_column + # * add_index + # * add_timestamp + # * create_table + # * remove_timestamps + # * rename_column + # * rename_index + # * rename_table + class CommandRecorder + attr_accessor :commands, :delegate + + def initialize(delegate = nil) + @commands = [] + @delegate = delegate + end + + # record +command+. +command+ should be a method name and arguments. + # For example: + # + # recorder.record(:method_name, [:arg1, arg2]) + def record(*command) + @commands << command + end + + # Returns a list that represents commands that are the inverse of the + # commands stored in +commands+. For example: + # + # recorder.record(:rename_table, [:old, :new]) + # recorder.inverse # => [:rename_table, [:new, :old]] + # + # This method will raise an IrreversibleMigration exception if it cannot + # invert the +commands+. + def inverse + @commands.reverse.map { |name, args| + method = :"invert_#{name}" + raise IrreversibleMigration unless respond_to?(method, true) + __send__(method, args) + } + end + + def respond_to?(*args) # :nodoc: + super || delegate.respond_to?(*args) + end + + def send(method, *args) # :nodoc: + return super unless respond_to?(method) + record(method, args) + end + + private + def invert_create_table(args) + [:drop_table, args] + end + + def invert_rename_table(args) + [:rename_table, args.reverse] + end + + def invert_add_column(args) + [:remove_column, args.first(2)] + end + + def invert_rename_index(args) + [:rename_index, args.reverse] + end + + def invert_rename_column(args) + [:rename_column, [args.first] + args.last(2).reverse] + end + + def invert_add_index(args) + table, columns, _ = *args + [:remove_index, [table, {:column => columns}]] + end + + def invert_remove_timestamps(args) + [:add_timestamps, args] + end + + def invert_add_timestamps(args) + [:remove_timestamps, args] + end + end + end +end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 0b92ba5caa..0f421560f0 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -2,12 +2,18 @@ require 'active_support/core_ext/array' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/class/attribute' module ActiveRecord # = Active Record Named \Scopes module NamedScope extend ActiveSupport::Concern + included do + class_attribute :scopes + self.scopes = {} + end + module ClassMethods # Returns an anonymous \scope. # @@ -33,10 +39,6 @@ module ActiveRecord end end - def scopes - read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {}) - end - # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query, # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>. # diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 0c3392263a..f1d3eaed38 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/class/attribute' module ActiveRecord module NestedAttributes #:nodoc: @@ -11,7 +12,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_inheritable_accessor :nested_attributes_options, :instance_writer => false + class_attribute :nested_attributes_options, :instance_writer => false self.nested_attributes_options = {} end @@ -268,7 +269,11 @@ module ActiveRecord if reflection = reflect_on_association(association_name) reflection.options[:autosave] = true add_autosave_association_callbacks(reflection) + + nested_attributes_options = self.nested_attributes_options.dup nested_attributes_options[association_name.to_sym] = options + self.nested_attributes_options = nested_attributes_options + type = (reflection.collection? ? :collection : :one_to_one) # def pirate_attributes=(attributes) @@ -315,7 +320,7 @@ module ActiveRecord # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) - options = nested_attributes_options[association_name] + options = self.nested_attributes_options[association_name] attributes = attributes.with_indifferent_access check_existing_record = (options[:update_only] || !attributes['id'].blank?) @@ -364,7 +369,7 @@ module ActiveRecord # { :id => '2', :_destroy => true } # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) - options = nested_attributes_options[association_name] + options = self.nested_attributes_options[association_name] unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" @@ -433,7 +438,7 @@ module ActiveRecord end def call_reject_if(association_name, attributes) - case callback = nested_attributes_options[association_name][:reject_if] + case callback = self.nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) when Proc diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index a2260e9a19..a07c321960 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,8 +1,15 @@ +require 'active_support/core_ext/class/attribute' + module ActiveRecord # = Active Record Reflection module Reflection # :nodoc: extend ActiveSupport::Concern + included do + class_attribute :reflections + self.reflections = {} + end + # Reflection enables to interrogate Active Record classes and objects # about their associations and aggregations. This information can, # for example, be used in a form builder that takes an Active Record object @@ -20,18 +27,9 @@ module ActiveRecord when :composed_of reflection = AggregateReflection.new(macro, name, options, active_record) end - write_inheritable_hash :reflections, name => reflection - reflection - end - # Returns a hash containing all AssociationReflection objects for the current class. - # Example: - # - # Invoice.reflections - # Account.reflections - # - def reflections - read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {}) + self.reflections = self.reflections.merge(name => reflection) + reflection end # Returns an array of AggregateReflection objects for all the aggregations in the class. diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 6bf698fe97..c8adaddfca 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -208,14 +208,16 @@ module ActiveRecord end def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: - group_attr = @group_values.first - association = @klass.reflect_on_association(group_attr.to_sym) - associated = association && association.macro == :belongs_to # only count belongs_to associations - group_field = associated ? association.primary_key_name : group_attr - group_alias = column_alias_for(group_field) - group_column = column_for(group_field) + group_attr = @group_values + association = @klass.reflect_on_association(group_attr.first.to_sym) + associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + group_fields = Array(associated ? association.primary_key_name : group_attr) + group_aliases = group_fields.map { |field| column_alias_for(field) } + group_columns = group_aliases.zip(group_fields).map { |aliaz,field| + [aliaz, column_for(field)] + } - group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field + group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields if operation == 'count' && column_name == :all aggregate_alias = 'count_all' @@ -223,22 +225,33 @@ module ActiveRecord aggregate_alias = column_alias_for(operation, column_name) end - relation = except(:group).group(group) - relation.select_values = [ - operation_over_aggregate_column(aggregate_column(column_name), operation, distinct).as(aggregate_alias), - "#{group_field} AS #{group_alias}" + select_values = [ + operation_over_aggregate_column( + aggregate_column(column_name), + operation, + distinct).as(aggregate_alias) ] + select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| + "#{field} AS #{aliaz}" + } + + relation = except(:group).group(group.join(',')) + relation.select_values = select_values + calculated_data = @klass.connection.select_all(relation.to_sql) if association - key_ids = calculated_data.collect { |row| row[group_alias] } + key_ids = calculated_data.collect { |row| row[group_aliases.first] } key_records = association.klass.base_class.find(key_ids) key_records = Hash[key_records.map { |r| [r.id, r] }] end ActiveSupport::OrderedHash[calculated_data.map do |row| - key = type_cast_calculated_value(row[group_alias], group_column) + key = group_columns.map { |aliaz, column| + type_cast_calculated_value(row[aliaz], column) + } + key = key.first if key.size == 1 key = key_records[key] if associated [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)] end] diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index c5428dccd6..32c7d08daa 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -25,6 +25,11 @@ module ActiveRecord attribute.in(values) when Range, Arel::Relation attribute.in(value) + when ActiveRecord::Base + attribute.eq(value.quoted_id) + when Class + # FIXME: I think we need to deprecate this behavior + attribute.eq(value.name) else attribute.eq(value) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 9c399d3333..9e7503a60d 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -141,7 +141,7 @@ module ActiveRecord "#{@klass.table_name}.#{@klass.primary_key} DESC" : reverse_sql_order(order_clause).join(', ') - except(:order).order(Arel::SqlLiteral.new(order)) + except(:order).order(Arel.sql(order)) end def arel @@ -174,10 +174,7 @@ module ActiveRecord arel = build_joins(arel, @joins_values) unless @joins_values.empty? - (@where_values - ['']).uniq.each do |where| - where = Arel.sql(where) if String === where - arel = arel.where(Arel::Nodes::Grouping.new(where)) - end + arel = collapse_wheres(arel, (@where_values - ['']).uniq) arel = arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty? @@ -198,6 +195,27 @@ module ActiveRecord private + def collapse_wheres(arel, wheres) + equalities = wheres.grep(Arel::Nodes::Equality) + + groups = equalities.group_by do |equality| + equality.left + end + + groups.each do |_, eqls| + test = eqls.inject(eqls.shift) do |memo, expr| + memo.or(expr) + end + arel = arel.where(test) + end + + (wheres - equalities).each do |where| + where = Arel.sql(where) if String === where + arel = arel.where(Arel::Nodes::Grouping.new(where)) + end + arel + end + def build_where(opts, other = []) case opts when String, Array diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index c1bc3214ea..c6bb5c1961 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -30,9 +30,7 @@ module ActiveRecord # ActiveRecord::Schema is only supported by database adapters that also # support migrations, the two features being very similar. class Schema < Migration - private_class_method :new - - def self.migrations_path + def migrations_path ActiveRecord::Migrator.migrations_path end @@ -48,11 +46,12 @@ module ActiveRecord # ... # end def self.define(info={}, &block) - instance_eval(&block) + schema = new + schema.instance_eval(&block) unless info[:version].blank? initialize_schema_migrations_table - assume_migrated_upto_version(info[:version], migrations_path) + assume_migrated_upto_version(info[:version], schema.migrations_path) end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 230adf6b2b..2ecbd906bd 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/class/attribute' + module ActiveRecord # = Active Record Timestamp # @@ -29,14 +31,14 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_inheritable_accessor :record_timestamps, :instance_writer => false + class_attribute :record_timestamps, :instance_writer => false self.record_timestamps = true end private def create #:nodoc: - if record_timestamps + if self.record_timestamps current_time = current_time_from_proper_timezone all_timestamp_attributes.each do |column| @@ -61,7 +63,7 @@ module ActiveRecord end def should_record_timestamps? - record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?) + self.record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?) end def timestamp_attributes_for_update_in_model diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 89eba15be1..0667be7d23 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -3,8 +3,8 @@ module ActiveRecord MAJOR = 3 MINOR = 1 TINY = 0 - BUILD = "beta" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index 8ac21c1410..126b6f434b 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -1,5 +1,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration - def self.up + def up <% attributes.each do |attribute| -%> <%- if migration_action -%> <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end %> @@ -7,7 +7,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration <%- end -%> end - def self.down + def down <% attributes.reverse.each do |attribute| -%> <%- if migration_action -%> <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><% end %> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb index 1f68487304..70e064be21 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb @@ -1,5 +1,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration - def self.up + def up create_table :<%= table_name %> do |t| <% for attribute in attributes -%> t.<%= attribute.type %> :<%= attribute.name %> @@ -10,7 +10,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration end end - def self.down + def down drop_table :<%= table_name %> end end diff --git a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb index 919822af7b..8f0bf1ef0d 100644 --- a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb @@ -1,5 +1,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration - def self.up + def up create_table :<%= session_table_name %> do |t| t.string :session_id, :null => false t.text :data @@ -10,7 +10,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration add_index :<%= session_table_name %>, :updated_at end - def self.down + def down drop_table :<%= session_table_name %> end end |