diff options
Diffstat (limited to 'activerecord')
248 files changed, 8866 insertions, 6189 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index c50229e779..a458f9e6e4 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,189 @@ +## Rails 4.0.0 (unreleased) ## + +* In previous releases, the following would generate a single query with + an `OUTER JOIN comments`, rather than two separate queries: + + Post.includes(:comments) + .where("comments.name = 'foo'") + + This behaviour relies on matching SQL string, which is an inherently + flawed idea unless we write an SQL parser, which we do not wish to + do. + + Therefore, it is now deprecated. + + To avoid deprecation warnings and for future compatibility, you must + explicitly state which tables you reference, when using SQL snippets: + + Post.includes(:comments) + .where("comments.name = 'foo'") + .references(:comments) + + Note that you do not need to explicitly specify references in the + following cases, as they can be automatically inferred: + + Post.where(comments: { name: 'foo' }) + Post.where('comments.name' => 'foo') + Post.order('comments.name') + + You also do not need to worry about this unless you are doing eager + 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] + +* 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 + connection pool can fill and an exception will be raised. + +* Added the `ActiveRecord::Model` module which can be included in a + class as an alternative to inheriting from `ActiveRecord::Base`: + + class Post + include ActiveRecord::Model + end + + Please note: + + * Up until now it has been safe to assume that all AR models are + descendants of `ActiveRecord::Base`. This is no longer a safe + assumption, but it may transpire that there are areas of the + code which still make this assumption. So there may be + 'teething difficulties' with this feature. (But please do try it + and report bugs.) + + * Plugins & libraries etc that add methods to `ActiveRecord::Base` + will not be compatible with `ActiveRecord::Model`. Those libraries + should add to `ActiveRecord::Model` instead (which is included in + `Base`), or better still, avoid monkey-patching AR and instead + provide a module that users can include where they need it. + + * To minimise the risk of conflicts with other code, it is + advisable to include `ActiveRecord::Model` early in your class + definition. + + *Jon Leighton* + +* PostgreSQL hstore records can be created. + +* PostgreSQL hstore types are automatically deserialized from the database. + ## Rails 3.2.0 (unreleased) ## +* Added a `with_lock` method to ActiveRecord objects, which starts + a transaction, locks the object (pessimistically) and yields to the block. + The method takes one (optional) parameter and passes it to `lock!`. + + Before: + + class Order < ActiveRecord::Base + def cancel! + transaction do + lock! + # ... cancelling logic + end + end + end + + After: + + class Order < ActiveRecord::Base + def cancel! + with_lock do + # ... cancelling logic + end + end + end + + *Olek Janiszewski* + +* 'on' and 'ON' boolean columns values are type casted to true + *Santiago Pastorino* + +* Added ability to run migrations only for given scope, which allows + to run migrations only from one engine (for example to revert changes + from engine that you want to remove). + + Example: + rake db:migrate SCOPE=blog + + *Piotr Sarnacki* + +* Migrations copied from engines are now scoped with engine's name, + for example 01_create_posts.blog.rb. *Piotr Sarnacki* + +* Implements `AR::Base.silence_auto_explain`. This method allows the user to + selectively disable automatic EXPLAINs within a block. *fxn* + +* Implements automatic EXPLAIN logging for slow queries. + + A new configuration parameter `config.active_record.auto_explain_threshold_in_seconds` + determines what's to be considered a slow query. Setting that to `nil` disables + this feature. Defaults are 0.5 in development mode, and `nil` in test and production + modes. + + As of this writing there's support for SQLite, MySQL (mysql2 adapter), and + PostgreSQL. + + *fxn* + +* Implemented ActiveRecord::Relation#pluck method + + Method returns Array of column value from table under ActiveRecord model + + Client.pluck(:id) + + *Bogdan Gusiev* + +* Automatic closure of connections in threads is deprecated. For example + the following code is deprecated: + + Thread.new { Post.find(1) }.join + + It should be changed to close the database connection at the end of + the thread: + + Thread.new { + Post.find(1) + Post.connection.close + }.join + + Only people who spawn threads in their application code need to worry + about this change. + +* Deprecated: + + * `set_table_name` + * `set_inheritance_column` + * `set_sequence_name` + * `set_primary_key` + * `set_locking_column` + + Use an assignment method instead. For example, instead of `set_table_name`, use `self.table_name=`: + + class Project < ActiveRecord::Base + self.table_name = "project" + end + + Or define your own `self.table_name` method: + + class Post < ActiveRecord::Base + def self.table_name + "special_" + super + end + end + Post.table_name # => "special_posts" + + *Jon Leighton* + +* Generated association methods are created within a separate module to allow overriding and + composition using `super`. For a class named `MyModel`, the module is named + `MyModel::GeneratedFeatureMethods`. It is included into the model class immediately after + the `generated_attributes_methods` module defined in ActiveModel, so association methods + override attribute methods of the same name. *Josh Susser* + * Implemented ActiveRecord::Relation#explain. *fxn* * Add ActiveRecord::Relation#uniq for generating unique queries. @@ -65,7 +249,7 @@ *Aaron Christy* -## Rails 3.1.3 (unreleased) ## +## Rails 3.1.3 (November 20, 2011) ## * Perf fix: If we're deleting all records in an association, don't add a IN(..) clause to the query. *GH 3672* @@ -78,7 +262,7 @@ *Christos Zisopoulos and Kenny J* -## Rails 3.1.2 (unreleased) ## +## Rails 3.1.2 (November 18, 2011) ## * Fix bug with PostgreSQLAdapter#indexes. When the search path has multiple schemas, spaces were not being stripped from the schema names after the first. diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index c73d1af096..03bde18130 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2011 David Heinemeier Hansson +Copyright (c) 2004-2012 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 70922ef864..30a66ff5f0 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -143,7 +143,7 @@ A short rundown of some of the major features: * Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]. - ActiveRecord::Base.logger = Logger.new(STDOUT) + ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) ActiveRecord::Base.logger = Log4r::Logger.new("Application Log") @@ -208,7 +208,9 @@ Source code can be downloaded as part of the Rails project on GitHub == License -Active Record is released under the MIT license. +Active Record is released under the MIT license: + +* http://www.opensource.org/licenses/MIT == Support diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 52a3d4a501..8484e1093e 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Object-relational mapper framework (part of Rails).' s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.' - s.required_ruby_version = '>= 1.8.7' + s.required_ruby_version = '>= 1.9.3' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' @@ -21,6 +21,6 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) - s.add_dependency('arel', '~> 3.0.0.pre') + s.add_dependency('arel', '~> 3.0.0') s.add_dependency('tzinfo', '~> 0.3.29') end diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index 63822731d5..31f3e02bb8 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -1,6 +1,6 @@ TIMES = (ENV['N'] || 10000).to_i -require 'rubygems' +require File.expand_path('../../../load_paths', __FILE__) require "active_record" conn = { :adapter => 'sqlite3', :database => ':memory:' } @@ -29,6 +29,14 @@ class Exhibit < ActiveRecord::Base def look; attributes end def feel; look; user.name end + def self.with_name + where("name IS NOT NULL") + end + + def self.with_notes + where("notes IS NOT NULL") + end + def self.look(exhibits) exhibits.each { |e| e.look } end def self.feel(exhibits) exhibits.each { |e| e.feel } end end @@ -37,7 +45,14 @@ puts 'Generating data...' module ActiveRecord class Faker - LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse non aliquet diam. Curabitur vel urna metus, quis malesuada elit. Integer consequat tincidunt felis. Etiam non erat dolor. Vivamus imperdiet nibh sit amet diam eleifend id posuere diam malesuada. Mauris at accumsan sem. Donec id lorem neque. Fusce erat lorem, ornare eu congue vitae, malesuada quis neque. Maecenas vel urna a velit pretium fermentum. Donec tortor enim, tempor venenatis egestas a, tempor sed ipsum. Ut arcu justo, faucibus non imperdiet ac, interdum at diam. Pellentesque ipsum enim, venenatis ut iaculis vitae, varius vitae sem. Sed rutrum quam ac elit euismod bibendum. Donec ultricies ultricies magna, at lacinia libero mollis aliquam. Sed ac arcu in tortor elementum tincidunt vel interdum sem. Curabitur eget erat arcu. Praesent eget eros leo. Nam magna enim, sollicitudin vehicula scelerisque in, vulputate ut libero. Praesent varius tincidunt commodo".split + LOREM = %Q{Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse non aliquet diam. Curabitur vel urna metus, quis malesuada elit. + Integer consequat tincidunt felis. Etiam non erat dolor. Vivamus imperdiet nibh sit amet diam eleifend id posuere diam malesuada. Mauris at accumsan sem. + Donec id lorem neque. Fusce erat lorem, ornare eu congue vitae, malesuada quis neque. Maecenas vel urna a velit pretium fermentum. Donec tortor enim, + tempor venenatis egestas a, tempor sed ipsum. Ut arcu justo, faucibus non imperdiet ac, interdum at diam. Pellentesque ipsum enim, venenatis ut iaculis vitae, + varius vitae sem. Sed rutrum quam ac elit euismod bibendum. Donec ultricies ultricies magna, at lacinia libero mollis aliquam. Sed ac arcu in tortor elementum + tincidunt vel interdum sem. Curabitur eget erat arcu. Praesent eget eros leo. Nam magna enim, sollicitudin vehicula scelerisque in, vulputate ut libero. + Praesent varius tincidunt commodo}.split + def self.name LOREM.grep(/^\w*$/).sort_by { rand }.first(2).join ' ' end @@ -102,6 +117,10 @@ Benchmark.bm(46) do |x| TIMES.times { Exhibit.first.look } end + x.report 'Model.named_scope' do + TIMES.times { Exhibit.limit(10).with_name.with_notes } + end + x.report("Model.all limit(100) (x#{(TIMES / 10).ceil})") do (TIMES / 10).ceil.times { Exhibit.look Exhibit.limit(100) } end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 3572c640eb..78e958442f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2011 David Heinemeier Hansson +# Copyright (c) 2004-2012 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -39,6 +39,7 @@ module ActiveRecord autoload :Aggregations autoload :Associations autoload :AttributeMethods + autoload :AttributeAssignment autoload :AutosaveAssociation autoload :Relation @@ -50,31 +51,45 @@ module ActiveRecord autoload :PredicateBuilder autoload :SpawnMethods autoload :Batches + autoload :Explain + autoload :Delegation end autoload :Base autoload :Callbacks + autoload :Core autoload :CounterCache + autoload :ConnectionHandling + autoload :DynamicMatchers autoload :DynamicFinderMatch autoload :DynamicScopeMatch + autoload :Explain + autoload :IdentityMap + autoload :Inheritance + autoload :Integration autoload :Migration autoload :Migrator, 'active_record/migration' - autoload :NamedScope + autoload :Model + autoload :ModelSchema autoload :NestedAttributes autoload :Observer autoload :Persistence autoload :QueryCache + autoload :Querying + autoload :ReadonlyAttributes autoload :Reflection autoload :Result + autoload :Sanitization autoload :Schema autoload :SchemaDumper + autoload :Scoping autoload :Serialization - autoload :Store autoload :SessionStore + autoload :Store autoload :Timestamp autoload :Transactions + autoload :Translation autoload :Validations - autoload :IdentityMap end module Coders @@ -92,6 +107,7 @@ module ActiveRecord autoload :Read autoload :TimeZoneConversion autoload :Write + autoload :Serialization end end @@ -113,6 +129,15 @@ module ActiveRecord end end + module Scoping + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Named + autoload :Default + end + end + autoload :TestCase autoload :TestFixtures, 'active_record/fixtures' end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 34684ad2f5..58725246c8 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/enumerable' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' @@ -70,7 +69,7 @@ module ActiveRecord end end - class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc + class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: def initialize(owner, reflection) super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") end @@ -196,6 +195,26 @@ module ActiveRecord # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> # <tt>Project#categories.delete(category1)</tt> # + # === Overriding generated methods + # + # Association methods are generated in a module that is included into the model class, + # which allows you to easily override with your own methods and call the original + # generated method with +super+. For example: + # + # class Car < ActiveRecord::Base + # belongs_to :owner + # belongs_to :old_owner + # def owner=(new_owner) + # self.old_owner = self.owner + # super + # end + # end + # + # If your model class is <tt>Project</tt>, the module is + # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is + # included in the model class immediately after the (anonymous) generated attributes methods + # module, meaning an association will override the methods for an attribute with the same name. + # # === A word of warning # # Don't create associations that have the same name as instance methods of @@ -1165,7 +1184,7 @@ module ActiveRecord # has_many :subscribers, :through => :subscriptions, :source => :user # has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new { # %Q{ - # SELECT DISTINCT people.* + # SELECT DISTINCT * # FROM people p, post_subscriptions ps # WHERE ps.post_id = #{id} AND ps.person_id = p.id # ORDER BY p.first_name diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index d1e3ff8e38..7887d59aad 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -231,10 +231,7 @@ module ActiveRecord def build_record(attributes, options) reflection.build_association(attributes, options) do |record| - record.assign_attributes( - create_scope.except(*record.changed), - :without_protection => true - ) + record.assign_attributes(create_scope.except(*record.changed), :without_protection => true) end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 6f8b76abda..0209ce36df 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -15,12 +15,12 @@ module ActiveRecord def scope scope = klass.unscoped - scope = scope.extending(*Array.wrap(options[:extend])) + scope = scope.extending(*Array(options[:extend])) # It's okay to just apply all these like this. The options will only be present if the # association supports that option; this is enforced by the association builder. scope = scope.apply_finder_options(options.slice( - :readonly, :include, :order, :limit, :joins, :group, :having, :offset, :select)) + :readonly, :include, :references, :order, :limit, :joins, :group, :having, :offset, :select)) if options[:through] && !options[:include] scope = scope.includes(source_options[:include]) @@ -90,8 +90,11 @@ module ActiveRecord scope = scope.joins(join(foreign_table, constraint)) - unless conditions.empty? - scope = scope.where(sanitize(conditions, table)) + conditions.each do |condition| + condition = interpolate(condition) + condition = { (table.table_alias || table.name) => condition } unless i == 0 + + scope = scope.where(condition) end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 96fca97440..6e2e5f9de0 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,7 +1,7 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: class_attribute :valid_options - self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate] + self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate, :references] # Set by subclasses class_attribute :macro @@ -16,6 +16,10 @@ module ActiveRecord::Associations::Builder @model, @name, @options = model, name, options end + def mixin + @model.generated_feature_methods + end + def build validate_options reflection = model.create_reflection(self.class.macro, name, options, model) @@ -36,16 +40,14 @@ module ActiveRecord::Associations::Builder def define_readers name = self.name - - model.redefine_method(name) do |*params| + mixin.redefine_method(name) do |*params| association(name).reader(*params) end end def define_writers name = self.name - - model.redefine_method("#{name}=") do |value| + mixin.redefine_method("#{name}=") do |value| association(name).writer(value) end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index f6d26840c2..4183c222de 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -25,16 +25,18 @@ module ActiveRecord::Associations::Builder name = self.name method_name = "belongs_to_counter_cache_after_create_for_#{name}" - model.redefine_method(method_name) do + mixin.redefine_method(method_name) do record = send(name) record.class.increment_counter(cache_column, record.id) unless record.nil? end model.after_create(method_name) method_name = "belongs_to_counter_cache_before_destroy_for_#{name}" - model.redefine_method(method_name) do - record = send(name) - record.class.decrement_counter(cache_column, record.id) unless record.nil? + mixin.redefine_method(method_name) do + unless marked_for_destruction? + record = send(name) + record.class.decrement_counter(cache_column, record.id) unless record.nil? + end end model.before_destroy(method_name) @@ -48,7 +50,7 @@ module ActiveRecord::Associations::Builder method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}" touch = options[:touch] - model.redefine_method(method_name) do + mixin.redefine_method(method_name) do record = send(name) unless record.nil? diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index f62209a226..768f70b6c9 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -32,7 +32,7 @@ module ActiveRecord::Associations::Builder private def wrap_block_extension - options[:extend] = Array.wrap(options[:extend]) + options[:extend] = Array(options[:extend]) if block_extension silence_warnings do @@ -51,14 +51,14 @@ module ActiveRecord::Associations::Builder # TODO : why do i need method_defined? I think its because of the inheritance chain model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name) - model.send("#{full_callback_name}=", Array.wrap(options[callback_name.to_sym])) + model.send("#{full_callback_name}=", Array(options[callback_name.to_sym])) end def define_readers super name = self.name - model.redefine_method("#{name.to_s.singularize}_ids") do + mixin.redefine_method("#{name.to_s.singularize}_ids") do association(name).ids_reader end end @@ -67,7 +67,7 @@ module ActiveRecord::Associations::Builder super name = self.name - model.redefine_method("#{name.to_s.singularize}_ids=") do |ids| + mixin.redefine_method("#{name.to_s.singularize}_ids=") do |ids| association(name).ids_writer(ids) end end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 30fc44b4c2..0b634ab944 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -18,7 +18,7 @@ module ActiveRecord::Associations::Builder model.send(:include, Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 def destroy_associations - association(#{name.to_sym.inspect}).delete_all + association(#{name.to_sym.inspect}).delete_all_on_destroy super end RUBY diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index ecbc70888f..fc6799fb15 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -28,15 +28,10 @@ module ActiveRecord::Associations::Builder def define_destroy_dependency_method name = self.name - model.send(:define_method, dependency_method_name) do + mixin.redefine_method(dependency_method_name) do send(name).each do |o| # No point in executing the counter update since we're going to destroy the parent anyway - counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym - if o.respond_to?(counter_method) - class << o - self - end.send(:define_method, counter_method, Proc.new {}) - end + o.mark_for_destruction end send(name).delete_all @@ -45,15 +40,21 @@ module ActiveRecord::Associations::Builder def define_delete_all_dependency_method name = self.name - model.send(:define_method, dependency_method_name) do + mixin.redefine_method(dependency_method_name) do + association(name).delete_all_on_destroy + end + end + + def define_nullify_dependency_method + name = self.name + mixin.redefine_method(dependency_method_name) do send(name).delete_all end end - alias :define_nullify_dependency_method :define_delete_all_dependency_method def define_restrict_dependency_method name = self.name - model.send(:define_method, dependency_method_name) do + mixin.redefine_method(dependency_method_name) do raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty? end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 88c0d3e90f..7a6cd3890f 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -44,18 +44,17 @@ module ActiveRecord::Associations::Builder end def define_destroy_dependency_method - model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) - def #{dependency_method_name} - association(#{name.to_sym.inspect}).delete - end - eoruby + name = self.name + mixin.redefine_method(dependency_method_name) do + association(name).delete + end end alias :define_delete_dependency_method :define_destroy_dependency_method alias :define_nullify_dependency_method :define_destroy_dependency_method def define_restrict_dependency_method name = self.name - model.redefine_method(dependency_method_name) do + mixin.redefine_method(dependency_method_name) do raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil? end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 0cbbba041a..436b6c1524 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -16,15 +16,15 @@ module ActiveRecord::Associations::Builder def define_constructors name = self.name - model.redefine_method("build_#{name}") do |*params, &block| + mixin.redefine_method("build_#{name}") do |*params, &block| association(name).build(*params, &block) end - model.redefine_method("create_#{name}") do |*params, &block| + mixin.redefine_method("create_#{name}") do |*params, &block| association(name).create(*params, &block) end - model.redefine_method("create_#{name}!") do |*params, &block| + mixin.redefine_method("create_#{name}!") do |*params, &block| association(name).create!(*params, &block) end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 362f1053cd..7aed64d48c 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/array/wrap' - module ActiveRecord module Associations # = Active Record Association Collection @@ -49,17 +47,25 @@ module ActiveRecord end else column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" + relation = scoped - scoped.select(column).map! do |record| - record.send(reflection.association_primary_key) + including = (relation.eager_load_values + relation.includes_values).uniq + + if including.any? + join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, []) + relation = join_dependency.join_associations.inject(relation) do |r, association| + association.join_relation(r) + end end + + relation.uniq.pluck(column) end end # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) pk_column = reflection.primary_key_column - ids = Array.wrap(ids).reject { |id| id.blank? } + ids = Array(ids).reject { |id| id.blank? } ids.map! { |i| pk_column.type_cast(i) } replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) end @@ -152,6 +158,13 @@ module ActiveRecord end end + # Called when the association is declared as :dependent => :delete_all. This is + # an optimised version which avoids loading the records into memory. Not really + # for public consumption. + def delete_all_on_destroy + scoped.delete_all + end + # Destroy all the records from this association. # # See destroy for more info. @@ -235,7 +248,7 @@ module ActiveRecord # This method is abstract in the sense that it relies on # +count_records+, which is a method descendants have to provide. def size - if owner.new_record? || (loaded? && !options[:uniq]) + if !find_target? || (loaded? && !options[:uniq]) target.size elsif !loaded? && options[:group] load_target.size @@ -385,12 +398,7 @@ module ActiveRecord return memory if persisted.empty? persisted.map! do |record| - # Unfortunately we cannot simply do memory.delete(record) since on 1.8 this returns - # record rather than memory.at(memory.index(record)). The behavior is fixed in 1.9. - mem_index = memory.index(record) - - if mem_index - mem_record = memory.delete_at(mem_index) + if mem_record = memory.delete(record) (record.attribute_names - mem_record.changes.keys).each do |name| mem_record[name] = record[name] diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 3181ca9a32..ba01df00e3 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -39,10 +39,9 @@ module ActiveRecord instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, - :lock, :readonly, :having, :to => :scoped + :lock, :readonly, :having, :pluck, :to => :scoped - delegate :target, :load_target, :loaded?, :scoped, - :to => :@association + delegate :target, :load_target, :loaded?, :to => :@association delegate :select, :find, :first, :last, :build, :create, :create!, @@ -53,7 +52,7 @@ module ActiveRecord def initialize(association) @association = association - Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) } + Array(association.options[:extend]).each { |ext| proxy_extend(ext) } end alias_method :new, :build @@ -62,6 +61,13 @@ module ActiveRecord @association end + def scoped + association = @association + association.scoped.extending do + define_method(:proxy_association) { association } + end + end + def respond_to?(name, include_private = false) super || (load_target && target.respond_to?(name, include_private)) || diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 1f917f58f2..a4cea99372 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -32,6 +32,10 @@ module ActiveRecord record end + # ActiveRecord::Relation#delete_all needs to support joins before we can use a + # SQL-only implementation. + alias delete_all_on_destroy delete_all + private def count_records diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 3353cdf1ef..059e6c77bc 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -89,12 +89,8 @@ module ActiveRecord records.each { |r| r.destroy } update_counter(-records.length) unless inverse_updates_counter_cache? else - scope = scoped - - unless records == load_target - keys = records.map { |r| r[reflection.association_primary_key] } - scope = scoped.where(reflection.association_primary_key => keys) - end + keys = records.map { |r| r[reflection.association_primary_key] } + scope = scoped.where(reflection.association_primary_key => keys) if method == :delete_all update_counter(-scope.delete_all) @@ -103,6 +99,10 @@ module ActiveRecord end end end + + def foreign_key_present? + owner.attribute_present?(reflection.association_primary_key) + end end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 7e6e3be382..9657cb081d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -54,6 +54,10 @@ module ActiveRecord record end + # ActiveRecord::Relation#delete_all needs to support joins before we can use a + # SQL-only implementation. + alias delete_all_on_destroy delete_all + private def through_association diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 6c878f0f00..827b01c5ac 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -184,7 +184,7 @@ module ActiveRecord macro = join_part.reflection.macro if macro == :has_one - return if record.association_cache.key?(join_part.reflection.name) + return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name) association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? set_target_and_inverse(join_part, association, record) else diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 03963ab060..0d7d28e458 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -95,8 +95,11 @@ module ActiveRecord conditions = self.conditions[i].dup conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type - unless conditions.empty? - constraint = constraint.and(sanitize(conditions, table)) + conditions.each do |condition| + condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) + condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) + + constraint = constraint.and(condition) end relation.from(join(table, constraint)) diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index f83138195c..cea6ad6944 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -40,16 +40,6 @@ module ActiveRecord def join(table, constraint) table.create_join(table, table.create_on(constraint), join_type) end - - def sanitize(conditions, table) - conditions = conditions.map do |condition| - condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) - condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) - condition - end - - conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions) - end end end end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 779f8164cc..253998fb23 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -95,8 +95,8 @@ module ActiveRecord def build_scope scope = klass.scoped - scope = scope.where(process_conditions(options[:conditions])) - scope = scope.where(process_conditions(preload_options[:conditions])) + scope = scope.where(interpolate(options[:conditions])) + scope = scope.where(interpolate(preload_options[:conditions])) scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star]) scope = scope.includes(preload_options[:include] || options[:include]) @@ -112,13 +112,11 @@ module ActiveRecord scope end - def process_conditions(conditions) + def interpolate(conditions) if conditions.respond_to?(:to_proc) - conditions = klass.send(:instance_eval, &conditions) - end - - if conditions - klass.send(:sanitize_sql, conditions) + klass.send(:instance_eval, &conditions) + else + conditions end end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb new file mode 100644 index 0000000000..bf9fe70b31 --- /dev/null +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -0,0 +1,221 @@ +require 'active_support/concern' + +module ActiveRecord + module AttributeAssignment + extend ActiveSupport::Concern + include ActiveModel::MassAssignmentSecurity + + module ClassMethods + private + + # The primary key and inheritance column can never be set by mass-assignment for security reasons. + def attributes_protected_by_default + default = [ primary_key, inheritance_column ] + default << 'id' unless primary_key.eql? 'id' + default + end + end + + # Allows you to set all the attributes at once by passing in a hash with keys + # matching the attribute names (which again matches the column names). + # + # If any attributes are protected by either +attr_protected+ or + # +attr_accessible+ then only settable attributes will be assigned. + # + # class User < ActiveRecord::Base + # attr_protected :is_admin + # end + # + # user = User.new + # user.attributes = { :username => 'Phusion', :is_admin => true } + # user.username # => "Phusion" + # user.is_admin? # => false + def attributes=(new_attributes) + return unless new_attributes.is_a?(Hash) + + assign_attributes(new_attributes) + end + + # Allows you to set all the attributes for a particular mass-assignment + # security role by passing in a hash of attributes with keys matching + # the attribute names (which again matches the column names) and the role + # name using the :as option. + # + # To bypass mass-assignment security you can use the :without_protection => true + # option. + # + # class User < ActiveRecord::Base + # attr_accessible :name + # attr_accessible :name, :is_admin, :as => :admin + # end + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }) + # user.name # => "Josh" + # user.is_admin? # => false + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) + # user.name # => "Josh" + # user.is_admin? # => true + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) + # user.name # => "Josh" + # user.is_admin? # => true + def assign_attributes(new_attributes, options = {}) + return unless new_attributes + + attributes = new_attributes.stringify_keys + multi_parameter_attributes = [] + nested_parameter_attributes = [] + @mass_assignment_options = options + + unless options[:without_protection] + attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) + end + + attributes.each do |k, v| + if k.include?("(") + multi_parameter_attributes << [ k, v ] + elsif respond_to?("#{k}=") + if v.is_a?(Hash) + nested_parameter_attributes << [ k, v ] + else + send("#{k}=", v) + end + else + raise(UnknownAttributeError, "unknown attribute: #{k}") + end + end + + # assign any deferred nested attributes after the base attributes have been set + nested_parameter_attributes.each do |k,v| + send("#{k}=", v) + end + + @mass_assignment_options = nil + assign_multiparameter_attributes(multi_parameter_attributes) + end + + protected + + def mass_assignment_options + @mass_assignment_options ||= {} + end + + def mass_assignment_role + mass_assignment_options[:as] || :default + end + + private + + # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done + # by calling new on the column type or aggregation type (through composed_of) object with these parameters. + # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate + # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the + # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, + # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the + # attribute will be set to nil. + def assign_multiparameter_attributes(pairs) + execute_callstack_for_multiparameter_attributes( + extract_callstack_for_multiparameter_attributes(pairs) + ) + end + + def instantiate_time_object(name, values) + if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) + Time.zone.local(*values) + else + Time.time_with_datetime_fallback(self.class.default_timezone, *values) + end + end + + def execute_callstack_for_multiparameter_attributes(callstack) + errors = [] + callstack.each do |name, values_with_empty_parameters| + begin + send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) + rescue => ex + errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name) + end + end + unless errors.empty? + raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes" + end + end + + def read_value_from_parameter(name, values_hash_from_param) + klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass + if values_hash_from_param.values.all?{|v|v.nil?} + nil + elsif klass == Time + read_time_parameter_value(name, values_hash_from_param) + elsif klass == Date + read_date_parameter_value(name, values_hash_from_param) + else + read_other_parameter_value(klass, name, values_hash_from_param) + end + end + + def read_time_parameter_value(name, values_hash_from_param) + # If Date bits were not provided, error + raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)} + max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) + # If Date bits were provided but blank, then return nil + return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} + + set_values = (1..max_position).collect{|position| values_hash_from_param[position] } + # If Time bits are not there, then default to 0 + (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]} + instantiate_time_object(name, set_values) + end + + def read_date_parameter_value(name, values_hash_from_param) + return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} + set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]] + begin + Date.new(*set_values) + rescue ArgumentError # if Date.new raises an exception on an invalid date + instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates + end + end + + def read_other_parameter_value(klass, name, values_hash_from_param) + max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) + values = (1..max_position).collect do |position| + raise "Missing Parameter" if !values_hash_from_param.has_key?(position) + values_hash_from_param[position] + end + klass.new(*values) + end + + def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) + [values_hash_from_param.keys.max,upper_cap].min + end + + def extract_callstack_for_multiparameter_attributes(pairs) + attributes = { } + + pairs.each do |pair| + multiparameter_name, value = pair + attribute_name = multiparameter_name.split("(").first + attributes[attribute_name] = {} unless attributes.include?(attribute_name) + + parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) + attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value + end + + attributes + end + + def type_cast_attribute_value(multiparameter_name, value) + multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value + end + + def find_parameter_position(multiparameter_name) + multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i + end + + end +end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index d7bfaa5655..a1e34a3aa1 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -7,35 +7,59 @@ module ActiveRecord extend ActiveSupport::Concern include ActiveModel::AttributeMethods + included do + include Read + include Write + include BeforeTypeCast + include Query + include PrimaryKey + include TimeZoneConversion + include Dirty + include Serialization + + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, + # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). + # (Alias for the protected read_attribute method). + def [](attr_name) + read_attribute(attr_name) + end + + # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. + # (Alias for the protected write_attribute method). + def []=(attr_name, value) + write_attribute(attr_name, value) + end + end + module ClassMethods # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods - return if attribute_methods_generated? - - if base_class == self + # Use a mutex; we don't want two thread simaltaneously trying to define + # attribute methods. + @attribute_methods_mutex.synchronize do + return if attribute_methods_generated? + superclass.define_attribute_methods unless self == base_class super(column_names) @attribute_methods_generated = true - else - base_class.define_attribute_methods end end def attribute_methods_generated? - if base_class == self - @attribute_methods_generated ||= false - else - base_class.attribute_methods_generated? - end + @attribute_methods_generated ||= false end - def undefine_attribute_methods(*args) - if base_class == self - super - @attribute_methods_generated = false - else - base_class.undefine_attribute_methods(*args) - end + # We will define the methods as instance methods, but will call them as singleton + # methods. This allows us to use method_defined? to check if the method exists, + # which is fast and won't give any false positives from the ancestors (because + # there are no ancestors). + def generated_external_attribute_methods + @generated_external_attribute_methods ||= Module.new { extend self } + end + + def undefine_attribute_methods + super if attribute_methods_generated? + @attribute_methods_generated = false end def instance_method_already_implemented?(method_name) @@ -43,19 +67,46 @@ module ActiveRecord raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" end - super + if [Base, Model].include?(active_record_super) + super + else + # If B < A and A defines its own attribute method, then we don't want to overwrite that. + defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods) + defined && !ActiveRecord::Base.method_defined?(method_name) || super + end end # A method name is 'dangerous' if it is already defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'save' is.) - def dangerous_attribute_method?(method_name) - active_record = ActiveRecord::Base - superclass = ActiveRecord::Base.superclass + def dangerous_attribute_method?(name) + method_defined_within?(name, Base) + end + + def method_defined_within?(name, klass, sup = klass.superclass) + if klass.method_defined?(name) || klass.private_method_defined?(name) + if sup.method_defined?(name) || sup.private_method_defined?(name) + klass.instance_method(name).owner != sup.instance_method(name).owner + else + true + end + else + false + end + end - (active_record.method_defined?(method_name) || - active_record.private_method_defined?(method_name)) && - !superclass.method_defined?(method_name) && - !superclass.private_method_defined?(method_name) + def attribute_method?(attribute) + super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) + end + + # Returns an array of column names as strings if it's not + # an abstract class and table exists. + # Otherwise it returns an empty array. + def attribute_names + @attribute_names ||= if !abstract_class? && table_exists? + column_names + else + [] + end end end @@ -94,9 +145,105 @@ module ActiveRecord super end + # Returns true if the given attribute is in the attributes hash + def has_attribute?(attr_name) + @attributes.has_key?(attr_name.to_s) + end + + # Returns an array of names for the attributes available on this object. + def attribute_names + @attributes.keys + end + + # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. + def attributes + Hash[@attributes.map { |name, _| [name, read_attribute(name)] }] + end + + # Returns an <tt>#inspect</tt>-like string for the value of the + # attribute +attr_name+. String attributes are truncated upto 50 + # characters, and Date and Time attributes are returned in the + # <tt>:db</tt> format. Other attributes return the value of + # <tt>#inspect</tt> without modification. + # + # person = Person.create!(:name => "David Heinemeier Hansson " * 3) + # + # person.attribute_for_inspect(:name) + # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."' + # + # person.attribute_for_inspect(:created_at) + # # => '"2009-01-12 04:48:57"' + def attribute_for_inspect(attr_name) + value = read_attribute(attr_name) + + if value.is_a?(String) && value.length > 50 + "#{value[0..50]}...".inspect + elsif value.is_a?(Date) || value.is_a?(Time) + %("#{value.to_s(:db)}") + else + value.inspect + end + end + + # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither + # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). + def attribute_present?(attribute) + value = read_attribute(attribute) + !value.nil? || (value.respond_to?(:empty?) && !value.empty?) + end + + # Returns the column object for the named attribute. + def column_for_attribute(name) + self.class.columns_hash[name.to_s] + end + protected - def attribute_method?(attr_name) - attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name)) + + def clone_attributes(reader_method = :read_attribute, attributes = {}) + attribute_names.each do |name| + attributes[name] = clone_attribute_value(reader_method, name) end + attributes + end + + def clone_attribute_value(reader_method, attribute_name) + value = send(reader_method, attribute_name) + value.duplicable? ? value.clone : value + rescue TypeError, NoMethodError + value + end + + # Returns a copy of the attributes hash where all the values have been safely quoted for use in + # an Arel insert/update method. + def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) + attrs = {} + klass = self.class + arel_table = klass.arel_table + + attribute_names.each do |name| + if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) + + if include_readonly_attributes || !self.class.readonly_attributes.include?(name) + + value = if klass.serialized_attributes.include?(name) + @attributes[name].serialized_value + else + # FIXME: we need @attributes to be used consistently. + # If the values stored in @attributes were already type + # casted, this code could be simplified + read_attribute(name) + end + + attrs[arel_table[name]] = value + end + end + end + + attrs + end + + def attribute_method?(attr_name) + attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name)) + end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 3eff3d54e3..40e4a97e73 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -13,7 +13,7 @@ module ActiveRecord raise "You cannot include Dirty after Timestamp" end - class_attribute :partial_updates + config_attribute :partial_updates self.partial_updates = true end @@ -34,9 +34,9 @@ module ActiveRecord @previously_changed = changes @changed_attributes.clear end - rescue - IdentityMap.remove(self) if IdentityMap.enabled? - raise + rescue + IdentityMap.remove(self) if IdentityMap.enabled? + raise end # <tt>reload</tt> the record and clears changed attributes. diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index a404a5edd7..7c59664703 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -5,15 +5,50 @@ module ActiveRecord # Returns this record's primary key value wrapped in an Array if one is available def to_key - key = send(self.class.primary_key) + key = self.id [key] if key end + # Returns the primary key value + def id + read_attribute(self.class.primary_key) + end + + # Sets the primary key value + def id=(value) + write_attribute(self.class.primary_key, value) + end + + # Queries the primary key value + def id? + query_attribute(self.class.primary_key) + end + module ClassMethods + def define_method_attribute(attr_name) + super + + if attr_name == primary_key && attr_name != 'id' + generated_attribute_methods.send(:alias_method, :id, primary_key) + generated_external_attribute_methods.module_eval <<-CODE, __FILE__, __LINE__ + def id(v, attributes, attributes_cache, attr_name) + attr_name = '#{primary_key}' + send(attr_name, attributes[attr_name], attributes, attributes_cache, attr_name) + end + CODE + end + end + + def dangerous_attribute_method?(method_name) + super && !['id', 'id=', 'id?'].include?(method_name) + end + # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the - # primary_key_prefix_type setting, though. + # primary_key_prefix_type setting, though. Since primary keys are usually protected from mass assignment, + # remember to let your database generate them or include the key in +attr_accessible+. def primary_key - @primary_key ||= reset_primary_key + @primary_key = reset_primary_key unless defined? @primary_key + @primary_key end # Returns a quoted version of the primary key name, used to construct SQL statements. @@ -22,11 +57,11 @@ module ActiveRecord end def reset_primary_key #:nodoc: - key = self == base_class ? get_primary_key(base_class.name) : - base_class.primary_key - - set_primary_key(key) - key + if self == base_class + self.primary_key = get_primary_key(base_class.name) + else + self.primary_key = base_class.primary_key + end end def get_primary_key(base_name) #:nodoc: @@ -38,35 +73,32 @@ module ActiveRecord when :table_name_with_underscore base_name.foreign_key else - if ActiveRecord::Base != self && connection.table_exists?(table_name) - connection.primary_key(table_name) + if ActiveRecord::Base != self && table_exists? + connection.schema_cache.primary_keys[table_name] else 'id' end end end - attr_accessor :original_primary_key - - # Attribute writer for the primary key column - def primary_key=(value) - @quoted_primary_key = nil - @primary_key = value - end - - # Sets the name of the primary key column to use to the given value, - # or (if the value is nil or false) to the value returned by the given - # block. + # Sets the name of the primary key column. + # + # class Project < ActiveRecord::Base + # self.primary_key = "sysid" + # end + # + # You can also define the primary_key method yourself: # # class Project < ActiveRecord::Base - # set_primary_key "sysid" + # def self.primary_key + # "foo_" + super + # end # end - def set_primary_key(value = nil, &block) - @quoted_primary_key = nil - @primary_key ||= '' - self.original_primary_key = @primary_key - value &&= value.to_s - self.primary_key = block_given? ? instance_eval(&block) : value + # Project.primary_key # => "foo_id" + def primary_key=(value) + @original_primary_key = @primary_key if defined?(@primary_key) + @primary_key = value && value.to_s + @quoted_primary_key = nil end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 4a5afcd585..964c4123ef 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -6,11 +6,8 @@ module ActiveRecord ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] included do - cattr_accessor :attribute_types_cached_by_default, :instance_writer => false + config_attribute :attribute_types_cached_by_default, :global => true self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - - # Undefine id so it can be used as an attribute name - undef_method(:id) if method_defined?(:id) end module ClassMethods @@ -32,107 +29,101 @@ module ActiveRecord cached_attributes.include?(attr_name) end + def undefine_attribute_methods + generated_external_attribute_methods.module_eval do + instance_methods.each { |m| undef_method(m) } + end + + super + end + + def type_cast_attribute(attr_name, attributes, cache = {}) #:nodoc: + return unless attr_name + attr_name = attr_name.to_s + + if generated_external_attribute_methods.method_defined?(attr_name) + if attributes.has_key?(attr_name) || attr_name == 'id' + generated_external_attribute_methods.send(attr_name, attributes[attr_name], attributes, cache, attr_name) + end + elsif !attribute_methods_generated? + # If we haven't generated the caster methods yet, do that and + # then try again + define_attribute_methods + type_cast_attribute(attr_name, attributes, cache) + else + # If we get here, the attribute has no associated DB column, so + # just return it verbatim. + attributes[attr_name] + end + end + protected + # We want to generate the methods via module_eval rather than define_method, + # because define_method is slower on dispatch and uses more memory (because it + # creates a closure). + # + # But sometimes the database might return columns with characters that are not + # allowed in normal method names (like 'my_column(omg)'. So to work around this + # we first define with the __temp__ identifier, and then use alias method to + # rename it to what we want. def define_method_attribute(attr_name) - if serialized_attributes.include?(attr_name) - define_read_method_for_serialized_attribute(attr_name) - else - define_read_method(attr_name, attr_name, columns_hash[attr_name]) - end + cast_code = attribute_cast_code(attr_name) - if attr_name == primary_key && attr_name != "id" - define_read_method('id', attr_name, columns_hash[attr_name]) - end + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__ + #{internal_attribute_access_code(attr_name, cast_code)} + end + alias_method '#{attr_name}', :__temp__ + undef_method :__temp__ + STR + + generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__(v, attributes, attributes_cache, attr_name) + #{external_attribute_access_code(attr_name, cast_code)} + end + alias_method '#{attr_name}', :__temp__ + undef_method :__temp__ + STR end private def cacheable_column?(column) - serialized_attributes.include?(column.name) || attribute_types_cached_by_default.include?(column.type) - end - - # Define read method for serialized attribute. - def define_read_method_for_serialized_attribute(attr_name) - access_code = "@attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}']" - generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__) + attribute_types_cached_by_default.include?(column.type) end - # Define an attribute reader method. Cope with nil column. - # method_name is the same as attr_name except when a non-standard primary key is used, - # we still define #id as an accessor for the key - def define_read_method(method_name, attr_name, column) - cast_code = column.type_cast_code('v') - access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}" + def internal_attribute_access_code(attr_name, cast_code) + access_code = "(v=@attributes[attr_name]) && #{cast_code}" - unless attr_name.to_s == self.primary_key.to_s - access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") + unless attr_name == primary_key + access_code.insert(0, "missing_attribute(attr_name, caller) unless @attributes.has_key?(attr_name); ") end if cache_attribute?(attr_name) - access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" + access_code = "@attributes_cache[attr_name] ||= (#{access_code})" end - # Where possible, generate the method by evalling a string, as this will result in - # faster accesses because it avoids the block eval and then string eval incurred - # by the second branch. - # - # The second, slower, branch is necessary to support instances where the database - # returns columns with extra stuff in (like 'my_column(omg)'). - if method_name =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ - def _#{method_name} - #{access_code} - end - - alias #{method_name} _#{method_name} - STR - else - generated_attribute_methods.module_eval do - define_method("_#{method_name}") { eval(access_code) } - alias_method(method_name, "_#{method_name}") - end - end + "attr_name = '#{attr_name}'; #{access_code}" end - end - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, - # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). - def read_attribute(attr_name) - method = "_#{attr_name}" - if respond_to? method - send method if @attributes.has_key?(attr_name.to_s) - else - _read_attribute attr_name - end - end + def external_attribute_access_code(attr_name, cast_code) + access_code = "v && #{cast_code}" - def _read_attribute(attr_name) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == 'id' - value = @attributes[attr_name] - unless value.nil? - if column = column_for_attribute(attr_name) - if unserializable_attribute?(attr_name, column) - unserialize_attribute(attr_name) - else - column.type_cast(value) + if cache_attribute?(attr_name) + access_code = "attributes_cache[attr_name] ||= (#{access_code})" end - else - value + + access_code end - end - end - # Returns true if the attribute is of a text column and marked for serialization. - def unserializable_attribute?(attr_name, column) - column.text? && self.class.serialized_attributes.include?(attr_name) + def attribute_cast_code(attr_name) + columns_hash[attr_name].type_cast_code('v') + end end - # Returns the unserialized object of the attribute. - def unserialize_attribute(attr_name) - coder = self.class.serialized_attributes[attr_name] - unserialized_object = coder.load(@attributes[attr_name]) - - @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, + # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). + def read_attribute(attr_name) + self.class.type_cast_attribute(attr_name, @attributes, @attributes_cache) end private diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb new file mode 100644 index 0000000000..0c8e4e4b9a --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -0,0 +1,93 @@ +module ActiveRecord + module AttributeMethods + module Serialization + extend ActiveSupport::Concern + + included do + # Returns a hash of all the attributes that have been specified for serialization as + # keys and their class restriction as values. + config_attribute :serialized_attributes + self.serialized_attributes = {} + end + + class Attribute < Struct.new(:coder, :value, :state) + def unserialized_value + state == :serialized ? unserialize : value + end + + def serialized_value + state == :unserialized ? serialize : value + end + + def unserialize + self.state = :unserialized + self.value = coder.load(value) + end + + def serialize + self.state = :serialized + self.value = coder.dump(value) + end + end + + module ClassMethods + # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, + # then specify the name of that attribute using this method and it will be handled automatically. + # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that + # class on retrieval or SerializationTypeMismatch will be raised. + # + # ==== Parameters + # + # * +attr_name+ - The field name that should be serialized. + # * +class_name+ - Optional, class name that the object type should be equal to. + # + # ==== Example + # # Serialize a preferences attribute + # class User < ActiveRecord::Base + # serialize :preferences + # end + def serialize(attr_name, class_name = Object) + coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } + class_name + else + Coders::YAMLColumn.new(class_name) + end + + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy + # has its own hash of own serialized attributes + self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) + end + + def initialize_attributes(attributes) #:nodoc: + super + + serialized_attributes.each do |key, coder| + if attributes.key?(key) + attributes[key] = Attribute.new(coder, attributes[key], :serialized) + end + end + + attributes + end + + private + + def attribute_cast_code(attr_name) + if serialized_attributes.include?(attr_name) + "v.unserialized_value" + else + super + end + end + end + + def type_cast_attribute_for_write(column, value) + if column && coder = self.class.serialized_attributes[column.name] + Attribute.new(coder, value, :unserialized) + else + super + end + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 62a3cfa9a5..2f86e32f41 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -7,30 +7,25 @@ module ActiveRecord extend ActiveSupport::Concern included do - cattr_accessor :time_zone_aware_attributes, :instance_writer => false + config_attribute :time_zone_aware_attributes, :global => true self.time_zone_aware_attributes = false - class_attribute :skip_time_zone_conversion_for_attributes, :instance_writer => false + config_attribute :skip_time_zone_conversion_for_attributes self.skip_time_zone_conversion_for_attributes = [] end module ClassMethods protected - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced read method automatically converts the UTC time stored in the database to the time + # The enhanced read method automatically converts the UTC time stored in the database to the time # zone stored in Time.zone. - def define_method_attribute(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body, line = <<-EOV, __LINE__ + 1 - def _#{attr_name} - cached = @attributes_cache['#{attr_name}'] - return cached if cached - time = _read_attribute('#{attr_name}') - @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time - end - alias #{attr_name} _#{attr_name} - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, line) + def attribute_cast_code(attr_name) + column = columns_hash[attr_name] + + if create_time_zone_conversion_attribute?(attr_name, column) + typecast = "v = #{super}" + time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v" + + "((#{typecast}) && (#{time_zone_conversion}))" else super end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index eb585ee906..fde55b95da 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -17,10 +17,6 @@ module ActiveRecord write_attribute(attr_name, new_value) end end - - if attr_name == primary_key && attr_name != "id" - generated_attribute_methods.module_eval("alias :id= :'#{primary_key}='") - end end end @@ -32,10 +28,8 @@ module ActiveRecord @attributes_cache.delete(attr_name) column = column_for_attribute(attr_name) - if column && column.number? - @attributes[attr_name] = convert_number_column_value(value) - elsif column || @attributes.has_key?(attr_name) - @attributes[attr_name] = value + if column || @attributes.has_key?(attr_name) + @attributes[attr_name] = type_cast_attribute_for_write(column, value) else raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" end @@ -47,6 +41,26 @@ module ActiveRecord def attribute=(attribute_name, value) write_attribute(attribute_name, value) end + + def type_cast_attribute_for_write(column, value) + if column && column.number? + convert_number_column_value(value) + else + value + end + end + + def convert_number_column_value(value) + if value == false + 0 + elsif value == true + 1 + elsif value.is_a?(String) && value.blank? + nil + else + value + end + end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 056170d82a..d468663084 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/array/wrap' - module ActiveRecord # = Active Record Autosave Association # @@ -21,6 +19,21 @@ module ActiveRecord # Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>. # When the <tt>:autosave</tt> option is not present new associations are saved. # + # == Validation + # + # Children records are validated unless <tt>:validate</tt> is +false+. + # + # == Callbacks + # + # Association with autosave option defines several callbacks on your + # model (before_save, after_create, after_update). Please note that + # callbacks are executed in the order they were defined in + # model. You should avoid modyfing the association content, before + # autosave callbacks are executed. Placing your callbacks after + # associations is usually a good practice. + # + # == Examples + # # === One-to-one Example # # class Post @@ -109,10 +122,7 @@ module ActiveRecord # Now it _is_ removed from the database: # # Comment.find_by_id(id).nil? # => true - # - # === Validation - # - # Children records are validated unless <tt>:validate</tt> is +false+. + module AutosaveAssociation extend ActiveSupport::Concern @@ -264,7 +274,7 @@ module ActiveRecord # turned on for the association. def validate_single_association(reflection) association = association_instance_get(reflection.name) - record = association && association.target + record = association && association.reader association_valid?(reflection, record) if record end @@ -285,7 +295,7 @@ module ActiveRecord def association_valid?(reflection, record) return true if record.destroyed? || record.marked_for_destruction? - unless valid = record.valid? + unless valid = record.valid?(validation_context) if reflection.options[:autosave] record.errors.each do |attribute, message| attribute = "#{reflection.name}.#{attribute}" @@ -331,7 +341,7 @@ module ActiveRecord if autosave saved = association.insert_record(record, false) else - association.insert_record(record) + association.insert_record(record) unless reflection.nested? end elsif autosave saved = record.save(:validate => false) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 7ba67b8540..d4d0220fb7 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1,8 +1,3 @@ -begin - require 'psych' -rescue LoadError -end - require 'yaml' require 'set' require 'active_support/benchmarkable' @@ -12,7 +7,6 @@ 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/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' @@ -23,9 +17,11 @@ require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/object/blank' +require 'active_support/deprecation' require 'arel' require 'active_record/errors' require 'active_record/log_subscriber' +require 'active_record/explain_subscriber' module ActiveRecord #:nodoc: # = Active Record @@ -325,1845 +321,8 @@ module ActiveRecord #:nodoc: # So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all # instances in the current object space. class Base - ## - # :singleton-method: - # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, - # which is then passed on to any new database connections made and which can be retrieved on both - # a class and instance level by calling +logger+. - cattr_accessor :logger, :instance_writer => false - - ## - # :singleton-method: - # Contains the database configuration - as is typically stored in config/database.yml - - # as a Hash. - # - # For example, the following database.yml... - # - # development: - # adapter: sqlite3 - # database: db/development.sqlite3 - # - # production: - # adapter: sqlite3 - # database: db/production.sqlite3 - # - # ...would result in ActiveRecord::Base.configurations to look like this: - # - # { - # 'development' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/development.sqlite3' - # }, - # 'production' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/production.sqlite3' - # } - # } - cattr_accessor :configurations, :instance_writer => false - @@configurations = {} - - ## - # :singleton-method: - # Accessor for the prefix type that will be prepended to every primary key column name. - # The options are :table_name and :table_name_with_underscore. If the first is specified, - # the Product class will look for "productid" instead of "id" as the primary column. If the - # latter is specified, the Product class will look for "product_id" instead of "id". Remember - # that this is a global setting for all Active Records. - cattr_accessor :primary_key_prefix_type, :instance_writer => false - @@primary_key_prefix_type = nil - - ## - # :singleton-method: - # Accessor for the name of the prefix string to prepend to every table name. So if set - # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people", - # etc. This is a convenient way of creating a namespace for tables in a shared database. - # By default, the prefix is the empty string. - # - # If you are organising your models within modules you can add a prefix to the models within - # a namespace by defining a singleton method in the parent module called table_name_prefix which - # returns your chosen prefix. - class_attribute :table_name_prefix, :instance_writer => false - self.table_name_prefix = "" - - ## - # :singleton-method: - # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", - # "people_basecamp"). By default, the suffix is the empty string. - class_attribute :table_name_suffix, :instance_writer => false - self.table_name_suffix = "" - - ## - # :singleton-method: - # Indicates whether table names should be the pluralized versions of the corresponding class names. - # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. - # See table_name for the full rules on table/class naming. This is true, by default. - class_attribute :pluralize_table_names, :instance_writer => false - self.pluralize_table_names = true - - ## - # :singleton-method: - # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling - # dates and times from the database. This is set to :local by default. - cattr_accessor :default_timezone, :instance_writer => false - @@default_timezone = :local - - ## - # :singleton-method: - # Specifies the format to use when dumping the database schema with Rails' - # Rakefile. If :sql, the schema is dumped as (potentially database- - # specific) SQL statements. If :ruby, the schema is dumped as an - # ActiveRecord::Schema file which can be loaded into any database that - # supports migrations. Use :ruby if you want to have different database - # adapters for, e.g., your development and test environments. - cattr_accessor :schema_format , :instance_writer => false - @@schema_format = :ruby - - ## - # :singleton-method: - # Specify whether or not to use timestamps for migration versions - cattr_accessor :timestamped_migrations , :instance_writer => false - @@timestamped_migrations = true - - # Determine whether to store the full constant name including namespace when using STI - class_attribute :store_full_sti_class - self.store_full_sti_class = true - - # Stores the default scope for the class - class_attribute :default_scopes, :instance_writer => false - self.default_scopes = [] - - # Returns a hash of all the attributes that have been specified for serialization as - # keys and their class restriction as values. - class_attribute :serialized_attributes - self.serialized_attributes = {} - - class_attribute :_attr_readonly, :instance_writer => false - self._attr_readonly = [] - - class << self # Class methods - delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped - delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped - delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped - delegate :find_each, :find_in_batches, :to => :scoped - delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, - :where, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :to => :scoped - delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped - - # Executes a custom SQL query against your database and returns all the results. The results will - # be returned as an array with columns requested encapsulated as attributes of the model you call - # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in - # a Product object with the attributes you specified in the SQL query. - # - # If you call a complicated SQL query which spans multiple tables the columns specified by the - # SELECT will be attributes of the model, whether or not they are columns of the corresponding - # table. - # - # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be - # no database agnostic conversions performed. This should be a last resort because using, for example, - # MySQL specific terms will lock you to using that particular database engine or require you to - # change your call if you switch engines. - # - # ==== Examples - # # A simple SQL query spanning multiple tables - # Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id" - # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...] - # - # # You can use the same string replacement techniques as you can with ActiveRecord#find - # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] - # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...] - def find_by_sql(sql, binds = []) - connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) } - end - - # Creates an object (or multiple objects) and saves it to the database, if validations pass. - # The resulting object is returned whether the object was saved successfully to the database or not. - # - # The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the - # attributes on the objects that are to be created. - # - # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options - # in the +options+ parameter. - # - # ==== Examples - # # Create a single new object - # User.create(:first_name => 'Jamie') - # - # # Create a single new object using the :admin mass-assignment security role - # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) - # - # # Create a single new object bypassing mass-assignment security - # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) - # - # # Create an Array of new objects - # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) - # - # # Create a single object and pass it into a block to set other attributes. - # User.create(:first_name => 'Jamie') do |u| - # u.is_admin = false - # end - # - # # Creating an Array of new objects using a block, where the block is executed for each object: - # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u| - # u.is_admin = false - # end - def create(attributes = nil, options = {}, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create(attr, options, &block) } - else - object = new(attributes, options, &block) - object.save - object - end - end - - # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. - # The use of this method should be restricted to complicated SQL queries that can't be executed - # using the ActiveRecord::Calculations class methods. Look into those before using this. - # - # ==== Parameters - # - # * +sql+ - An SQL statement which should return a count query from the database, see the example below. - # - # ==== Examples - # - # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" - def count_by_sql(sql) - sql = sanitize_conditions(sql) - connection.select_value(sql, "#{name} Count").to_i - end - - # Attributes listed as readonly will be used to create a new record but update operations will - # ignore these fields. - def attr_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 - 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, - # then specify the name of that attribute using this method and it will be handled automatically. - # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that - # class on retrieval or SerializationTypeMismatch will be raised. - # - # ==== Parameters - # - # * +attr_name+ - The field name that should be serialized. - # * +class_name+ - Optional, class name that the object type should be equal to. - # - # ==== Example - # # Serialize a preferences attribute - # class User < ActiveRecord::Base - # serialize :preferences - # end - def serialize(attr_name, class_name = Object) - coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } - class_name - else - Coders::YAMLColumn.new(class_name) - end - - # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy - # has its own hash of own serialized attributes - self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) - end - - # Guesses the table name (in forced lower-case) based on the name of the class in the - # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy - # looks like: Reply < Message < ActiveRecord::Base, then Message is used - # to guess the table name even when called on Reply. The rules used to do the guess - # are handled by the Inflector class in Active Support, which knows almost all common - # English inflections. You can add new inflections in config/initializers/inflections.rb. - # - # Nested classes are given table names prefixed by the singular form of - # the parent's table name. Enclosing modules are not considered. - # - # ==== Examples - # - # class Invoice < ActiveRecord::Base - # end - # - # file class table_name - # invoice.rb Invoice invoices - # - # class Invoice < ActiveRecord::Base - # class Lineitem < ActiveRecord::Base - # end - # end - # - # file class table_name - # invoice.rb Invoice::Lineitem invoice_lineitems - # - # module Invoice - # class Lineitem < ActiveRecord::Base - # end - # end - # - # file class table_name - # invoice/lineitem.rb Invoice::Lineitem lineitems - # - # Additionally, the class-level +table_name_prefix+ is prepended and the - # +table_name_suffix+ is appended. So if you have "myapp_" as a prefix, - # the table name guess for an Invoice class becomes "myapp_invoices". - # Invoice::Lineitem becomes "myapp_invoice_lineitems". - # - # You can also overwrite this class method to allow for unguessable - # links, such as a Mouse class with a link to a "mice" table. Example: - # - # class Mouse < ActiveRecord::Base - # set_table_name "mice" - # end - def table_name - reset_table_name - end - - # Returns a quoted version of the table name, used to construct SQL statements. - def quoted_table_name - @quoted_table_name ||= connection.quote_table_name(table_name) - end - - # Computes the table name, (re)sets it internally, and returns it. - def reset_table_name #:nodoc: - return if abstract_class? - - self.table_name = compute_table_name - end - - def full_table_name_prefix #:nodoc: - (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix - end - - # Defines the column name for use with single table inheritance. Use - # <tt>set_inheritance_column</tt> to set a different value. - def inheritance_column - @inheritance_column ||= "type" - end - - # Lazy-set the sequence name to the connection's default. This method - # is only ever called once since set_sequence_name overrides it. - def sequence_name #:nodoc: - reset_sequence_name - end - - def reset_sequence_name #:nodoc: - default = connection.default_sequence_name(table_name, primary_key) - set_sequence_name(default) - default - end - - # Sets the table name. If the value is nil or false then the value returned by the given - # block is used. - # - # class Project < ActiveRecord::Base - # set_table_name "project" - # end - def set_table_name(value = nil, &block) - @quoted_table_name = nil - define_attr_method :table_name, value, &block - @arel_table = nil - - @relation = Relation.new(self, arel_table) - end - alias :table_name= :set_table_name - - # Sets the name of the inheritance column to use to the given value, - # or (if the value # is nil or false) to the value returned by the - # given block. - # - # class Project < ActiveRecord::Base - # set_inheritance_column do - # original_inheritance_column + "_id" - # end - # end - def set_inheritance_column(value = nil, &block) - define_attr_method :inheritance_column, value, &block - end - alias :inheritance_column= :set_inheritance_column - - # Sets the name of the sequence to use when generating ids to the given - # value, or (if the value is nil or false) to the value returned by the - # given block. This is required for Oracle and is useful for any - # database which relies on sequences for primary key generation. - # - # If a sequence name is not explicitly set when using Oracle or Firebird, - # it will default to the commonly used pattern of: #{table_name}_seq - # - # If a sequence name is not explicitly set when using PostgreSQL, it - # will discover the sequence corresponding to your primary key for you. - # - # class Project < ActiveRecord::Base - # set_sequence_name "projectseq" # default would have been "project_seq" - # end - def set_sequence_name(value = nil, &block) - define_attr_method :sequence_name, value, &block - end - alias :sequence_name= :set_sequence_name - - # Indicates whether the table associated with this class exists - def table_exists? - connection.table_exists?(table_name) - end - - # Returns an array of column objects for the table associated with this class. - def columns - if defined?(@primary_key) - connection.schema_cache.primary_keys[table_name] ||= primary_key - end - - connection.schema_cache.columns[table_name] - end - - # Returns a hash of column objects for the table associated with this class. - def columns_hash - connection.schema_cache.columns_hash[table_name] - end - - # Returns a hash where the keys are column names and the values are - # default values when instantiating the AR object for this table. - def column_defaults - connection.schema_cache.column_defaults[table_name] - end - - # Returns an array of column names as strings. - def column_names - @column_names ||= columns.map { |column| column.name } - end - - # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", - # and columns used for single table inheritance have been removed. - def content_columns - @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } - end - - # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key - # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute - # is available. - def column_methods_hash #:nodoc: - @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr| - attr_name = attr.to_s - methods[attr.to_sym] = attr_name - methods["#{attr}=".to_sym] = attr_name - methods["#{attr}?".to_sym] = attr_name - methods["#{attr}_before_type_cast".to_sym] = attr_name - methods - end - end - - # Resets all the cached information about columns, which will cause them - # to be reloaded on the next request. - # - # The most common usage pattern for this method is probably in a migration, - # when just after creating a table you want to populate it with some default - # values, eg: - # - # class CreateJobLevels < ActiveRecord::Migration - # def up - # create_table :job_levels do |t| - # t.integer :id - # t.string :name - # - # t.timestamps - # end - # - # JobLevel.reset_column_information - # %w{assistant executive manager director}.each do |type| - # JobLevel.create(:name => type) - # end - # end - # - # def down - # drop_table :job_levels - # end - # end - def reset_column_information - connection.clear_cache! - undefine_attribute_methods - connection.schema_cache.clear_table_cache!(table_name) if table_exists? - - @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil - @arel_engine = @relation = nil - end - - def clear_cache! # :nodoc: - connection.schema_cache.clear! - end - - def attribute_method?(attribute) - super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) - end - - # Returns an array of column names as strings if it's not - # an abstract class and table exists. - # Otherwise it returns an empty array. - def attribute_names - @attribute_names ||= if !abstract_class? && table_exists? - column_names - else - [] - end - end - - # Set the lookup ancestors for ActiveModel. - 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 - end - - # Set the i18n scope to overwrite ActiveModel. - def i18n_scope #:nodoc: - :activerecord - end - - # True if this isn't a concrete subclass needing a STI type condition. - def descends_from_active_record? - if superclass.abstract_class? - superclass.descends_from_active_record? - else - superclass == Base || !columns_hash.include?(inheritance_column) - end - end - - def finder_needs_type_condition? #:nodoc: - # This is like this because benchmarking justifies the strange :false stuff - :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) - end - - # Returns a string like 'Post(id:integer, title:string, body:text)' - def inspect - if self == Base - super - elsif abstract_class? - "#{super}(abstract)" - elsif table_exists? - attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' - "#{super}(#{attr_list})" - else - "#{super}(Table doesn't exist)" - end - end - - def quote_value(value, column = nil) #:nodoc: - connection.quote(value,column) - end - - # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. - def sanitize(object) #:nodoc: - connection.quote(object) - end - - # Overwrite the default class equality method to provide support for association proxies. - def ===(object) - object.is_a?(self) - end - - def symbolized_base_class - @symbolized_base_class ||= base_class.to_s.to_sym - end - - def symbolized_sti_name - @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class - end - - # Returns the base AR subclass that this class descends from. If A - # extends AR::Base, A.base_class will return A. If B descends from A - # through some arbitrarily deep hierarchy, B.base_class will return A. - # - # If B < A and C < B and if A is an abstract_class then both B.base_class - # and C.base_class would return B as the answer since A is an abstract_class. - def base_class - class_of_active_record_descendant(self) - end - - # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). - attr_accessor :abstract_class - - # Returns whether this class is an abstract class or not. - def abstract_class? - defined?(@abstract_class) && @abstract_class == true - end - - def respond_to?(method_id, include_private = false) - if match = DynamicFinderMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - elsif match = DynamicScopeMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - end - - super - end - - def sti_name - store_full_sti_class ? name : name.demodulize - end - - def arel_table - @arel_table ||= Arel::Table.new(table_name, arel_engine) - end - - def arel_engine - @arel_engine ||= begin - if self == ActiveRecord::Base - ActiveRecord::Base - else - connection_handler.connection_pools[name] ? self : superclass.arel_engine - end - end - end - - # Returns a scope for this class without taking into account the default_scope. - # - # class Post < ActiveRecord::Base - # def self.default_scope - # where :published => true - # end - # end - # - # Post.all # Fires "SELECT * FROM posts WHERE published = true" - # Post.unscoped.all # Fires "SELECT * FROM posts" - # - # This method also accepts a block meaning that all queries inside the block will - # not use the default_scope: - # - # Post.unscoped { - # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" - # } - # - # It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt> - # does not work. Assuming that <tt>published</tt> is a <tt>scope</tt> following two statements are same. - # - # Post.unscoped.published - # Post.published - def unscoped #:nodoc: - block_given? ? relation.scoping { yield } : relation - end - - def before_remove_const #:nodoc: - self.current_scope = nil - end - - # Finder methods must instantiate through this method to work with the - # single-table inheritance model that makes it possible to create - # objects of different types from the same table. - def instantiate(record) - sti_class = find_sti_class(record[inheritance_column]) - record_id = sti_class.primary_key && record[sti_class.primary_key] - - if ActiveRecord::IdentityMap.enabled? && record_id - if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? - record_id = record_id.to_i - end - if instance = IdentityMap.get(sti_class, record_id) - instance.reinit_with('attributes' => record) - else - instance = sti_class.allocate.init_with('attributes' => record) - IdentityMap.add(instance) - end - else - instance = sti_class.allocate.init_with('attributes' => record) - end - - instance - end - - private - - def relation #:nodoc: - @relation ||= Relation.new(self, arel_table) - - if finder_needs_type_condition? - @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) - else - @relation - end - end - - def find_sti_class(type_name) - if type_name.blank? || !columns_hash.include?(inheritance_column) - self - else - begin - if store_full_sti_class - ActiveSupport::Dependencies.constantize(type_name) - else - compute_type(type_name) - end - rescue NameError - raise SubclassNotFound, - "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + - "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + - "Please rename this column if you didn't intend it to be used for storing the inheritance class " + - "or overwrite #{name}.inheritance_column to use another column for that information." - end - end - end - - def construct_finder_arel(options = {}, scope = nil) - relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options - relation = scope.merge(relation) if scope - relation - end - - def type_condition(table = arel_table) - sti_column = table[inheritance_column.to_sym] - sti_names = ([self] + descendants).map { |model| model.sti_name } - - sti_column.in(sti_names) - end - - # Guesses the table name, but does not decorate it with prefix and suffix information. - def undecorated_table_name(class_name = base_class.name) - table_name = class_name.to_s.demodulize.underscore - table_name = table_name.pluralize if pluralize_table_names - table_name - end - - # Computes and returns a table name according to default conventions. - def compute_table_name - base = base_class - if self == base - # Nested classes are prefixed with singular parent table name. - if parent < ActiveRecord::Base && !parent.abstract_class? - contained = parent.table_name - contained = contained.singularize if parent.pluralize_table_names - contained += '_' - end - "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" - else - # STI subclasses always use their superclass' table. - base.table_name - end - end - - # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and - # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders - # section at the top of this file for more detailed information. - # - # It's even possible to use all the additional parameters to +find+. For example, the - # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. - # - # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it - # is first invoked, so that future attempts to use it do not run through method_missing. - def method_missing(method_id, *arguments, &block) - if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id)) - attribute_names = match.attribute_names - super unless all_attributes_exists?(attribute_names) - if arguments.size < attribute_names.size - method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'" - backtrace = [method_trace] + caller - raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace - end - if match.respond_to?(:scope?) && match.scope? - self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) - attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)] - # - scoped(:conditions => attributes) # scoped(:conditions => attributes) - end # end - METHOD - send(method_id, *arguments) - elsif match.finder? - options = arguments.extract_options! - relation = options.any? ? scoped(options) : scoped - relation.send :find_by_attributes, match, attribute_names, *arguments, &block - elsif match.instantiator? - scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block - end - else - super - end - end - - # Similar in purpose to +expand_hash_conditions_for_aggregates+. - def expand_attribute_names_for_aggregates(attribute_names) - attribute_names.map { |attribute_name| - unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? - aggregate_mapping(aggregation).map do |field_attr, _| - field_attr.to_sym - end - else - attribute_name.to_sym - end - }.flatten - end - - def all_attributes_exists?(attribute_names) - (expand_attribute_names_for_aggregates(attribute_names) - - column_methods_hash.keys).empty? - end - - protected - # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be - # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while - # <tt>:create</tt> parameters are an attributes hash. - # - # class Article < ActiveRecord::Base - # def self.create_with_scope - # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do - # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 - # a = create(1) - # a.blog_id # => 1 - # end - # end - # end - # - # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of - # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged. - # - # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing - # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the - # array of strings format for your joins. - # - # class Article < ActiveRecord::Base - # def self.find_with_scope - # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do - # with_scope(:find => limit(10)) do - # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 - # end - # with_scope(:find => where(:author_id => 3)) do - # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 - # end - # end - # end - # end - # - # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method. - # - # class Article < ActiveRecord::Base - # def self.find_with_exclusive_scope - # with_scope(:find => where(:blog_id => 1).limit(1)) do - # with_exclusive_scope(:find => limit(10)) do - # all # => SELECT * from articles LIMIT 10 - # end - # end - # end - # end - # - # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. - def with_scope(scope = {}, action = :merge, &block) - # If another Active Record class has been passed in, get its current scope - scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope) - - previous_scope = self.current_scope - - if scope.is_a?(Hash) - # Dup first and second level of hash (method and params). - scope = scope.dup - scope.each do |method, params| - scope[method] = params.dup unless params == true - end - - scope.assert_valid_keys([ :find, :create ]) - relation = construct_finder_arel(scope[:find] || {}) - relation.default_scoped = true unless action == :overwrite - - if previous_scope && previous_scope.create_with_value && scope[:create] - scope_for_create = if action == :merge - previous_scope.create_with_value.merge(scope[:create]) - else - scope[:create] - end - - relation = relation.create_with(scope_for_create) - else - scope_for_create = scope[:create] - scope_for_create ||= previous_scope.create_with_value if previous_scope - relation = relation.create_with(scope_for_create) if scope_for_create - end - - scope = relation - end - - scope = previous_scope.merge(scope) if previous_scope && action == :merge - - self.current_scope = scope - begin - yield - ensure - self.current_scope = previous_scope - end - end - - # Works like with_scope, but discards any nested properties. - def with_exclusive_scope(method_scoping = {}, &block) - if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) } - raise ArgumentError, <<-MSG -New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope: - - User.unscoped.where(:active => true) - -Or call unscoped with a block: - - User.unscoped do - User.where(:active => true).all - end - -MSG - end - with_scope(method_scoping, :overwrite, &block) - end - - def current_scope #:nodoc: - Thread.current["#{self}_current_scope"] - end - - def current_scope=(scope) #:nodoc: - Thread.current["#{self}_current_scope"] = scope - end - - # Use this macro in your model to set a default scope for all operations on - # the model. - # - # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # end - # - # Article.all # => SELECT * FROM articles WHERE published = true - # - # The <tt>default_scope</tt> is also applied while creating/building a record. It is not - # applied while updating a record. - # - # Article.new.published # => true - # Article.create.published # => true - # - # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: - # - # class Article < ActiveRecord::Base - # default_scope { where(:published_at => Time.now - 1.week) } - # end - # - # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> - # macro, and it will be called when building the default scope.) - # - # If you use multiple <tt>default_scope</tt> declarations in your model then they will - # be merged together: - # - # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # default_scope where(:rating => 'G') - # end - # - # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' - # - # This is also the case with inheritance and module includes where the parent or module - # defines a <tt>default_scope</tt> and the child or including class defines a second one. - # - # If you need to do more complex things with a default scope, you can alternatively - # define it as a class method: - # - # class Article < ActiveRecord::Base - # def self.default_scope - # # Should return a scope, you can call 'super' here etc. - # end - # end - def default_scope(scope = {}) - scope = Proc.new if block_given? - self.default_scopes = default_scopes + [scope] - end - - def build_default_scope #:nodoc: - if method(:default_scope).owner != Base.singleton_class - evaluate_default_scope { default_scope } - elsif default_scopes.any? - evaluate_default_scope do - default_scopes.inject(relation) do |default_scope, scope| - if scope.is_a?(Hash) - default_scope.apply_finder_options(scope) - elsif !scope.is_a?(Relation) && scope.respond_to?(:call) - default_scope.merge(scope.call) - else - default_scope.merge(scope) - end - end - end - end - end - - def ignore_default_scope? #:nodoc: - Thread.current["#{self}_ignore_default_scope"] - end - - def ignore_default_scope=(ignore) #:nodoc: - Thread.current["#{self}_ignore_default_scope"] = ignore - end - - # The ignore_default_scope flag is used to prevent an infinite recursion situation where - # a default scope references a scope which has a default scope which references a scope... - def evaluate_default_scope - return if ignore_default_scope? - - begin - self.ignore_default_scope = true - yield - ensure - self.ignore_default_scope = false - end - end - - # Returns the class type of the record using the current module as a prefix. So descendants of - # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. - def compute_type(type_name) - if type_name.match(/^::/) - # If the type is prefixed with a scope operator then we assume that - # the type_name is an absolute reference. - ActiveSupport::Dependencies.constantize(type_name) - else - # Build a list of candidates to search for - candidates = [] - name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } - candidates << type_name - - candidates.each do |candidate| - begin - constant = ActiveSupport::Dependencies.constantize(candidate) - return constant if candidate == constant.to_s - rescue NameError => e - # We don't want to swallow NoMethodError < NameError errors - raise e unless e.instance_of?(NameError) - end - end - - raise NameError, "uninitialized constant #{candidates.first}" - end - end - - # Returns the class descending directly from ActiveRecord::Base or an - # abstract class, if any, in the inheritance hierarchy. - def class_of_active_record_descendant(klass) - if klass == Base || klass.superclass == Base || klass.superclass.abstract_class? - klass - elsif klass.superclass.nil? - raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" - else - class_of_active_record_descendant(klass.superclass) - end - end - - # Accepts an array, hash, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a WHERE clause. - # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" - # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'" - # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" - def sanitize_sql_for_conditions(condition, table_name = self.table_name) - return nil if condition.blank? - - case condition - when Array; sanitize_sql_array(condition) - when Hash; sanitize_sql_hash_for_conditions(condition, table_name) - else condition - end - end - alias_method :sanitize_sql, :sanitize_sql_for_conditions - - # Accepts an array, hash, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a SET clause. - # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'" - def sanitize_sql_for_assignment(assignments) - case assignments - when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments) - else assignments - end - end - - def aggregate_mapping(reflection) - mapping = reflection.options[:mapping] || [reflection.name, reflection.name] - mapping.first.is_a?(Array) ? mapping : [mapping] - end - - # Accepts a hash of SQL conditions and replaces those attributes - # that correspond to a +composed_of+ relationship with their expanded - # aggregate attribute values. - # Given: - # class Person < ActiveRecord::Base - # composed_of :address, :class_name => "Address", - # :mapping => [%w(address_street street), %w(address_city city)] - # end - # Then: - # { :address => Address.new("813 abc st.", "chicago") } - # # => { :address_street => "813 abc st.", :address_city => "chicago" } - def expand_hash_conditions_for_aggregates(attrs) - expanded_attrs = {} - attrs.each do |attr, value| - unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil? - mapping = aggregate_mapping(aggregation) - mapping.each do |field_attr, aggregate_attr| - if mapping.size == 1 && !value.respond_to?(aggregate_attr) - expanded_attrs[field_attr] = value - else - expanded_attrs[field_attr] = value.send(aggregate_attr) - end - end - else - expanded_attrs[attr] = value - end - end - expanded_attrs - end - - # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause. - # { :name => "foo'bar", :group_id => 4 } - # # => "name='foo''bar' and group_id= 4" - # { :status => nil, :group_id => [1,2,3] } - # # => "status IS NULL and group_id IN (1,2,3)" - # { :age => 13..18 } - # # => "age BETWEEN 13 AND 18" - # { 'other_records.id' => 7 } - # # => "`other_records`.`id` = 7" - # { :other_records => { :id => 7 } } - # # => "`other_records`.`id` = 7" - # And for value objects on a composed_of relationship: - # { :address => Address.new("123 abc st.", "chicago") } - # # => "address_street='123 abc st.' and address_city='chicago'" - def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) - attrs = expand_hash_conditions_for_aggregates(attrs) - - table = Arel::Table.new(table_name).alias(default_table_name) - PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| - connection.visitor.accept b - }.join(' AND ') - end - alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions - - # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. - # { :status => nil, :group_id => 1 } - # # => "status = NULL , group_id = 1" - def sanitize_sql_hash_for_assignment(attrs) - attrs.map do |attr, value| - "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}" - end.join(', ') - end - - # Accepts an array of conditions. The array has each value - # sanitized and interpolated into the SQL statement. - # ["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) && statement =~ /:\w+/ - replace_named_bind_variables(statement, values.first) - elsif statement.include?('?') - replace_bind_variables(statement, values) - elsif statement.blank? - statement - else - statement % values.collect { |value| connection.quote_string(value.to_s) } - end - end - - alias_method :sanitize_conditions, :sanitize_sql - - def replace_bind_variables(statement, values) #:nodoc: - raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) - bound = values.dup - c = connection - statement.gsub('?') { quote_bound_value(bound.shift, c) } - end - - def replace_named_bind_variables(statement, bind_vars) #:nodoc: - statement.gsub(/(:?):([a-zA-Z]\w*)/) do - if $1 == ':' # skip postgresql casts - $& # return the whole match - elsif bind_vars.include?(match = $2.to_sym) - quote_bound_value(bind_vars[match]) - else - raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" - end - end - end - - def expand_range_bind_variables(bind_vars) #:nodoc: - expanded = [] - - bind_vars.each do |var| - next if var.is_a?(Hash) - - if var.is_a?(Range) - expanded << var.first - expanded << var.last - else - expanded << var - end - end - - expanded - end - - def quote_bound_value(value, c = connection) #:nodoc: - if value.respond_to?(:map) && !value.acts_like?(:string) - if value.respond_to?(:empty?) && value.empty? - c.quote(nil) - else - value.map { |v| c.quote(v) }.join(',') - end - else - c.quote(value) - end - end - - def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc: - unless expected == provided - raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" - end - end - - def encode_quoted_value(value) #:nodoc: - quoted_value = connection.quote(value) - quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) " - quoted_value - end - end - - public - # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with - # attributes but not yet saved (pass a hash with key names matching the associated table column names). - # In both instances, valid attribute keys are determined by the column names of the associated table -- - # hence you can't have attributes that aren't part of the table columns. - # - # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options - # in the +options+ parameter. - # - # ==== Examples - # # Instantiates a single new object - # User.new(:first_name => 'Jamie') - # - # # Instantiates a single new object using the :admin mass-assignment security role - # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) - # - # # Instantiates a single new object bypassing mass-assignment security - # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) - def initialize(attributes = nil, options = {}) - @attributes = attributes_from_column_definition - @association_cache = {} - @aggregation_cache = {} - @attributes_cache = {} - @new_record = true - @readonly = false - @destroyed = false - @marked_for_destruction = false - @previously_changed = {} - @changed_attributes = {} - @relation = nil - - ensure_proper_type - set_serialized_attributes - - populate_with_current_scope_attributes - - assign_attributes(attributes, options) if attributes - - yield self if block_given? - run_callbacks :initialize - end - - # Populate +coder+ with attributes about this record that should be - # serialized. The structure of +coder+ defined in this method is - # guaranteed to match the structure of +coder+ passed to the +init_with+ - # method. - # - # Example: - # - # class Post < ActiveRecord::Base - # end - # coder = {} - # Post.new.encode_with(coder) - # coder # => { 'id' => nil, ... } - def encode_with(coder) - coder['attributes'] = attributes - end - - # Initialize an empty model object from +coder+. +coder+ must contain - # the attributes necessary for initializing an empty model object. For - # example: - # - # class Post < ActiveRecord::Base - # end - # - # post = Post.allocate - # post.init_with('attributes' => { 'title' => 'hello world' }) - # post.title # => 'hello world' - def init_with(coder) - @attributes = coder['attributes'] - @relation = nil - - set_serialized_attributes - - @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} - @association_cache = {} - @aggregation_cache = {} - @readonly = @destroyed = @marked_for_destruction = false - @new_record = false - run_callbacks :find - run_callbacks :initialize - - self - end - - # Returns a String, which Action Pack uses for constructing an URL to this - # object. The default implementation returns this record's id as a String, - # or nil if this record's unsaved. - # - # For example, suppose that you have a User model, and that you have a - # <tt>resources :users</tt> route. Normally, +user_path+ will - # construct a path with the user object's 'id' in it: - # - # user = User.find_by_name('Phusion') - # user_path(user) # => "/users/1" - # - # You can override +to_param+ in your model to make +user_path+ construct - # a path using the user's name instead of the user's id: - # - # class User < ActiveRecord::Base - # def to_param # overridden - # name - # end - # end - # - # user = User.find_by_name('Phusion') - # user_path(user) # => "/users/Phusion" - def to_param - # We can't use alias_method here, because method 'id' optimizes itself on the fly. - id && id.to_s # Be sure to stringify the id for routes - end - - # Returns a cache key that can be used to identify this record. - # - # ==== Examples - # - # Product.new.cache_key # => "products/new" - # Product.find(5).cache_key # => "products/5" (updated_at not available) - # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) - def cache_key - case - when new_record? - "#{self.class.model_name.cache_key}/new" - when timestamp = self[:updated_at] - timestamp = timestamp.utc.to_s(:number) - "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" - else - "#{self.class.model_name.cache_key}/#{id}" - end - end - - def quoted_id #:nodoc: - quote_value(id, column_for_attribute(self.class.primary_key)) - end - - # Returns true if the given attribute is in the attributes hash - def has_attribute?(attr_name) - @attributes.has_key?(attr_name.to_s) - end - - # Returns an array of names for the attributes available on this object. - def attribute_names - @attributes.keys - end - - # Allows you to set all the attributes at once by passing in a hash with keys - # matching the attribute names (which again matches the column names). - # - # If any attributes are protected by either +attr_protected+ or - # +attr_accessible+ then only settable attributes will be assigned. - # - # class User < ActiveRecord::Base - # attr_protected :is_admin - # end - # - # user = User.new - # user.attributes = { :username => 'Phusion', :is_admin => true } - # user.username # => "Phusion" - # user.is_admin? # => false - def attributes=(new_attributes) - return unless new_attributes.is_a?(Hash) - - assign_attributes(new_attributes) - end - - # Allows you to set all the attributes for a particular mass-assignment - # security role by passing in a hash of attributes with keys matching - # the attribute names (which again matches the column names) and the role - # name using the :as option. - # - # To bypass mass-assignment security you can use the :without_protection => true - # option. - # - # class User < ActiveRecord::Base - # attr_accessible :name - # attr_accessible :name, :is_admin, :as => :admin - # end - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }) - # user.name # => "Josh" - # user.is_admin? # => false - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) - # user.name # => "Josh" - # user.is_admin? # => true - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) - # user.name # => "Josh" - # user.is_admin? # => true - def assign_attributes(new_attributes, options = {}) - return unless new_attributes - - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] - @mass_assignment_options = options - - unless options[:without_protection] - attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) - end - - attributes.each do |k, v| - if k.include?("(") - multi_parameter_attributes << [ k, v ] - elsif respond_to?("#{k}=") - send("#{k}=", v) - else - raise(UnknownAttributeError, "unknown attribute: #{k}") - end - end - - @mass_assignment_options = nil - assign_multiparameter_attributes(multi_parameter_attributes) - end - - # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. - def attributes - Hash[@attributes.map { |name, _| [name, read_attribute(name)] }] - end - - # Returns an <tt>#inspect</tt>-like string for the value of the - # attribute +attr_name+. String attributes are truncated upto 50 - # characters, and Date and Time attributes are returned in the - # <tt>:db</tt> format. Other attributes return the value of - # <tt>#inspect</tt> without modification. - # - # person = Person.create!(:name => "David Heinemeier Hansson " * 3) - # - # person.attribute_for_inspect(:name) - # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."' - # - # person.attribute_for_inspect(:created_at) - # # => '"2009-01-12 04:48:57"' - def attribute_for_inspect(attr_name) - value = read_attribute(attr_name) - - if value.is_a?(String) && value.length > 50 - "#{value[0..50]}...".inspect - elsif value.is_a?(Date) || value.is_a?(Time) - %("#{value.to_s(:db)}") - else - value.inspect - end - end - - # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither - # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). - def attribute_present?(attribute) - value = _read_attribute(attribute) - !value.nil? || (value.respond_to?(:empty?) && !value.empty?) - end - - # Returns the column object for the named attribute. - def column_for_attribute(name) - self.class.columns_hash[name.to_s] - end - - # 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) - super || - comparison_object.instance_of?(self.class) && - id.present? && - comparison_object.id == id - end - alias :eql? :== - - # Delegates to id in order to allow two records of the same type and id to work with something like: - # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] - def hash - id.hash - end - - # Freeze the attributes hash such that associations are still accessible, even on destroyed records. - def freeze - @attributes.freeze; self - end - - # Returns +true+ if the attributes hash has been frozen. - def frozen? - @attributes.frozen? - end - - # Allows sort on objects - def <=>(other_object) - if other_object.is_a?(self.class) - self.to_key <=> other_object.to_key - else - nil - end - end - - # Backport dup from 1.9 so that initialize_dup() gets called - unless Object.respond_to?(:initialize_dup) - def dup # :nodoc: - copy = super - copy.initialize_dup(self) - copy - end - end - - # Duped objects have no id assigned and are treated as new records. Note - # that this is a "shallow" copy as it copies the object's attributes - # only, not its associations. The extent of a "deep" copy is application - # specific and is therefore left to the application to implement according - # to its need. - # The dup method does not preserve the timestamps (created|updated)_(at|on). - def initialize_dup(other) - cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - cloned_attributes.delete(self.class.primary_key) - - @attributes = cloned_attributes - - _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) - - @changed_attributes = {} - attributes_from_column_definition.each do |attr, orig_value| - @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) - end - - @aggregation_cache = {} - @association_cache = {} - @attributes_cache = {} - @new_record = true - - ensure_proper_type - populate_with_current_scope_attributes - super - end - - # Returns +true+ if the record is read only. Records loaded through joins with piggy-back - # attributes will be marked as read only since they cannot be saved. - def readonly? - @readonly - end - - # Marks this record as read only. - def readonly! - @readonly = true - end - - # Returns the contents of the record as a nicely formatted string. - def inspect - inspection = if @attributes - self.class.column_names.collect { |name| - if has_attribute?(name) - "#{name}: #{attribute_for_inspect(name)}" - end - }.compact.join(", ") - else - "not initialized" - end - "#<#{self.class} #{inspection}>" - end - - protected - def clone_attributes(reader_method = :read_attribute, attributes = {}) - attribute_names.each do |name| - attributes[name] = clone_attribute_value(reader_method, name) - end - attributes - end - - def clone_attribute_value(reader_method, attribute_name) - value = send(reader_method, attribute_name) - value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - value - end - - def mass_assignment_options - @mass_assignment_options ||= {} - end - - def mass_assignment_role - mass_assignment_options[:as] || :default - end - - private - - # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements - # of the array, and then rescues from the possible NoMethodError. If those elements are - # ActiveRecord::Base's, then this triggers the various method_missing's that we have, - # which significantly impacts upon performance. - # - # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here. - # - # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/ - def to_ary # :nodoc: - nil - end - - def set_serialized_attributes - sattrs = self.class.serialized_attributes - - sattrs.each do |key, coder| - @attributes[key] = coder.load @attributes[key] if @attributes.key?(key) - end - end - - # Sets the attribute used for single table inheritance to this class name if this is not the - # ActiveRecord::Base descendant. - # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to - # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. - # No such attribute would be set for objects of the Message class in that example. - def ensure_proper_type - klass = self.class - if klass.finder_needs_type_condition? - write_attribute(klass.inheritance_column, klass.sti_name) - end - end - - # The primary key and inheritance column can never be set by mass-assignment for security reasons. - def self.attributes_protected_by_default - default = [ primary_key, inheritance_column ] - default << 'id' unless primary_key.eql? 'id' - default - end - - # Returns a copy of the attributes hash where all the values have been safely quoted for use in - # an Arel insert/update method. - def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) - attrs = {} - klass = self.class - arel_table = klass.arel_table - - attribute_names.each do |name| - if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) - - if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name)) - - value = if coder = klass.serialized_attributes[name] - coder.dump @attributes[name] - else - # FIXME: we need @attributes to be used consistently. - # If the values stored in @attributes were already type - # casted, this code could be simplified - read_attribute(name) - end - - attrs[arel_table[name]] = value - end - end - end - attrs - end - - # Quote strings appropriately for SQL statements. - def quote_value(value, column = nil) - self.class.connection.quote(value, column) - end - - # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done - # by calling new on the column type or aggregation type (through composed_of) object with these parameters. - # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate - # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the - # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, - # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the - # attribute will be set to nil. - def assign_multiparameter_attributes(pairs) - execute_callstack_for_multiparameter_attributes( - extract_callstack_for_multiparameter_attributes(pairs) - ) - end - - def instantiate_time_object(name, values) - if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) - Time.zone.local(*values) - else - Time.time_with_datetime_fallback(@@default_timezone, *values) - end - end - - def execute_callstack_for_multiparameter_attributes(callstack) - errors = [] - callstack.each do |name, values_with_empty_parameters| - begin - send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) - rescue => ex - errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name) - end - end - unless errors.empty? - raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes" - end - end - - def read_value_from_parameter(name, values_hash_from_param) - klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass - if values_hash_from_param.values.all?{|v|v.nil?} - nil - elsif klass == Time - read_time_parameter_value(name, values_hash_from_param) - elsif klass == Date - read_date_parameter_value(name, values_hash_from_param) - else - read_other_parameter_value(klass, name, values_hash_from_param) - end - end - - def read_time_parameter_value(name, values_hash_from_param) - # If Date bits were not provided, error - raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)} - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) - # If Date bits were provided but blank, then return nil - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - - set_values = (1..max_position).collect{|position| values_hash_from_param[position] } - # If Time bits are not there, then default to 0 - (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]} - instantiate_time_object(name, set_values) - end - - def read_date_parameter_value(name, values_hash_from_param) - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]] - begin - Date.new(*set_values) - rescue ArgumentError # if Date.new raises an exception on an invalid date - instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates - end - end - - def read_other_parameter_value(klass, name, values_hash_from_param) - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) - values = (1..max_position).collect do |position| - raise "Missing Parameter" if !values_hash_from_param.has_key?(position) - values_hash_from_param[position] - end - klass.new(*values) - end - - def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) - [values_hash_from_param.keys.max,upper_cap].min - end - - def extract_callstack_for_multiparameter_attributes(pairs) - attributes = { } - - pairs.each do |pair| - multiparameter_name, value = pair - attribute_name = multiparameter_name.split("(").first - attributes[attribute_name] = {} unless attributes.include?(attribute_name) - - parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) - attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value - end - - attributes - end - - def type_cast_attribute_value(multiparameter_name, value) - multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value - end - - def find_parameter_position(multiparameter_name) - multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i - end - - # Returns a comma-separated pair list, like "key1 = val1, key2 = val2". - def comma_pair_list(hash) - hash.map { |k,v| "#{k} = #{v}" }.join(", ") - end - - def quote_columns(quoter, hash) - Hash[hash.map { |name, value| [quoter.quote_column_name(name), value] }] - end - - def quoted_comma_pair_list(quoter, hash) - comma_pair_list(quote_columns(quoter, hash)) - end - - def convert_number_column_value(value) - if value == false - 0 - elsif value == true - 1 - elsif value.is_a?(String) && value.blank? - nil - else - value - end - end - - def populate_with_current_scope_attributes - return unless self.class.scope_attributes? - - self.class.scope_attributes.each do |att,value| - send("#{att}=", value) if respond_to?("#{att}=") - end - end - end - - Base.class_eval do - include ActiveRecord::Persistence - extend ActiveModel::Naming - extend QueryCache::ClassMethods - extend ActiveSupport::Benchmarkable - extend ActiveSupport::DescendantsTracker - - include ActiveModel::Conversion - include Validations - extend CounterCache - include Locking::Optimistic, Locking::Pessimistic - include AttributeMethods - include AttributeMethods::Read, AttributeMethods::Write, AttributeMethods::BeforeTypeCast, AttributeMethods::Query - include AttributeMethods::PrimaryKey - include AttributeMethods::TimeZoneConversion - include AttributeMethods::Dirty - include ActiveModel::MassAssignmentSecurity - include Callbacks, ActiveModel::Observing, Timestamp - include Associations, NamedScope - include IdentityMap - include ActiveModel::SecurePassword - - # AutosaveAssociation needs to be included before Transactions, because we want - # #save_with_autosave_associations to be wrapped inside a transaction. - include AutosaveAssociation, NestedAttributes - include Aggregations, Transactions, Reflection, Serialization, Store - - NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner) - - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, - # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). - # (Alias for the protected read_attribute method). - alias [] read_attribute - - # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. - # (Alias for the protected write_attribute method). - alias []= write_attribute - - public :[], :[]= + include ActiveRecord::Model end end -require 'active_record/connection_adapters/abstract/connection_specification' -ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) +ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy) diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index a175bf003c..a050fabf35 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/array/wrap' - module ActiveRecord # = Active Record Callbacks # @@ -25,7 +23,7 @@ module ActiveRecord # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and # <tt>after_rollback</tt>. # - # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that + # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that # is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects # are instantiated as well. # @@ -215,24 +213,48 @@ module ActiveRecord # instead of quietly returning +false+. # # == Debugging callbacks - # - # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support + # + # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support # <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property # defines what part of the chain the callback runs in. - # - # To find all callbacks in the before_save callback chain: - # + # + # To find all callbacks in the before_save callback chain: + # # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) } - # + # # Returns an array of callback objects that form the before_save chain. - # + # # To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object: - # + # # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead) - # + # # Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model. - # + # module Callbacks + # We can't define callbacks directly on ActiveRecord::Model because + # it is a module. So we queue up the definitions and execute them + # when ActiveRecord::Model is included. + module Register #:nodoc: + def self.extended(base) + base.config_attribute :_callbacks_register + base._callbacks_register = [] + end + + def self.setup(base) + base._callbacks_register.each do |item| + base.send(*item) + end + end + + def define_callbacks(*args) + self._callbacks_register << [:define_callbacks, *args] + end + + def define_model_callbacks(*args) + self._callbacks_register << [:define_model_callbacks, *args] + end + end + extend ActiveSupport::Concern CALLBACKS = [ @@ -242,8 +264,11 @@ module ActiveRecord :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback ] + module ClassMethods + include ActiveModel::Callbacks + end + included do - extend ActiveModel::Callbacks include ActiveModel::Validations::Callbacks define_model_callbacks :initialize, :find, :touch, :only => :after diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index e32154780a..d69f02d504 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,7 +1,6 @@ require 'thread' require 'monitor' require 'set' -require 'active_support/core_ext/module/synchronization' require 'active_support/core_ext/module/deprecation' module ActiveRecord @@ -10,6 +9,13 @@ module ActiveRecord class ConnectionTimeoutError < ConnectionNotEstablished end + # Raised when a connection pool is full and another connection is requested + class PoolFullError < ConnectionNotEstablished + def initialize size, timeout + super("Connection pool of size #{size} and timeout #{timeout}s is full") + end + end + module ConnectionAdapters # Connection pool base class for managing Active Record database # connections. @@ -58,8 +64,35 @@ module ActiveRecord # * +wait_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). class ConnectionPool - attr_accessor :automatic_reconnect - attr_reader :spec, :connections + # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. + # A reaper instantiated with a nil frequency will never reap the + # connection pool. + # + # Configure the frequency by setting "reaping_frequency" in your + # database yaml file. + class Reaper + attr_reader :pool, :frequency + + def initialize(pool, frequency) + @pool = pool + @frequency = frequency + end + + def run + return unless frequency + Thread.new(frequency, pool) { |t, p| + while true + sleep t + p.reap + end + } + end + end + + include MonitorMixin + + attr_accessor :automatic_reconnect, :timeout + attr_reader :spec, :connections, :size, :reaper # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, @@ -68,23 +101,22 @@ module ActiveRecord # # The default ConnectionPool maximum size is 5. def initialize(spec) + super() + @spec = spec # The cache of reserved connections mapped to threads @reserved_connections = {} - # The mutex used to synchronize pool access - @connection_mutex = Monitor.new - @queue = @connection_mutex.new_cond @timeout = spec.config[:wait_timeout] || 5 + @reaper = Reaper.new self, spec.config[:reaping_frequency] + @reaper.run # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 @connections = [] - @checked_out = [] @automatic_reconnect = true - @visitor = nil end # Retrieve the connection associated with the current thread, or call @@ -99,7 +131,7 @@ module ActiveRecord # Check to see if there is an active connection in this connection # pool. def active_connection? - @reserved_connections.key? current_connection_id + active_connections.any? end # Signal that the thread is finished with the current connection. @@ -115,7 +147,7 @@ module ActiveRecord # connection when finished. def with_connection connection_id = current_connection_id - fresh_connection = true unless @reserved_connections[connection_id] + fresh_connection = true unless active_connection? yield connection ensure release_connection(connection_id) if fresh_connection @@ -123,122 +155,80 @@ module ActiveRecord # Returns true if a connection has already been opened. def connected? - !@connections.empty? + synchronize { @connections.any? } end # Disconnects all connections in the pool, and clears the pool. def disconnect! - @reserved_connections.each do |name,conn| - checkin conn - end - @reserved_connections = {} - @connections.each do |conn| - conn.disconnect! + synchronize do + @reserved_connections = {} + @connections.each do |conn| + checkin conn + conn.disconnect! + end + @connections = [] end - @connections = [] end # Clears the cache which maps classes. def clear_reloadable_connections! - @reserved_connections.each do |name, conn| - checkin conn - end - @reserved_connections = {} - @connections.each do |conn| - conn.disconnect! if conn.requires_reloading? - end - @connections.delete_if do |conn| - conn.requires_reloading? + synchronize do + @reserved_connections = {} + @connections.each do |conn| + checkin conn + conn.disconnect! if conn.requires_reloading? + end + @connections.delete_if do |conn| + conn.requires_reloading? + end end end # Verify active connections and remove and disconnect connections # associated with stale threads. def verify_active_connections! #:nodoc: - clear_stale_cached_connections! - @connections.each do |connection| - connection.verify! - end - end - - def columns - with_connection do |c| - c.schema_cache.columns - end - end - deprecate :columns - - def columns_hash - with_connection do |c| - c.schema_cache.columns_hash - end - end - deprecate :columns_hash - - def primary_keys - with_connection do |c| - c.schema_cache.primary_keys + synchronize do + @connections.each do |connection| + connection.verify! + end end end - deprecate :primary_keys - def clear_cache! - with_connection do |c| - c.schema_cache.clear! - end - end - deprecate :clear_cache! - - # Return any checked-out connections back to the pool by threads that - # are no longer alive. - def clear_stale_cached_connections! - keys = @reserved_connections.keys - Thread.list.find_all { |t| - t.alive? - }.map { |thread| thread.object_id } - keys.each do |key| - checkin @reserved_connections[key] - @reserved_connections.delete(key) - end + def clear_stale_cached_connections! # :nodoc: end + deprecate :clear_stale_cached_connections! # Check-out a database connection from the pool, indicating that you want # to use it. You should call #checkin when you no longer need this. # - # This is done by either returning an existing connection, or by creating - # a new connection. If the maximum number of connections for this pool has - # already been reached, but the pool is empty (i.e. they're all being used), - # then this method will wait until a thread has checked in a connection. - # The wait time is bounded however: if no connection can be checked out - # within the timeout specified for this pool, then a ConnectionTimeoutError - # exception will be raised. + # This is done by either returning and leasing existing connection, or by + # creating a new connection and leasing it. + # + # If all connections are leased and the pool is at capacity (meaning the + # number of currently leased connections is greater than or equal to the + # size limit set), an ActiveRecord::PoolFullError exception will be raised. # # Returns: an AbstractAdapter object. # # Raises: - # - ConnectionTimeoutError: no connection can be obtained from the pool - # within the timeout period. + # - PoolFullError: no connection can be obtained from the pool. def checkout # Checkout an available connection - @connection_mutex.synchronize do - loop do - conn = if @checked_out.size < @connections.size - checkout_existing_connection - elsif @connections.size < @size - checkout_new_connection - end - return conn if conn - - @queue.wait(@timeout) - - if(@checked_out.size < @connections.size) - next - else - clear_stale_cached_connections! - if @size == @checked_out.size - raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it." - end - end + synchronize do + # Try to find a connection that hasn't been leased, and lease it + conn = connections.find { |c| c.lease } + + # If all connections were leased, and we have room to expand, + # create a new connection and lease it. + if !conn && connections.size < size + conn = checkout_new_connection + conn.lease + end + if conn + checkout_and_verify conn + else + raise PoolFullError.new(size, timeout) end end end @@ -249,16 +239,39 @@ module ActiveRecord # +conn+: an AbstractAdapter object, which was obtained by earlier by # calling +checkout+ on this pool. def checkin(conn) - @connection_mutex.synchronize do + synchronize do conn.run_callbacks :checkin do - @checked_out.delete conn - @queue.signal + conn.expire end end end - synchronize :clear_reloadable_connections!, :verify_active_connections!, - :connected?, :disconnect!, :with => :@connection_mutex + # Remove a connection from the connection pool. The connection will + # remain open and active but will no longer be managed by this pool. + def remove(conn) + synchronize do + @connections.delete conn + + # FIXME: we might want to store the key on the connection so that removing + # from the reserved hash will be a little easier. + thread_id = @reserved_connections.keys.find { |k| + @reserved_connections[k] == conn + } + @reserved_connections.delete thread_id if thread_id + end + end + + # Removes dead connections from the pool. A dead connection can occur + # if a programmer forgets to close a connection at the end of a thread + # or a thread dies unexpectedly. + def reap + synchronize do + stale = Time.now - @timeout + connections.dup.each do |conn| + remove conn if conn.in_use? && stale > conn.last_use && !conn.active? + end + end + end private @@ -274,22 +287,21 @@ module ActiveRecord raise ConnectionNotEstablished unless @automatic_reconnect c = new_connection + c.pool = self @connections << c - checkout_and_verify(c) - end - - def checkout_existing_connection - c = (@connections - @checked_out).first - checkout_and_verify(c) + c end def checkout_and_verify(c) c.run_callbacks :checkout do c.verify! - @checked_out << c end c end + + def active_connections + @connections.find_all { |c| c.in_use? } + end end # ConnectionHandler is a collection of ConnectionPool objects. It is used @@ -320,10 +332,12 @@ module ActiveRecord def initialize(pools = {}) @connection_pools = pools + @class_to_pool = {} end def establish_connection(name, spec) - @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec) + @connection_pools[spec] ||= ConnectionAdapters::ConnectionPool.new(spec) + @class_to_pool[name] = @connection_pools[spec] end # Returns true if there are any active connections among the connection @@ -374,52 +388,24 @@ module ActiveRecord # can be used as an argument for establish_connection, for easily # re-establishing the connection. def remove_connection(klass) - pool = @connection_pools.delete(klass.name) + pool = @class_to_pool.delete(klass.name) return nil unless pool + @connection_pools.delete pool.spec pool.automatic_reconnect = false pool.disconnect! pool.spec.config end def retrieve_connection_pool(klass) - pool = @connection_pools[klass.name] + pool = @class_to_pool[klass.name] return pool if pool - return nil if ActiveRecord::Base == klass - retrieve_connection_pool klass.superclass + return nil if ActiveRecord::Model == klass + retrieve_connection_pool klass.active_record_super end end class ConnectionManagement - class Proxy # :nodoc: - attr_reader :body, :testing - - def initialize(body, testing = false) - @body = body - @testing = testing - end - - def method_missing(method_sym, *arguments, &block) - @body.send(method_sym, *arguments, &block) - end - - def respond_to?(method_sym, include_private = false) - super || @body.respond_to?(method_sym) - end - - def each(&block) - body.each(&block) - end - - def close - body.close if body.respond_to?(:close) - - # Don't return connection (and perform implicit rollback) if - # this request is a part of integration test - ActiveRecord::Base.clear_active_connections! unless testing - end - end - def initialize(app) @app = app end @@ -427,9 +413,12 @@ module ActiveRecord def call(env) testing = env.key?('rack.test') - status, headers, body = @app.call(env) + response = @app.call(env) + response[2] = ::Rack::BodyProxy.new(response[2]) do + ActiveRecord::Base.clear_active_connections! unless testing + end - [status, headers, Proxy.new(body, testing)] + response rescue ActiveRecord::Base.clear_active_connections! unless testing raise diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb deleted file mode 100644 index 3d0f146fed..0000000000 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ /dev/null @@ -1,161 +0,0 @@ -module ActiveRecord - class Base - class ConnectionSpecification #:nodoc: - attr_reader :config, :adapter_method - def initialize (config, adapter_method) - @config, @adapter_method = config, adapter_method - end - end - - ## - # :singleton-method: - # The connection handler - class_attribute :connection_handler, :instance_writer => false - self.connection_handler = ConnectionAdapters::ConnectionHandler.new - - # Returns the connection currently associated with the class. This can - # also be used to "borrow" the connection to do database work that isn't - # easily done without going straight to SQL. - def connection - self.class.connection - end - - # Establishes the connection to the database. Accepts a hash as input where - # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case) - # example for regular databases (MySQL, Postgresql, etc): - # - # ActiveRecord::Base.establish_connection( - # :adapter => "mysql", - # :host => "localhost", - # :username => "myuser", - # :password => "mypass", - # :database => "somedatabase" - # ) - # - # Example for SQLite database: - # - # ActiveRecord::Base.establish_connection( - # :adapter => "sqlite", - # :database => "path/to/dbfile" - # ) - # - # Also accepts keys as strings (for parsing from YAML for example): - # - # ActiveRecord::Base.establish_connection( - # "adapter" => "sqlite", - # "database" => "path/to/dbfile" - # ) - # - # Or a URL: - # - # ActiveRecord::Base.establish_connection( - # "postgres://myuser:mypass@localhost/somedatabase" - # ) - # - # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError - # may be returned on an error. - def self.establish_connection(spec = ENV["DATABASE_URL"]) - case spec - when nil - raise AdapterNotSpecified unless defined?(Rails.env) - establish_connection(Rails.env) - when ConnectionSpecification - self.connection_handler.establish_connection(name, spec) - when Symbol, String - if configuration = configurations[spec.to_s] - establish_connection(configuration) - elsif spec.is_a?(String) && hash = connection_url_to_hash(spec) - establish_connection(hash) - else - raise AdapterNotSpecified, "#{spec} database is not configured" - end - else - spec = spec.symbolize_keys - unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end - - begin - require "active_record/connection_adapters/#{spec[:adapter]}_adapter" - rescue LoadError => e - raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})" - end - - adapter_method = "#{spec[:adapter]}_connection" - unless respond_to?(adapter_method) - raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" - end - - remove_connection - establish_connection(ConnectionSpecification.new(spec, adapter_method)) - end - end - - def self.connection_url_to_hash(url) # :nodoc: - config = URI.parse url - adapter = config.scheme - adapter = "postgresql" if adapter == "postgres" - spec = { :adapter => adapter, - :username => config.user, - :password => config.password, - :port => config.port, - :database => config.path.sub(%r{^/},""), - :host => config.host } - spec.reject!{ |_,value| !value } - if config.query - options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys - spec.merge!(options) - end - spec - end - - class << self - # Returns the connection currently associated with the class. This can - # also be used to "borrow" the connection to do database work unrelated - # to any of the specific Active Records. - def connection - retrieve_connection - end - - def connection_id - Thread.current['ActiveRecord::Base.connection_id'] - end - - def connection_id=(connection_id) - Thread.current['ActiveRecord::Base.connection_id'] = connection_id - end - - # Returns the configuration of the associated connection as a hash: - # - # ActiveRecord::Base.connection_config - # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"} - # - # Please use only for reading. - def connection_config - connection_pool.spec.config - end - - def connection_pool - connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished - end - - def retrieve_connection - connection_handler.retrieve_connection(self) - end - - # Returns true if Active Record is connected. - def connected? - connection_handler.connected?(self) - end - - def remove_connection(klass = self) - connection_handler.remove_connection(klass) - end - - def clear_active_connections! - connection_handler.clear_active_connections! - end - - delegate :clear_reloadable_connections!, - :clear_all_connections!,:verify_active_connections!, :to => :connection_handler - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index dc4a53034b..eb8cff9610 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -130,7 +130,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html + # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL, but not SQLite3. # # It is safe to call this method if a database transaction is already open, @@ -341,7 +341,7 @@ module ActiveRecord # Send a rollback message to all records after they have been rolled back. If rollback # is false, only rollback records since the last save point. - def rollback_transaction_records(rollback) #:nodoc + def rollback_transaction_records(rollback) if rollback records = @_current_transaction_records.flatten @_current_transaction_records.clear @@ -361,7 +361,7 @@ module ActiveRecord end # Send a commit message to all records after they have been committed. - def commit_transaction_records #:nodoc + def commit_transaction_records records = @_current_transaction_records.flatten @_current_transaction_records.clear unless records.blank? diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index f93c7cd74a..44ac37c498 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -71,7 +71,8 @@ module ActiveRecord when Date, Time then quoted_date(value) when Symbol then value.to_s else - YAML.dump(value) + to_type = column ? " to #{column.type}" : "" + raise TypeError, "can't cast #{value.class}#{to_type}" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 6f135b56b5..132ca10f79 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -66,6 +66,7 @@ module ActiveRecord def initialize(base) @columns = [] + @columns_hash = {} @base = base end @@ -86,7 +87,7 @@ module ActiveRecord # Returns a ColumnDefinition for the column with name +name+. def [](name) - @columns.find {|column| column.name.to_s == name.to_s} + @columns_hash[name.to_s] end # Instantiates a new column for the table. @@ -224,28 +225,31 @@ module ActiveRecord # t.references :taggable, :polymorphic => { :default => 'Photo' } # end def column(name, type, options = {}) - column = self[name] || ColumnDefinition.new(@base, name, type) - if options[:limit] - column.limit = options[:limit] - elsif native[type.to_sym].is_a?(Hash) - column.limit = native[type.to_sym][:limit] + name = name.to_s + type = type.to_sym + + column = self[name] || new_column_definition(@base, name, type) + + limit = options.fetch(:limit) do + native[type][:limit] if native[type].is_a?(Hash) end + + column.limit = limit column.precision = options[:precision] - column.scale = options[:scale] - column.default = options[:default] - column.null = options[:null] - @columns << column unless @columns.include? column + column.scale = options[:scale] + column.default = options[:default] + column.null = options[:null] self end %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| class_eval <<-EOV, __FILE__, __LINE__ + 1 - def #{column_type}(*args) # def string(*args) - options = args.extract_options! # options = args.extract_options! - column_names = args # column_names = args - # - column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) } - end # end + def #{column_type}(*args) # def string(*args) + options = args.extract_options! # options = args.extract_options! + column_names = args # column_names = args + type = :'#{column_type}' # type = :string + column_names.each { |name| column(name, type, options) } # column_names.each { |name| column(name, type, options) } + end # end EOV end @@ -275,9 +279,16 @@ module ActiveRecord end private - def native - @base.native_database_types - end + def new_column_definition(base, name, type) + definition = ColumnDefinition.new base, name, type + @columns << definition + @columns_hash[name] = definition + definition + end + + def native + @base.native_database_types + end end # Represents an SQL table in an abstract way for updating a table. @@ -453,13 +464,13 @@ module ActiveRecord def #{column_type}(*args) # def string(*args) options = args.extract_options! # options = args.extract_options! column_names = args # column_names = args - # + type = :'#{column_type}' # type = :string column_names.each do |name| # column_names.each do |name| - column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string') + column = ColumnDefinition.new(@base, name.to_s, type) # column = ColumnDefinition.new(@base, name, type) if options[:limit] # if options[:limit] column.limit = options[:limit] # column.limit = options[:limit] - elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash) - column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit] + elsif native[type].is_a?(Hash) # elsif native[type].is_a?(Hash) + column.limit = native[type][:limit] # column.limit = native[type][:limit] end # end column.precision = options[:precision] # column.precision = options[:precision] column.scale = options[:scale] # column.scale = options[:scale] diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index faa42e2d19..0cac6d1391 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,5 +1,5 @@ -require 'active_support/core_ext/array/wrap' require 'active_support/deprecation/reporting' +require 'active_record/schema_migration' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -16,8 +16,6 @@ module ActiveRecord table_name[0...table_alias_length].gsub(/\./, '_') end - # def tables(name = nil) end - # Checks to see if the table +table_name+ exists on the database. # # === Example @@ -44,7 +42,7 @@ module ActiveRecord # # Check an index with a custom name exists # index_exists?(:suppliers, :company_id, :name => "idx_company_id" def index_exists?(table_name, column_name, options = {}) - column_names = Array.wrap(column_name) + column_names = Array(column_name) index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names) if options[:unique] indexes(table_name).any?{ |i| i.unique && i.name == index_name } @@ -114,7 +112,7 @@ module ActiveRecord # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. # # Also note that this just sets the primary key in the table. You additionally - # need to configure the primary key in the model via the +set_primary_key+ macro. + # need to configure the primary key in the model via +self.primary_key=+. # Models do NOT auto-detect the primary key from their table definition. # # [<tt>:options</tt>] @@ -303,15 +301,8 @@ module ActiveRecord # Adds a new index to the table. +column_name+ can be a single Symbol, or # an Array of Symbols. # - # The index will be named after the table and the first column name, - # unless you pass <tt>:name</tt> as an option. - # - # When creating an index on multiple columns, the first column is used as a name - # for the index. For example, when you specify an index on two columns - # [<tt>:first</tt>, <tt>:last</tt>], the DBMS creates an index for both columns as well as an - # index for the first column <tt>:first</tt>. Using just the first name for this index - # makes sense, because you will never have to create a singular index with this - # name. + # The index will be named after the table and the column name(s), unless + # you pass <tt>:name</tt> as an option. # # ===== Examples # @@ -386,7 +377,7 @@ module ActiveRecord def index_name(table_name, options) #:nodoc: if Hash === options # legacy support if options[:column] - "index_#{table_name}_on_#{Array.wrap(options[:column]) * '_and_'}" + "index_#{table_name}_on_#{Array(options[:column]) * '_and_'}" elsif options[:name] options[:name] else @@ -414,38 +405,20 @@ module ActiveRecord def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name - migrated = select_values("SELECT version FROM #{sm_table} ORDER BY version") - migrated.map { |v| "INSERT INTO #{sm_table} (version) VALUES ('#{v}');" }.join("\n\n") + + ActiveRecord::SchemaMigration.order('version').all.map { |sm| + "INSERT INTO #{sm_table} (version) VALUES ('#{sm.version}');" + }.join "\n\n" end # Should not be called normally, but this operation is non-destructive. # The migrations module handles this automatically. def initialize_schema_migrations_table - sm_table = ActiveRecord::Migrator.schema_migrations_table_name - - unless table_exists?(sm_table) - create_table(sm_table, :id => false) do |schema_migrations_table| - schema_migrations_table.column :version, :string, :null => false - end - add_index sm_table, :version, :unique => true, - :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" - - # Backwards-compatibility: if we find schema_info, assume we've - # migrated up to that point: - si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix - - if table_exists?(si_table) - ActiveRecord::Deprecation.warn "Usage of the schema table `#{si_table}` is deprecated. Please switch to using `schema_migrations` table" - - old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i - assume_migrated_upto_version(old_version) - drop_table(si_table) - end - end + ActiveRecord::SchemaMigration.create_table end def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths) - migrations_paths = Array.wrap(migrations_paths) + migrations_paths = Array(migrations_paths) version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) @@ -560,7 +533,7 @@ module ActiveRecord end def add_index_options(table_name, column_name, options = {}) - column_names = Array.wrap(column_name) + column_names = Array(column_name) index_name = index_name(table_name, :column => column_names) if Hash === options # legacy support, since this param was a string diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 75e568b557..edea414db7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,12 +4,14 @@ require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_support/deprecation' require 'active_record/connection_adapters/schema_cache' +require 'monitor' module ActiveRecord module ConnectionAdapters # :nodoc: extend ActiveSupport::Autoload autoload :Column + autoload :ConnectionSpecification autoload_under 'abstract' do autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions' @@ -25,7 +27,6 @@ module ActiveRecord autoload :ConnectionPool autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool' autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool' - autoload :ConnectionSpecification autoload :QueryCache end @@ -48,21 +49,42 @@ module ActiveRecord include DatabaseLimits include QueryCache include ActiveSupport::Callbacks + include MonitorMixin define_callbacks :checkout, :checkin - attr_accessor :visitor - attr_reader :schema_cache - - def initialize(connection, logger = nil) #:nodoc: - @active = nil - @connection, @logger = connection, logger + attr_accessor :visitor, :pool + attr_reader :schema_cache, :last_use, :in_use + alias :in_use? :in_use + + def initialize(connection, logger = nil, pool = nil) #:nodoc: + super() + + @active = nil + @connection = connection + @in_use = false + @instrumenter = ActiveSupport::Notifications.instrumenter + @last_use = false + @logger = logger + @open_transactions = 0 + @pool = pool + @query_cache = Hash.new { |h,sql| h[sql] = {} } @query_cache_enabled = false - @query_cache = Hash.new { |h,sql| h[sql] = {} } - @open_transactions = 0 - @instrumenter = ActiveSupport::Notifications.instrumenter - @visitor = nil - @schema_cache = SchemaCache.new self + @schema_cache = SchemaCache.new self + @visitor = nil + end + + def lease + synchronize do + unless in_use + @in_use = true + @last_use = Time.now + end + end + end + + def expire + @in_use = false end # Returns the human-readable name of the adapter. Use mixed case - one @@ -120,6 +142,12 @@ module ActiveRecord false end + # Does this adapter support explain? As of this writing sqlite3, + # mysql2, and postgresql are the only ones that do. + def supports_explain? + false + end + # QUOTING ================================================== # Override to return the quoted table name. Defaults to column quoting. @@ -236,6 +264,11 @@ module ActiveRecord "active_record_#{open_transactions}" end + # Check the connection back in to the connection pool + def close + pool.checkin self + end + protected def log(sql, name = "SQL", binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index f143fd348e..560773ca86 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -152,7 +152,7 @@ module ActiveRecord true end - # Technically MySQL allows to create indexes with the sort order syntax + # Technically MySQL allows to create indexes with the sort order syntax # but at the moment (5.5) it doesn't yet implement them def supports_index_sort_order? true @@ -225,80 +225,6 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== - def explain(arel) - sql = "EXPLAIN #{to_sql(arel)}" - start = Time.now - result = exec_query(sql, 'EXPLAIN') - elapsed = Time.now - start - - ExplainPrettyPrinter.new.pp(result, elapsed) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # MySQL shell: - # - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | - # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # 2 rows in set (0.00 sec) - # - # This is an exercise in Ruby hyperrealism :). - def pp(result, elapsed) - widths = compute_column_widths(result) - separator = build_separator(widths) - - pp = [] - - pp << separator - pp << build_cells(result.columns, widths) - pp << separator - - result.rows.each do |row| - pp << build_cells(row, widths) - end - - pp << separator - pp << build_footer(result.rows.length, elapsed) - - pp.join("\n") + "\n" - end - - private - - def compute_column_widths(result) - [].tap do |widths| - result.columns.each_with_index do |column, i| - cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} - widths << cells_in_column.map(&:length).max - end - end - end - - def build_separator(widths) - padding = 1 - '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' - end - - def build_cells(items, widths) - cells = [] - items.each_with_index do |item, i| - item = 'NULL' if item.nil? - justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' - cells << item.to_s.send(justifier, widths[i]) - end - '| ' + cells.join(' | ') + ' |' - end - - def build_footer(nrows, elapsed) - rows_label = nrows == 1 ? 'row' : 'rows' - "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed - end - end - # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) if name == :skip_logging @@ -437,8 +363,10 @@ module ActiveRecord show_variable 'collation_database' end - def tables(name = nil, database = nil) #:nodoc: - sql = ["SHOW TABLES", database].compact.join(' IN ') + def tables(name = nil, database = nil, like = nil) #:nodoc: + sql = "SHOW TABLES " + sql << "IN #{database} " if database + sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| result.collect { |field| field.first } @@ -446,7 +374,8 @@ module ActiveRecord end def table_exists?(name) - return true if super + return false unless name + return true if tables(nil, nil, name).any? name = name.to_s schema, table = name.split('.', 2) @@ -456,7 +385,7 @@ module ActiveRecord schema = nil end - tables(nil, schema).include? table + tables(nil, schema, table).any? end # Returns an array of indexes for the given table. @@ -573,9 +502,14 @@ module ActiveRecord # Returns a table's primary key and belonging sequence. def pk_and_sequence_for(table) - execute_and_free("SHOW INDEX FROM #{quote_table_name(table)} WHERE Key_name = 'PRIMARY'", 'SCHEMA') do |result| - keys = each_hash(result).map { |row| row[:Column_name] } - keys.length == 1 ? [keys.first, nil] : nil + execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result| + create_table = each_hash(result).first[:"Create Table"] + if create_table.to_s =~ /PRIMARY KEY\s+\((.+)\)/ + keys = $1.split(",").map { |key| key.gsub(/`/, "") } + keys.length == 1 ? [keys.first, nil] : nil + else + nil + end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index a7856539b7..2ecb198edb 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -5,8 +5,8 @@ module ActiveRecord module ConnectionAdapters # An abstract definition of a column in a table. class Column - TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set - FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set + TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON'].to_set + FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set module Format ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ @@ -80,9 +80,10 @@ module ActiveRecord when :decimal then klass.value_to_decimal(value) when :datetime, :timestamp then klass.string_to_time(value) when :time then klass.string_to_dummy_time(value) - when :date then klass.string_to_date(value) + when :date then klass.value_to_date(value) when :binary then klass.binary_to_string(value) when :boolean then klass.value_to_boolean(value) + when :hstore then klass.cast_hstore(value) else value end end @@ -97,9 +98,10 @@ module ActiveRecord when :decimal then "#{klass}.value_to_decimal(#{var_name})" when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" when :time then "#{klass}.string_to_dummy_time(#{var_name})" - when :date then "#{klass}.string_to_date(#{var_name})" + when :date then "#{klass}.value_to_date(#{var_name})" when :binary then "#{klass}.binary_to_string(#{var_name})" when :boolean then "#{klass}.value_to_boolean(#{var_name})" + when :hstore then "#{klass}.cast_hstore(#{var_name})" else var_name end end @@ -132,11 +134,15 @@ module ActiveRecord value end - def string_to_date(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_date(string) || fallback_string_to_date(string) + def value_to_date(value) + if value.is_a?(String) + return nil if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end end def string_to_time(string) diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb new file mode 100644 index 0000000000..8491d42b86 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -0,0 +1,83 @@ +module ActiveRecord + module ConnectionAdapters + class ConnectionSpecification #:nodoc: + attr_reader :config, :adapter_method + + def initialize(config, adapter_method) + @config, @adapter_method = config, adapter_method + end + + def initialize_dup(original) + @config = original.config.dup + end + + ## + # Builds a ConnectionSpecification from user input + class Resolver # :nodoc: + attr_reader :config, :klass, :configurations + + def initialize(config, configurations) + @config = config + @configurations = configurations + end + + def spec + case config + when nil + raise AdapterNotSpecified unless defined?(Rails.env) + resolve_string_connection Rails.env + when Symbol, String + resolve_string_connection config.to_s + when Hash + resolve_hash_connection config + end + end + + private + def resolve_string_connection(spec) # :nodoc: + hash = configurations.fetch(spec) do |k| + connection_url_to_hash(k) + end + + raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash + + resolve_hash_connection hash + end + + def resolve_hash_connection(spec) # :nodoc: + spec = spec.symbolize_keys + + raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) + + begin + require "active_record/connection_adapters/#{spec[:adapter]}_adapter" + rescue LoadError => e + raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace + end + + adapter_method = "#{spec[:adapter]}_connection" + + ConnectionSpecification.new(spec, adapter_method) + end + + def connection_url_to_hash(url) # :nodoc: + config = URI.parse url + adapter = config.scheme + adapter = "postgresql" if adapter == "postgres" + spec = { :adapter => adapter, + :username => config.user, + :password => config.password, + :port => config.port, + :database => config.path.sub(%r{^/},""), + :host => config.host } + spec.reject!{ |_,value| !value } + if config.query + options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys + spec.merge!(options) + end + spec + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 971f3c35f3..6086c32dbe 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,12 +1,12 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' -gem 'mysql2', '~> 0.3.6' +gem 'mysql2', '~> 0.3.10' require 'mysql2' module ActiveRecord - class Base + module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects. - def self.mysql2_connection(config) + def mysql2_connection(config) config[:username] = 'root' if config[:username].nil? if Mysql2::Client.const_defined? :FOUND_ROWS @@ -35,6 +35,10 @@ module ActiveRecord configure_connection end + def supports_explain? + true + end + # HELPER METHODS =========================================== def each_hash(result) # :nodoc: @@ -93,6 +97,80 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel)}" + start = Time.now + result = exec_query(sql, 'EXPLAIN', binds) + elapsed = Time.now - start + + ExplainPrettyPrinter.new.pp(result, elapsed) + end + + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # MySQL shell: + # + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | + # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # 2 rows in set (0.00 sec) + # + # This is an exercise in Ruby hyperrealism :). + def pp(result, elapsed) + widths = compute_column_widths(result) + separator = build_separator(widths) + + pp = [] + + pp << separator + pp << build_cells(result.columns, widths) + pp << separator + + result.rows.each do |row| + pp << build_cells(row, widths) + end + + pp << separator + pp << build_footer(result.rows.length, elapsed) + + pp.join("\n") + "\n" + end + + private + + def compute_column_widths(result) + [].tap do |widths| + result.columns.each_with_index do |column, i| + cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} + widths << cells_in_column.map(&:length).max + end + end + end + + def build_separator(widths) + padding = 1 + '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' + end + + def build_cells(items, widths) + cells = [] + items.each_with_index do |item, i| + item = 'NULL' if item.nil? + justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' + cells << item.to_s.send(justifier, widths[i]) + end + '| ' + cells.join(' | ') + ' |' + end + + def build_footer(nrows, elapsed) + rows_label = nrows == 1 ? 'row' : 'rows' + "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed + end + end + # FIXME: re-enable the following once a "better" query_cache solution is in core # # The overrides below perform much better than the originals in AbstractAdapter diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index f092edecda..e432c5af32 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -18,9 +18,9 @@ class Mysql end module ActiveRecord - class Base + module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects. - def self.mysql_connection(config) # :nodoc: + def mysql_connection(config) # :nodoc: config = config.symbolize_keys host = config[:host] port = config[:port] @@ -224,52 +224,48 @@ module ActiveRecord @statements.clear end - if "<3".respond_to?(:encode) - # Taken from here: - # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb - # Author: TOMITA Masahiro <tommy@tmtm.org> - ENCODINGS = { - "armscii8" => nil, - "ascii" => Encoding::US_ASCII, - "big5" => Encoding::Big5, - "binary" => Encoding::ASCII_8BIT, - "cp1250" => Encoding::Windows_1250, - "cp1251" => Encoding::Windows_1251, - "cp1256" => Encoding::Windows_1256, - "cp1257" => Encoding::Windows_1257, - "cp850" => Encoding::CP850, - "cp852" => Encoding::CP852, - "cp866" => Encoding::IBM866, - "cp932" => Encoding::Windows_31J, - "dec8" => nil, - "eucjpms" => Encoding::EucJP_ms, - "euckr" => Encoding::EUC_KR, - "gb2312" => Encoding::EUC_CN, - "gbk" => Encoding::GBK, - "geostd8" => nil, - "greek" => Encoding::ISO_8859_7, - "hebrew" => Encoding::ISO_8859_8, - "hp8" => nil, - "keybcs2" => nil, - "koi8r" => Encoding::KOI8_R, - "koi8u" => Encoding::KOI8_U, - "latin1" => Encoding::ISO_8859_1, - "latin2" => Encoding::ISO_8859_2, - "latin5" => Encoding::ISO_8859_9, - "latin7" => Encoding::ISO_8859_13, - "macce" => Encoding::MacCentEuro, - "macroman" => Encoding::MacRoman, - "sjis" => Encoding::SHIFT_JIS, - "swe7" => nil, - "tis620" => Encoding::TIS_620, - "ucs2" => Encoding::UTF_16BE, - "ujis" => Encoding::EucJP_ms, - "utf8" => Encoding::UTF_8, - "utf8mb4" => Encoding::UTF_8, - } - else - ENCODINGS = Hash.new { |h,k| h[k] = k } - end + # Taken from here: + # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb + # Author: TOMITA Masahiro <tommy@tmtm.org> + ENCODINGS = { + "armscii8" => nil, + "ascii" => Encoding::US_ASCII, + "big5" => Encoding::Big5, + "binary" => Encoding::ASCII_8BIT, + "cp1250" => Encoding::Windows_1250, + "cp1251" => Encoding::Windows_1251, + "cp1256" => Encoding::Windows_1256, + "cp1257" => Encoding::Windows_1257, + "cp850" => Encoding::CP850, + "cp852" => Encoding::CP852, + "cp866" => Encoding::IBM866, + "cp932" => Encoding::Windows_31J, + "dec8" => nil, + "eucjpms" => Encoding::EucJP_ms, + "euckr" => Encoding::EUC_KR, + "gb2312" => Encoding::EUC_CN, + "gbk" => Encoding::GBK, + "geostd8" => nil, + "greek" => Encoding::ISO_8859_7, + "hebrew" => Encoding::ISO_8859_8, + "hp8" => nil, + "keybcs2" => nil, + "koi8r" => Encoding::KOI8_R, + "koi8u" => Encoding::KOI8_U, + "latin1" => Encoding::ISO_8859_1, + "latin2" => Encoding::ISO_8859_2, + "latin5" => Encoding::ISO_8859_9, + "latin7" => Encoding::ISO_8859_13, + "macce" => Encoding::MacCentEuro, + "macroman" => Encoding::MacRoman, + "sjis" => Encoding::SHIFT_JIS, + "swe7" => nil, + "tis620" => Encoding::TIS_620, + "ucs2" => Encoding::UTF_16BE, + "ujis" => Encoding::EucJP_ms, + "utf8" => Encoding::UTF_8, + "utf8mb4" => Encoding::UTF_8, + } # Get the client encoding for this database def client_encoding diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 2f01fbb829..dbfb375ba8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -7,24 +7,26 @@ gem 'pg', '~> 0.11' require 'pg' module ActiveRecord - class Base + module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects - def self.postgresql_connection(config) # :nodoc: - config = config.symbolize_keys - host = config[:host] - port = config[:port] || 5432 - username = config[:username].to_s if config[:username] - password = config[:password].to_s if config[:password] + def postgresql_connection(config) # :nodoc: + conn_params = config.symbolize_keys - if config.key?(:database) - database = config[:database] - else - raise ArgumentError, "No database specified. Missing argument: database." + # Forward any unused config params to PGconn.connect. + [:statement_limit, :encoding, :min_messages, :schema_search_path, + :schema_order, :adapter, :pool, :wait_timeout, + :reaping_frequency].each do |key| + conn_params.delete key end + conn_params.delete_if { |k,v| v.nil? } + + # Map ActiveRecords param names to PGs. + conn_params[:user] = conn_params.delete(:username) if conn_params[:username] + conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database] # The postgres drivers don't allow the creation of an unconnected PGconn object, # so just pass a nil connection object for the time being. - ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config) + ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config) end end @@ -49,6 +51,42 @@ module ActiveRecord super end end + + def cast_hstore(object) + if Hash === object + object.map { |k,v| + "#{escape_hstore(k)}=>#{escape_hstore(v)}" + }.join ', ' + else + kvs = object.scan(/(?<!\\)".*?(?<!\\)"/).map { |o| + unescape_hstore(o[1...-1]) + } + Hash[kvs.each_slice(2).to_a] + end + end + + private + HSTORE_ESCAPE = { + ' ' => '\\ ', + '\\' => '\\\\', + '"' => '\\"', + '=' => '\\=', + } + HSTORE_ESCAPE_RE = Regexp.union(HSTORE_ESCAPE.keys) + HSTORE_UNESCAPE = HSTORE_ESCAPE.invert + HSTORE_UNESCAPE_RE = Regexp.union(HSTORE_UNESCAPE.keys) + + def unescape_hstore(value) + value.gsub(HSTORE_UNESCAPE_RE) do |match| + HSTORE_UNESCAPE[match] + end + end + + def escape_hstore(value) + value.gsub(HSTORE_ESCAPE_RE) do |match| + HSTORE_ESCAPE[match] + end + end end # :startdoc: @@ -79,67 +117,69 @@ module ActiveRecord # Maps PostgreSQL-specific data types to logical Rails types. def simplified_type(field_type) case field_type - # Numeric and monetary types - when /^(?:real|double precision)$/ - :float - # Monetary types - when 'money' - :decimal - # Character types - when /^(?:character varying|bpchar)(?:\(\d+\))?$/ - :string - # Binary data types - when 'bytea' - :binary - # Date/time types - when /^timestamp with(?:out)? time zone$/ - :datetime - when 'interval' - :string - # Geometric types - when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ - :string - # Network address types - when /^(?:cidr|inet|macaddr)$/ - :string - # Bit strings - when /^bit(?: varying)?(?:\(\d+\))?$/ - :string - # XML type - when 'xml' - :xml - # tsvector type - when 'tsvector' - :tsvector - # Arrays - when /^\D+\[\]$/ - :string - # Object identifier types - when 'oid' - :integer - # UUID type - when 'uuid' - :string - # Small and big integer types - when /^(?:small|big)int$/ - :integer - # Pass through all types that are not specific to PostgreSQL. - else - super + # Numeric and monetary types + when /^(?:real|double precision)$/ + :float + # Monetary types + when 'money' + :decimal + when 'hstore' + :hstore + # Character types + when /^(?:character varying|bpchar)(?:\(\d+\))?$/ + :string + # Binary data types + when 'bytea' + :binary + # Date/time types + when /^timestamp with(?:out)? time zone$/ + :datetime + when 'interval' + :string + # Geometric types + when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ + :string + # Network address types + when /^(?:cidr|inet|macaddr)$/ + :string + # Bit strings + when /^bit(?: varying)?(?:\(\d+\))?$/ + :string + # XML type + when 'xml' + :xml + # tsvector type + when 'tsvector' + :tsvector + # Arrays + when /^\D+\[\]$/ + :string + # Object identifier types + when 'oid' + :integer + # UUID type + when 'uuid' + :string + # Small and big integer types + when /^(?:small|big)int$/ + :integer + # Pass through all types that are not specific to PostgreSQL. + else + super end end # Extracts the value from a PostgreSQL column default definition. def self.extract_value_from_default(default) + # This is a performance optimization for Ruby 1.9.2 in development. + # If the value is nil, we return nil straight away without checking + # the regular expressions. If we check each regular expression, + # Regexp#=== will call NilClass#to_str, which will trigger + # method_missing (defined by whiny nil in ActiveSupport) which + # makes this method very very slow. + return default unless default + case default - # This is a performance optimization for Ruby 1.9.2 in development. - # If the value is nil, we return nil straight away without checking - # the regular expressions. If we check each regular expression, - # Regexp#=== will call NilClass#to_str, which will trigger - # method_missing (defined by whiny nil in ActiveSupport) which - # makes this method very very slow. - when NilClass - nil # Numeric types when /\A\(?(-?\d+(\.\d*)?\)?)\z/ $1 @@ -188,22 +228,29 @@ module ActiveRecord end end - # The PostgreSQL adapter works both with the native C (http://ruby.scripting.ca/postgres/) and the pure - # Ruby (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1944) drivers. + # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver. # # Options: # - # * <tt>:host</tt> - Defaults to "localhost". + # * <tt>:host</tt> - Defaults to a Unix-domain socket in /tmp. On machines without Unix-domain sockets, + # the default is to connect to localhost. # * <tt>:port</tt> - Defaults to 5432. - # * <tt>:username</tt> - Defaults to nothing. - # * <tt>:password</tt> - Defaults to nothing. - # * <tt>:database</tt> - The name of the database. No default, must be provided. + # * <tt>:username</tt> - Defaults to be the same as the operating system name of the user running the application. + # * <tt>:password</tt> - Password to be used if the server demands password authentication. + # * <tt>:database</tt> - Defaults to be the same as the user name. # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given # as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option. # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO # <encoding></tt> call on the connection. # * <tt>:min_messages</tt> - An optional client min messages that is used in a # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. + # + # Any further options are used as connection parameters to libpq. See + # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the + # list of parameters. + # + # 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 TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition def xml(*args) @@ -215,6 +262,10 @@ module ActiveRecord options = args.extract_options! column(args[0], 'tsvector', options) end + + def hstore(name, options = {}) + column(name, 'hstore', options) + end end ADAPTER_NAME = 'PostgreSQL' @@ -390,6 +441,11 @@ module ActiveRecord true end + # Returns true. + def supports_explain? + true + end + # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length @table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i @@ -514,9 +570,9 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== - def explain(arel) + def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel)}" - ExplainPrettyPrinter.new.pp(exec_query(sql)) + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) end class ExplainPrettyPrinter # :nodoc: @@ -829,7 +885,7 @@ module ActiveRecord # add info on sort order for columns (only desc order is explicitly specified, asc is the default) desc_order_columns = inddef.scan(/(\w+) DESC/).flatten orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} - + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders) end.compact end @@ -1153,7 +1209,7 @@ module ActiveRecord # Connects to a PostgreSQL server and sets up the adapter depending on the # connected server's characteristics. def connect - @connection = PGconn.connect(*@connection_parameters) + @connection = PGconn.connect(@connection_parameters) # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index b14b37ce89..4e8932a695 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -2,22 +2,14 @@ module ActiveRecord module ConnectionAdapters class SchemaCache attr_reader :columns, :columns_hash, :primary_keys, :tables - attr_reader :column_defaults attr_reader :connection def initialize(conn) @connection = conn - @tables = {} + @tables = {} - @columns = Hash.new do |h, table_name| - h[table_name] = - # Fetch a list of columns - conn.columns(table_name, "#{table_name} Columns").tap do |cs| - # set primary key information - cs.each do |column| - column.primary = column.name == primary_keys[table_name] - end - end + @columns = Hash.new do |h, table_name| + h[table_name] = conn.columns(table_name, "#{table_name} Columns") end @columns_hash = Hash.new do |h, table_name| @@ -26,15 +18,8 @@ module ActiveRecord }] end - @column_defaults = Hash.new do |h, table_name| - h[table_name] = Hash[columns[table_name].map { |col| - [col.name, col.default] - }] - end - @primary_keys = Hash.new do |h, table_name| - h[table_name] = table_exists?(table_name) ? - conn.primary_key(table_name) : 'id' + h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil end end @@ -42,21 +27,14 @@ module ActiveRecord def table_exists?(name) return @tables[name] if @tables.key? name - connection.tables.each { |table| @tables[table] = true } - @tables[name] = connection.table_exists?(name) if !@tables.key?(name) - - @tables[name] + @tables[name] = connection.table_exists?(name) end - # Clears out internal caches: - # - # * columns - # * columns_hash - # * tables + # Clears out internal caches def clear! @columns.clear @columns_hash.clear - @column_defaults.clear + @primary_keys.clear @tables.clear end @@ -64,8 +42,8 @@ module ActiveRecord def clear_table_cache!(table_name) @columns.delete table_name @columns_hash.delete table_name - @column_defaults.delete table_name @primary_keys.delete table_name + @tables.delete table_name end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 0a0da0b5d3..ee5d10859c 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,12 +1,12 @@ require 'active_record/connection_adapters/sqlite_adapter' -gem 'sqlite3', '~> 1.3.4' +gem 'sqlite3', '~> 1.3.5' require 'sqlite3' module ActiveRecord - class Base + module ConnectionHandling # sqlite3 adapter reuses sqlite_connection. - def self.sqlite3_connection(config) # :nodoc: + def sqlite3_connection(config) # :nodoc: # Require database. unless config[:database] raise ArgumentError, "No database file specified. Missing argument: database" @@ -47,11 +47,7 @@ module ActiveRecord # Returns the current database encoding format as a string, eg: 'UTF-8' def encoding - if @connection.respond_to?(:encoding) - @connection.encoding.to_s - else - @connection.execute('PRAGMA encoding')[0]['encoding'] - end + @connection.encoding.to_s end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index c11f82a33f..69750a911d 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -16,7 +16,7 @@ module ActiveRecord end def binary_to_string(value) - if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT + if value.encoding != Encoding::ASCII_8BIT value = value.force_encoding(Encoding::ASCII_8BIT) end @@ -122,6 +122,11 @@ module ActiveRecord true end + # Returns true. + def supports_explain? + true + end + def requires_reloading? true end @@ -196,32 +201,24 @@ module ActiveRecord end end - if "<3".encoding_aware? - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value + def type_cast(value, column) # :nodoc: + return value.to_f if BigDecimal === value + return super unless String === value + return super unless column && value - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - @logger.error "Binary data inserted for `string` type on column `#{column.name}`" - value.encode! 'utf-8' - end - value - end - else - def type_cast(value, column) # :nodoc: - return super unless BigDecimal === value - - value.to_f + value = super + if column.type == :string && value.encoding == Encoding::ASCII_8BIT + @logger.error "Binary data inserted for `string` type on column `#{column.name}`" + value.encode! 'utf-8' end + value end # DATABASE STATEMENTS ====================================== - def explain(arel) + def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN')) + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) end class ExplainPrettyPrinter @@ -324,18 +321,23 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def tables(name = 'SCHEMA') #:nodoc: + def tables(name = 'SCHEMA', table_name = nil) #:nodoc: sql = <<-SQL SELECT name FROM sqlite_master WHERE type = 'table' AND NOT name = 'sqlite_sequence' SQL + sql << " AND name = #{quote_table_name(table_name)}" if table_name exec_query(sql, name).map do |row| row['name'] end end + def table_exists?(name) + name && tables('SCHEMA', name).any? + end + # Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+. def columns(table_name, name = nil) #:nodoc: table_structure(table_name).map do |field| diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb new file mode 100644 index 0000000000..d7746826a9 --- /dev/null +++ b/activerecord/lib/active_record/connection_handling.rb @@ -0,0 +1,96 @@ +require 'active_support/core_ext/module/delegation' + +module ActiveRecord + module ConnectionHandling + # Establishes the connection to the database. Accepts a hash as input where + # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case) + # example for regular databases (MySQL, Postgresql, etc): + # + # ActiveRecord::Base.establish_connection( + # :adapter => "mysql", + # :host => "localhost", + # :username => "myuser", + # :password => "mypass", + # :database => "somedatabase" + # ) + # + # Example for SQLite database: + # + # ActiveRecord::Base.establish_connection( + # :adapter => "sqlite", + # :database => "path/to/dbfile" + # ) + # + # Also accepts keys as strings (for parsing from YAML for example): + # + # ActiveRecord::Base.establish_connection( + # "adapter" => "sqlite", + # "database" => "path/to/dbfile" + # ) + # + # Or a URL: + # + # ActiveRecord::Base.establish_connection( + # "postgres://myuser:mypass@localhost/somedatabase" + # ) + # + # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError + # may be returned on an error. + def establish_connection(spec = ENV["DATABASE_URL"]) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations + spec = resolver.spec + + unless respond_to?(spec.adapter_method) + raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" + end + + remove_connection + connection_handler.establish_connection name, spec + end + + # Returns the connection currently associated with the class. This can + # also be used to "borrow" the connection to do database work unrelated + # to any of the specific Active Records. + def connection + retrieve_connection + end + + def connection_id + Thread.current['ActiveRecord::Base.connection_id'] + end + + def connection_id=(connection_id) + Thread.current['ActiveRecord::Base.connection_id'] = connection_id + end + + # Returns the configuration of the associated connection as a hash: + # + # ActiveRecord::Base.connection_config + # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"} + # + # Please use only for reading. + def connection_config + connection_pool.spec.config + end + + def connection_pool + connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished + end + + def retrieve_connection + connection_handler.retrieve_connection(self) + end + + # Returns true if Active Record is connected. + def connected? + connection_handler.connected?(self) + end + + def remove_connection(klass = self) + connection_handler.remove_connection(klass) + end + + delegate :clear_active_connections!, :clear_reloadable_connections!, + :clear_all_connections!, :verify_active_connections!, :to => :connection_handler + end +end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb new file mode 100644 index 0000000000..adba3f710c --- /dev/null +++ b/activerecord/lib/active_record/core.rb @@ -0,0 +1,341 @@ +require 'active_support/concern' +require 'thread' + +module ActiveRecord + module Core + extend ActiveSupport::Concern + + included do + ## + # :singleton-method: + # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, + # which is then passed on to any new database connections made and which can be retrieved on both + # a class and instance level by calling +logger+. + config_attribute :logger, :global => true + + ## + # :singleton-method: + # Contains the database configuration - as is typically stored in config/database.yml - + # as a Hash. + # + # For example, the following database.yml... + # + # development: + # adapter: sqlite3 + # database: db/development.sqlite3 + # + # production: + # adapter: sqlite3 + # database: db/production.sqlite3 + # + # ...would result in ActiveRecord::Base.configurations to look like this: + # + # { + # 'development' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/development.sqlite3' + # }, + # 'production' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/production.sqlite3' + # } + # } + config_attribute :configurations, :global => true + self.configurations = {} + + ## + # :singleton-method: + # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling + # dates and times from the database. This is set to :utc by default. + config_attribute :default_timezone, :global => true + self.default_timezone = :utc + + ## + # :singleton-method: + # Specifies the format to use when dumping the database schema with Rails' + # Rakefile. If :sql, the schema is dumped as (potentially database- + # specific) SQL statements. If :ruby, the schema is dumped as an + # ActiveRecord::Schema file which can be loaded into any database that + # supports migrations. Use :ruby if you want to have different database + # adapters for, e.g., your development and test environments. + config_attribute :schema_format, :global => true + self.schema_format = :ruby + + ## + # :singleton-method: + # Specify whether or not to use timestamps for migration versions + config_attribute :timestamped_migrations, :global => true + self.timestamped_migrations = true + + ## + # :singleton-method: + # The connection handler + config_attribute :connection_handler + self.connection_handler = ConnectionAdapters::ConnectionHandler.new + end + + module ClassMethods + def inherited(child_class) #:nodoc: + child_class.initialize_generated_modules + super + end + + def initialize_generated_modules + @attribute_methods_mutex = Mutex.new + + # force attribute methods to be higher in inheritance hierarchy than other generated methods + generated_attribute_methods + generated_feature_methods + end + + def generated_feature_methods + @generated_feature_methods ||= begin + mod = const_set(:GeneratedFeatureMethods, Module.new) + include mod + mod + end + end + + # Returns a string like 'Post(id:integer, title:string, body:text)' + def inspect + if self == Base + super + elsif abstract_class? + "#{super}(abstract)" + elsif table_exists? + attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' + "#{super}(#{attr_list})" + else + "#{super}(Table doesn't exist)" + end + end + + # Overwrite the default class equality method to provide support for association proxies. + def ===(object) + object.is_a?(self) + end + + def arel_table + @arel_table ||= Arel::Table.new(table_name, arel_engine) + end + + def arel_engine + @arel_engine ||= connection_handler.connection_pools[name] ? self : active_record_super.arel_engine + end + + private + + def relation #:nodoc: + @relation ||= Relation.new(self, arel_table) + + if finder_needs_type_condition? + @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + else + @relation + end + end + end + + # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with + # attributes but not yet saved (pass a hash with key names matching the associated table column names). + # In both instances, valid attribute keys are determined by the column names of the associated table -- + # hence you can't have attributes that aren't part of the table columns. + # + # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options + # in the +options+ parameter. + # + # ==== Examples + # # Instantiates a single new object + # User.new(:first_name => 'Jamie') + # + # # Instantiates a single new object using the :admin mass-assignment security role + # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) + # + # # Instantiates a single new object bypassing mass-assignment security + # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) + def initialize(attributes = nil, options = {}) + @attributes = self.class.initialize_attributes(self.class.column_defaults.dup) + + init_internals + + ensure_proper_type + + populate_with_current_scope_attributes + + assign_attributes(attributes, options) if attributes + + yield self if block_given? + run_callbacks :initialize + end + + # Initialize an empty model object from +coder+. +coder+ must contain + # the attributes necessary for initializing an empty model object. For + # example: + # + # class Post < ActiveRecord::Base + # end + # + # post = Post.allocate + # post.init_with('attributes' => { 'title' => 'hello world' }) + # post.title # => 'hello world' + def init_with(coder) + @attributes = self.class.initialize_attributes(coder['attributes']) + + init_internals + + @new_record = false + + run_callbacks :find + run_callbacks :initialize + + self + end + + # Duped objects have no id assigned and are treated as new records. Note + # that this is a "shallow" copy as it copies the object's attributes + # only, not its associations. The extent of a "deep" copy is application + # specific and is therefore left to the application to implement according + # to its need. + # The dup method does not preserve the timestamps (created|updated)_(at|on). + def initialize_dup(other) + cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) + cloned_attributes.delete(self.class.primary_key) + + @attributes = cloned_attributes + + run_callbacks(:initialize) if _initialize_callbacks.any? + + @changed_attributes = {} + self.class.column_defaults.each do |attr, orig_value| + @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) + end + + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + + @new_record = true + + ensure_proper_type + populate_with_current_scope_attributes + super + end + + # Populate +coder+ with attributes about this record that should be + # serialized. The structure of +coder+ defined in this method is + # guaranteed to match the structure of +coder+ passed to the +init_with+ + # method. + # + # Example: + # + # class Post < ActiveRecord::Base + # end + # coder = {} + # Post.new.encode_with(coder) + # coder # => { 'id' => nil, ... } + def encode_with(coder) + coder['attributes'] = attributes + end + + # 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) + super || + comparison_object.instance_of?(self.class) && + id.present? && + comparison_object.id == id + end + alias :eql? :== + + # Delegates to id in order to allow two records of the same type and id to work with something like: + # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] + def hash + id.hash + end + + # Freeze the attributes hash such that associations are still accessible, even on destroyed records. + def freeze + @attributes.freeze; self + end + + # Returns +true+ if the attributes hash has been frozen. + def frozen? + @attributes.frozen? + end + + # Allows sort on objects + def <=>(other_object) + if other_object.is_a?(self.class) + self.to_key <=> other_object.to_key + else + nil + end + end + + # Returns +true+ if the record is read only. Records loaded through joins with piggy-back + # attributes will be marked as read only since they cannot be saved. + def readonly? + @readonly + end + + # Marks this record as read only. + def readonly! + @readonly = true + end + + # Returns the connection currently associated with the class. This can + # also be used to "borrow" the connection to do database work that isn't + # easily done without going straight to SQL. + def connection + self.class.connection + end + + # Returns the contents of the record as a nicely formatted string. + def inspect + inspection = if @attributes + self.class.column_names.collect { |name| + if has_attribute?(name) + "#{name}: #{attribute_for_inspect(name)}" + end + }.compact.join(", ") + else + "not initialized" + end + "#<#{self.class} #{inspection}>" + end + + private + + # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements + # of the array, and then rescues from the possible NoMethodError. If those elements are + # ActiveRecord::Base's, then this triggers the various method_missing's that we have, + # which significantly impacts upon performance. + # + # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here. + # + # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/ + def to_ary # :nodoc: + nil + end + + def init_internals + @relation = nil + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + @previously_changed = {} + @changed_attributes = {} + @readonly = false + @destroyed = false + @marked_for_destruction = false + @new_record = true + end + end +end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 3c7defedac..224f5276eb 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -19,15 +19,10 @@ module ActiveRecord counters.each do |association| has_many_association = reflect_on_association(association.to_sym) - expected_name = if has_many_association.options[:as] - has_many_association.options[:as].to_s.classify - else - self.name - end - + foreign_key = has_many_association.foreign_key.to_s child_class = has_many_association.klass belongs_to = child_class.reflect_on_all_associations(:belongs_to) - reflection = belongs_to.find { |e| e.class_name == expected_name } + reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key } counter_name = reflection.counter_cache_column stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ @@ -65,7 +60,7 @@ module ActiveRecord # Post.update_counters [10, 15], :comment_count => 1 # # Executes the following SQL: # # UPDATE posts - # # SET comment_count = COALESCE(comment_count, 0) + 1, + # # SET comment_count = COALESCE(comment_count, 0) + 1 # # WHERE id IN (10, 15) def update_counters(id, counters) updates = counters.map do |counter_name, value| diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb new file mode 100644 index 0000000000..e9068089f0 --- /dev/null +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -0,0 +1,79 @@ +module ActiveRecord + module DynamicMatchers + def respond_to?(method_id, include_private = false) + if match = DynamicFinderMatch.match(method_id) + return true if all_attributes_exists?(match.attribute_names) + elsif match = DynamicScopeMatch.match(method_id) + return true if all_attributes_exists?(match.attribute_names) + end + + super + end + + private + + # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and + # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders + # section at the top of this file for more detailed information. + # + # It's even possible to use all the additional parameters to +find+. For example, the + # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. + # + # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it + # is first invoked, so that future attempts to use it do not run through method_missing. + def method_missing(method_id, *arguments, &block) + if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id)) + attribute_names = match.attribute_names + super unless all_attributes_exists?(attribute_names) + if arguments.size < attribute_names.size + method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'" + backtrace = [method_trace] + caller + raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace + end + if match.respond_to?(:scope?) && match.scope? + self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 + def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) + attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)] + # + scoped(:conditions => attributes) # scoped(:conditions => attributes) + end # end + METHOD + send(method_id, *arguments) + elsif match.finder? + options = arguments.extract_options! + relation = options.any? ? scoped(options) : scoped + relation.send :find_by_attributes, match, attribute_names, *arguments, &block + elsif match.instantiator? + scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block + end + else + super + end + end + + # Similar in purpose to +expand_hash_conditions_for_aggregates+. + def expand_attribute_names_for_aggregates(attribute_names) + attribute_names.map { |attribute_name| + unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? + aggregate_mapping(aggregation).map do |field_attr, _| + field_attr.to_sym + end + else + attribute_name.to_sym + end + }.flatten + end + + def all_attributes_exists?(attribute_names) + (expand_attribute_names_for_aggregates(attribute_names) - + column_methods_hash.keys).empty? + end + + def aggregate_mapping(reflection) + mapping = reflection.options[:mapping] || [reflection.name, reflection.name] + mapping.first.is_a?(Array) ? mapping : [mapping] + end + + + end +end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb new file mode 100644 index 0000000000..e502d7e52b --- /dev/null +++ b/activerecord/lib/active_record/explain.rb @@ -0,0 +1,80 @@ +require 'active_support/core_ext/class/attribute' + +module ActiveRecord + module Explain + def self.extended(base) + # If a query takes longer than these many seconds we log its query plan + # automatically. nil disables this feature. + base.config_attribute :auto_explain_threshold_in_seconds, :global => true + end + + # If auto explain is enabled, this method triggers EXPLAIN logging for the + # queries triggered by the block if it takes more than the threshold as a + # whole. That is, the threshold is not checked against each individual + # query, but against the duration of the entire block. This approach is + # convenient for relations. + # + # The available_queries_for_explain thread variable collects the queries + # to be explained. If the value is nil, it means queries are not being + # currently collected. A false value indicates collecting is turned + # off. Otherwise it is an array of queries. + def logging_query_plan # :nodoc: + threshold = auto_explain_threshold_in_seconds + current = Thread.current + if threshold && current[:available_queries_for_explain].nil? + begin + queries = current[:available_queries_for_explain] = [] + start = Time.now + result = yield + logger.warn(exec_explain(queries)) if Time.now - start > threshold + result + ensure + current[:available_queries_for_explain] = nil + end + else + yield + end + end + + # Relation#explain needs to be able to collect the queries regardless of + # whether auto explain is enabled. This method serves that purpose. + def collecting_queries_for_explain # :nodoc: + current = Thread.current + original, current[:available_queries_for_explain] = current[:available_queries_for_explain], [] + return yield, current[:available_queries_for_explain] + ensure + # Note that the return value above does not depend on this assigment. + current[:available_queries_for_explain] = original + end + + # Makes the adapter execute EXPLAIN for the tuples of queries and bindings. + # Returns a formatted string ready to be logged. + def exec_explain(queries) # :nodoc: + queries && queries.map do |sql, bind| + [].tap do |msg| + msg << "EXPLAIN for: #{sql}" + unless bind.empty? + bind_msg = bind.map {|col, val| [col.name, val]}.inspect + msg.last << " #{bind_msg}" + end + msg << connection.explain(sql, bind) + end.join("\n") + end.join("\n") + end + + # Silences automatic EXPLAIN logging for the duration of the block. + # + # This has high priority, no EXPLAINs will be run even if downwards + # the threshold is set to 0. + # + # As the name of the method suggests this only applies to automatic + # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run. + def silence_auto_explain + current = Thread.current + original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false + yield + ensure + current[:available_queries_for_explain] = original + end + end +end diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb new file mode 100644 index 0000000000..fc76410499 --- /dev/null +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -0,0 +1,21 @@ +require 'active_support/notifications' + +module ActiveRecord + class ExplainSubscriber # :nodoc: + def call(*args) + if queries = Thread.current[:available_queries_for_explain] + payload = args.last + queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload) + end + end + + # SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on + # our own EXPLAINs now matter how loopingly beautiful that would be. + IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN) + def ignore_payload?(payload) + payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) + end + + ActiveSupport::Notifications.subscribe("sql.active_record", new) + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index cad9417216..cf315b687c 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -1,16 +1,8 @@ require 'erb' - -begin - require 'psych' -rescue LoadError -end - require 'yaml' require 'zlib' require 'active_support/dependencies' -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/logger' require 'active_support/ordered_hash' require 'active_record/fixtures/file' @@ -22,8 +14,6 @@ else end end -class FixturesFileNotFound < StandardError; end - module ActiveRecord # \Fixtures are a way of organizing data that you want to test against; in short, sample data. # @@ -391,10 +381,15 @@ module ActiveRecord @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.find_table_name(table_name) # :nodoc: + def self.default_fixture_model_name(fixture_name) # :nodoc: ActiveRecord::Base.pluralize_table_names ? - table_name.to_s.singularize.camelize : - table_name.to_s.camelize + fixture_name.singularize.camelize : + fixture_name.camelize + end + + def self.default_fixture_table_name(fixture_name) # :nodoc: + "#{ActiveRecord::Base.table_name_prefix}"\ + "#{fixture_name.tr('/', '_')}#{ActiveRecord::Base.table_name_suffix}".to_sym end def self.reset_cache @@ -443,10 +438,8 @@ module ActiveRecord self.all_loaded_fixtures = {} def self.create_fixtures(fixtures_directory, table_names, class_names = {}) - table_names = [table_names].flatten.map { |n| n.to_s } - table_names.each { |n| - class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') - } + table_names = Array(table_names).map(&:to_s) + class_names = class_names.stringify_keys # FIXME: Apparently JK uses this. connection = block_given? ? yield : ActiveRecord::Base.connection @@ -460,12 +453,12 @@ module ActiveRecord fixtures_map = {} fixture_files = files_to_read.map do |path| - table_name = path.tr '/', '_' + fixture_name = path - fixtures_map[path] = ActiveRecord::Fixtures.new( + fixtures_map[fixture_name] = new( # ActiveRecord::Fixtures.new connection, - table_name, - class_names[table_name.to_sym] || table_name.classify, + fixture_name, + class_names[fixture_name.to_s] || default_fixture_model_name(fixture_name), ::File.join(fixtures_directory, path)) end @@ -489,8 +482,8 @@ module ActiveRecord # Cap primary key sequences to max(pk). if connection.respond_to?(:reset_pk_sequence!) - table_names.each do |table_name| - connection.reset_pk_sequence!(table_name.tr('/', '_')) + fixture_files.each do |ff| + connection.reset_pk_sequence!(ff.table_name) end end end @@ -509,25 +502,27 @@ module ActiveRecord attr_reader :table_name, :name, :fixtures, :model_class - def initialize(connection, table_name, class_name, fixture_path) + def initialize(connection, fixture_name, class_name, fixture_path) @connection = connection - @table_name = table_name @fixture_path = fixture_path - @name = table_name # preserve fixture base name + @name = fixture_name @class_name = class_name @fixtures = ActiveSupport::OrderedHash.new - @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" # Should be an AR::Base type class if class_name.is_a?(Class) - @table_name = class_name.table_name - @connection = class_name.connection - @model_class = class_name + @model_class = class_name else - @model_class = class_name.constantize rescue nil + @model_class = class_name.constantize rescue nil end + @connection = model_class.connection if model_class && model_class.respond_to?(:connection) + + @table_name = ( model_class.respond_to?(:table_name) ? + model_class.table_name : + self.class.default_fixture_table_name(fixture_name) ) + read_fixture_files end @@ -562,7 +557,7 @@ module ActiveRecord rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash - if model_class && model_class < ActiveRecord::Base + if model_class && model_class < ActiveRecord::Model # fill in timestamp columns if they aren't specified and the model is set to record_timestamps if model_class.record_timestamps timestamp_column_names.each do |name| @@ -644,14 +639,6 @@ module ActiveRecord end def read_fixture_files - if ::File.file?(yaml_file_path) - read_yaml_fixture_files - else - raise FixturesFileNotFound, "Could not find #{yaml_file_path}" - end - end - - def read_yaml_fixture_files yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path] @@ -734,20 +721,37 @@ module ActiveRecord self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false - self.fixture_class_names = Hash.new do |h, table_name| - h[table_name] = ActiveRecord::Fixtures.find_table_name(table_name) + self.fixture_class_names = Hash.new do |h, fixture_name| + fixture_name = fixture_name.to_s + h[fixture_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_name) end end module ClassMethods + # Sets the model class for a fixture when the class name cannot be inferred from the fixture name. + # + # Examples: + # + # set_fixture_class :some_fixture => SomeModel, + # 'namespaced/fixture' => Another::Model + # + # The keys must be the fixture names, that coincide with the short paths to the fixture files. + #-- + # It is also possible to pass the class name instead of the class: + # set_fixture_class 'some_fixture' => 'SomeModel' + # I think this option is redundant, i propose to deprecate it. + # Isn't it easier to always pass the class itself? + # (2011-12-20 alexeymuranov) + #++ def set_fixture_class(class_names = {}) - self.fixture_class_names = self.fixture_class_names.merge(class_names) + self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) end def fixtures(*fixture_names) if fixture_names.first == :all - fixture_names = Dir["#{fixture_path}/**/*.{yml}"] - fixture_names.map! { |f| f[(fixture_path.size + 1)..-5] } + fixture_names = Dir["#{fixture_path}/**/*.yml"].map { |f| + File.basename f, '.yml' + } else fixture_names = fixture_names.flatten.map { |n| n.to_s } end @@ -778,12 +782,13 @@ module ActiveRecord end def setup_fixture_accessors(fixture_names = nil) - fixture_names = Array.wrap(fixture_names || fixture_table_names) + fixture_names = Array(fixture_names || fixture_table_names) methods = Module.new do fixture_names.each do |fixture_name| - fixture_name = fixture_name.to_s.tr('./', '_') + fixture_name = fixture_name.to_s + accessor_name = fixture_name.tr('/', '_').to_sym - define_method(fixture_name) do |*fixtures| + define_method(accessor_name) do |*fixtures| force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload @fixture_cache[fixture_name] ||= {} @@ -796,13 +801,13 @@ module ActiveRecord @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find end else - raise StandardError, "No fixture with name '#{fixture}' found for table '#{fixture_name}'" + raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'" end end instances.size == 1 ? instances.first : instances end - private fixture_name + private accessor_name end end include methods @@ -832,6 +837,7 @@ module ActiveRecord end @fixture_cache = {} + @fixture_connections = [] @@already_loaded_fixtures ||= {} # Load fixtures once and begin transaction. diff --git a/activerecord/lib/active_record/fixtures/file.rb b/activerecord/lib/active_record/fixtures/file.rb index 6bad36abb9..6547791144 100644 --- a/activerecord/lib/active_record/fixtures/file.rb +++ b/activerecord/lib/active_record/fixtures/file.rb @@ -1,8 +1,3 @@ -begin - require 'psych' -rescue LoadError -end - require 'erb' require 'yaml' diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb index b15b5a8133..d9777bb2f6 100644 --- a/activerecord/lib/active_record/identity_map.rb +++ b/activerecord/lib/active_record/identity_map.rb @@ -111,37 +111,18 @@ module ActiveRecord # model object. def reinit_with(coder) @attributes_cache = {} - dirty = @changed_attributes.keys - @attributes.update(coder['attributes'].except(*dirty)) + dirty = @changed_attributes.keys + attributes = self.class.initialize_attributes(coder['attributes'].except(*dirty)) + @attributes.update(attributes) @changed_attributes.update(coder['attributes'].slice(*dirty)) @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]} - set_serialized_attributes - run_callbacks :find self end class Middleware - class Body #:nodoc: - def initialize(target, original) - @target = target - @original = original - end - - def each(&block) - @target.each(&block) - end - - def close - @target.close if @target.respond_to?(:close) - ensure - IdentityMap.enabled = @original - IdentityMap.clear - end - end - def initialize(app) @app = app end @@ -149,8 +130,14 @@ module ActiveRecord def call(env) enabled = IdentityMap.enabled IdentityMap.enabled = true - status, headers, body = @app.call(env) - [status, headers, Body.new(body, enabled)] + + response = @app.call(env) + response[2] = Rack::BodyProxy.new(response[2]) do + IdentityMap.enabled = enabled + IdentityMap.clear + end + + response end end end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb new file mode 100644 index 0000000000..eaa7deac5a --- /dev/null +++ b/activerecord/lib/active_record/inheritance.rb @@ -0,0 +1,182 @@ +require 'active_support/concern' + +module ActiveRecord + module Inheritance + extend ActiveSupport::Concern + + included do + # Determine whether to store the full constant name including namespace when using STI + config_attribute :store_full_sti_class + self.store_full_sti_class = true + end + + module ClassMethods + # True if this isn't a concrete subclass needing a STI type condition. + def descends_from_active_record? + sup = active_record_super + + if sup.abstract_class? + sup.descends_from_active_record? + elsif self == Base + false + else + [Base, Model].include?(sup) || !columns_hash.include?(inheritance_column) + end + end + + def finder_needs_type_condition? #:nodoc: + # This is like this because benchmarking justifies the strange :false stuff + :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) + end + + def symbolized_base_class + @symbolized_base_class ||= base_class.to_s.to_sym + end + + def symbolized_sti_name + @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class + end + + # Returns the base AR subclass that this class descends from. If A + # extends AR::Base, A.base_class will return A. If B descends from A + # through some arbitrarily deep hierarchy, B.base_class will return A. + # + # If B < A and C < B and if A is an abstract_class then both B.base_class + # and C.base_class would return B as the answer since A is an abstract_class. + def base_class + class_of_active_record_descendant(self) + end + + # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). + attr_accessor :abstract_class + + # Returns whether this class is an abstract class or not. + def abstract_class? + defined?(@abstract_class) && @abstract_class == true + end + + def sti_name + store_full_sti_class ? name : name.demodulize + end + + # Finder methods must instantiate through this method to work with the + # single-table inheritance model that makes it possible to create + # objects of different types from the same table. + def instantiate(record) + sti_class = find_sti_class(record[inheritance_column]) + record_id = sti_class.primary_key && record[sti_class.primary_key] + + if ActiveRecord::IdentityMap.enabled? && record_id + if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? + record_id = record_id.to_i + end + if instance = IdentityMap.get(sti_class, record_id) + instance.reinit_with('attributes' => record) + else + instance = sti_class.allocate.init_with('attributes' => record) + IdentityMap.add(instance) + end + else + instance = sti_class.allocate.init_with('attributes' => record) + end + + instance + end + + # For internal use. + # + # If this class includes ActiveRecord::Model then it won't have a + # superclass. So this provides a way to get to the 'root' (ActiveRecord::Model). + def active_record_super #:nodoc: + superclass < Model ? superclass : Model + end + + protected + + # Returns the class descending directly from ActiveRecord::Base or an + # abstract class, if any, in the inheritance hierarchy. + def class_of_active_record_descendant(klass) + unless klass < Model + raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" + end + + sup = klass.active_record_super + if [Base, Model].include?(klass) || [Base, Model].include?(sup) || sup.abstract_class? + klass + else + class_of_active_record_descendant(sup) + end + end + + # Returns the class type of the record using the current module as a prefix. So descendants of + # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. + def compute_type(type_name) + if type_name.match(/^::/) + # If the type is prefixed with a scope operator then we assume that + # the type_name is an absolute reference. + ActiveSupport::Dependencies.constantize(type_name) + else + # Build a list of candidates to search for + candidates = [] + name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } + candidates << type_name + + candidates.each do |candidate| + begin + constant = ActiveSupport::Dependencies.constantize(candidate) + return constant if candidate == constant.to_s + rescue NameError => e + # We don't want to swallow NoMethodError < NameError errors + raise e unless e.instance_of?(NameError) + end + end + + raise NameError, "uninitialized constant #{candidates.first}" + end + end + + private + + def find_sti_class(type_name) + if type_name.blank? || !columns_hash.include?(inheritance_column) + self + else + begin + if store_full_sti_class + ActiveSupport::Dependencies.constantize(type_name) + else + compute_type(type_name) + end + rescue NameError + raise SubclassNotFound, + "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + + "Please rename this column if you didn't intend it to be used for storing the inheritance class " + + "or overwrite #{name}.inheritance_column to use another column for that information." + end + end + end + + def type_condition(table = arel_table) + sti_column = table[inheritance_column.to_sym] + sti_names = ([self] + descendants).map { |model| model.sti_name } + + sti_column.in(sti_names) + end + end + + private + + # Sets the attribute used for single table inheritance to this class name if this is not the + # ActiveRecord::Base descendant. + # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to + # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. + # No such attribute would be set for objects of the Message class in that example. + def ensure_proper_type + klass = self.class + if klass.finder_needs_type_condition? + write_attribute(klass.inheritance_column, klass.sti_name) + end + end + end +end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb new file mode 100644 index 0000000000..2c42f4cca5 --- /dev/null +++ b/activerecord/lib/active_record/integration.rb @@ -0,0 +1,49 @@ +module ActiveRecord + module Integration + # Returns a String, which Action Pack uses for constructing an URL to this + # object. The default implementation returns this record's id as a String, + # or nil if this record's unsaved. + # + # For example, suppose that you have a User model, and that you have a + # <tt>resources :users</tt> route. Normally, +user_path+ will + # construct a path with the user object's 'id' in it: + # + # user = User.find_by_name('Phusion') + # user_path(user) # => "/users/1" + # + # You can override +to_param+ in your model to make +user_path+ construct + # a path using the user's name instead of the user's id: + # + # class User < ActiveRecord::Base + # def to_param # overridden + # name + # end + # end + # + # user = User.find_by_name('Phusion') + # user_path(user) # => "/users/Phusion" + def to_param + # We can't use alias_method here, because method 'id' optimizes itself on the fly. + id && id.to_s # Be sure to stringify the id for routes + end + + # Returns a cache key that can be used to identify this record. + # + # ==== Examples + # + # Product.new.cache_key # => "products/new" + # Product.find(5).cache_key # => "products/5" (updated_at not available) + # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) + def cache_key + case + when new_record? + "#{self.class.model_name.cache_key}/new" + when timestamp = self[:updated_at] + timestamp = timestamp.utc.to_s(:number) + "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" + else + "#{self.class.model_name.cache_key}/#{id}" + end + end + end +end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 1a29ded787..e643c0d437 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -49,12 +49,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - cattr_accessor :lock_optimistically, :instance_writer => false + config_attribute :lock_optimistically, :global => true self.lock_optimistically = true - - class << self - alias_method :locking_column=, :set_locking_column - end end def locking_enabled? #:nodoc: @@ -68,21 +64,6 @@ module ActiveRecord send(lock_col + '=', previous_lock_value + 1) end - def attributes_from_column_definition - result = super - - # If the locking column has no default value set, - # start the lock version at zero. Note we can't use - # <tt>locking_enabled?</tt> at this point as - # <tt>@attributes</tt> may not have been initialized yet. - - if result.key?(self.class.locking_column) && lock_optimistically - result[self.class.locking_column] ||= 0 - end - - result - end - def update(attribute_names = @attributes.keys) #:nodoc: return super unless locking_enabled? return 0 if attribute_names.empty? @@ -149,14 +130,15 @@ module ActiveRecord end # Set the column to use for optimistic locking. Defaults to +lock_version+. - def set_locking_column(value = nil, &block) - define_attr_method :locking_column, value, &block - value + def locking_column=(value) + @original_locking_column = @locking_column if defined?(@locking_column) + @locking_column = value.to_s end # The version column used for optimistic locking. Defaults to +lock_version+. def locking_column - reset_locking_column + reset_locking_column unless defined?(@locking_column) + @locking_column end # Quote the column name used for optimistic locking. @@ -166,7 +148,7 @@ module ActiveRecord # Reset the column used for optimistic locking back to the +lock_version+ default. def reset_locking_column - set_locking_column DEFAULT_LOCKING_COLUMN + self.locking_column = DEFAULT_LOCKING_COLUMN end # Make sure the lock version column gets updated when counters are @@ -175,6 +157,18 @@ module ActiveRecord counters = counters.merge(locking_column => 1) if locking_enabled? super end + + # If the locking column has no default value set, + # start the lock version at zero. Note we can't use + # <tt>locking_enabled?</tt> at this point as + # <tt>@attributes</tt> may not have been initialized yet. + def initialize_attributes(attributes) #:nodoc: + if attributes.key?(locking_column) && lock_optimistically + attributes[locking_column] ||= 0 + end + + attributes + end end end end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 66994e4797..58af92f0b1 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -38,6 +38,18 @@ module ActiveRecord # account2.save! # end # + # You can start a transaction and acquire the lock in one go by calling + # <tt>with_lock</tt> with a block. The block is called from within + # a transaction, the object is already locked. Example: + # + # account = Account.first + # account.with_lock do + # # This block is called within a transaction, + # # account is already locked. + # account.balance -= 100 + # account.save! + # end + # # Database-specific information on row locking: # MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE @@ -50,6 +62,16 @@ module ActiveRecord reload(:lock => lock) if persisted? self end + + # Wraps the passed block in a transaction, locking the object + # before yielding. You pass can the SQL locking clause + # as argument (see <tt>lock!</tt>). + def with_lock(lock = true) + transaction do + lock!(lock) + yield + end + end end end end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 3a015ee8c2..a25f2c7bca 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -26,9 +26,9 @@ module ActiveRecord return if 'SCHEMA' == payload[:name] - name = '%s (%.1fms)' % [payload[:name], event.duration] - sql = payload[:sql].squeeze(' ') - binds = nil + name = '%s (%.1fms)' % [payload[:name], event.duration] + sql = payload[:sql].squeeze(' ') + binds = nil unless (payload[:binds] || []).empty? binds = " " + payload[:binds].map { |col,v| diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index d70c7d1d34..2a9139749d 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,6 +1,8 @@ require "active_support/core_ext/module/delegation" require "active_support/core_ext/class/attribute_accessors" -require "active_support/core_ext/array/wrap" +require 'active_support/deprecation' +require 'active_record/schema_migration' +require 'set' module ActiveRecord # Exception that can be raised to stop migrations from going backwards. @@ -302,7 +304,7 @@ module ActiveRecord # # class TenderloveMigration < ActiveRecord::Migration # def change - # create_table(:horses) do + # create_table(:horses) do |t| # t.column :content, :text # t.column :remind_at, :datetime # end @@ -341,16 +343,28 @@ module ActiveRecord attr_accessor :name, :version - def initialize - @name = self.class.name - @version = nil + def initialize(name = self.class.name, version = nil) + @name = name + @version = version @connection = nil + @reverting = false end # instantiate the delegate object after initialize is defined self.verbose = true self.delegate = new + def revert + @reverting = true + yield + ensure + @reverting = false + end + + def reverting? + @reverting + end + def up self.class.delegate = self return unless self.class.respond_to?(:up) @@ -384,9 +398,11 @@ module ActiveRecord end @connection = conn time = Benchmark.measure { - recorder.inverse.each do |cmd, args| - send(cmd, *args) - end + self.revert { + recorder.inverse.each do |cmd, args| + send(cmd, *args) + end + } } else time = Benchmark.measure { change } @@ -441,8 +457,11 @@ module ActiveRecord 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) + unless reverting? + unless arguments.empty? || method == :execute + arguments[0] = Migrator.proper_table_name(arguments.first) + arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table + end end return super unless connection.respond_to?(method) connection.send(method, *arguments, &block) @@ -456,26 +475,28 @@ module ActiveRecord destination_migrations = ActiveRecord::Migrator.migrations(destination) last = destination_migrations.last - sources.each do |name, path| + sources.each do |scope, path| source_migrations = ActiveRecord::Migrator.migrations(path) source_migrations.each do |migration| source = File.read(migration.filename) - source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}" + source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}" 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] + if options[:on_skip] && duplicate.scope != scope.to_s + options[:on_skip].call(scope, migration) + end next end migration.version = next_migration_number(last ? last.version + 1 : 0).to_i - new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb") + new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.#{scope}.rb") old_path, migration.filename = migration.filename, new_path last = migration - FileUtils.cp(old_path, migration.filename) + File.open(migration.filename, "w") { |f| f.write source } copied << migration - options[:on_copy].call(name, migration, old_path) if options[:on_copy] + options[:on_copy].call(scope, migration, old_path) if options[:on_copy] destination_migrations << migration end end @@ -494,9 +515,9 @@ module ActiveRecord # MigrationProxy is used to defer loading of the actual migration classes # until they are needed - class MigrationProxy < Struct.new(:name, :version, :filename) + class MigrationProxy < Struct.new(:name, :version, :filename, :scope) - def initialize(name, version, filename) + def initialize(name, version, filename, scope) super @migration = nil end @@ -505,7 +526,7 @@ module ActiveRecord File.basename(filename) end - delegate :migrate, :announce, :write, :to=>:migration + delegate :migrate, :announce, :write, :to => :migration private @@ -525,16 +546,16 @@ module ActiveRecord attr_writer :migrations_paths alias :migrations_path= :migrations_paths= - def migrate(migrations_paths, target_version = nil) + def migrate(migrations_paths, target_version = nil, &block) case - when target_version.nil? - up(migrations_paths, target_version) - when current_version == 0 && target_version == 0 - [] - when current_version > target_version - down(migrations_paths, target_version) - else - up(migrations_paths, target_version) + when target_version.nil? + up(migrations_paths, target_version, &block) + when current_version == 0 && target_version == 0 + [] + when current_version > target_version + down(migrations_paths, target_version, &block) + else + up(migrations_paths, target_version, &block) end end @@ -547,24 +568,33 @@ module ActiveRecord end def up(migrations_paths, target_version = nil) - self.new(:up, migrations_paths, target_version).migrate + migrations = migrations(migrations_paths) + migrations.select! { |m| yield m } if block_given? + + self.new(:up, migrations, target_version).migrate end - def down(migrations_paths, target_version = nil) - self.new(:down, migrations_paths, target_version).migrate + def down(migrations_paths, target_version = nil, &block) + migrations = migrations(migrations_paths) + migrations.select! { |m| yield m } if block_given? + + self.new(:down, migrations, target_version).migrate end def run(direction, migrations_paths, target_version) - self.new(direction, migrations_paths, target_version).run + self.new(direction, migrations(migrations_paths), target_version).run + end + + def open(migrations_paths) + self.new(:up, migrations(migrations_paths), nil) end def schema_migrations_table_name - Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix + SchemaMigration.table_name end def get_all_versions - table = Arel::Table.new(schema_migrations_table_name) - Base.connection.select_values(table.project(table['version'])).map{ |v| v.to_i }.sort + SchemaMigration.all.map { |x| x.version.to_i }.sort end def current_version @@ -584,7 +614,7 @@ module ActiveRecord def migrations_paths @migrations_paths ||= ['db/migrate'] # just to not break things if someone uses: migration_path = some_string - Array.wrap(@migrations_paths) + Array(@migrations_paths) end def migrations_path @@ -592,25 +622,18 @@ module ActiveRecord end def migrations(paths) - paths = Array.wrap(paths) - - files = Dir[*paths.map { |p| "#{p}/[0-9]*_*.rb" }] + paths = Array(paths) - seen = Hash.new false + files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }] migrations = files.map do |file| - version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first + version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?.rb/).first raise IllegalMigrationNameError.new(file) unless version version = version.to_i name = name.camelize - raise DuplicateMigrationVersionError.new(version) if seen[version] - raise DuplicateMigrationNameError.new(name) if seen[name] - - seen[version] = seen[name] = true - - MigrationProxy.new(name, version, file) + MigrationProxy.new(name, version, file, scope) end migrations.sort_by(&:version) @@ -619,7 +642,7 @@ module ActiveRecord private def move(direction, migrations_paths, steps) - migrator = self.new(direction, migrations_paths) + migrator = self.new(direction, migrations(migrations_paths)) start_index = migrator.migrations.index(migrator.current_migration) if start_index @@ -630,19 +653,33 @@ module ActiveRecord end end - def initialize(direction, migrations_paths, target_version = nil) + def initialize(direction, migrations, target_version = nil) raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations? - Base.connection.initialize_schema_migrations_table - @direction, @migrations_paths, @target_version = direction, migrations_paths, target_version + + @direction = direction + @target_version = target_version + @migrated_versions = nil + + if Array(migrations).grep(String).empty? + @migrations = migrations + else + ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations" + @migrations = self.class.migrations(migrations) + end + + validate(@migrations) + + ActiveRecord::SchemaMigration.create_table end def current_version - migrated.last || 0 + migrated.sort.last || 0 end def current_migration migrations.detect { |m| m.version == current_version } end + alias :current :current_migration def run target = migrations.detect { |m| m.version == @target_version } @@ -654,96 +691,108 @@ module ActiveRecord end def migrate - current = migrations.detect { |m| m.version == current_version } - target = migrations.detect { |m| m.version == @target_version } - - if target.nil? && @target_version && @target_version > 0 + if !target && @target_version && @target_version > 0 raise UnknownMigrationVersionError.new(@target_version) end - start = up? ? 0 : (migrations.index(current) || 0) - finish = migrations.index(target) || migrations.size - 1 - runnable = migrations[start..finish] + running = runnable - # skip the last migration if we're headed down, but not ALL the way down - runnable.pop if down? && target + if block_given? + ActiveSupport::Deprecation.warn(<<-eomsg) +block argument to migrate is deprecated, please filter migrations before constructing the migrator + eomsg + running.select! { |m| yield m } + end - ran = [] - runnable.each do |migration| + running.each do |migration| Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger - seen = migrated.include?(migration.version.to_i) - - # On our way up, we skip migrating the ones we've already migrated - next if up? && seen - - # On our way down, we skip reverting the ones we've never migrated - if down? && !seen - migration.announce 'never migrated, skipping'; migration.write - next - end - begin ddl_transaction do migration.migrate(@direction) record_version_state_after_migrating(migration.version) end - ran << migration rescue => e canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : "" raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace end end - ran end - def migrations - @migrations ||= begin - migrations = self.class.migrations(@migrations_paths) - down? ? migrations.reverse : migrations + def runnable + runnable = migrations[start..finish] + if up? + runnable.reject { |m| ran?(m) } + else + # skip the last migration if we're headed down, but not ALL the way down + runnable.pop if target + runnable.find_all { |m| ran?(m) } end end + def migrations + down? ? @migrations.reverse : @migrations.sort_by(&:version) + end + def pending_migrations already_migrated = migrated - migrations.reject { |m| already_migrated.include?(m.version.to_i) } + migrations.reject { |m| already_migrated.include?(m.version) } end def migrated - @migrated_versions ||= self.class.get_all_versions + @migrated_versions ||= Set.new(self.class.get_all_versions) end private - def record_version_state_after_migrating(version) - table = Arel::Table.new(self.class.schema_migrations_table_name) - - @migrated_versions ||= [] - if down? - @migrated_versions.delete(version) - stmt = table.where(table["version"].eq(version.to_s)).compile_delete - Base.connection.delete stmt - else - @migrated_versions.push(version).sort! - stmt = table.compile_insert table["version"] => version.to_s - Base.connection.insert stmt - end - end + def ran?(migration) + migrated.include?(migration.version.to_i) + end - def up? - @direction == :up - end + def target + migrations.detect { |m| m.version == @target_version } + end + + def finish + migrations.index(target) || migrations.size - 1 + end - def down? - @direction == :down + def start + up? ? 0 : (migrations.index(current) || 0) + end + + def validate(migrations) + name ,= migrations.group_by(&:name).find { |_,v| v.length > 1 } + raise DuplicateMigrationNameError.new(name) if name + + version ,= migrations.group_by(&:version).find { |_,v| v.length > 1 } + raise DuplicateMigrationVersionError.new(version) if version + end + + def record_version_state_after_migrating(version) + if down? + migrated.delete(version) + ActiveRecord::SchemaMigration.where(:version => version.to_s).delete_all + else + migrated << version + ActiveRecord::SchemaMigration.create!(:version => version.to_s) end + end - # Wrap the migration in a transaction only if supported by the adapter. - def ddl_transaction(&block) - if Base.connection.supports_ddl_transactions? - Base.transaction { block.call } - else - block.call - end + def up? + @direction == :up + end + + def down? + @direction == :down + end + + # Wrap the migration in a transaction only if supported by the adapter. + def ddl_transaction + if Base.connection.supports_ddl_transactions? + Base.transaction { yield } + else + yield end + end end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index ffee5a081a..4e27293cb4 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -59,7 +59,7 @@ module ActiveRecord private def invert_create_table(args) - [:drop_table, args] + [:drop_table, [args.first]] end def invert_rename_table(args) diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb new file mode 100644 index 0000000000..86de5ab2fa --- /dev/null +++ b/activerecord/lib/active_record/model.rb @@ -0,0 +1,109 @@ +require 'active_support/deprecation' + +module ActiveRecord + # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record persistence. + # This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. Example: + # + # class Post + # include ActiveRecord::Model + # end + # + module Model + module ClassMethods #:nodoc: + include ActiveSupport::Callbacks::ClassMethods + include ActiveModel::Naming + include QueryCache::ClassMethods + include ActiveSupport::Benchmarkable + include ActiveSupport::DescendantsTracker + + include Querying + include Translation + include DynamicMatchers + include CounterCache + include Explain + include ConnectionHandling + end + + def self.included(base) + return if base.singleton_class < ClassMethods + + base.class_eval do + extend ClassMethods + Callbacks::Register.setup(self) + initialize_generated_modules unless self == Base + end + end + + extend ActiveModel::Configuration + extend ActiveModel::Callbacks + extend ActiveModel::MassAssignmentSecurity::ClassMethods + extend ActiveModel::AttributeMethods::ClassMethods + extend Callbacks::Register + extend Explain + extend ConnectionHandling + + def self.extend(*modules) + ClassMethods.send(:include, *modules) + end + + include Persistence + include ReadonlyAttributes + include ModelSchema + include Inheritance + include Scoping + include Sanitization + include Integration + include AttributeAssignment + include ActiveModel::Conversion + include Validations + include Locking::Optimistic, Locking::Pessimistic + include AttributeMethods + include Callbacks, ActiveModel::Observing, Timestamp + include Associations + include IdentityMap + include ActiveModel::SecurePassword + include AutosaveAssociation, NestedAttributes + include Aggregations, Transactions, Reflection, Serialization, Store + include Core + + class << self + def arel_engine + self + end + + def abstract_class? + false + end + + def inheritance_column + 'type' + end + end + + module DeprecationProxy #:nodoc: + class << self + instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ } + + def method_missing(name, *args, &block) + if Model.respond_to?(name) + Model.send(name, *args, &block) + else + ActiveSupport::Deprecation.warn( + "The object passed to the active_record load hook was previously ActiveRecord::Base " \ + "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ + "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ + "a module rather than a class. (Model is included in Base, so anything added to Model " \ + "will be available on Base as well.)" + ) + Base.send(name, *args, &block) + end + end + + alias send method_missing + end + end + end + + # Load Base at this point, because the active_record load hook is run in that file. + Base +end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb new file mode 100644 index 0000000000..261f6fa974 --- /dev/null +++ b/activerecord/lib/active_record/model_schema.rb @@ -0,0 +1,307 @@ +require 'active_support/concern' +require 'active_support/core_ext/class/attribute_accessors' + +module ActiveRecord + module ModelSchema + extend ActiveSupport::Concern + + included do + ## + # :singleton-method: + # Accessor for the prefix type that will be prepended to every primary key column name. + # The options are :table_name and :table_name_with_underscore. If the first is specified, + # the Product class will look for "productid" instead of "id" as the primary column. If the + # latter is specified, the Product class will look for "product_id" instead of "id". Remember + # that this is a global setting for all Active Records. + config_attribute :primary_key_prefix_type, :global => true + + ## + # :singleton-method: + # Accessor for the name of the prefix string to prepend to every table name. So if set + # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people", + # etc. This is a convenient way of creating a namespace for tables in a shared database. + # By default, the prefix is the empty string. + # + # If you are organising your models within modules you can add a prefix to the models within + # a namespace by defining a singleton method in the parent module called table_name_prefix which + # returns your chosen prefix. + config_attribute :table_name_prefix + self.table_name_prefix = "" + + ## + # :singleton-method: + # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", + # "people_basecamp"). By default, the suffix is the empty string. + config_attribute :table_name_suffix + self.table_name_suffix = "" + + ## + # :singleton-method: + # Indicates whether table names should be the pluralized versions of the corresponding class names. + # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. + # See table_name for the full rules on table/class naming. This is true, by default. + config_attribute :pluralize_table_names + self.pluralize_table_names = true + end + + module ClassMethods + # Guesses the table name (in forced lower-case) based on the name of the class in the + # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy + # looks like: Reply < Message < ActiveRecord::Base, then Message is used + # to guess the table name even when called on Reply. The rules used to do the guess + # are handled by the Inflector class in Active Support, which knows almost all common + # English inflections. You can add new inflections in config/initializers/inflections.rb. + # + # Nested classes are given table names prefixed by the singular form of + # the parent's table name. Enclosing modules are not considered. + # + # ==== Examples + # + # class Invoice < ActiveRecord::Base + # end + # + # file class table_name + # invoice.rb Invoice invoices + # + # class Invoice < ActiveRecord::Base + # class Lineitem < ActiveRecord::Base + # end + # end + # + # file class table_name + # invoice.rb Invoice::Lineitem invoice_lineitems + # + # module Invoice + # class Lineitem < ActiveRecord::Base + # end + # end + # + # file class table_name + # invoice/lineitem.rb Invoice::Lineitem lineitems + # + # Additionally, the class-level +table_name_prefix+ is prepended and the + # +table_name_suffix+ is appended. So if you have "myapp_" as a prefix, + # the table name guess for an Invoice class becomes "myapp_invoices". + # Invoice::Lineitem becomes "myapp_invoice_lineitems". + # + # You can also set your own table name explicitly: + # + # class Mouse < ActiveRecord::Base + # self.table_name = "mice" + # end + # + # Alternatively, you can override the table_name method to define your + # own computation. (Possibly using <tt>super</tt> to manipulate the default + # table name.) Example: + # + # class Post < ActiveRecord::Base + # def self.table_name + # "special_" + super + # end + # end + # Post.table_name # => "special_posts" + def table_name + reset_table_name unless defined?(@table_name) + @table_name + end + + # Sets the table name explicitly. Example: + # + # class Project < ActiveRecord::Base + # self.table_name = "project" + # end + # + # You can also just define your own <tt>self.table_name</tt> method; see + # the documentation for ActiveRecord::Base#table_name. + def table_name=(value) + @original_table_name = @table_name if defined?(@table_name) + @table_name = value + @quoted_table_name = nil + @arel_table = nil + @relation = Relation.new(self, arel_table) + end + + # Returns a quoted version of the table name, used to construct SQL statements. + def quoted_table_name + @quoted_table_name ||= connection.quote_table_name(table_name) + end + + # Computes the table name, (re)sets it internally, and returns it. + def reset_table_name #:nodoc: + if abstract_class? + self.table_name = if active_record_super == Base || active_record_super.abstract_class? + nil + else + active_record_super.table_name + end + elsif active_record_super.abstract_class? + self.table_name = active_record_super.table_name || compute_table_name + else + self.table_name = compute_table_name + end + end + + def full_table_name_prefix #:nodoc: + (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix + end + + # The name of the column containing the object's class when Single Table Inheritance is used + def inheritance_column + (@inheritance_column ||= nil) || active_record_super.inheritance_column + end + + # Sets the value of inheritance_column + def inheritance_column=(value) + @original_inheritance_column = inheritance_column + @inheritance_column = value.to_s + end + + def sequence_name + if base_class == self + @sequence_name ||= reset_sequence_name + else + (@sequence_name ||= nil) || base_class.sequence_name + end + end + + def reset_sequence_name #:nodoc: + self.sequence_name = connection.default_sequence_name(table_name, primary_key) + end + + # Sets the name of the sequence to use when generating ids to the given + # value, or (if the value is nil or false) to the value returned by the + # given block. This is required for Oracle and is useful for any + # database which relies on sequences for primary key generation. + # + # If a sequence name is not explicitly set when using Oracle or Firebird, + # it will default to the commonly used pattern of: #{table_name}_seq + # + # If a sequence name is not explicitly set when using PostgreSQL, it + # will discover the sequence corresponding to your primary key for you. + # + # class Project < ActiveRecord::Base + # self.sequence_name = "projectseq" # default would have been "project_seq" + # end + def sequence_name=(value) + @original_sequence_name = @sequence_name if defined?(@sequence_name) + @sequence_name = value.to_s + end + + # Indicates whether the table associated with this class exists + def table_exists? + connection.schema_cache.table_exists?(table_name) + end + + # Returns an array of column objects for the table associated with this class. + def columns + @columns ||= connection.schema_cache.columns[table_name].map do |col| + col = col.dup + col.primary = (col.name == primary_key) + col + end + end + + # Returns a hash of column objects for the table associated with this class. + def columns_hash + @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + end + + # Returns a hash where the keys are column names and the values are + # default values when instantiating the AR object for this table. + def column_defaults + @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }] + end + + # Returns an array of column names as strings. + def column_names + @column_names ||= columns.map { |column| column.name } + end + + # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", + # and columns used for single table inheritance have been removed. + def content_columns + @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } + end + + # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key + # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute + # is available. + def column_methods_hash #:nodoc: + @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr| + attr_name = attr.to_s + methods[attr.to_sym] = attr_name + methods["#{attr}=".to_sym] = attr_name + methods["#{attr}?".to_sym] = attr_name + methods["#{attr}_before_type_cast".to_sym] = attr_name + methods + end + end + + # Resets all the cached information about columns, which will cause them + # to be reloaded on the next request. + # + # The most common usage pattern for this method is probably in a migration, + # when just after creating a table you want to populate it with some default + # values, eg: + # + # class CreateJobLevels < ActiveRecord::Migration + # def up + # create_table :job_levels do |t| + # t.integer :id + # t.string :name + # + # t.timestamps + # end + # + # JobLevel.reset_column_information + # %w{assistant executive manager director}.each do |type| + # JobLevel.create(:name => type) + # end + # end + # + # def down + # drop_table :job_levels + # end + # end + def reset_column_information + connection.clear_cache! + undefine_attribute_methods + connection.schema_cache.clear_table_cache!(table_name) if table_exists? + + @column_names = @content_columns = @column_defaults = @columns = @columns_hash = nil + @dynamic_methods_hash = @inheritance_column = nil + @arel_engine = @relation = nil + end + + def clear_cache! # :nodoc: + connection.schema_cache.clear! + end + + private + + # Guesses the table name, but does not decorate it with prefix and suffix information. + def undecorated_table_name(class_name = base_class.name) + table_name = class_name.to_s.demodulize.underscore + table_name = table_name.pluralize if pluralize_table_names + table_name + end + + # Computes and returns a table name according to default conventions. + def compute_table_name + base = base_class + if self == base + # Nested classes are prefixed with singular parent table name. + if parent < ActiveRecord::Model && !parent.abstract_class? + contained = parent.table_name + contained = contained.singularize if parent.pluralize_table_names + contained += '_' + end + "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" + else + # STI subclasses always use their superclass' table. + base.table_name + end + end + end + end +end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb deleted file mode 100644 index 0313abe456..0000000000 --- a/activerecord/lib/active_record/named_scope.rb +++ /dev/null @@ -1,200 +0,0 @@ -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 - - module ClassMethods - # Returns an anonymous \scope. - # - # posts = Post.scoped - # posts.size # Fires "select count(*) from posts" and returns the count - # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects - # - # fruits = Fruit.scoped - # fruits = fruits.where(:color => 'red') if options[:red_only] - # fruits = fruits.limit(10) if limited? - # - # Anonymous \scopes tend to be useful when procedurally generating complex - # queries, where passing intermediate values (\scopes) around as first-class - # objects is convenient. - # - # You can define a \scope that applies to all finders using - # ActiveRecord::Base.default_scope. - def scoped(options = nil) - if options - scoped.apply_finder_options(options) - else - if current_scope - current_scope.clone - else - scope = relation.clone - scope.default_scoped = true - scope - end - end - end - - ## - # Collects attributes from scopes that should be applied when creating - # an AR instance for the particular class this is called on. - def scope_attributes # :nodoc: - if current_scope - current_scope.scope_for_create - else - scope = relation.clone - scope.default_scoped = true - scope.scope_for_create - end - end - - ## - # Are there default attributes associated with this scope? - def scope_attributes? # :nodoc: - current_scope || default_scopes.any? - 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>. - # - # class Shirt < ActiveRecord::Base - # scope :red, where(:color => 'red') - # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) - # end - # - # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, - # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>. - # - # Note that this is simply 'syntactic sugar' for defining an actual class method: - # - # class Shirt < ActiveRecord::Base - # def self.red - # where(:color => 'red') - # end - # end - # - # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it - # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, - # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. - # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; - # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> - # all behave as if Shirt.red really was an Array. - # - # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce - # all shirts that are both red and dry clean only. - # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> - # returns the number of garments for which these criteria obtain. Similarly with - # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. - # - # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which - # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If, - # - # class Person < ActiveRecord::Base - # has_many :shirts - # end - # - # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean - # only shirts. - # - # Named \scopes can also be procedural: - # - # class Shirt < ActiveRecord::Base - # scope :colored, lambda { |color| where(:color => color) } - # end - # - # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts. - # - # On Ruby 1.9 you can use the 'stabby lambda' syntax: - # - # scope :colored, ->(color) { where(:color => color) } - # - # Note that scopes defined with \scope will be evaluated when they are defined, rather than - # when they are used. For example, the following would be incorrect: - # - # class Post < ActiveRecord::Base - # scope :recent, where('published_at >= ?', Time.now - 1.week) - # end - # - # The example above would be 'frozen' to the <tt>Time.now</tt> value when the <tt>Post</tt> - # class was defined, and so the resultant SQL query would always be the same. The correct - # way to do this would be via a lambda, which will re-evaluate the scope each time - # it is called: - # - # class Post < ActiveRecord::Base - # scope :recent, lambda { where('published_at >= ?', Time.now - 1.week) } - # end - # - # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations: - # - # class Shirt < ActiveRecord::Base - # scope :red, where(:color => 'red') do - # def dom_id - # 'red_shirts' - # end - # end - # end - # - # Scopes can also be used while creating/building a record. - # - # class Article < ActiveRecord::Base - # scope :published, where(:published => true) - # end - # - # Article.published.new.published # => true - # Article.published.create.published # => true - # - # Class methods on your model are automatically available - # on scopes. Assuming the following setup: - # - # class Article < ActiveRecord::Base - # scope :published, where(:published => true) - # scope :featured, where(:featured => true) - # - # def self.latest_article - # order('published_at desc').first - # end - # - # def self.titles - # map(&:title) - # end - # - # end - # - # We are able to call the methods like this: - # - # Article.published.featured.latest_article - # Article.featured.titles - - def scope(name, scope_options = {}) - name = name.to_sym - valid_scope_name?(name) - extension = Module.new(&Proc.new) if block_given? - - scope_proc = lambda do |*args| - options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options - options = scoped.apply_finder_options(options) if options.is_a?(Hash) - - relation = scoped.merge(options) - - extension ? relation.extending(extension) : relation - end - - singleton_class.send(:redefine_method, name, &scope_proc) - end - - protected - - def valid_scope_name?(name) - if respond_to?(name, true) - logger.warn "Creating scope :#{name}. " \ - "Overwriting existing method #{self.name}.#{name}." - end - end - end - end -end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index d2065d701f..9e21039c4f 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -12,7 +12,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :nested_attributes_options, :instance_writer => false + config_attribute :nested_attributes_options self.nested_attributes_options = {} end @@ -382,7 +382,7 @@ module ActiveRecord if attributes_collection.is_a? Hash keys = attributes_collection.keys attributes_collection = if keys.include?('id') || keys.include?(:id) - Array.wrap(attributes_collection) + [attributes_collection] else attributes_collection.values end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index f047a1d9fa..09ee2ba61d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,6 +1,53 @@ +require 'active_support/concern' + module ActiveRecord # = Active Record Persistence module Persistence + extend ActiveSupport::Concern + + module ClassMethods + # Creates an object (or multiple objects) and saves it to the database, if validations pass. + # The resulting object is returned whether the object was saved successfully to the database or not. + # + # The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the + # attributes on the objects that are to be created. + # + # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options + # in the +options+ parameter. + # + # ==== Examples + # # Create a single new object + # User.create(:first_name => 'Jamie') + # + # # Create a single new object using the :admin mass-assignment security role + # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) + # + # # Create a single new object bypassing mass-assignment security + # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) + # + # # Create an Array of new objects + # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) + # + # # Create a single object and pass it into a block to set other attributes. + # User.create(:first_name => 'Jamie') do |u| + # u.is_admin = false + # end + # + # # Creating an Array of new objects using a block, where the block is executed for each object: + # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u| + # u.is_admin = false + # end + def create(attributes = nil, options = {}, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr, options, &block) } + else + object = new(attributes, options, &block) + object.save + object + end + end + end + # Returns true if this object hasn't been saved yet -- that is, a record # for the object doesn't exist in the data store yet; otherwise, returns false. def new_record? @@ -12,8 +59,8 @@ module ActiveRecord @destroyed end - # Returns if the record is persisted, i.e. it's not a new record and it was - # not destroyed. + # Returns true if the record is persisted, i.e. it's not a new record and it was + # not destroyed, otherwise returns false. def persisted? !(new_record? || destroyed?) end @@ -162,7 +209,7 @@ module ActiveRecord # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - self.assign_attributes(attributes, options) + assign_attributes(attributes, options) save end end @@ -173,7 +220,7 @@ module ActiveRecord # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - self.assign_attributes(attributes, options) + assign_attributes(attributes, options) save! end end @@ -238,7 +285,7 @@ module ActiveRecord clear_association_cache IdentityMap.without do - fresh_object = self.class.unscoped { self.class.find(self.id, options) } + fresh_object = self.class.unscoped { self.class.find(id, options) } @attributes.update(fresh_object.instance_variable_get('@attributes')) end @@ -321,13 +368,5 @@ module ActiveRecord @new_record = false id end - - # Initializes the attributes array with keys matching the columns from the linked table and - # the values matching the corresponding default value of that column, so - # that a new instance, or one populated from a passed-in Hash, still has all the attributes - # that instances loaded from the database would. - def attributes_from_column_definition - self.class.column_defaults.dup - end end end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 466d148901..9701898415 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -27,47 +27,22 @@ module ActiveRecord @app = app end - class BodyProxy # :nodoc: - def initialize(original_cache_value, target, connection_id) - @original_cache_value = original_cache_value - @target = target - @connection_id = connection_id - end - - def method_missing(method_sym, *arguments, &block) - @target.send(method_sym, *arguments, &block) - end - - def respond_to?(method_sym, include_private = false) - super || @target.respond_to?(method_sym) - end - - def each(&block) - @target.each(&block) - end + def call(env) + enabled = ActiveRecord::Base.connection.query_cache_enabled + connection_id = ActiveRecord::Base.connection_id + ActiveRecord::Base.connection.enable_query_cache! - def close - @target.close if @target.respond_to?(:close) - ensure - ActiveRecord::Base.connection_id = @connection_id + response = @app.call(env) + response[2] = Rack::BodyProxy.new(response[2]) do + ActiveRecord::Base.connection_id = connection_id ActiveRecord::Base.connection.clear_query_cache - unless @original_cache_value - ActiveRecord::Base.connection.disable_query_cache! - end + ActiveRecord::Base.connection.disable_query_cache! unless enabled end - end - def call(env) - old = ActiveRecord::Base.connection.query_cache_enabled - ActiveRecord::Base.connection.enable_query_cache! - - status, headers, body = @app.call(env) - [status, headers, BodyProxy.new(old, body, ActiveRecord::Base.connection_id)] + response rescue Exception => e ActiveRecord::Base.connection.clear_query_cache - unless old - ActiveRecord::Base.connection.disable_query_cache! - end + ActiveRecord::Base.connection.disable_query_cache! unless enabled raise e end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb new file mode 100644 index 0000000000..94e34e1bd4 --- /dev/null +++ b/activerecord/lib/active_record/querying.rb @@ -0,0 +1,58 @@ +require 'active_support/core_ext/module/delegation' + +module ActiveRecord + module Querying + delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped + delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped + delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped + delegate :find_each, :find_in_batches, :to => :scoped + delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, + :where, :preload, :eager_load, :includes, :from, :lock, :readonly, + :having, :create_with, :uniq, :references, :to => :scoped + delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped + + # Executes a custom SQL query against your database and returns all the results. The results will + # be returned as an array with columns requested encapsulated as attributes of the model you call + # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in + # a Product object with the attributes you specified in the SQL query. + # + # If you call a complicated SQL query which spans multiple tables the columns specified by the + # SELECT will be attributes of the model, whether or not they are columns of the corresponding + # table. + # + # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be + # no database agnostic conversions performed. This should be a last resort because using, for example, + # MySQL specific terms will lock you to using that particular database engine or require you to + # change your call if you switch engines. + # + # ==== Examples + # # A simple SQL query spanning multiple tables + # Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id" + # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...] + # + # # You can use the same string replacement techniques as you can with ActiveRecord#find + # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] + # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...] + def find_by_sql(sql, binds = []) + logging_query_plan do + connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) } + end + end + + # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. + # The use of this method should be restricted to complicated SQL queries that can't be executed + # using the ActiveRecord::Calculations class methods. Look into those before using this. + # + # ==== Parameters + # + # * +sql+ - An SQL statement which should return a count query from the database, see the example below. + # + # ==== Examples + # + # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" + def count_by_sql(sql) + sql = sanitize_conditions(sql) + connection.select_value(sql, "#{name} Count").to_i + end + end +end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 47133e77e8..058dd58efb 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -22,6 +22,13 @@ module ActiveRecord config.app_middleware.insert_after "::ActionDispatch::Callbacks", "ActiveRecord::ConnectionAdapters::ConnectionManagement" + config.action_dispatch.rescue_responses.merge!( + 'ActiveRecord::RecordNotFound' => :not_found, + 'ActiveRecord::StaleObjectError' => :conflict, + 'ActiveRecord::RecordInvalid' => :unprocessable_entity, + 'ActiveRecord::RecordNotSaved' => :unprocessable_entity + ) + rake_tasks do load "active_record/railties/databases.rake" end @@ -31,7 +38,8 @@ module ActiveRecord # first time. Also, make it output to STDERR. console do |app| require "active_record/railties/console_sandbox" if app.sandbox? - ActiveRecord::Base.logger = Logger.new(STDERR) + console = ActiveSupport::Logger.new(STDERR) + Rails.logger.extend ActiveSupport::Logger.broadcast console end initializer "active_record.initialize_timezone" do @@ -78,18 +86,30 @@ module ActiveRecord end end - initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app| - ActiveSupport.on_load(:active_record) do - ActionDispatch::Reloader.to_cleanup do - ActiveRecord::Base.clear_reloadable_connections! - ActiveRecord::Base.clear_cache! + initializer "active_record.set_reloader_hooks" do |app| + hook = lambda do + ActiveRecord::Base.clear_reloadable_connections! + ActiveRecord::Base.clear_cache! + end + + if app.config.reload_classes_only_on_change + ActiveSupport.on_load(:active_record) do + ActionDispatch::Reloader.to_prepare(&hook) + end + else + ActiveSupport.on_load(:active_record) do + ActionDispatch::Reloader.to_cleanup(&hook) end end end + initializer "active_record.add_watchable_files" do |app| + config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"] + end + config.after_initialize do ActiveSupport.on_load(:active_record) do - instantiate_observers + ActiveRecord::Base.instantiate_observers ActionDispatch::Reloader.to_prepare do ActiveRecord::Base.instantiate_observers diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index abd71793fd..822b51e838 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -38,6 +38,7 @@ db_namespace = namespace :db do desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' task :create => :load_config do configs_for_environment.each { |config| create_database(config) } + ActiveRecord::Base.establish_connection(configs_for_environment.first) end def mysql_creation_options(config) @@ -111,8 +112,7 @@ db_namespace = namespace :db do end end else - # Bug with 1.9.2 Calling return within begin still executes else - $stderr.puts "#{config['database']} already exists" unless config['adapter'] =~ /sqlite/ + $stderr.puts "#{config['database']} already exists" end end @@ -149,7 +149,9 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false)." task :migrate => [:environment, :load_config] do ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true - ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) + ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration| + ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope) + end db_namespace['_dump'].invoke end @@ -369,36 +371,37 @@ db_namespace = namespace :db do end namespace :structure do - desc 'Dump the database structure to an SQL file' + desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' task :dump => :environment do abcs = ActiveRecord::Base.configurations + filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") case abcs[Rails.env]['adapter'] when /mysql/, 'oci', 'oracle' ActiveRecord::Base.establish_connection(abcs[Rails.env]) - File.open("#{Rails.root}/db/structure.sql", "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } + File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } when /postgresql/ set_psql_env(abcs[Rails.env]) search_path = abcs[Rails.env]['schema_search_path'] unless search_path.blank? search_path = search_path.split(",").map{|search_path_part| "--schema=#{search_path_part.strip}" }.join(" ") end - `pg_dump -i -s -x -O -f db/structure.sql #{search_path} #{abcs[Rails.env]['database']}` + `pg_dump -i -s -x -O -f #{filename} #{search_path} #{abcs[Rails.env]['database']}` raise 'Error dumping database' if $?.exitstatus == 1 when /sqlite/ dbfile = abcs[Rails.env]['database'] - `sqlite3 #{dbfile} .schema > db/structure.sql` + `sqlite3 #{dbfile} .schema > #{filename}` when 'sqlserver' - `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f db\\structure.sql -A -U` + `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f #{filename} -A -U` when "firebird" set_firebird_env(abcs[Rails.env]) db_string = firebird_db_string(abcs[Rails.env]) - sh "isql -a #{db_string} > #{Rails.root}/db/structure.sql" + sh "isql -a #{db_string} > #{filename}" else raise "Task not supported by '#{abcs[Rails.env]["adapter"]}'" end if ActiveRecord::Base.connection.supports_migrations? - File.open("#{Rails.root}/db/structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } + File.open(filename, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } end end @@ -407,30 +410,31 @@ db_namespace = namespace :db do env = ENV['RAILS_ENV'] || 'test' abcs = ActiveRecord::Base.configurations + filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") case abcs[env]['adapter'] when /mysql/ ActiveRecord::Base.establish_connection(abcs[env]) ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') - IO.read("#{Rails.root}/db/structure.sql").split("\n\n").each do |table| + IO.read(filename).split("\n\n").each do |table| ActiveRecord::Base.connection.execute(table) end when /postgresql/ set_psql_env(abcs[env]) - `psql -f "#{Rails.root}/db/structure.sql" #{abcs[env]['database']} #{abcs[env]['template']}` + `psql -f "#{filename}" #{abcs[env]['database']} #{abcs[env]['template']}` when /sqlite/ dbfile = abcs[env]['database'] - `sqlite3 #{dbfile} < "#{Rails.root}/db/structure.sql"` + `sqlite3 #{dbfile} < "#{filename}"` when 'sqlserver' - `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i db\\structure.sql` + `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}` when 'oci', 'oracle' ActiveRecord::Base.establish_connection(abcs[env]) - IO.read("#{Rails.root}/db/structure.sql").split(";\n\n").each do |ddl| + IO.read(filename).split(";\n\n").each do |ddl| ActiveRecord::Base.connection.execute(ddl) end when 'firebird' set_firebird_env(abcs[env]) db_string = firebird_db_string(abcs[env]) - sh "isql -i #{Rails.root}/db/structure.sql #{db_string}" + sh "isql -i #{filename} #{db_string}" else raise "Task not supported by '#{abcs[env]['adapter']}'" end @@ -442,26 +446,39 @@ db_namespace = namespace :db do end namespace :test do - # desc "Recreate the test database from the current schema.rb" + + # desc "Recreate the test database from the current schema" task :load => 'db:test:purge' do - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) - ActiveRecord::Schema.verbose = false - db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby + case ActiveRecord::Base.schema_format + when :ruby + db_namespace["test:load_schema"].invoke + when :sql + db_namespace["test:load_structure"].invoke + end + end + # desc "Recreate the test database from an existent structure.sql file" + task :load_structure => 'db:test:purge' do begin old_env, ENV['RAILS_ENV'] = ENV['RAILS_ENV'], 'test' - db_namespace["structure:load"].invoke if ActiveRecord::Base.schema_format == :sql + db_namespace["structure:load"].invoke ensure ENV['RAILS_ENV'] = old_env end + end + # desc "Recreate the test database from an existent schema.rb file" + task :load_schema => 'db:test:purge' do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) + ActiveRecord::Schema.verbose = false + db_namespace["schema:load"].invoke end - # desc "Recreate the test database from the current environment's database schema" - task :clone => %w(db:schema:dump db:test:load) + # desc "Recreate the test database from a fresh schema.rb file" + task :clone => %w(db:schema:dump db:test:load_schema) - # desc "Recreate the test databases from the structure.sql file" - task :clone_structure => [ "db:structure:dump", "db:test:load" ] + # desc "Recreate the test database from a fresh structure.sql file" + task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ] # desc "Empty the test database" task :purge => :environment do @@ -522,7 +539,7 @@ end namespace :railties do namespace :install do - # desc "Copies missing migrations from Railties (e.g. plugins, engines). You can specify Railties to use with FROM=railtie1,railtie2" + # desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2" task :migrations => :'db:load_config' do to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip } railties = ActiveSupport::OrderedHash.new diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb index 6b9af2a0cb..0b75983484 100644 --- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb +++ b/activerecord/lib/active_record/railties/jdbcmysql_error.rb @@ -1,5 +1,5 @@ #FIXME Remove if ArJdbcMysql will give. -module ArJdbcMySQL +module ArJdbcMySQL #:nodoc: class Error < StandardError attr_accessor :error_number, :sql_state diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb new file mode 100644 index 0000000000..836b15e2ce --- /dev/null +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -0,0 +1,26 @@ +require 'active_support/concern' +require 'active_support/core_ext/class/attribute' + +module ActiveRecord + module ReadonlyAttributes + extend ActiveSupport::Concern + + included do + config_attribute :_attr_readonly + self._attr_readonly = [] + end + + module ClassMethods + # Attributes listed as readonly will be used to create a new record but update operations will + # ignore these fields. + def attr_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 + self._attr_readonly + end + end + end +end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 52968070cb..f02f0544c5 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -7,7 +7,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :reflections + extend ActiveModel::Configuration + config_attribute :reflections self.reflections = {} end @@ -174,7 +175,7 @@ module ActiveRecord def initialize(macro, name, options, active_record) super - @collection = macro.in?([:has_many, :has_and_belongs_to_many]) + @collection = [:has_many, :has_and_belongs_to_many].include?(macro) end # Returns a new, unsaved instance of the associated class. +options+ will @@ -262,6 +263,10 @@ module ActiveRecord [self] end + def nested? + false + end + # An array of arrays of conditions. Each item in the outside array corresponds to a reflection # in the #chain. The inside arrays are simply conditions (and each condition may itself be # a hash, array, arel predicate, etc...) @@ -457,7 +462,7 @@ module ActiveRecord source_reflection.source_macro end - # A through association is nested iff there would be more than one join table + # A through association is nested if there would be more than one join table def nested? chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 0c32ad5139..ac70aeba67 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,20 +1,16 @@ # -*- coding: utf-8 -*- require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/module/delegation' +require 'active_support/deprecation' module ActiveRecord # = Active Record Relation class Relation JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] - MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order, :uniq] + MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind, :references] + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :uniq] - include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches - - # These are explicitly delegated to improve performance (avoids method_missing) - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a - delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :column_hash,:to => :klass + include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded attr_accessor :extensions, :default_scoped @@ -137,30 +133,35 @@ module ActiveRecord first || new(attributes, options, &block) end - def respond_to?(method, include_private = false) - arel.respond_to?(method, include_private) || - Array.method_defined?(method) || - @klass.respond_to?(method, include_private) || - super - end - + # Runs EXPLAIN on the query or queries triggered by this relation and + # returns the result as a string. The string is formatted imitating the + # ones printed by the database shell. + # + # Note that this method actually runs the queries, since the results of some + # are needed by the next ones when eager loading is going on. + # + # Please see further details in the + # {Active Record Query Interface guide}[http://edgeguides.rubyonrails.org/active_record_querying.html#running-explain]. def explain - queries = [] - callback = lambda do |*args| - payload = args.last - queries << payload[:sql] unless payload[:exception] || %w(SCHEMA EXPLAIN).include?(payload[:name]) - end + _, queries = collecting_queries_for_explain { exec_queries } + exec_explain(queries) + end - ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do - to_a + def to_a + # We monitor here the entire execution rather than individual SELECTs + # because from the point of view of the user fetching the records of a + # relation is a single unit of work. You want to know if this call takes + # too long, not if the individual queries take too long. + # + # It could be the case that none of the queries involved surpass the + # threshold, and at the same time the sum of them all does. The user + # should get a query plan logged in that case. + logging_query_plan do + exec_queries end - - queries.map do |sql| - "EXPLAIN for: #{sql}\n#{@klass.connection.explain(sql)}" - end.join("\n") end - def to_a + def exec_queries return @records if loaded? default_scoped = with_default_scope @@ -191,6 +192,7 @@ module ActiveRecord @loaded = true @records end + private :exec_queries def as_json(options = nil) #:nodoc: to_a.as_json(options) @@ -236,7 +238,7 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - @klass.send(:with_scope, self, :overwrite) { yield } + @klass.with_scope(self, :overwrite) { yield } end # Updates all records with details given if they match a set of conditions supplied, limits and order can @@ -354,7 +356,7 @@ module ActiveRecord end end - # Destroy an object (or multiple objects) that has the given id, the object is instantiated first, + # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, # therefore all callbacks and filters are fired off before the object is deleted. This method is # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run. # @@ -494,6 +496,10 @@ module ActiveRecord to_a.inspect end + def pretty_print(q) + q.pp(self.to_a) + end + def with_default_scope #:nodoc: if default_scoped? && default_scope = klass.send(:build_default_scope) default_scope = default_scope.merge(self) @@ -504,20 +510,6 @@ module ActiveRecord end end - protected - - def method_missing(method, *args, &block) - if Array.method_defined?(method) - to_a.send(method, *args, &block) - elsif @klass.respond_to?(method) - scoping { @klass.send(method, *args, &block) } - elsif arel.respond_to?(method) - arel.send(method, *args, &block) - else - super - end - end - private def references_eager_loaded_tables? @@ -533,8 +525,30 @@ module ActiveRecord # always convert table names to downcase as in Oracle quoted table names are in uppercase joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq - - (tables_in_string(to_sql) - joined_tables).any? + string_tables = tables_in_string(to_sql) + + if (references_values - joined_tables).any? + true + elsif (string_tables - joined_tables).any? + ActiveSupport::Deprecation.warn( + "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \ + "that are referenced in a string SQL snippet. For example: \n" \ + "\n" \ + " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \ + "\n" \ + "Currently, Active Record recognises the table in the string, and knows to JOIN the " \ + "comments table to the query, rather than loading comments in a separate query. " \ + "However, doing this without writing a full-blown SQL parser is inherently flawed. " \ + "Since we don't want to write an SQL parser, we are removing this functionality. " \ + "From now on, you must explicitly tell Active Record when you are referencing a table " \ + "from a string:\n" \ + "\n" \ + " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n\n" + ) + true + else + false + end end def tables_in_string(string) @@ -543,6 +557,5 @@ module ActiveRecord # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] end - end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index af86771d2d..bf9b4bf1c9 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -166,6 +166,22 @@ module ActiveRecord 0 end + # This method is designed to perform select by a single column as direct SQL query + # Returns <tt>Array</tt> with values of the specified column name + # The values has same data type as column. + # + # Examples: + # + # Person.pluck(:id) # SELECT people.id FROM people + # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people + # Person.where(:confirmed => true).limit(5).pluck(:id) + # + def pluck(column_name) + klass.connection.select_all(select(column_name).arel).map! do |attributes| + klass.type_cast_attribute(attributes.keys.first, klass.initialize_attributes(attributes)) + end + end + private def perform_calculation(operation, column_name, options = {}) diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb new file mode 100644 index 0000000000..f5fdf437bf --- /dev/null +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -0,0 +1,49 @@ +require 'active_support/core_ext/module/delegation' + +module ActiveRecord + module Delegation + # Set up common delegations for performance (avoids method_missing) + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a + delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, + :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass + + def self.delegate_to_scoped_klass(method) + if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/ + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + scoping { @klass.#{method}(*args, &block) } + end + RUBY + else + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + scoping { @klass.send(#{method.inspect}, *args, &block) } + end + RUBY + end + end + + def respond_to?(method, include_private = false) + super || Array.method_defined?(method) || + @klass.respond_to?(method, include_private) || + arel.respond_to?(method, include_private) + end + + protected + + def method_missing(method, *args, &block) + if Array.method_defined?(method) + ::ActiveRecord::Delegation.delegate method, :to => :to_a + to_a.send(method, *args, &block) + elsif @klass.respond_to?(method) + ::ActiveRecord::Delegation.delegate_to_scoped_klass(method) + scoping { @klass.send(method, *args, &block) } + elsif arel.respond_to?(method) + ::ActiveRecord::Delegation.delegate method, :to => :arel + arel.send(method, *args, &block) + else + super + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3c8e0f2052..f1ac421a50 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -134,7 +134,7 @@ module ActiveRecord def last(*args) if args.any? if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) - if order_values.empty? && reorder_value.nil? + if order_values.empty? order("#{primary_key} DESC").limit(*args).reverse else to_a.last(*args) @@ -187,7 +187,7 @@ module ActiveRecord def exists?(id = false) return false if id.nil? - id = id.id if ActiveRecord::Base === id + id = id.id if ActiveRecord::Model === id join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index a789f48725..1d04e763f6 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -15,44 +15,61 @@ module ActiveRecord table = Arel::Table.new(table_name, engine) end - attribute = table[column.to_sym] - - case value - when ActiveRecord::Relation - value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? - attribute.in(value.arel.ast) - when Array, ActiveRecord::Associations::CollectionProxy - values = value.to_a.map {|x| x.is_a?(ActiveRecord::Base) ? x.id : x} - ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)} - - array_predicates = ranges.map {|range| attribute.in(range)} - - if values.include?(nil) - values = values.compact - if values.empty? - array_predicates << attribute.eq(nil) - else - array_predicates << attribute.in(values.compact).or(attribute.eq(nil)) - end + build(table[column.to_sym], value) + end + end + predicates.flatten + end + + def self.references(attributes) + references = attributes.map do |key, value| + if value.is_a?(Hash) + key + else + key = key.to_s + key.split('.').first.to_sym if key.include?('.') + end + end + references.compact + end + + private + def self.build(attribute, value) + case value + when ActiveRecord::Relation + value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? + attribute.in(value.arel.ast) + when Array, ActiveRecord::Associations::CollectionProxy + values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x} + ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)} + + array_predicates = ranges.map {|range| attribute.in(range)} + + if values.include?(nil) + values = values.compact + case values.length + when 0 + array_predicates << attribute.eq(nil) + when 1 + array_predicates << attribute.eq(values.first).or(attribute.eq(nil)) else - array_predicates << attribute.in(values) + array_predicates << attribute.in(values).or(attribute.eq(nil)) end - - array_predicates.inject {|composite, predicate| composite.or(predicate)} - when Range, Arel::Relation - attribute.in(value) - when ActiveRecord::Base - attribute.eq(value.id) - when Class - # FIXME: I think we need to deprecate this behavior - attribute.eq(value.name) else - attribute.eq(value) + array_predicates << attribute.in(values) end + + array_predicates.inject {|composite, predicate| composite.or(predicate)} + when Range, Arel::Relation + attribute.in(value) + when ActiveRecord::Model + attribute.eq(value.id) + when Class + # FIXME: I think we need to deprecate this behavior + attribute.eq(value.name) + else + attribute.eq(value) end end - - predicates.flatten - end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index c281bead0d..a8ae7208fc 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -9,8 +9,8 @@ module ActiveRecord :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, :bind_values, :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, - :from_value, :reorder_value, :reverse_order_value, - :uniq_value + :from_value, :reordering_value, :reverse_order_value, + :uniq_value, :references_values def includes(*args) args.reject! {|a| a.blank? } @@ -38,6 +38,24 @@ module ActiveRecord relation end + # Used to indicate that an association is referenced by an SQL string, and should + # therefore be JOINed in any query rather than loaded separately. + # + # For example: + # + # User.includes(:posts).where("posts.name = 'foo'") + # # => Doesn't JOIN the posts table, resulting in an error. + # + # User.includes(:posts).where("posts.name = 'foo'").references(:posts) + # # => Query now knows the string references posts, so adds a JOIN + def references(*args) + return self if args.blank? + + relation = clone + relation.references_values = (references_values + args.flatten.map(&:to_s)).uniq + relation + end + # Works in two unique ways. # # First: takes a block so it can be used just like Array#select. @@ -57,16 +75,16 @@ module ActiveRecord # array, it actually returns a relation object and can have other query # methods appended to it, such as the other methods in ActiveRecord::QueryMethods. # - # This method will also take multiple parameters: + # The argument to the method can also be an array of fields. # - # >> Model.select(:field, :other_field, :and_one_more) + # >> Model.select([:field, :other_field, :and_one_more]) # => [#<Model field: "value", other_field: "value", and_one_more: "value">] # - # Any attributes that do not have fields retrieved by a select - # will return `nil` when the getter method for that attribute is used: + # Accessing attributes of an object that do not have fields retrieved by a select + # will throw <tt>ActiveModel::MissingAttributeError</tt>: # # >> Model.select(:field).first.other_field - # => nil + # => ActiveModel::MissingAttributeError: missing attribute: other_field def select(value = Proc.new) if block_given? to_a.select {|*block_args| value.call(*block_args) } @@ -88,16 +106,33 @@ module ActiveRecord def order(*args) return self if args.blank? + args = args.flatten + references = args.reject { |arg| Arel::Node === arg } + .map { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 } + .compact + relation = clone - relation.order_values += args.flatten + relation = relation.references(references) if references.any? + relation.order_values += args relation end + # Replaces any existing order defined on the relation with the specified order. + # + # User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC' + # + # Subsequent calls to order on the same relation will be appended. For example: + # + # User.order('email DESC').reorder('id ASC').order('name ASC') + # + # generates a query with 'ORDER BY id ASC, name ASC'. + # def reorder(*args) return self if args.blank? relation = clone - relation.reorder_value = args.flatten + relation.reordering_value = true + relation.order_values = args.flatten relation end @@ -122,6 +157,7 @@ module ActiveRecord return self if opts.blank? relation = clone + relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts relation.where_values += build_where(opts, rest) relation end @@ -130,6 +166,7 @@ module ActiveRecord return self if opts.blank? relation = clone + relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts relation.having_values += build_where(opts, rest) relation end @@ -263,7 +300,7 @@ module ActiveRecord arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? - order = @reorder_value ? @reorder_value : @order_values + order = @order_values order = reverse_sql_order(order) if @reverse_order_value arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty? diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index ba882beca9..7131aa29b6 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -22,7 +22,7 @@ module ActiveRecord end end - (Relation::MULTI_VALUE_METHODS - [:joins, :where]).each do |method| + (Relation::MULTI_VALUE_METHODS - [:joins, :where, :order]).each do |method| value = r.send(:"#{method}_values") merged_relation.send(:"#{method}_values=", merged_relation.send(:"#{method}_values") + value) if value.present? end @@ -48,7 +48,7 @@ module ActiveRecord merged_relation.where_values = merged_wheres - (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with, :reordering]).each do |method| value = r.send(:"#{method}_value") merged_relation.send(:"#{method}_value=", value) unless value.nil? end @@ -57,6 +57,15 @@ module ActiveRecord merged_relation = merged_relation.create_with(r.create_with_value) unless r.create_with_value.empty? + if (r.reordering_value) + # override any order specified in the original relation + merged_relation.reordering_value = true + merged_relation.order_values = r.order_values + else + # merge in order_values from r + merged_relation.order_values += r.order_values + end + # Apply scope extension modules merged_relation.send :apply_modules, r.extensions @@ -113,7 +122,7 @@ module ActiveRecord result end - VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, + VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, :references, :order, :select, :readonly, :group, :having, :from, :lock ] def apply_finder_options(options) @@ -124,7 +133,7 @@ module ActiveRecord finders = options.dup finders.delete_if { |key, value| value.nil? && key != :limit } - ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder| + ((VALID_FIND_OPTIONS - [:conditions, :include, :extend]) & finders.keys).each do |finder| relation = relation.send(finder, finders[finder]) end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb new file mode 100644 index 0000000000..2d7d83d160 --- /dev/null +++ b/activerecord/lib/active_record/sanitization.rb @@ -0,0 +1,194 @@ +require 'active_support/concern' + +module ActiveRecord + module Sanitization + extend ActiveSupport::Concern + + module ClassMethods + def quote_value(value, column = nil) #:nodoc: + connection.quote(value,column) + end + + # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. + def sanitize(object) #:nodoc: + connection.quote(object) + end + + protected + + # Accepts an array, hash, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a WHERE clause. + # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" + # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'" + # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" + def sanitize_sql_for_conditions(condition, table_name = self.table_name) + return nil if condition.blank? + + case condition + when Array; sanitize_sql_array(condition) + when Hash; sanitize_sql_hash_for_conditions(condition, table_name) + else condition + end + end + alias_method :sanitize_sql, :sanitize_sql_for_conditions + + # Accepts an array, hash, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a SET clause. + # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'" + def sanitize_sql_for_assignment(assignments) + case assignments + when Array; sanitize_sql_array(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments) + else assignments + end + end + + # Accepts a hash of SQL conditions and replaces those attributes + # that correspond to a +composed_of+ relationship with their expanded + # aggregate attribute values. + # Given: + # class Person < ActiveRecord::Base + # composed_of :address, :class_name => "Address", + # :mapping => [%w(address_street street), %w(address_city city)] + # end + # Then: + # { :address => Address.new("813 abc st.", "chicago") } + # # => { :address_street => "813 abc st.", :address_city => "chicago" } + def expand_hash_conditions_for_aggregates(attrs) + expanded_attrs = {} + attrs.each do |attr, value| + unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil? + mapping = aggregate_mapping(aggregation) + mapping.each do |field_attr, aggregate_attr| + if mapping.size == 1 && !value.respond_to?(aggregate_attr) + expanded_attrs[field_attr] = value + else + expanded_attrs[field_attr] = value.send(aggregate_attr) + end + end + else + expanded_attrs[attr] = value + end + end + expanded_attrs + end + + # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause. + # { :name => "foo'bar", :group_id => 4 } + # # => "name='foo''bar' and group_id= 4" + # { :status => nil, :group_id => [1,2,3] } + # # => "status IS NULL and group_id IN (1,2,3)" + # { :age => 13..18 } + # # => "age BETWEEN 13 AND 18" + # { 'other_records.id' => 7 } + # # => "`other_records`.`id` = 7" + # { :other_records => { :id => 7 } } + # # => "`other_records`.`id` = 7" + # And for value objects on a composed_of relationship: + # { :address => Address.new("123 abc st.", "chicago") } + # # => "address_street='123 abc st.' and address_city='chicago'" + def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) + attrs = expand_hash_conditions_for_aggregates(attrs) + + table = Arel::Table.new(table_name).alias(default_table_name) + PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| + connection.visitor.accept b + }.join(' AND ') + end + alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions + + # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. + # { :status => nil, :group_id => 1 } + # # => "status = NULL , group_id = 1" + def sanitize_sql_hash_for_assignment(attrs) + attrs.map do |attr, value| + "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}" + end.join(', ') + end + + # Accepts an array of conditions. The array has each value + # sanitized and interpolated into the SQL statement. + # ["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) && statement =~ /:\w+/ + replace_named_bind_variables(statement, values.first) + elsif statement.include?('?') + replace_bind_variables(statement, values) + elsif statement.blank? + statement + else + statement % values.collect { |value| connection.quote_string(value.to_s) } + end + end + + alias_method :sanitize_conditions, :sanitize_sql + + def replace_bind_variables(statement, values) #:nodoc: + raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) + bound = values.dup + c = connection + statement.gsub('?') { quote_bound_value(bound.shift, c) } + end + + def replace_named_bind_variables(statement, bind_vars) #:nodoc: + statement.gsub(/(:?):([a-zA-Z]\w*)/) do + if $1 == ':' # skip postgresql casts + $& # return the whole match + elsif bind_vars.include?(match = $2.to_sym) + quote_bound_value(bind_vars[match]) + else + raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" + end + end + end + + def expand_range_bind_variables(bind_vars) #:nodoc: + expanded = [] + + bind_vars.each do |var| + next if var.is_a?(Hash) + + if var.is_a?(Range) + expanded << var.first + expanded << var.last + else + expanded << var + end + end + + expanded + end + + def quote_bound_value(value, c = connection) #:nodoc: + if value.respond_to?(:map) && !value.acts_like?(:string) + if value.respond_to?(:empty?) && value.empty? + c.quote(nil) + else + value.map { |v| c.quote(v) }.join(',') + end + else + c.quote(value) + end + end + + def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc: + unless expected == provided + raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" + end + end + end + + # TODO: Deprecate this + def quoted_id #:nodoc: + quote_value(id, column_for_attribute(self.class.primary_key)) + end + + private + + # Quote strings appropriately for SQL statements. + def quote_value(value, column = nil) + self.class.connection.quote(value, column) + end + end +end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index cdde5cf3b9..2a565b51c6 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -112,7 +112,7 @@ HEADER # 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/].any? { |e| e.match(column.sql_type) } + spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type 'decimal' else column.type.to_s @@ -127,10 +127,14 @@ HEADER end.compact # find all migration keys used in this table - keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map{ |k| k.keys }.flatten + keys = [:name, :limit, :precision, :scale, :default, :null] # figure out the lengths for each column based on above keys - lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max } + lengths = keys.map { |key| + column_specs.map { |spec| + spec[key] ? spec[key].length + 2 : 0 + }.max + } # the string we're going to sprintf our values against, with standardized column widths format_string = lengths.map{ |len| "%-#{len}s" } diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb new file mode 100644 index 0000000000..257963c2ce --- /dev/null +++ b/activerecord/lib/active_record/schema_migration.rb @@ -0,0 +1,32 @@ +require 'active_record/scoping/default' +require 'active_record/scoping/named' +require 'active_record/base' + +module ActiveRecord + class SchemaMigration < ActiveRecord::Base + def self.table_name + Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix + end + + def self.create_table + unless connection.table_exists?(table_name) + connection.create_table(table_name, :id => false) do |t| + t.column :version, :string, :null => false + end + connection.add_index table_name, :version, :unique => true, + :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" + end + end + + def self.drop_table + if connection.table_exists?(table_name) + connection.remove_index table_name, :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" + connection.drop_table(table_name) + end + end + + def version + super.to_i + end + end +end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb new file mode 100644 index 0000000000..a8f5e96190 --- /dev/null +++ b/activerecord/lib/active_record/scoping.rb @@ -0,0 +1,152 @@ +require 'active_support/concern' + +module ActiveRecord + module Scoping + extend ActiveSupport::Concern + + included do + include Default + include Named + end + + module ClassMethods + # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be + # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while + # <tt>:create</tt> parameters are an attributes hash. + # + # class Article < ActiveRecord::Base + # def self.create_with_scope + # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do + # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 + # a = create(1) + # a.blog_id # => 1 + # end + # end + # end + # + # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of + # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged. + # + # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing + # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the + # array of strings format for your joins. + # + # class Article < ActiveRecord::Base + # def self.find_with_scope + # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do + # with_scope(:find => limit(10)) do + # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 + # end + # with_scope(:find => where(:author_id => 3)) do + # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 + # end + # end + # end + # end + # + # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method. + # + # class Article < ActiveRecord::Base + # def self.find_with_exclusive_scope + # with_scope(:find => where(:blog_id => 1).limit(1)) do + # with_exclusive_scope(:find => limit(10)) do + # all # => SELECT * from articles LIMIT 10 + # end + # end + # end + # end + # + # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. + def with_scope(scope = {}, action = :merge, &block) + # If another Active Record class has been passed in, get its current scope + scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope) + + previous_scope = self.current_scope + + if scope.is_a?(Hash) + # Dup first and second level of hash (method and params). + scope = scope.dup + scope.each do |method, params| + scope[method] = params.dup unless params == true + end + + scope.assert_valid_keys([ :find, :create ]) + relation = construct_finder_arel(scope[:find] || {}) + relation.default_scoped = true unless action == :overwrite + + if previous_scope && previous_scope.create_with_value && scope[:create] + scope_for_create = if action == :merge + previous_scope.create_with_value.merge(scope[:create]) + else + scope[:create] + end + + relation = relation.create_with(scope_for_create) + else + scope_for_create = scope[:create] + scope_for_create ||= previous_scope.create_with_value if previous_scope + relation = relation.create_with(scope_for_create) if scope_for_create + end + + scope = relation + end + + scope = previous_scope.merge(scope) if previous_scope && action == :merge + + self.current_scope = scope + begin + yield + ensure + self.current_scope = previous_scope + end + end + + protected + + # Works like with_scope, but discards any nested properties. + def with_exclusive_scope(method_scoping = {}, &block) + if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) } + raise ArgumentError, <<-MSG + New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope: + + User.unscoped.where(:active => true) + + Or call unscoped with a block: + + User.unscoped do + User.where(:active => true).all + end + + MSG + end + with_scope(method_scoping, :overwrite, &block) + end + + def current_scope #:nodoc: + Thread.current["#{self}_current_scope"] + end + + def current_scope=(scope) #:nodoc: + Thread.current["#{self}_current_scope"] = scope + end + + private + + def construct_finder_arel(options = {}, scope = nil) + relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options + relation = scope.merge(relation) if scope + relation + end + + end + + def populate_with_current_scope_attributes + return unless self.class.scope_attributes? + + self.class.scope_attributes.each do |att,value| + send("#{att}=", value) if respond_to?("#{att}=") + end + end + + end +end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb new file mode 100644 index 0000000000..5f05d146f2 --- /dev/null +++ b/activerecord/lib/active_record/scoping/default.rb @@ -0,0 +1,142 @@ +require 'active_support/concern' + +module ActiveRecord + module Scoping + module Default + extend ActiveSupport::Concern + + included do + # Stores the default scope for the class + config_attribute :default_scopes + self.default_scopes = [] + end + + module ClassMethods + # Returns a scope for the model without the default_scope. + # + # class Post < ActiveRecord::Base + # def self.default_scope + # where :published => true + # end + # end + # + # Post.all # Fires "SELECT * FROM posts WHERE published = true" + # Post.unscoped.all # Fires "SELECT * FROM posts" + # + # This method also accepts a block. All queries inside the block will + # not use the default_scope: + # + # Post.unscoped { + # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" + # } + # + # It is recommended to use the block form of unscoped because chaining + # unscoped with <tt>scope</tt> does not work. Assuming that + # <tt>published</tt> is a <tt>scope</tt>, the following two statements + # are equal: the default_scope is applied on both. + # + # Post.unscoped.published + # Post.published + def unscoped #:nodoc: + block_given? ? relation.scoping { yield } : relation + end + + def before_remove_const #:nodoc: + self.current_scope = nil + end + + protected + + # Use this macro in your model to set a default scope for all operations on + # the model. + # + # class Article < ActiveRecord::Base + # default_scope where(:published => true) + # end + # + # Article.all # => SELECT * FROM articles WHERE published = true + # + # The <tt>default_scope</tt> is also applied while creating/building a record. It is not + # applied while updating a record. + # + # Article.new.published # => true + # Article.create.published # => true + # + # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: + # + # class Article < ActiveRecord::Base + # default_scope { where(:published_at => Time.now - 1.week) } + # end + # + # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> + # macro, and it will be called when building the default scope.) + # + # If you use multiple <tt>default_scope</tt> declarations in your model then they will + # be merged together: + # + # class Article < ActiveRecord::Base + # default_scope where(:published => true) + # default_scope where(:rating => 'G') + # end + # + # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' + # + # This is also the case with inheritance and module includes where the parent or module + # defines a <tt>default_scope</tt> and the child or including class defines a second one. + # + # If you need to do more complex things with a default scope, you can alternatively + # define it as a class method: + # + # class Article < ActiveRecord::Base + # def self.default_scope + # # Should return a scope, you can call 'super' here etc. + # end + # end + def default_scope(scope = {}) + scope = Proc.new if block_given? + self.default_scopes = default_scopes + [scope] + end + + def build_default_scope #:nodoc: + if method(:default_scope).owner != ActiveRecord::Scoping::Default::ClassMethods + evaluate_default_scope { default_scope } + elsif default_scopes.any? + evaluate_default_scope do + default_scopes.inject(relation) do |default_scope, scope| + if scope.is_a?(Hash) + default_scope.apply_finder_options(scope) + elsif !scope.is_a?(Relation) && scope.respond_to?(:call) + default_scope.merge(scope.call) + else + default_scope.merge(scope) + end + end + end + end + end + + def ignore_default_scope? #:nodoc: + Thread.current["#{self}_ignore_default_scope"] + end + + def ignore_default_scope=(ignore) #:nodoc: + Thread.current["#{self}_ignore_default_scope"] = ignore + end + + # The ignore_default_scope flag is used to prevent an infinite recursion situation where + # a default scope references a scope which has a default scope which references a scope... + def evaluate_default_scope + return if ignore_default_scope? + + begin + self.ignore_default_scope = true + yield + ensure + self.ignore_default_scope = false + end + end + + end + end + end +end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb new file mode 100644 index 0000000000..0edc3f1dcc --- /dev/null +++ b/activerecord/lib/active_record/scoping/named.rb @@ -0,0 +1,202 @@ +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 Scoping + module Named + extend ActiveSupport::Concern + + module ClassMethods + # Returns an anonymous \scope. + # + # posts = Post.scoped + # posts.size # Fires "select count(*) from posts" and returns the count + # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects + # + # fruits = Fruit.scoped + # fruits = fruits.where(:color => 'red') if options[:red_only] + # fruits = fruits.limit(10) if limited? + # + # Anonymous \scopes tend to be useful when procedurally generating complex + # queries, where passing intermediate values (\scopes) around as first-class + # objects is convenient. + # + # You can define a \scope that applies to all finders using + # ActiveRecord::Base.default_scope. + def scoped(options = nil) + if options + scoped.apply_finder_options(options) + else + if current_scope + current_scope.clone + else + scope = relation.clone + scope.default_scoped = true + scope + end + end + end + + ## + # Collects attributes from scopes that should be applied when creating + # an AR instance for the particular class this is called on. + def scope_attributes # :nodoc: + if current_scope + current_scope.scope_for_create + else + scope = relation.clone + scope.default_scoped = true + scope.scope_for_create + end + end + + ## + # Are there default attributes associated with this scope? + def scope_attributes? # :nodoc: + current_scope || default_scopes.any? + 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>. + # + # class Shirt < ActiveRecord::Base + # scope :red, where(:color => 'red') + # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) + # end + # + # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, + # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>. + # + # Note that this is simply 'syntactic sugar' for defining an actual class method: + # + # class Shirt < ActiveRecord::Base + # def self.red + # where(:color => 'red') + # end + # end + # + # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it + # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, + # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. + # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; + # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> + # all behave as if Shirt.red really was an Array. + # + # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce + # all shirts that are both red and dry clean only. + # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> + # returns the number of garments for which these criteria obtain. Similarly with + # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. + # + # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which + # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If, + # + # class Person < ActiveRecord::Base + # has_many :shirts + # end + # + # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean + # only shirts. + # + # Named \scopes can also be procedural: + # + # class Shirt < ActiveRecord::Base + # scope :colored, lambda { |color| where(:color => color) } + # end + # + # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts. + # + # On Ruby 1.9 you can use the 'stabby lambda' syntax: + # + # scope :colored, ->(color) { where(:color => color) } + # + # Note that scopes defined with \scope will be evaluated when they are defined, rather than + # when they are used. For example, the following would be incorrect: + # + # class Post < ActiveRecord::Base + # scope :recent, where('published_at >= ?', Time.current - 1.week) + # end + # + # The example above would be 'frozen' to the <tt>Time.current</tt> value when the <tt>Post</tt> + # class was defined, and so the resultant SQL query would always be the same. The correct + # way to do this would be via a lambda, which will re-evaluate the scope each time + # it is called: + # + # class Post < ActiveRecord::Base + # scope :recent, lambda { where('published_at >= ?', Time.current - 1.week) } + # end + # + # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations: + # + # class Shirt < ActiveRecord::Base + # scope :red, where(:color => 'red') do + # def dom_id + # 'red_shirts' + # end + # end + # end + # + # Scopes can also be used while creating/building a record. + # + # class Article < ActiveRecord::Base + # scope :published, where(:published => true) + # end + # + # Article.published.new.published # => true + # Article.published.create.published # => true + # + # Class methods on your model are automatically available + # on scopes. Assuming the following setup: + # + # class Article < ActiveRecord::Base + # scope :published, where(:published => true) + # scope :featured, where(:featured => true) + # + # def self.latest_article + # order('published_at desc').first + # end + # + # def self.titles + # map(&:title) + # end + # + # end + # + # We are able to call the methods like this: + # + # Article.published.featured.latest_article + # Article.featured.titles + + def scope(name, scope_options = {}) + name = name.to_sym + valid_scope_name?(name) + extension = Module.new(&Proc.new) if block_given? + + scope_proc = lambda do |*args| + options = scope_options.respond_to?(:call) ? unscoped { scope_options.call(*args) } : scope_options + options = scoped.apply_finder_options(options) if options.is_a?(Hash) + + relation = scoped.merge(options) + + extension ? relation.extending(extension) : relation + end + + singleton_class.send(:redefine_method, name, &scope_proc) + end + + protected + + def valid_scope_name?(name) + if respond_to?(name, true) + logger.warn "Creating scope :#{name}. " \ + "Overwriting existing method #{self.name}.#{name}." + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index c23514c465..41e3b92499 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -2,13 +2,13 @@ module ActiveRecord #:nodoc: # = Active Record Serialization module Serialization extend ActiveSupport::Concern - include ActiveModel::Serializable::JSON + include ActiveModel::Serializers::JSON def serializable_hash(options = nil) options = options.try(:clone) || {} - options[:except] = Array.wrap(options[:except]).map { |n| n.to_s } - options[:except] |= Array.wrap(self.class.inheritance_column) + options[:except] = Array(options[:except]).map { |n| n.to_s } + options[:except] |= Array(self.class.inheritance_column) super(options) end diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 2da836ef0c..7f1dba5095 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -1,9 +1,8 @@ -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/hash/conversions' module ActiveRecord #:nodoc: module Serialization - include ActiveModel::Serializable::XML + include ActiveModel::Serializers::Xml # Builds an XML document to represent the model. Some configuration is # available through +options+. However more complicated cases should @@ -176,13 +175,13 @@ module ActiveRecord #:nodoc: end end - class XmlSerializer < ActiveModel::Serializable::XML::Serializer #:nodoc: + class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: def initialize(*args) super - options[:except] = Array.wrap(options[:except]) | Array.wrap(@serializable.class.inheritance_column) + options[:except] = Array(options[:except]) | Array(@serializable.class.inheritance_column) end - class Attribute < ActiveModel::Serializable::XML::Serializer::Attribute #:nodoc: + class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class type = if klass.serialized_attributes.key?(name) diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index e3bbd06f7e..ce43ae8066 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -51,11 +51,11 @@ module ActiveRecord class SessionStore < ActionDispatch::Session::AbstractStore module ClassMethods # :nodoc: def marshal(data) - ActiveSupport::Base64.encode64(Marshal.dump(data)) if data + ::Base64.encode64(Marshal.dump(data)) if data end def unmarshal(data) - Marshal.load(ActiveSupport::Base64.decode64(data)) if data + Marshal.load(::Base64.decode64(data)) if data end def drop_table! @@ -116,7 +116,7 @@ module ActiveRecord define_method(:session_id) { sessid } define_method(:session_id=) { |session_id| self.sessid = session_id } else - class << self; remove_method :find_by_session_id; end + class << self; remove_possible_method :find_by_session_id; end def self.find_by_session_id(session_id) find :first, :conditions => {:session_id=>session_id} @@ -169,11 +169,11 @@ module ActiveRecord # are implemented as class methods that you may override. By default, # marshaling data is # - # ActiveSupport::Base64.encode64(Marshal.dump(data)) + # ::Base64.encode64(Marshal.dump(data)) # # and unmarshaling data is # - # Marshal.load(ActiveSupport::Base64.decode64(data)) + # Marshal.load(::Base64.decode64(data)) # # This marshaling behavior is intended to store the widest range of # binary session data in a +text+ column. For higher performance, diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index ffe9b08dce..64ecef2077 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -9,15 +9,12 @@ module ActiveRecord cleanup_identity_map end - def cleanup_identity_map - ActiveRecord::IdentityMap.clear + def teardown + ActiveRecord::SQLCounter.log.clear end - # Backport skip to Ruby 1.8. test/unit doesn't support it, so just - # make it a noop. - unless instance_methods.map(&:to_s).include?("skip") - def skip(message) - end + def cleanup_identity_map + ActiveRecord::IdentityMap.clear end def assert_date_from_db(expected, actual, message = nil) @@ -57,17 +54,8 @@ module ActiveRecord ActiveRecord::SQLCounter.ignored_sql = prev_ignored_sql end - def with_kcode(kcode) - if RUBY_VERSION < '1.9' - orig_kcode, $KCODE = $KCODE, kcode - begin - yield - ensure - $KCODE = orig_kcode - end - else - yield - end + def sqlite3? connection + connection.class.name.split('::').last == "SQLite3Adapter" end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 0c760e9850..c717fdea47 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -33,7 +33,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :record_timestamps + config_attribute :record_timestamps, :instance_writer => true self.record_timestamps = true end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index ae97a3f3ca..b492377d18 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -211,7 +211,7 @@ module ActiveRecord def after_commit(*args, &block) options = args.last if options.is_a?(Hash) && options[:on] - options[:if] = Array.wrap(options[:if]) + options[:if] = Array(options[:if]) options[:if] << "transaction_include_action?(:#{options[:on]})" end set_callback(:commit, :after, *args, &block) @@ -220,7 +220,7 @@ module ActiveRecord def after_rollback(*args, &block) options = args.last if options.is_a?(Hash) && options[:on] - options[:if] = Array.wrap(options[:if]) + options[:if] = Array(options[:if]) options[:if] << "transaction_include_action?(:#{options[:on]})" end set_callback(:rollback, :after, *args, &block) @@ -301,7 +301,7 @@ module ActiveRecord protected # Save the new record state and id of a record so it can be restored later if a transaction fails. - def remember_transaction_record_state #:nodoc + def remember_transaction_record_state #:nodoc: @_start_transaction_state ||= {} @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) unless @_start_transaction_state.include?(:new_record) @@ -314,7 +314,7 @@ module ActiveRecord end # Clear the new record state and id of a record. - def clear_transaction_record_state #:nodoc + def clear_transaction_record_state #:nodoc: if defined?(@_start_transaction_state) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1 @@ -322,7 +322,7 @@ module ActiveRecord end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. - def restore_transaction_record_state(force = false) #:nodoc + def restore_transaction_record_state(force = false) #:nodoc: if defined?(@_start_transaction_state) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 @@ -341,12 +341,12 @@ module ActiveRecord end # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. - def transaction_record_state(state) #:nodoc + def transaction_record_state(state) #:nodoc: @_start_transaction_state[state] if defined?(@_start_transaction_state) end # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. - def transaction_include_action?(action) #:nodoc + def transaction_include_action?(action) #:nodoc: case action when :create transaction_record_state(:new_record) diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb new file mode 100644 index 0000000000..ddcb5f2a7a --- /dev/null +++ b/activerecord/lib/active_record/translation.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module Translation + include ActiveModel::Translation + + # Set the lookup ancestors for ActiveModel. + 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 + end + + # Set the i18n scope to overwrite ActiveModel. + def i18n_scope #:nodoc: + :activerecord + end + end +end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 7af0352a31..afce149da9 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -2,8 +2,9 @@ module ActiveRecord module Validations class AssociatedValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all? - record.errors.add(attribute, :invalid, options.merge(:value => value)) + if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?(record.validation_context) }.any? + record.errors.add(attribute, :invalid, options.merge(:value => value)) + end end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 2e2ea8c42b..3a741ba600 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/array/wrap' - module ActiveRecord module Validations class UniquenessValidator < ActiveModel::EachValidator @@ -25,8 +23,13 @@ module ActiveRecord relation = build_relation(finder_class, table, attribute, value) relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted? - Array.wrap(options[:scope]).each do |scope_item| + Array(options[:scope]).each do |scope_item| scope_value = record.send(scope_item) + reflection = record.class.reflect_on_association(scope_item) + if reflection + scope_value = record.send(reflection.foreign_key) + scope_item = reflection.foreign_key + end relation = relation.and(table[scope_item].eq(scope_value)) end @@ -53,14 +56,22 @@ module ActiveRecord end def build_relation(klass, table, attribute, value) #:nodoc: - column = klass.columns_hash[attribute.to_s] - value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text? + reflection = klass.reflect_on_association(attribute) + column = nil + if(reflection) + column = klass.columns_hash[reflection.foreign_key] + attribute = reflection.foreign_key + value = value.attributes[reflection.primary_key_column.name] + else + column = klass.columns_hash[attribute.to_s] + end + value = column.limit ? value.to_s[0, column.limit] : value.to_s if !value.nil? && column.text? if !options[:case_sensitive] && value && column.text? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation relation = klass.connection.case_insensitive_comparison(table, attribute, column, value) else - value = klass.connection.case_sensitive_modifier(value) + value = klass.connection.case_sensitive_modifier(value) unless value.nil? relation = table[attribute].eq(value) end @@ -81,7 +92,7 @@ module ActiveRecord # # class Person < ActiveRecord::Base # validates_uniqueness_of :user_name, :scope => :account_id - # end + # end # # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once # per semester for a particular class. diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 838aa8fb1e..0c35adc11d 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,7 +1,7 @@ module ActiveRecord module VERSION #:nodoc: - MAJOR = 3 - MINOR = 2 + MAJOR = 4 + MINOR = 0 TINY = 0 PRE = "beta" diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index f6159deeeb..1509e34473 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -3,7 +3,7 @@ require 'rails/generators/active_record' module ActiveRecord module Generators class MigrationGenerator < Base - argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]" def create_migration_file set_local_assigns! 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 ce8d7eed42..d084a00ed7 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -2,14 +2,20 @@ class <%= migration_class_name %> < ActiveRecord::Migration <%- if migration_action == 'add' -%> def change <% attributes.each do |attribute| -%> - add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %> + add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- if attribute.has_index? -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end %> <%- end -%> end <%- else -%> def up <% attributes.each do |attribute| -%> <%- if migration_action -%> - <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end %> + <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> + <% if attribute.has_index? && migration_action == 'add' %> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <% end -%> <%- end -%> <%- end -%> end @@ -17,7 +23,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration 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 %> + <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> <%- end -%> <%- end -%> end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index f7caa43ac8..99a022461e 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -3,7 +3,7 @@ require 'rails/generators/active_record' module ActiveRecord module Generators class ModelGenerator < Base - argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]" check_class_collision @@ -26,6 +26,10 @@ module ActiveRecord template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke end + def attributes_with_index + attributes.select { |a| a.has_index? || (a.reference? && options[:indexes]) } + end + hook_for :test_framework protected 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 851930344a..3a3cf86d73 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb @@ -2,16 +2,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration def change create_table :<%= table_name %> do |t| <% attributes.each do |attribute| -%> - t.<%= attribute.type %> :<%= attribute.name %> + t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> <% end -%> <% if options[:timestamps] %> t.timestamps <% end -%> end -<% if options[:indexes] -%> -<% attributes.select {|attr| attr.reference? }.each do |attribute| -%> - add_index :<%= table_name %>, :<%= attribute.name %>_id -<% end -%> +<% attributes_with_index.each do |attribute| -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> <% end -%> end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index f1023ed7ef..852fc0e26e 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -1,159 +1,162 @@ require "cases/helper" -class AdapterTest < ActiveRecord::TestCase - def setup - @connection = ActiveRecord::Base.connection - end - - def test_tables - tables = @connection.tables - assert tables.include?("accounts") - assert tables.include?("authors") - assert tables.include?("tasks") - assert tables.include?("topics") - end +module ActiveRecord + class AdapterTest < ActiveRecord::TestCase + def setup + @connection = ActiveRecord::Base.connection + end - def test_table_exists? - assert @connection.table_exists?("accounts") - assert !@connection.table_exists?("nonexistingtable") - end + def test_tables + tables = @connection.tables + assert tables.include?("accounts") + assert tables.include?("authors") + assert tables.include?("tasks") + assert tables.include?("topics") + end - def test_indexes - idx_name = "accounts_idx" - - if @connection.respond_to?(:indexes) - indexes = @connection.indexes("accounts") - assert indexes.empty? - - @connection.add_index :accounts, :firm_id, :name => idx_name - indexes = @connection.indexes("accounts") - assert_equal "accounts", indexes.first.table - # OpenBase does not have the concept of a named index - # Indexes are merely properties of columns. - assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter) - assert !indexes.first.unique - assert_equal ["firm_id"], indexes.first.columns - else - warn "#{@connection.class} does not respond to #indexes" + def test_table_exists? + assert @connection.table_exists?("accounts") + assert !@connection.table_exists?("nonexistingtable") + assert !@connection.table_exists?(nil) end - ensure - @connection.remove_index(:accounts, :name => idx_name) rescue nil - end + def test_indexes + idx_name = "accounts_idx" + + if @connection.respond_to?(:indexes) + indexes = @connection.indexes("accounts") + assert indexes.empty? + + @connection.add_index :accounts, :firm_id, :name => idx_name + indexes = @connection.indexes("accounts") + assert_equal "accounts", indexes.first.table + # OpenBase does not have the concept of a named index + # Indexes are merely properties of columns. + assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter) + assert !indexes.first.unique + assert_equal ["firm_id"], indexes.first.columns + else + warn "#{@connection.class} does not respond to #indexes" + end - def test_current_database - if @connection.respond_to?(:current_database) - assert_equal ARTest.connection_config['arunit']['database'], @connection.current_database + ensure + @connection.remove_index(:accounts, :name => idx_name) rescue nil end - end - if current_adapter?(:MysqlAdapter) - def test_charset - assert_not_nil @connection.charset - assert_not_equal 'character_set_database', @connection.charset - assert_equal @connection.show_variable('character_set_database'), @connection.charset + def test_current_database + if @connection.respond_to?(:current_database) + assert_equal ARTest.connection_config['arunit']['database'], @connection.current_database + end end - def test_collation - assert_not_nil @connection.collation - assert_not_equal 'collation_database', @connection.collation - assert_equal @connection.show_variable('collation_database'), @connection.collation - end + if current_adapter?(:MysqlAdapter) + def test_charset + assert_not_nil @connection.charset + assert_not_equal 'character_set_database', @connection.charset + assert_equal @connection.show_variable('character_set_database'), @connection.charset + end - def test_show_nonexistent_variable_returns_nil - assert_nil @connection.show_variable('foo_bar_baz') - end + def test_collation + assert_not_nil @connection.collation + assert_not_equal 'collation_database', @connection.collation + assert_equal @connection.show_variable('collation_database'), @connection.collation + end - def test_not_specifying_database_name_for_cross_database_selects - begin - assert_nothing_raised do - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database)) + def test_show_nonexistent_variable_returns_nil + assert_nil @connection.show_variable('foo_bar_baz') + end - config = ARTest.connection_config - ActiveRecord::Base.connection.execute( - "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \ - "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses" - ) + def test_not_specifying_database_name_for_cross_database_selects + begin + assert_nothing_raised do + ActiveRecord::Model.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database)) + + config = ARTest.connection_config + ActiveRecord::Model.connection.execute( + "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \ + "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses" + ) + end + ensure + ActiveRecord::Model.establish_connection 'arunit' end - ensure - ActiveRecord::Base.establish_connection 'arunit' end end - end - def test_table_alias - def @connection.test_table_alias_length() 10; end - class << @connection - alias_method :old_table_alias_length, :table_alias_length - alias_method :table_alias_length, :test_table_alias_length - end + def test_table_alias + def @connection.test_table_alias_length() 10; end + class << @connection + alias_method :old_table_alias_length, :table_alias_length + alias_method :table_alias_length, :test_table_alias_length + end - assert_equal 'posts', @connection.table_alias_for('posts') - assert_equal 'posts_comm', @connection.table_alias_for('posts_comments') - assert_equal 'dbo_posts', @connection.table_alias_for('dbo.posts') + assert_equal 'posts', @connection.table_alias_for('posts') + assert_equal 'posts_comm', @connection.table_alias_for('posts_comments') + assert_equal 'dbo_posts', @connection.table_alias_for('dbo.posts') - class << @connection - remove_method :table_alias_length - alias_method :table_alias_length, :old_table_alias_length + class << @connection + remove_method :table_alias_length + alias_method :table_alias_length, :old_table_alias_length + end end - end - # test resetting sequences in odd tables in postgreSQL - if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) - require 'models/movie' - require 'models/subscriber' + # test resetting sequences in odd tables in postgreSQL + if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) + require 'models/movie' + require 'models/subscriber' - def test_reset_empty_table_with_custom_pk - Movie.delete_all - Movie.connection.reset_pk_sequence! 'movies' - assert_equal 1, Movie.create(:name => 'fight club').id - end + def test_reset_empty_table_with_custom_pk + Movie.delete_all + Movie.connection.reset_pk_sequence! 'movies' + assert_equal 1, Movie.create(:name => 'fight club').id + end - if ActiveRecord::Base.connection.adapter_name != "FrontBase" - def test_reset_table_with_non_integer_pk - Subscriber.delete_all - Subscriber.connection.reset_pk_sequence! 'subscribers' - sub = Subscriber.new(:name => 'robert drake') - sub.id = 'bob drake' - assert_nothing_raised { sub.save! } + if ActiveRecord::Base.connection.adapter_name != "FrontBase" + def test_reset_table_with_non_integer_pk + Subscriber.delete_all + Subscriber.connection.reset_pk_sequence! 'subscribers' + sub = Subscriber.new(:name => 'robert drake') + sub.id = 'bob drake' + assert_nothing_raised { sub.save! } + end end end - end - def test_uniqueness_violations_are_translated_to_specific_exception - @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" - assert_raises(ActiveRecord::RecordNotUnique) do + def test_uniqueness_violations_are_translated_to_specific_exception @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" + assert_raises(ActiveRecord::RecordNotUnique) do + @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" + end end - end - def test_foreign_key_violations_are_translated_to_specific_exception - unless @connection.adapter_name == 'SQLite' - assert_raises(ActiveRecord::InvalidForeignKey) do - # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method - if @connection.prefetch_primary_key? - id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) - @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" - else - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + def test_foreign_key_violations_are_translated_to_specific_exception + unless @connection.adapter_name == 'SQLite' + assert_raises(ActiveRecord::InvalidForeignKey) do + # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method + if @connection.prefetch_primary_key? + id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) + @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" + else + @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + end end end end - end - def test_disable_referential_integrity - assert_nothing_raised do - @connection.disable_referential_integrity do - # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method - if @connection.prefetch_primary_key? - id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) - @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" - else - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + def test_disable_referential_integrity + assert_nothing_raised do + @connection.disable_referential_integrity do + # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method + if @connection.prefetch_primary_key? + id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) + @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" + else + @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + end + # should deleted created record as otherwise disable_referential_integrity will try to enable contraints after executed block + # and will fail (at least on Oracle) + @connection.execute "DELETE FROM fk_test_has_fk" end - # should deleted created record as otherwise disable_referential_integrity will try to enable contraints after executed block - # and will fail (at least on Oracle) - @connection.execute "DELETE FROM fk_test_has_fk" end end end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 2a89430da9..fa2ba8d592 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -3,13 +3,13 @@ require "cases/helper" class MysqlConnectionTest < ActiveRecord::TestCase def setup super - @connection = ActiveRecord::Base.connection + @connection = ActiveRecord::Model.connection end def test_mysql_reconnect_attribute_after_connection_with_reconnect_true run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => true})) - assert ActiveRecord::Base.connection.raw_connection.reconnect + ActiveRecord::Model.establish_connection(orig_connection.merge({:reconnect => true})) + assert ActiveRecord::Model.connection.raw_connection.reconnect end end @@ -25,8 +25,8 @@ class MysqlConnectionTest < ActiveRecord::TestCase def test_mysql_reconnect_attribute_after_connection_with_reconnect_false run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => false})) - assert !ActiveRecord::Base.connection.raw_connection.reconnect + ActiveRecord::Model.establish_connection(orig_connection.merge({:reconnect => false})) + assert !ActiveRecord::Model.connection.raw_connection.reconnect end end @@ -114,7 +114,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase # Test that MySQL allows multiple results for stored procedures if defined?(Mysql) && Mysql.const_defined?(:CLIENT_MULTI_RESULTS) def test_multi_results - rows = ActiveRecord::Base.connection.select_rows('CALL ten();') + rows = ActiveRecord::Model.connection.select_rows('CALL ten();') assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}" assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'" end @@ -123,11 +123,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase private def run_without_connection - original_connection = ActiveRecord::Base.remove_connection + original_connection = ActiveRecord::Model.remove_connection begin yield original_connection ensure - ActiveRecord::Base.establish_connection(original_connection) + ActiveRecord::Model.establish_connection(original_connection) end end end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 146b77a95c..7fe2c02c04 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -17,11 +17,7 @@ module ActiveRecord end def test_client_encoding - if "<3".respond_to?(:encoding) - assert_equal Encoding::UTF_8, @conn.client_encoding - else - assert_equal 'utf8', @conn.client_encoding - end + assert_equal Encoding::UTF_8, @conn.client_encoding end def test_exec_insert_number @@ -41,13 +37,11 @@ module ActiveRecord value = result.rows.last.last - if "<3".respond_to?(:encoding) - # FIXME: this should probably be inside the mysql AR adapter? - value.force_encoding(@conn.client_encoding) + # FIXME: this should probably be inside the mysql AR adapter? + value.force_encoding(@conn.client_encoding) - # The strings in this file are utf-8, so transcode to utf-8 - value.encode!(Encoding::UTF_8) - end + # The strings in this file are utf-8, so transcode to utf-8 + value.encode!(Encoding::UTF_8) assert_equal str, value end diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index 1aa034ed53..29f885c6e7 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -14,7 +14,7 @@ module ActiveRecord @db_name = db @omgpost = Class.new(ActiveRecord::Base) do - set_table_name "#{db}.#{table}" + self.table_name = "#{db}.#{table}" def self.name; 'Post'; end end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 26091c713b..8e2b9ca9a5 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -3,7 +3,7 @@ require "cases/helper" class MysqlConnectionTest < ActiveRecord::TestCase def setup super - @connection = ActiveRecord::Base.connection + @connection = ActiveRecord::Model.connection end def test_no_automatic_reconnection_after_timeout @@ -32,11 +32,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase private def run_without_connection - original_connection = ActiveRecord::Base.remove_connection + original_connection = ActiveRecord::Model.remove_connection begin yield original_connection ensure - ActiveRecord::Base.establish_connection(original_connection) + ActiveRecord::Model.establish_connection(original_connection) end end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 49514e1539..d5676bc522 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -14,7 +14,7 @@ module ActiveRecord @db_name = db @omgpost = Class.new(ActiveRecord::Base) do - set_table_name "#{db}.#{table}" + self.table_name = "#{db}.#{table}" def self.name; 'Post'; end end end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 21b97b3b39..4baec749ff 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -2,6 +2,9 @@ require "cases/helper" module ActiveRecord class PostgresqlConnectionTest < ActiveRecord::TestCase + class NonExistentTable < ActiveRecord::Base + end + def setup super @connection = ActiveRecord::Base.connection @@ -10,5 +13,17 @@ module ActiveRecord def test_encoding assert_not_nil @connection.encoding end + + # Ensure, we can set connection params using the example of Generic + # Query Optimizer (geqo). It is 'on' per default. + def test_connection_options + params = ActiveRecord::Base.connection_config.dup + params[:options] = "-c geqo=off" + NonExistentTable.establish_connection(params) + + # Verify the connection param has been applied. + expect = NonExistentTable.connection.query('show geqo').first.first + assert_equal 'off', expect + end end end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb new file mode 100644 index 0000000000..33bf4478cc --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -0,0 +1,89 @@ +require "cases/helper" + +class PostgresqlHstoreTest < ActiveRecord::TestCase + class Hstore < ActiveRecord::Base + self.table_name = 'hstores' + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.transaction do + @connection.create_table('hstores') do |t| + t.hstore 'tags' + end + end + rescue ActiveRecord::StatementInvalid + return skip "do not test on PG without hstore" + end + end + + def teardown + @connection.execute 'drop table if exists hstores' + end + + def test_column + column = Hstore.columns.find { |c| c.name == 'tags' } + assert column + assert_equal :hstore, column.type + end + + def test_type_cast_hstore + column = Hstore.columns.find { |c| c.name == 'tags' } + assert column + + data = "\"1\"=>\"2\"" + hash = column.class.cast_hstore data + assert_equal({'1' => '2'}, hash) + assert_equal({'1' => '2'}, column.type_cast(data)) + end + + def test_select + @connection.execute "insert into hstores (tags) VALUES ('1=>2')" + x = Hstore.find :first + assert_equal({'1' => '2'}, x.tags) + end + + def test_select_multikey + @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" + x = Hstore.find :first + assert_equal({'1' => '2', '2' => '3'}, x.tags) + end + + def test_create + assert_cycle('a' => 'b', '1' => '2') + end + + def test_quotes + assert_cycle('a' => 'b"ar', '1"foo' => '2') + end + + def test_whitespace + assert_cycle('a b' => 'b ar', '1"foo' => '2') + end + + def test_backslash + assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2') + end + + def test_comma + assert_cycle('a, b' => 'bar', '1"foo' => '2') + end + + def test_arrow + assert_cycle('a=>b' => 'bar', '1"foo' => '2') + end + + private + def assert_cycle hash + x = Hstore.create!(:tags => hash) + x.reload + assert_equal(hash, x.tags) + + # make sure updates work + x.tags = hash + x.save! + x.reload + assert_equal(hash, x.tags) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 467e5d7b86..18670b4177 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -26,23 +26,23 @@ class SchemaTest < ActiveRecord::TestCase PK_TABLE_NAME = 'table_with_pk' class Thing1 < ActiveRecord::Base - set_table_name "test_schema.things" + self.table_name = "test_schema.things" end class Thing2 < ActiveRecord::Base - set_table_name "test_schema2.things" + self.table_name = "test_schema2.things" end class Thing3 < ActiveRecord::Base - set_table_name 'test_schema."things.table"' + self.table_name = 'test_schema."things.table"' end class Thing4 < ActiveRecord::Base - set_table_name 'test_schema."Things"' + self.table_name = 'test_schema."Things"' end class Thing5 < ActiveRecord::Base - set_table_name 'things' + self.table_name = 'things' end def setup @@ -68,11 +68,15 @@ class SchemaTest < ActiveRecord::TestCase end def test_schema_change_with_prepared_stmt + altered = false @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]] @connection.exec_query "alter table developers add column zomg int", 'sql', [] + altered = true @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]] ensure - @connection.exec_query "alter table developers drop column if exists zomg", 'sql', [] + # We are not using DROP COLUMN IF EXISTS because that syntax is only + # supported by pg 9.X + @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered end def test_table_exists? diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb index 5f08f79171..9e7b08ef34 100644 --- a/activerecord/test/cases/adapters/postgresql/utils_test.rb +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -1,3 +1,5 @@ +require 'cases/helper' + class PostgreSQLUtilsTest < ActiveSupport::TestCase include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb index 303ba9245a..66e07b71a0 100644 --- a/activerecord/test/cases/adapters/postgresql/view_test.rb +++ b/activerecord/test/cases/adapters/postgresql/view_test.rb @@ -14,7 +14,7 @@ class ViewTest < ActiveRecord::TestCase ] class ThingView < ActiveRecord::Base - set_table_name 'test_schema.view_things' + self.table_name = 'test_schema.view_things' end def setup diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index e0152e7ccf..46da1b0a2b 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -70,9 +70,9 @@ module ActiveRecord assert_equal bd.to_f, @conn.type_cast(bd, nil) end - def test_type_cast_unknown + def test_type_cast_unknown_should_raise_error obj = Class.new.new - assert_equal YAML.dump(obj), @conn.type_cast(obj, nil) + assert_raise(TypeError) { @conn.type_cast(obj, nil) } end def test_quoted_id diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 97b56d38d7..17bde6cb62 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -23,8 +23,6 @@ module ActiveRecord end def test_column_types - return skip('only test encoding on 1.9') unless "<3".encoding_aware? - owner = Owner.create!(:name => "hello".encode('ascii-8bit')) owner.reload select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ', ' @@ -144,8 +142,6 @@ module ActiveRecord end def test_quote_binary_column_escapes_it - return unless "<3".respond_to?(:encode) - DualEncoding.connection.execute(<<-eosql) CREATE TABLE dual_encodings ( id integer PRIMARY KEY AUTOINCREMENT, @@ -159,9 +155,7 @@ module ActiveRecord assert_equal str, binary.data ensure - if "<3".respond_to?(:encode) - DualEncoding.connection.drop_table('dual_encodings') - end + DualEncoding.connection.drop_table('dual_encodings') end def test_execute diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index ff376a68d8..6733f3e889 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -61,7 +61,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_cascaded_eager_association_loading_with_duplicated_includes - categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null") + categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null").references(:categorizations) assert_nothing_raised do assert_equal 3, categories.count assert_equal 3, categories.all.size @@ -69,7 +69,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_cascaded_eager_association_loading_with_twice_includes_edge_cases - categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null") + categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null").references(:posts) assert_nothing_raised do assert_equal 3, categories.count assert_equal 3, categories.all.size @@ -127,7 +127,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase silly.parent_id = 1 assert silly.save - topics = Topic.find(:all, :include => :replies, :order => 'topics.id, replies_topics.id') + topics = Topic.find(:all, :include => :replies, :order => ['topics.id', 'replies_topics.id']) assert_no_queries do assert_equal 2, topics[0].replies.size assert_equal 0, topics[1].replies.size diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb index d75791cab9..7965bb404c 100644 --- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -4,7 +4,7 @@ require 'models/tagging' module Namespaced class Post < ActiveRecord::Base - set_table_name 'posts' + self.table_name = 'posts' has_one :tagging, :as => :taggable, :class_name => 'Tagging' end end diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index 2cf9f89c3c..1e1958410c 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -6,7 +6,6 @@ require 'models/comment' require 'models/category' require 'models/categorization' require 'models/tagging' -require 'active_support/core_ext/array/random_access' module Remembered extend ActiveSupport::Concern diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index c6e451fc57..b79c69bbb5 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -61,7 +61,10 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_loading_conditions_with_or - posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'") + posts = authors(:david).posts.find( + :all, :include => :comments, :references => :comments, + :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'" + ) assert_nil posts.detect { |p| p.author_id != authors(:david).id }, "expected to find only david's posts" end @@ -164,7 +167,7 @@ class EagerAssociationTest < ActiveRecord::TestCase car_post.categories << categories(:technology) comment = car_post.comments.create!(:body => "hmm") - categories = Category.find(:all, :conditions => ["posts.id=?", car_post.id], + categories = Category.find(:all, :conditions => { 'posts.id' => car_post.id }, :include => {:posts => :comments}) categories.each do |category| assert_equal [comment], category.posts[0].comments @@ -252,6 +255,50 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_nested_loading_through_has_one_association + aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}) + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_order + aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'author_addresses.id') + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_order_on_association + aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'authors.id') + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_order_on_nested_association + aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'posts.id') + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_conditions + aa = AuthorAddress.find( + author_addresses(:david_address).id, :include => {:author => :posts}, + :conditions => "author_addresses.id > 0", :references => :author_addresses + ) + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_conditions_on_association + aa = AuthorAddress.find( + author_addresses(:david_address).id, :include => {:author => :posts}, + :conditions => "authors.id > 0", :references => :authors + ) + assert_equal aa.author.posts.count, aa.author.posts.length + end + + def test_nested_loading_through_has_one_association_with_conditions_on_nested_association + aa = AuthorAddress.find( + author_addresses(:david_address).id, :include => {:author => :posts}, + :conditions => "posts.id > 0", :references => :posts + ) + assert_equal aa.author.posts.count, aa.author.posts.length + end + def test_eager_association_loading_with_belongs_to_and_foreign_keys pets = Pet.find(:all, :include => :owner) assert_equal 3, pets.length @@ -297,7 +344,9 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name assert_nothing_raised do - Comment.find(:all, :include => :post, :conditions => ['posts.id = ?',4]) + ActiveSupport::Deprecation.silence do + Comment.find(:all, :include => :post, :conditions => ['posts.id = ?',4]) + end end end @@ -316,7 +365,9 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_string_with_quoted_table_name quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id') assert_nothing_raised do - Comment.find(:all, :include => :post, :conditions => ["#{quoted_posts_id} = ?",4]) + ActiveSupport::Deprecation.silence do + Comment.find(:all, :include => :post, :conditions => ["#{quoted_posts_id} = ?",4]) + end end end @@ -329,7 +380,9 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id') assert_nothing_raised do - Comment.find(:all, :include => :post, :order => quoted_posts_id) + ActiveSupport::Deprecation.silence do + Comment.find(:all, :include => :post, :order => quoted_posts_id) + end end end @@ -493,22 +546,27 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) + posts = ActiveSupport::Deprecation.silence do + Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) + end assert_equal 2, posts.size - count = Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) + count = ActiveSupport::Deprecation.silence do + Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) + end assert_equal count, posts.size end def test_eager_with_has_many_and_limit_and_high_offset - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ]) + posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => { 'authors.name' => 'David' }) assert_equal 0, posts.size end def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions assert_queries(1) do posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, - :conditions => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ]) + :conditions => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ], + :references => [:authors, :comments]) assert_equal 0, posts.size end end @@ -522,7 +580,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_count_eager_with_has_many_and_limit_and_high_offset - posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ]) + posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => { 'authors.name' => 'David' }) assert_equal 0, posts end @@ -534,7 +592,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_count_performed_on_a_has_many_association_with_multi_table_conditional author = authors(:david) author_posts_without_comments = author.posts.select { |post| post.comments.blank? } - assert_equal author_posts_without_comments.size, author.posts.count(:all, :include => :comments, :conditions => 'comments.id is null') + assert_equal author_posts_without_comments.size, author.posts.count(:all, :include => :comments, :conditions => 'comments.id is null', :references => :comments) end def test_eager_count_performed_on_a_has_many_through_association_with_multi_table_conditional @@ -573,6 +631,7 @@ class EagerAssociationTest < ActiveRecord::TestCase posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", + :references => :comments, :limit => 2 ) assert_equal 2, posts.size @@ -580,6 +639,7 @@ class EagerAssociationTest < ActiveRecord::TestCase count = Post.count( :include => [ :comments, :author ], :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", + :references => [:authors, :comments], :limit => 2 ) assert_equal count, posts.size @@ -589,7 +649,8 @@ class EagerAssociationTest < ActiveRecord::TestCase posts = nil Post.send(:with_scope, :find => { :include => :comments, - :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'" + :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", + :references => :comments }) do posts = authors(:david).posts.find(:all, :limit => 2) assert_equal 2, posts.size @@ -597,7 +658,8 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.send(:with_scope, :find => { :include => [ :comments, :author ], - :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')" + :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", + :references => [:authors, :comments] }) do count = Post.count(:limit => 2) assert_equal count, posts.size @@ -609,6 +671,7 @@ class EagerAssociationTest < ActiveRecord::TestCase posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", + :references => :comments, :limit => 2 ) assert_equal 2, posts.size @@ -616,6 +679,7 @@ class EagerAssociationTest < ActiveRecord::TestCase count = Post.count( :include => [ :comments, :author ], :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", + :references => [:authors, :comments], :limit => 2 ) assert_equal count, posts.size @@ -623,9 +687,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_scoped_order_using_association_limiting_without_explicit_scope - posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :order => 'posts.id DESC', :limit => 2) + posts_with_explicit_order = Post.find( + :all, :conditions => 'comments.id is not null', :references => :comments, + :include => :comments, :order => 'posts.id DESC', :limit => 2 + ) posts_with_scoped_order = Post.send(:with_scope, :find => {:order => 'posts.id DESC'}) do - Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :limit => 2) + Post.find( + :all, :conditions => 'comments.id is not null', + :references => :comments, :include => :comments, :limit => 2 + ) end assert_equal posts_with_explicit_order, posts_with_scoped_order end @@ -738,17 +808,49 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_limited_eager_with_order - assert_equal posts(:thinking, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title)', :limit => 2, :offset => 1) - assert_equal posts(:sti_post_and_comments, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1) + assert_equal( + posts(:thinking, :sti_comments), + Post.find( + :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + :order => 'UPPER(posts.title)', :limit => 2, :offset => 1 + ) + ) + assert_equal( + posts(:sti_post_and_comments, :sti_comments), + Post.find( + :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1 + ) + ) end def test_limited_eager_with_multiple_order_columns - assert_equal posts(:thinking, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => ['UPPER(posts.title)', 'posts.id'], :limit => 2, :offset => 1) - assert_equal posts(:sti_post_and_comments, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => ['UPPER(posts.title) DESC', 'posts.id'], :limit => 2, :offset => 1) + assert_equal( + posts(:thinking, :sti_comments), + Post.find( + :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + :order => ['UPPER(posts.title)', 'posts.id'], :limit => 2, :offset => 1 + ) + ) + assert_equal( + posts(:sti_post_and_comments, :sti_comments), + Post.find( + :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + :order => ['UPPER(posts.title) DESC', 'posts.id'], :limit => 2, :offset => 1 + ) + ) end def test_limited_eager_with_numeric_in_association - assert_equal people(:david, :susan), Person.find(:all, :include => [:readers, :primary_contact, :number1_fan], :conditions => "number1_fans_people.first_name like 'M%'", :order => 'people.id', :limit => 2, :offset => 0) + assert_equal( + people(:david, :susan), + Person.find( + :all, :include => [:readers, :primary_contact, :number1_fan], + :conditions => "number1_fans_people.first_name like 'M%'", + :references => :number1_fans_people, + :order => 'people.id', :limit => 2, :offset => 0 + ) + ) end def test_preload_with_interpolation @@ -863,11 +965,11 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_count_with_include if current_adapter?(:SybaseAdapter) - assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15") + assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15", :references => :comments) elsif current_adapter?(:OpenBaseAdapter) - assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(FETCHBLOB(comments.body)) > 15") + assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(FETCHBLOB(comments.body)) > 15", :references => :comments) else - assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(comments.body) > 15") + assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(comments.body) > 15", :references => :comments) end end @@ -878,7 +980,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_conditions_on_join_table_with_include_and_limit - assert_equal 3, Developer.find(:all, :include => 'projects', :conditions => 'developers_projects.access_level = 1', :limit => 5).size + assert_equal 3, Developer.find(:all, :include => 'projects', :conditions => { 'developers_projects.access_level' => 1 }, :limit => 5).size end def test_order_on_join_table_with_include_and_limit @@ -1060,4 +1162,11 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.includes(:comments).order(nil).where(:comments => {:body => "Thank you for the welcome"}).first end end + + def test_deep_including_through_habtm + posts = Post.find(:all, :include => {:categories => :categorizations}, :order => "posts.id") + assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length } + assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length } + assert_no_queries { assert_equal 2, posts[1].categories[0].categorizations.length } + end end diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 8dc1423375..d7c489c2b5 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -36,11 +36,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase end def test_marshalling_extensions - if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" - return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ - "to be a Ruby bug.") - end - david = developers(:david) assert_equal projects(:action_controller), david.projects.find_most_recent @@ -51,11 +46,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase end def test_marshalling_named_extensions - if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" - return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ - "to be a Ruby bug.") - end - david = developers(:david) assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent @@ -71,6 +61,12 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer) end + def test_proxy_association_after_scoped + post = posts(:welcome) + assert_equal post.association(:comments), post.comments.the_association + assert_equal post.association(:comments), post.comments.scoped.the_association + end + private def extension_name(model) diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 34d90cc395..f457dfb9b3 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -23,7 +23,7 @@ require 'models/treaty' require 'active_support/core_ext/string/conversions' class ProjectWithAfterCreateHook < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' has_and_belongs_to_many :developers, :class_name => "DeveloperForProjectWithAfterCreateHook", :join_table => "developers_projects", @@ -39,7 +39,7 @@ class ProjectWithAfterCreateHook < ActiveRecord::Base end class DeveloperForProjectWithAfterCreateHook < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' has_and_belongs_to_many :projects, :class_name => "ProjectWithAfterCreateHook", :join_table => "developers_projects", @@ -48,7 +48,7 @@ class DeveloperForProjectWithAfterCreateHook < ActiveRecord::Base end class ProjectWithSymbolsForKeys < ActiveRecord::Base - set_table_name 'projects' + self.table_name = 'projects' has_and_belongs_to_many :developers, :class_name => "DeveloperWithSymbolsForKeys", :join_table => :developers_projects, @@ -57,7 +57,7 @@ class ProjectWithSymbolsForKeys < ActiveRecord::Base end class DeveloperWithSymbolsForKeys < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' has_and_belongs_to_many :projects, :class_name => "ProjectWithSymbolsForKeys", :join_table => :developers_projects, @@ -66,7 +66,7 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base end class DeveloperWithCounterSQL < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' has_and_belongs_to_many :projects, :class_name => "DeveloperWithCounterSQL", :join_table => "developers_projects", @@ -77,7 +77,7 @@ end class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, - :parrots, :pirates, :treasures, :price_estimates, :tags, :taggings + :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings def setup_data_for_habtm_case ActiveRecord::Base.connection.execute('delete from countries_treaties') @@ -445,6 +445,26 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert david.projects(true).empty? end + def test_destroy_associations_destroys_multiple_associations + george = parrots(:george) + assert !george.pirates.empty? + assert !george.treasures.empty? + + assert_no_difference "Pirate.count" do + assert_no_difference "Treasure.count" do + george.destroy_associations + end + end + + join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}") + assert join_records.empty? + assert george.pirates(true).empty? + + join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}") + assert join_records.empty? + assert george.treasures(true).empty? + end + def test_deprecated_push_with_attributes_was_removed jamis = developers(:jamis) assert_raise(NoMethodError) do @@ -659,7 +679,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_join_table_alias - assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL').size + assert_equal( + 3, + Developer.find( + :all, :include => {:projects => :developers}, :references => :developers_projects_join, + :conditions => 'developers_projects_join.joined_on IS NOT NULL' + ).size + ) end def test_join_with_group @@ -669,7 +695,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end Project.columns.each { |c| group << "projects.#{c.name}" } - assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL', :group => group.join(",")).size + assert_equal( + 3, + Developer.find( + :all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL', + :references => :developers_projects_join, :group => group.join(",") + ).size + ) end def test_find_grouped @@ -805,12 +837,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase # clear cache possibly created by other tests david.projects.reset_column_information - # One query for columns, one for primary key - assert_queries(2) { david.projects.columns; david.projects.columns } + assert_queries(1) { david.projects.columns; david.projects.columns } ## and again to verify that reset_column_information clears the cache correctly david.projects.reset_column_information - assert_queries(2) { david.projects.columns; david.projects.columns } + assert_queries(1) { david.projects.columns; david.projects.columns } end def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index a60af7c046..f1a341437f 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -8,6 +8,7 @@ require 'models/reply' require 'models/category' require 'models/post' require 'models/author' +require 'models/essay' require 'models/comment' require 'models/person' require 'models/reader' @@ -56,12 +57,22 @@ class HasManyAssociationsTestForCountDistinctWithFinderSql < ActiveRecord::TestC end end +class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase + fixtures :authors, :posts, :comments + + def test_should_generate_valid_sql + author = authors(:david) + # this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression + # if the reorder clauses are not correctly handled + assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.taggings_count DESC').last + end +end class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, - :people, :posts, :readers, :taggings, :cars + :people, :posts, :readers, :taggings, :cars, :essays def setup Client.destroyed_client_ids.clear @@ -1390,6 +1401,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.last end end + + def test_custom_primary_key_on_new_record_should_fetch_with_query + author = Author.new(:name => "David") + assert !author.essays.loaded? + + assert_queries 1 do + assert_equal 1, author.essays.size + end + + assert_equal author.essays, Essay.find_all_by_writer_id("David") + + end + + def test_has_many_custom_primary_key + david = authors(:david) + assert_equal david.essays, Essay.find_all_by_writer_id("David") + end + + def test_blank_custom_primary_key_on_new_record_should_not_run_queries + author = Author.new + assert !author.essays.loaded? + + assert_queries 0 do + assert_equal 0, author.essays.size + end + end def test_calling_first_or_last_with_find_options_on_loaded_association_should_fetch_with_query firm = companies(:first_firm) diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 7a6aba6a6b..4612bc2618 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -528,6 +528,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort end + def test_get_ids_for_has_many_through_with_conditions_should_not_preload + Tagging.create!(:taggable_type => 'Post', :taggable_id => posts(:welcome).id, :tag => tags(:misc)) + ActiveRecord::Associations::Preloader.expects(:new).never + posts(:welcome).misc_tag_ids + end + def test_get_ids_for_loaded_associations person = people(:michael) person.posts(true) @@ -841,7 +847,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_preloading_empty_through_association_via_joins person = Person.create!(:first_name => "Gaga") - person = Person.where(:id => person.id).where('readers.id = 1 or 1=1').includes(:posts).to_a.first + person = Person.where(:id => person.id).where('readers.id = 1 or 1=1').references(:readers).includes(:posts).to_a.first assert person.posts.loaded?, 'person.posts should be loaded' assert_equal [], person.posts diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index e5e9ca6131..68a1e62328 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -67,7 +67,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_find_with_implicit_inner_joins_does_not_set_associations authors = Author.joins(:posts).select('authors.*') assert !authors.empty?, "expected authors to be non-empty" - assert authors.all? {|a| !a.send(:instance_variable_names).include?("@posts")}, "expected no authors to have the @posts association loaded" + assert authors.all? { |a| !a.instance_variable_defined?(:@posts) }, "expected no authors to have the @posts association loaded" end def test_count_honors_implicit_inner_joins diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 4ce8b85098..301755249c 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -362,7 +362,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end assert_raise ActiveRecord::EagerLoadPolymorphicError do - tags(:general).taggings.find(:all, :include => :taggable, :conditions => 'bogus_table.column = 1') + tags(:general).taggings.find(:all, :include => :taggable, :references => :bogus_table, :conditions => 'bogus_table.column = 1') end end @@ -733,7 +733,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}" Post.find(post_id).update_column :type, class_name klass = Object.const_set(class_name, Class.new(ActiveRecord::Base)) - klass.set_table_name 'posts' + klass.table_name = 'posts' klass.send(association, association_name, :as => :taggable, :dependent => dependency) klass.find(post_id) end diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 530f5212a2..03d99d19f6 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -505,7 +505,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase def test_nested_has_many_through_with_conditions_on_through_associations_preload_via_joins # Pointless condition to force single-query loading assert_includes_and_joins_equal( - Author.where('tags.id = tags.id'), + Author.where('tags.id = tags.id').references(:tags), [authors(:bob)], :misc_post_first_blue_tags ) end @@ -526,7 +526,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase def test_nested_has_many_through_with_conditions_on_source_associations_preload_via_joins # Pointless condition to force single-query loading assert_includes_and_joins_equal( - Author.where('tags.id = tags.id'), + Author.where('tags.id = tags.id').references(:tags), [authors(:bob)], :misc_post_first_blue_tags_2 ) end @@ -545,6 +545,15 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert_equal [organizations(:nsa)], organizations end + def test_nested_has_many_through_should_not_be_autosaved + c = Categorization.new + c.author = authors(:david) + c.post_taggings.to_a + assert !c.post_taggings.empty? + c.save + assert !c.post_taggings.empty? + end + private def assert_includes_and_joins_equal(query, expected, association) diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index ffe2993e0f..017905e0ac 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'models/computer' require 'models/developer' require 'models/project' require 'models/company' @@ -28,7 +29,7 @@ class AssociationsTest < ActiveRecord::TestCase molecule.electrons.create(:name => 'electron_1') molecule.electrons.create(:name => 'electron_2') - liquids = Liquid.includes(:molecules => :electrons).where('molecules.id is not null') + liquids = Liquid.includes(:molecules => :electrons).references(:molecules).where('molecules.id is not null') assert_equal 1, liquids[0].molecules.length end @@ -128,6 +129,11 @@ class AssociationsTest < ActiveRecord::TestCase end end + def test_association_with_references + firm = companies(:first_firm) + assert_equal ['foo'], firm.association_with_references.scoped.references_values + end + end class AssociationProxyTest < ActiveRecord::TestCase @@ -273,3 +279,18 @@ class OverridingAssociationsTest < ActiveRecord::TestCase ) end end + +class GeneratedMethodsTest < ActiveRecord::TestCase + fixtures :developers, :computers, :posts, :comments + def test_association_methods_override_attribute_methods_of_same_name + assert_equal(developers(:david), computers(:workstation).developer) + # this next line will fail if the attribute methods module is generated lazily + # after the association methods module is generated + assert_equal(developers(:david), computers(:workstation).developer) + assert_equal(developers(:david).id, computers(:workstation)[:developer]) + end + + def test_model_method_overrides_association_method + assert_equal(comments(:greetings).body, posts(:welcome).first_comment) + end +end diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index e03ed33591..764305459d 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'active_support/core_ext/object/inclusion' +require 'thread' module ActiveRecord module AttributeMethods @@ -14,8 +15,18 @@ module ActiveRecord def setup @klass = Class.new do + def self.superclass; Base; end + def self.active_record_super; Base; end + def self.base_class; self; end + include ActiveRecord::AttributeMethods - include ActiveRecord::AttributeMethods::Read + + def self.define_attribute_methods + # Created in the inherited/included hook for "proper" ARs + @attribute_methods_mutex ||= Mutex.new + + super + end def self.column_names %w{ one two three } @@ -33,9 +44,6 @@ module ActiveRecord [name, FakeColumn.new(name)] }] end - - def self.serialized_attributes; {}; end - def self.base_class; self; end end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 9300c57819..d6de668a17 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -53,6 +53,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert Boolean.find(b4.id).attribute_present?(:value) end + def test_caching_nil_primary_key + klass = Class.new(Minimalistic) + klass.expects(:reset_primary_key).returns(nil).once + 2.times { klass.primary_key } + end + def test_attribute_keys_on_new_instance t = Topic.new assert_equal nil, t.title, "The topics table has a title column, so it should be nil" @@ -96,7 +102,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_respond_to? topic = Topic.find(1) assert_respond_to topic, "title" - assert_respond_to topic, "_title" assert_respond_to topic, "title?" assert_respond_to topic, "title=" assert_respond_to topic, :title @@ -113,9 +118,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_not_nil keyboard.key_number assert_equal keyboard.key_number, keyboard.id assert keyboard.respond_to?('key_number') - assert keyboard.respond_to?('_key_number') assert keyboard.respond_to?('id') - assert keyboard.respond_to?('_id') end # Syck calls respond_to? before actually calling initialize @@ -255,8 +258,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic.send(:write_attribute, :title, "Still another topic") assert_equal "Still another topic", topic.title - topic.send(:write_attribute, "title", "Still another topic: part 2") + topic[:title] = "Still another topic: part 2" assert_equal "Still another topic: part 2", topic.title + + topic.send(:write_attribute, "title", "Still another topic: part 3") + assert_equal "Still another topic: part 3", topic.title + + topic["title"] = "Still another topic: part 4" + assert_equal "Still another topic: part 4", topic.title end def test_read_attribute @@ -309,6 +318,39 @@ class AttributeMethodsTest < ActiveRecord::TestCase # puts "" end + def test_overridden_write_attribute + topic = Topic.new + def topic.write_attribute(attr_name, value) + super(attr_name, value.downcase) + end + + topic.send(:write_attribute, :title, "Yet another topic") + assert_equal "yet another topic", topic.title + + topic[:title] = "Yet another topic: part 2" + assert_equal "yet another topic: part 2", topic.title + + topic.send(:write_attribute, "title", "Yet another topic: part 3") + assert_equal "yet another topic: part 3", topic.title + + topic["title"] = "Yet another topic: part 4" + assert_equal "yet another topic: part 4", topic.title + end + + def test_overridden_read_attribute + topic = Topic.new + topic.title = "Stop changing the topic" + def topic.read_attribute(attr_name) + super(attr_name).upcase + end + + assert_equal "STOP CHANGING THE TOPIC", topic.send(:read_attribute, "title") + assert_equal "STOP CHANGING THE TOPIC", topic["title"] + + assert_equal "STOP CHANGING THE TOPIC", topic.send(:read_attribute, :title) + assert_equal "STOP CHANGING THE TOPIC", topic[:title] + end + def test_read_overridden_attribute topic = Topic.new(:title => 'a') def topic.title() 'b' end @@ -507,6 +549,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_write_time_to_date_attributes + in_time_zone "Pacific Time (US & Canada)" do + record = @target.new + record.last_read = Time.utc(2010, 1, 1, 10) + assert_equal Date.civil(2010, 1, 1), record.last_read + end + end + def test_time_attributes_are_retrieved_in_current_time_zone in_time_zone "Pacific Time (US & Canada)" do utc_time = Time.utc(2008, 1, 1) @@ -542,6 +592,17 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_setting_time_zone_aware_read_attribute + utc_time = Time.utc(2008, 1, 1) + cst_time = utc_time.in_time_zone("Central Time (US & Canada)") + in_time_zone "Pacific Time (US & Canada)" do + record = @target.create(:written_on => cst_time).reload + assert_equal utc_time, record[:written_on] + assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record[:written_on].time_zone + assert_equal Time.utc(2007, 12, 31, 16), record[:written_on].time + end + end + def test_setting_time_zone_aware_attribute_with_string utc_time = Time.utc(2008, 1, 1) (-11..13).each do |timezone_offset| @@ -561,6 +622,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase record = @target.new record.written_on = ' ' assert_nil record.written_on + assert_nil record[:written_on] end end @@ -675,7 +737,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = Topic.new(:id => 5) topic.id = 5 - topic.method(:id).owner.send(:remove_method, :id) + topic.method(:id).owner.send(:undef_method, :id) assert_deprecated do assert_equal 5, topic.id @@ -684,19 +746,39 @@ class AttributeMethodsTest < ActiveRecord::TestCase Topic.undefine_attribute_methods end + def test_read_attribute_with_nil_should_not_asplode + assert_equal nil, Topic.new.read_attribute(nil) + end + + # If B < A, and A defines an accessor for 'foo', we don't want to override + # that by defining a 'foo' method in the generated methods module for B. + # (That module will be inserted between the two, e.g. [B, <GeneratedAttributes>, A].) + def test_inherited_custom_accessors + klass = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + self.abstract_class = true + def title; "omg"; end + def title=(val); self.author_name = val; end + end + subklass = Class.new(klass) + [klass, subklass].each(&:define_attribute_methods) + + topic = subklass.find(1) + assert_equal "omg", topic.title + + topic.title = "lol" + assert_equal "lol", topic.author_name + end + private def cached_columns - @cached_columns ||= (time_related_columns_on_topic + serialized_columns_on_topic).map(&:name) + @cached_columns ||= time_related_columns_on_topic.map(&:name) end def time_related_columns_on_topic Topic.columns.select { |c| c.type.in?([:time, :date, :datetime, :timestamp]) } end - def serialized_columns_on_topic - Topic.columns.select { |c| Topic.serialized_attributes.include?(c.name) } - end - def in_time_zone(zone) old_zone = Time.zone old_tz = ActiveRecord::Base.time_zone_aware_attributes diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 4ad2cdfc7e..1376810adf 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -347,6 +347,17 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test client.save! assert_no_queries { assert_equal apple, client.firm } end + + def test_validation_does_not_validate_stale_association_target + valid_developer = Developer.create!(:name => "Dude", :salary => 50_000) + invalid_developer = Developer.new() + + auditlog = AuditLog.new(:message => "foo") + auditlog.developer = invalid_developer + auditlog.developer_id = valid_developer.id + + assert auditlog.valid? + end end class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase @@ -575,6 +586,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase self.use_transactional_fixtures = false unless supports_savepoints? def setup + super @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning') end @@ -897,6 +909,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase self.use_transactional_fixtures = false unless supports_savepoints? def setup + super @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning') end @@ -1020,6 +1033,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase self.use_transactional_fixtures = false unless supports_savepoints? def setup + super @ship = Ship.create(:name => 'Nights Dirty Lightning') @pirate = @ship.create_pirate(:catchphrase => "Don' botharrr talkin' like one, savvy?") end @@ -1268,6 +1282,7 @@ class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase self.use_transactional_fixtures = false unless supports_savepoints? def setup + super @association_name = :birds @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @@ -1282,6 +1297,7 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::T self.use_transactional_fixtures = false unless supports_savepoints? def setup + super @association_name = :parrots @habtm = true @@ -1297,6 +1313,7 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te self.use_transactional_fixtures = false unless supports_savepoints? def setup + super @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @pirate.birds.create(:name => 'cookoo') end @@ -1313,6 +1330,7 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes self.use_transactional_fixtures = false unless supports_savepoints? def setup + super @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @pirate.create_ship(:name => 'titanic') super @@ -1335,6 +1353,7 @@ class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord:: self.use_transactional_fixtures = false unless supports_savepoints? def setup + super @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") end @@ -1355,6 +1374,7 @@ class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::Test self.use_transactional_fixtures = false unless supports_savepoints? def setup + super @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") end @@ -1377,6 +1397,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas self.use_transactional_fixtures = false unless supports_savepoints? def setup + super @pirate = Pirate.new end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 997c9e7e9d..0ea1b824e1 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -23,10 +23,18 @@ require 'models/edge' require 'models/joke' require 'models/bulb' require 'models/bird' +require 'models/teapot' require 'rexml/document' require 'active_support/core_ext/exception' require 'bcrypt' +class FirstAbstractClass < ActiveRecord::Base + self.abstract_class = true +end +class SecondAbstractClass < FirstAbstractClass + self.abstract_class = true +end +class Photo < SecondAbstractClass; end class Category < ActiveRecord::Base; end class Categorization < ActiveRecord::Base; end class Smarts < ActiveRecord::Base; end @@ -69,6 +77,16 @@ end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts + def test_generated_methods_modules + modules = Computer.ancestors + assert modules.include?(Computer::GeneratedFeatureMethods) + assert_equal(Computer::GeneratedFeatureMethods, Computer.generated_feature_methods) + assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_feature_methods), + "generated_attribute_methods must be higher in inheritance hierarchy than generated_feature_methods") + assert_not_equal Computer.generated_feature_methods, Post.generated_feature_methods + assert(modules.index(Computer.generated_attribute_methods) < modules.index(ActiveRecord::Base.ancestors[1])) + end + def test_column_names_are_escaped conn = ActiveRecord::Base.connection classname = conn.class.name[/[^:]*$/] @@ -169,6 +187,31 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_previously_changed + topic = Topic.find :first + topic.title = '<3<3<3' + assert_equal({}, topic.previous_changes) + + topic.save! + expected = ["The First Topic", "<3<3<3"] + assert_equal(expected, topic.previous_changes['title']) + end + + def test_previously_changed_dup + topic = Topic.find :first + topic.title = '<3<3<3' + topic.save! + + t2 = topic.dup + + assert_equal(topic.previous_changes, t2.previous_changes) + + topic.title = "lolwut" + topic.save! + + assert_not_equal(topic.previous_changes, t2.previous_changes) + end + def test_preserving_time_objects assert_kind_of( Time, Topic.find(1).bonus_time, @@ -561,10 +604,12 @@ class BasicsTest < ActiveRecord::TestCase weird = Weird.create('a$b' => 'value') weird.reload assert_equal 'value', weird.send('a$b') + assert_equal 'value', weird.read_attribute('a$b') weird.update_column('a$b', 'value2') weird.reload assert_equal 'value2', weird.send('a$b') + assert_equal 'value2', weird.read_attribute('a$b') end def test_multiparameter_attributes_on_date @@ -644,7 +689,7 @@ class BasicsTest < ActiveRecord::TestCase } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on end def test_multiparameter_attributes_on_time_with_no_date @@ -893,7 +938,7 @@ class BasicsTest < ActiveRecord::TestCase } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time + assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time end def test_boolean @@ -947,10 +992,9 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "b", duped_topic.title # test if the attribute values have been duped - topic.title = {"a" => "b"} duped_topic = topic.dup - duped_topic.title["a"] = "c" - assert_equal "b", topic.title["a"] + duped_topic.title.replace "c" + assert_equal "a", topic.title # test if attributes set as part of after_initialize are duped correctly assert_equal topic.author_email_address, duped_topic.author_email_address @@ -961,8 +1005,7 @@ class BasicsTest < ActiveRecord::TestCase assert_not_equal duped_topic.id, topic.id duped_topic.reload - # FIXME: I think this is poor behavior, and will fix it with #5686 - assert_equal({'a' => 'c'}.to_yaml, duped_topic.title) + assert_equal("c", duped_topic.title) end def test_dup_with_aggregate_of_same_name_as_attribute @@ -1185,19 +1228,6 @@ class BasicsTest < ActiveRecord::TestCase assert(auto.id > 0) end - def quote_column_name(name) - "<#{name}>" - end - - def test_quote_keys - ar = AutoId.new - source = {"foo" => "bar", "baz" => "quux"} - actual = ar.send(:quote_columns, self, source) - inverted = actual.invert - assert_equal("<foo>", inverted["bar"]) - assert_equal("<baz>", inverted["quux"]) - end - def test_sql_injection_via_find assert_raise(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do Topic.find("123456 OR id > 0") @@ -1235,6 +1265,27 @@ class BasicsTest < ActiveRecord::TestCase assert_equal(myobj, topic.content) end + def test_serialized_attribute_in_base_class + Topic.serialize("content", Hash) + + hash = { 'content1' => 'value1', 'content2' => 'value2' } + important_topic = ImportantTopic.create("content" => hash) + assert_equal(hash, important_topic.content) + + important_topic.reload + assert_equal(hash, important_topic.content) + end + + def test_serialized_attribute_declared_in_subclass + hash = { 'important1' => 'value1', 'important2' => 'value2' } + important_topic = ImportantTopic.create("important" => hash) + assert_equal(hash, important_topic.important) + + important_topic.reload + assert_equal(hash, important_topic.important) + assert_equal(hash, important_topic.read_attribute(:important)) + end + def test_serialized_time_attribute myobj = Time.local(2008,1,1,1,0) topic = Topic.create("content" => myobj).reload @@ -1359,22 +1410,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal author_name, Topic.find(topic.id).author_name end - if RUBY_VERSION < '1.9' - def test_quote_chars - with_kcode('UTF8') do - str = 'The Narrator' - topic = Topic.create(:author_name => str) - assert_equal str, topic.author_name - - assert_kind_of ActiveSupport::Multibyte.proxy_class, str.mb_chars - topic = Topic.find_by_author_name(str.mb_chars) - - assert_kind_of Topic, topic - assert_equal str, topic.author_name, "The right topic should have been found by name even with name passed as Chars" - end - end - end - def test_toggle_attribute assert !topics(:first).approved? topics(:first).toggle!(:approved) @@ -1401,37 +1436,12 @@ class BasicsTest < ActiveRecord::TestCase assert_equal dev, dev.reload end - def test_define_attr_method_with_value - k = Class.new( ActiveRecord::Base ) - k.send(:define_attr_method, :table_name, "foo") - assert_equal "foo", k.table_name - end - - def test_define_attr_method_with_block - k = Class.new( ActiveRecord::Base ) do - class << self - attr_accessor :foo_key - end - end - k.foo_key = "id" - k.send(:define_attr_method, :foo_key) { "sys_" + original_foo_key } - assert_equal "sys_id", k.foo_key - end - - def test_set_table_name_with_value - k = Class.new( ActiveRecord::Base ) - k.table_name = "foo" - assert_equal "foo", k.table_name - k.set_table_name "bar" - assert_equal "bar", k.table_name - end - def test_switching_between_table_name assert_difference("GoodJoke.count") do - Joke.set_table_name "cold_jokes" + Joke.table_name = "cold_jokes" Joke.create - Joke.set_table_name "funny_jokes" + Joke.table_name = "funny_jokes" Joke.create end end @@ -1439,48 +1449,30 @@ class BasicsTest < ActiveRecord::TestCase def test_quoted_table_name_after_set_table_name klass = Class.new(ActiveRecord::Base) - klass.set_table_name "foo" + klass.table_name = "foo" assert_equal "foo", klass.table_name assert_equal klass.connection.quote_table_name("foo"), klass.quoted_table_name - klass.set_table_name "bar" + klass.table_name = "bar" assert_equal "bar", klass.table_name assert_equal klass.connection.quote_table_name("bar"), klass.quoted_table_name end - def test_set_table_name_with_block - k = Class.new( ActiveRecord::Base ) - k.set_table_name { "ks" } - assert_equal "ks", k.table_name - end - - def test_set_primary_key_with_value - k = Class.new( ActiveRecord::Base ) - k.primary_key = "foo" - assert_equal "foo", k.primary_key - k.set_primary_key "bar" - assert_equal "bar", k.primary_key - end - - def test_set_primary_key_with_block + def test_set_table_name_with_inheritance k = Class.new( ActiveRecord::Base ) - k.primary_key = 'id' - k.set_primary_key { "sys_" + original_primary_key } - assert_equal "sys_id", k.primary_key + def k.name; "Foo"; end + def k.table_name; super + "ks"; end + assert_equal "foosks", k.table_name end - def test_set_inheritance_column_with_value - k = Class.new( ActiveRecord::Base ) - k.inheritance_column = "foo" - assert_equal "foo", k.inheritance_column - k.set_inheritance_column "bar" - assert_equal "bar", k.inheritance_column - end - - def test_set_inheritance_column_with_block - k = Class.new( ActiveRecord::Base ) - k.set_inheritance_column { original_inheritance_column + "_id" } - assert_equal "type_id", k.inheritance_column + def test_sequence_name_with_abstract_class + ak = Class.new(ActiveRecord::Base) + ak.abstract_class = true + k = Class.new(ak) + k.table_name = "projects" + orig_name = k.sequence_name + return skip "sequences not supported by db" unless orig_name + assert_equal k.reset_sequence_name, orig_name end def test_count_with_join @@ -1673,10 +1665,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_descends_from_active_record - # Tries to call Object.abstract_class? - assert_raise(NoMethodError) do - ActiveRecord::Base.descends_from_active_record? - end + assert !ActiveRecord::Base.descends_from_active_record? # Abstract subclass of AR::Base. assert LoosePerson.descends_from_active_record? @@ -1699,6 +1688,10 @@ class BasicsTest < ActiveRecord::TestCase # Concrete subclasses an abstract class which has a type column. assert !SubStiPost.descends_from_active_record? + + assert Teapot.descends_from_active_record? + assert !OtherTeapot.descends_from_active_record? + assert CoolTeapot.descends_from_active_record? end def test_find_on_abstract_base_class_doesnt_use_type_condition @@ -1732,7 +1725,7 @@ class BasicsTest < ActiveRecord::TestCase def test_inspect_instance topic = topics(:first) - assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_s(:db)}", updated_at: "#{topic.updated_at.to_s(:db)}">), topic.inspect + assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", important: nil, approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_s(:db)}", updated_at: "#{topic.updated_at.to_s(:db)}">), topic.inspect end def test_inspect_new_instance @@ -1772,7 +1765,7 @@ class BasicsTest < ActiveRecord::TestCase def test_silence_sets_log_level_to_error_in_block original_logger = ActiveRecord::Base.logger log = StringIO.new - ActiveRecord::Base.logger = Logger.new(log) + ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) ActiveRecord::Base.logger.level = Logger::DEBUG ActiveRecord::Base.silence do ActiveRecord::Base.logger.warn "warn" @@ -1786,7 +1779,7 @@ class BasicsTest < ActiveRecord::TestCase def test_silence_sets_log_level_back_to_level_before_yield original_logger = ActiveRecord::Base.logger log = StringIO.new - ActiveRecord::Base.logger = Logger.new(log) + ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) ActiveRecord::Base.logger.level = Logger::WARN ActiveRecord::Base.silence do end @@ -1798,7 +1791,7 @@ class BasicsTest < ActiveRecord::TestCase def test_benchmark_with_log_level original_logger = ActiveRecord::Base.logger log = StringIO.new - ActiveRecord::Base.logger = Logger.new(log) + ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) ActiveRecord::Base.logger.level = Logger::WARN ActiveRecord::Base.benchmark("Debug Topic Count", :level => :debug) { Topic.count } ActiveRecord::Base.benchmark("Warn Topic Count", :level => :warn) { Topic.count } @@ -1813,10 +1806,8 @@ class BasicsTest < ActiveRecord::TestCase def test_benchmark_with_use_silence original_logger = ActiveRecord::Base.logger log = StringIO.new - ActiveRecord::Base.logger = Logger.new(log) - ActiveRecord::Base.benchmark("Logging", :level => :debug, :silence => true) { ActiveRecord::Base.logger.debug "Loud" } + ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) ActiveRecord::Base.benchmark("Logging", :level => :debug, :silence => false) { ActiveRecord::Base.logger.debug "Quiet" } - assert_no_match(/Loud/, log.string) assert_match(/Quiet/, log.string) ensure ActiveRecord::Base.logger = original_logger @@ -1848,9 +1839,9 @@ class BasicsTest < ActiveRecord::TestCase def test_clear_cache! # preheat cache - c1 = Post.columns + c1 = Post.connection.schema_cache.columns['posts'] ActiveRecord::Base.clear_cache! - c2 = Post.columns + c2 = Post.connection.schema_cache.columns['posts'] assert_not_equal c1, c2 end @@ -1867,11 +1858,6 @@ class BasicsTest < ActiveRecord::TestCase end def test_marshal_round_trip - if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" - return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ - "to be a Ruby bug.") - end - expected = posts(:welcome) marshalled = Marshal.dump(expected) actual = Marshal.load(marshalled) @@ -1880,11 +1866,6 @@ class BasicsTest < ActiveRecord::TestCase end def test_marshal_new_record_round_trip - if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" - return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ - "to be a Ruby bug.") - end - marshalled = Marshal.dump(Post.new) post = Marshal.load(marshalled) @@ -1892,11 +1873,6 @@ class BasicsTest < ActiveRecord::TestCase end def test_marshalling_with_associations - if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" - return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ - "to be a Ruby bug.") - end - post = Post.new post.comments.build @@ -1915,7 +1891,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal [], NonExistentTable.attribute_names end - def test_attribtue_names_on_abstract_class + def test_attribute_names_on_abstract_class assert_equal [], AbstractCompany.attribute_names end @@ -1949,4 +1925,17 @@ class BasicsTest < ActiveRecord::TestCase Bird.stubs(:scoped).returns(mock(:uniq => scope)) assert_equal scope, Bird.uniq end + + def test_active_record_super + assert_equal ActiveRecord::Model, ActiveRecord::Base.active_record_super + assert_equal ActiveRecord::Base, Topic.active_record_super + assert_equal Topic, ImportantTopic.active_record_super + assert_equal ActiveRecord::Model, Teapot.active_record_super + assert_equal Teapot, OtherTeapot.active_record_super + assert_equal ActiveRecord::Model, CoolTeapot.active_record_super + end + + def test_table_name_with_2_abstract_subclasses + assert_equal "photos", Photo.table_name + end end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index 06c14cb108..f97aade311 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -12,7 +12,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter) def test_mixed_encoding str = "\x80" - str.force_encoding('ASCII-8BIT') if str.respond_to?(:force_encoding) + str.force_encoding('ASCII-8BIT') binary = Binary.new :name => 'いただきます!', :data => str binary.save! @@ -23,7 +23,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter) # Mysql adapter doesn't properly encode things, so we have to do it if current_adapter?(:MysqlAdapter) - name.force_encoding('UTF-8') if name.respond_to?(:force_encoding) + name.force_encoding('UTF-8') end assert_equal 'いただきます!', name end @@ -33,7 +33,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter) FIXTURES.each do |filename| data = File.read(ASSETS_ROOT + "/#{filename}") - data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding) + data.force_encoding('ASCII-8BIT') data.freeze bin = Binary.new(:data => data) diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index c38814713a..7c9ebf528e 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/company' +require "models/contract" require 'models/topic' require 'models/edge' require 'models/club' @@ -48,11 +49,11 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_get_maximum_of_field_with_include - assert_equal 55, Account.maximum(:credit_limit, :include => :firm, :conditions => "companies.name != 'Summit'") + assert_equal 55, Account.maximum(:credit_limit, :include => :firm, :references => :companies, :conditions => "companies.name != 'Summit'") end def test_should_get_maximum_of_field_with_scoped_include - Account.send :with_scope, :find => { :include => :firm, :conditions => "companies.name != 'Summit'" } do + Account.send :with_scope, :find => { :include => :firm, :references => :companies, :conditions => "companies.name != 'Summit'" } do assert_equal 55, Account.maximum(:credit_limit) end end @@ -269,7 +270,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_not_modify_options_when_using_includes - options = {:conditions => 'companies.id > 1', :include => :firm} + options = {:conditions => 'companies.id > 1', :include => :firm, :references => :companies} options_copy = options.dup Account.count(:all, options) @@ -446,4 +447,35 @@ class CalculationsTest < ActiveRecord::TestCase distinct_authors_for_approved_count = Topic.group(:approved).count(:author_name, :distinct => true)[true] assert_equal distinct_authors_for_approved_count, 2 end + + def test_pluck + assert_equal [1,2,3,4], Topic.order(:id).pluck(:id) + end + + def test_pluck_type_cast + topic = topics(:first) + relation = Topic.where(:id => topic.id) + assert_equal [ topic.approved ], relation.pluck(:approved) + assert_equal [ topic.last_read ], relation.pluck(:last_read) + assert_equal [ topic.written_on ], relation.pluck(:written_on) + end + + def test_pluck_and_uniq + assert_equal [50, 53, 55, 60], Account.order(:credit_limit).uniq.pluck(:credit_limit) + end + + def test_pluck_in_relation + company = Company.first + contract = company.contracts.create! + assert_equal [contract.id], company.contracts.pluck(:id) + end + + def test_pluck_with_serialization + t = Topic.create!(:content => { :foo => :bar }) + assert_equal [{:foo => :bar}], Topic.where(:id => t.id).pluck(:content) + end + + def test_pluck_with_qualified_column_name + assert_equal [1,2,3,4], Topic.order(:id).pluck("topics.id") + end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 7f4d25790b..7690769226 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class CallbackDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' class << self def callback_string(callback_method) @@ -48,7 +48,7 @@ class CallbackDeveloperWithFalseValidation < CallbackDeveloper end class ParentDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' attr_accessor :after_save_called before_validation {|record| record.after_save_called = true} end @@ -58,7 +58,7 @@ class ChildDeveloper < ParentDeveloper end class RecursiveCallbackDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' before_save :on_before_save after_save :on_after_save @@ -79,7 +79,7 @@ class RecursiveCallbackDeveloper < ActiveRecord::Base end class ImmutableDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' validates_inclusion_of :salary, :in => 50000..200000 @@ -98,7 +98,7 @@ class ImmutableDeveloper < ActiveRecord::Base end class ImmutableMethodDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' validates_inclusion_of :salary, :in => 50000..200000 @@ -118,7 +118,7 @@ class ImmutableMethodDeveloper < ActiveRecord::Base end class OnCallbacksDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' before_validation { history << :before_validation } before_validation(:on => :create){ history << :before_validation_on_create } @@ -138,7 +138,7 @@ class OnCallbacksDeveloper < ActiveRecord::Base end class CallbackCancellationDeveloper < ActiveRecord::Base - set_table_name 'developers' + self.table_name = 'developers' attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb new file mode 100644 index 0000000000..ccc57cb876 --- /dev/null +++ b/activerecord/test/cases/column_test.rb @@ -0,0 +1,29 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class ColumnTest < ActiveRecord::TestCase + def test_type_cast_boolean + column = Column.new("field", nil, "boolean") + assert column.type_cast(true) + assert column.type_cast(1) + assert column.type_cast('1') + assert column.type_cast('t') + assert column.type_cast('T') + assert column.type_cast('true') + assert column.type_cast('TRUE') + assert column.type_cast('on') + assert column.type_cast('ON') + assert !column.type_cast(false) + assert !column.type_cast(0) + assert !column.type_cast('0') + assert !column.type_cast('f') + assert !column.type_cast('F') + assert !column.type_cast('false') + assert !column.type_cast('FALSE') + assert !column.type_cast('off') + assert !column.type_cast('OFF') + end + end + end +end diff --git a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb new file mode 100644 index 0000000000..7dc6e8afcb --- /dev/null +++ b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class AbstractAdapterTest < ActiveRecord::TestCase + attr_reader :adapter + + def setup + @adapter = AbstractAdapter.new nil, nil + end + + def test_in_use? + # FIXME: change to refute in Rails 4.0 / mt + assert !adapter.in_use?, 'adapter is not in use' + assert adapter.lease, 'lease adapter' + assert adapter.in_use?, 'adapter is in use' + end + + def test_lease_twice + assert adapter.lease, 'should lease adapter' + assert !adapter.lease, 'should not lease adapter' + end + + def test_last_use + assert !adapter.last_use + adapter.lease + assert adapter.last_use + end + + def test_expire_mutates_in_use + assert adapter.lease, 'lease adapter' + assert adapter.in_use?, 'adapter is in use' + adapter.expire + assert !adapter.in_use?, 'adapter is in use' + end + + def test_close + pool = ConnectionPool.new(ConnectionSpecification.new({}, nil)) + pool.connections << adapter + adapter.pool = pool + + # Make sure the pool marks the connection in use + assert_equal adapter, pool.connection + assert adapter.in_use? + + # Close should put the adapter back in the pool + adapter.close + assert !adapter.in_use? + + assert_equal adapter, pool.connection + end + end + end +end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index bd0d161838..dc99ac665c 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -8,6 +8,9 @@ module ActiveRecord @handler.establish_connection 'america', Base.connection_pool.spec @klass = Class.new do def self.name; 'america'; end + class << self + alias active_record_super superclass + end end @subklass = Class.new(@klass) do def self.name; 'north america'; end @@ -40,8 +43,6 @@ module ActiveRecord def test_retrieve_connection_pool_uses_superclass_pool_after_subclass_establish_and_remove @handler.establish_connection 'north america', Base.connection_pool.spec - assert_not_same @handler.retrieve_connection_pool(@klass), - @handler.retrieve_connection_pool(@subklass) @handler.remove_connection @subklass assert_same @handler.retrieve_connection_pool(@klass), diff --git a/activerecord/test/cases/connection_adapters/connection_specification_test.rb b/activerecord/test/cases/connection_adapters/connection_specification_test.rb new file mode 100644 index 0000000000..ea2196cda2 --- /dev/null +++ b/activerecord/test/cases/connection_adapters/connection_specification_test.rb @@ -0,0 +1,12 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class ConnectionSpecificationTest < ActiveRecord::TestCase + def test_dup_deep_copy_config + spec = ConnectionSpecification.new({ :a => :b }, "bar") + assert_not_equal(spec.config.object_id, spec.dup.config.object_id) + end + end + end +end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 79e842f5e1..42e39d534c 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -6,12 +6,6 @@ module ActiveRecord def setup connection = ActiveRecord::Base.connection @cache = SchemaCache.new connection - - if in_memory_db? - connection.create_table :posts do |t| - t.integer :cololumn - end - end end def test_primary_key @@ -19,16 +13,7 @@ module ActiveRecord end def test_primary_key_for_non_existent_table - assert_equal 'id', @cache.primary_keys['omgponies'] - end - - def test_primary_key_is_set_on_columns - posts_columns = @cache.columns_hash['posts'] - assert posts_columns['id'].primary - - (posts_columns.keys - ['id']).each do |key| - assert !posts_columns[key].primary - end + assert_nil @cache.primary_keys['omgponies'] end def test_caches_columns @@ -41,14 +26,18 @@ module ActiveRecord assert_equal columns_hash, @cache.columns_hash['posts'] end - def test_clearing_column_cache + def test_clearing @cache.columns['posts'] @cache.columns_hash['posts'] + @cache.tables['posts'] + @cache.primary_keys['posts'] @cache.clear! assert_equal 0, @cache.columns.size assert_equal 0, @cache.columns_hash.size + assert_equal 0, @cache.tables.size + assert_equal 0, @cache.primary_keys.size end end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index f554ceef35..496cd78136 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "rack" module ActiveRecord module ConnectionAdapters @@ -25,40 +26,6 @@ module ActiveRecord assert ActiveRecord::Base.connection_handler.active_connections? end - class FakeBase < ActiveRecord::Base - def self.establish_connection spec - String === spec ? super : spec - end - end - - def test_url_host_no_db - spec = FakeBase.establish_connection 'postgres://foo?encoding=utf8' - assert_equal({ - :adapter => "postgresql", - :database => "", - :host => "foo", - :encoding => "utf8" }, spec) - end - - def test_url_host_db - spec = FakeBase.establish_connection 'postgres://foo/bar?encoding=utf8' - assert_equal({ - :adapter => "postgresql", - :database => "bar", - :host => "foo", - :encoding => "utf8" }, spec) - end - - def test_url_port - spec = FakeBase.establish_connection 'postgres://foo:123?encoding=utf8' - assert_equal({ - :adapter => "postgresql", - :database => "", - :port => 123, - :host => "foo", - :encoding => "utf8" }, spec) - end - def test_app_delegation manager = ConnectionManagement.new(@app) @@ -114,9 +81,10 @@ module ActiveRecord test "proxy is polite to it's body and responds to it" do body = Class.new(String) { def to_path; "/path"; end }.new - proxy = ConnectionManagement::Proxy.new(body) - assert proxy.respond_to?(:to_path) - assert_equal proxy.to_path, "/path" + app = lambda { |_| [200, {}, body] } + response_body = ConnectionManagement.new(app).call(@env)[2] + assert response_body.respond_to?(:to_path) + assert_equal response_body.to_path, "/path" end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 1550fa5530..2c69bfde5b 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -4,6 +4,8 @@ module ActiveRecord module ConnectionAdapters class ConnectionPoolTest < ActiveRecord::TestCase def setup + super + # Keep a duplicate pool so we do not bother others @pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec @@ -18,36 +20,76 @@ module ActiveRecord end end - def test_active_connection? - assert !@pool.active_connection? - assert @pool.connection - assert @pool.active_connection? - @pool.release_connection - assert !@pool.active_connection? + def teardown + super + @pool.disconnect! end - def test_clear_stale_cached_connections! - pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec + def test_full_pool_exception + assert_raises(PoolFullError) do + (@pool.size + 1).times do + @pool.checkout + end + end + end - threads = [ - Thread.new { pool.connection }, - Thread.new { pool.connection }] + def test_reap_and_active + @pool.checkout + @pool.checkout + @pool.checkout + @pool.timeout = 0 - threads.map { |t| t.join } + connections = @pool.connections.dup - pool.extend Module.new { - attr_accessor :checkins - def checkin conn - @checkins << conn - conn.object_id - end - } - pool.checkins = [] + @pool.reap + + assert_equal connections.length, @pool.connections.length + end + + def test_reap_inactive + @pool.checkout + @pool.checkout + @pool.checkout + @pool.timeout = 0 + + connections = @pool.connections.dup + connections.each do |conn| + conn.extend(Module.new { def active?; false; end; }) + end + + @pool.reap - cleared_threads = pool.clear_stale_cached_connections! - assert((cleared_threads - threads.map { |x| x.object_id }).empty?, - "threads should have been removed") - assert_equal pool.checkins.length, threads.length + assert_equal 0, @pool.connections.length + ensure + connections.each(&:close) + end + + def test_remove_connection + conn = @pool.checkout + assert conn.in_use? + + length = @pool.connections.length + @pool.remove conn + assert conn.in_use? + assert_equal(length - 1, @pool.connections.length) + ensure + conn.close + end + + def test_remove_connection_for_thread + conn = @pool.connection + @pool.remove conn + assert_not_equal(conn, @pool.connection) + ensure + conn.close if conn + end + + def test_active_connection? + assert !@pool.active_connection? + assert @pool.connection + assert @pool.active_connection? + @pool.release_connection + assert !@pool.active_connection? end def test_checkout_behaviour @@ -59,24 +101,16 @@ module ActiveRecord threads << Thread.new(i) do |pool_count| connection = pool.connection assert_not_nil connection + connection.close end end - threads.each {|t| t.join} + threads.each(&:join) Thread.new do - threads.each do |t| - thread_ids = pool.instance_variable_get(:@reserved_connections).keys - assert thread_ids.include?(t.object_id) - end - - pool.connection - threads.each do |t| - thread_ids = pool.instance_variable_get(:@reserved_connections).keys - assert !thread_ids.include?(t.object_id) - end - end.join() - + assert pool.connection + pool.connection.close + end.join end def test_automatic_reconnect= diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb new file mode 100644 index 0000000000..e6cb1b9521 --- /dev/null +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -0,0 +1,41 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class ConnectionSpecification + class ResolverTest < ActiveRecord::TestCase + def resolve(spec) + Resolver.new(spec, {}).spec.config + end + + def test_url_host_no_db + spec = resolve 'mysql://foo?encoding=utf8' + assert_equal({ + :adapter => "mysql", + :database => "", + :host => "foo", + :encoding => "utf8" }, spec) + end + + def test_url_host_db + spec = resolve 'mysql://foo/bar?encoding=utf8' + assert_equal({ + :adapter => "mysql", + :database => "bar", + :host => "foo", + :encoding => "utf8" }, spec) + end + + def test_url_port + spec = resolve 'mysql://foo:123?encoding=utf8' + assert_equal({ + :adapter => "mysql", + :database => "", + :port => 123, + :host => "foo", + :encoding => "utf8" }, spec) + end + end + end + end +end diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 3ed96a3ec8..cd3d19e783 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -6,9 +6,11 @@ require 'models/engine' require 'models/reply' require 'models/category' require 'models/categorization' +require 'models/dog' +require 'models/dog_lover' class CounterCacheTest < ActiveRecord::TestCase - fixtures :topics, :categories, :categorizations, :cars + fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers class ::SpecialTopic < ::Topic has_many :special_replies, :foreign_key => 'parent_id' @@ -61,7 +63,7 @@ class CounterCacheTest < ActiveRecord::TestCase end end - test "reset counter should with belongs_to which has class_name" do + test "reset counter with belongs_to which has class_name" do car = cars(:honda) assert_nothing_raised do Car.reset_counters(car.id, :engines) @@ -71,6 +73,20 @@ class CounterCacheTest < ActiveRecord::TestCase end end + test "reset the right counter if two have the same class_name" do + david = dog_lovers(:david) + + DogLover.increment_counter(:bred_dogs_count, david.id) + DogLover.increment_counter(:trained_dogs_count, david.id) + + assert_difference 'david.reload.bred_dogs_count', -1 do + DogLover.reset_counters(david.id, :bred_dogs) + end + assert_difference 'david.reload.trained_dogs_count', -1 do + DogLover.reset_counters(david.id, :trained_dogs) + end + end + test "update counter with initial null value" do category = categories(:general) assert_equal 2, category.categorizations.count diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 0236f9b0a1..9705a11387 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -99,5 +99,13 @@ module ActiveRecord assert_not_nil new_topic.created_at end + def test_dup_after_initialize_callbacks + topic = Topic.new + assert Topic.after_initialize_called + Topic.after_initialize_called = false + topic.dup + assert Topic.after_initialize_called + end + end end diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb new file mode 100644 index 0000000000..6ae6f83446 --- /dev/null +++ b/activerecord/test/cases/explain_test.rb @@ -0,0 +1,101 @@ +require 'cases/helper' +require 'models/car' +require 'active_support/core_ext/string/strip' + +if ActiveRecord::Base.connection.supports_explain? + class ExplainTest < ActiveRecord::TestCase + fixtures :cars + + def base + ActiveRecord::Base + end + + def connection + base.connection + end + + def test_logging_query_plan + base.logger.expects(:warn).with do |message| + message.starts_with?('EXPLAIN for:') + end + + with_threshold(0) do + Car.where(:name => 'honda').all + end + end + + def test_collect_queries_for_explain + base.auto_explain_threshold_in_seconds = nil + queries = Thread.current[:available_queries_for_explain] = [] + + with_threshold(0) do + Car.where(:name => 'honda').all + end + + sql, binds = queries[0] + assert_match "SELECT", sql + assert_match "honda", sql + assert_equal [], binds + ensure + Thread.current[:available_queries_for_explain] = nil + end + + def test_collecting_queries_for_explain + result, queries = ActiveRecord::Base.collecting_queries_for_explain do + Car.where(:name => 'honda').all + end + + sql, binds = queries[0] + assert_match "SELECT", sql + assert_match "honda", sql + assert_equal [], binds + assert_equal [cars(:honda)], result + end + + def test_exec_explain_with_no_binds + sqls = %w(foo bar) + binds = [[], []] + queries = sqls.zip(binds) + + connection.stubs(:explain).returns('query plan foo', 'query plan bar') + expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n") + assert_equal expected, base.exec_explain(queries) + end + + def test_exec_explain_with_binds + cols = [Object.new, Object.new] + cols[0].expects(:name).returns('wadus') + cols[1].expects(:name).returns('chaflan') + + sqls = %w(foo bar) + binds = [[[cols[0], 1]], [[cols[1], 2]]] + queries = sqls.zip(binds) + + connection.stubs(:explain).returns("query plan foo\n", "query plan bar\n") + expected = <<-SQL.strip_heredoc + EXPLAIN for: #{sqls[0]} [["wadus", 1]] + query plan foo + + EXPLAIN for: #{sqls[1]} [["chaflan", 2]] + query plan bar + SQL + assert_equal expected, base.exec_explain(queries) + end + + def test_silence_auto_explain + base.expects(:collecting_sqls_for_explain).never + base.logger.expects(:warn).never + base.silence_auto_explain do + with_threshold(0) { Car.all } + end + end + + def with_threshold(threshold) + current_threshold = base.auto_explain_threshold_in_seconds + base.auto_explain_threshold_in_seconds = threshold + yield + ensure + base.auto_explain_threshold_in_seconds = current_threshold + end + end +end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 05c4b15407..7d80a56858 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -1119,10 +1119,10 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct - assert_equal 2, Post.find(:all, :include => { :authors => :author_address }, :order => ' author_addresses.id DESC ', :limit => 2).size + assert_equal 2, Post.find(:all, :include => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).size assert_equal 3, Post.find(:all, :include => { :author => :author_address, :authors => :author_address}, - :order => ' author_addresses_authors.id DESC ', :limit => 3).size + :order => 'author_addresses_authors.id DESC ', :limit => 3).size end def test_find_with_nil_inside_set_passed_for_one_attribute @@ -1149,7 +1149,10 @@ class FinderTest < ActiveRecord::TestCase end def test_with_limiting_with_custom_select - posts = Post.find(:all, :include => :author, :select => ' posts.*, authors.id as "author_id"', :limit => 3, :order => 'posts.id') + posts = Post.find( + :all, :include => :author, :select => ' posts.*, authors.id as "author_id"', + :references => :authors, :limit => 3, :order => 'posts.id' + ) assert_equal 3, posts.size assert_equal [0, 1, 1], posts.map(&:author_id).sort end @@ -1163,7 +1166,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_one_message_with_custom_primary_key - Toy.set_primary_key :name + Toy.primary_key = :name begin Toy.find 'Hello World!' rescue ActiveRecord::RecordNotFound => e diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 7e2dafcd01..5c67cbfcce 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -1,31 +1,34 @@ -require "cases/helper" -require 'models/post' +require 'cases/helper' +require 'models/admin' +require 'models/admin/account' +require 'models/admin/randomly_named_c1' +require 'models/admin/user' require 'models/binary' -require 'models/topic' +require 'models/book' +require 'models/category' +require 'models/company' require 'models/computer' +require 'models/course' require 'models/developer' -require 'models/company' -require 'models/task' -require 'models/reply' require 'models/joke' -require 'models/course' -require 'models/category' +require 'models/matey' require 'models/parrot' require 'models/pirate' -require 'models/treasure' -require 'models/traffic_light' -require 'models/matey' +require 'models/post' +require 'models/randomly_named_c1' +require 'models/reply' require 'models/ship' -require 'models/book' -require 'models/admin' -require 'models/admin/account' -require 'models/admin/user' +require 'models/task' +require 'models/topic' +require 'models/traffic_light' +require 'models/treasure' require 'tempfile' class FixturesTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true self.use_transactional_fixtures = false + # other_topics fixture should not be included here fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes, :binaries, :traffic_lights FIXTURES = %w( accounts binaries companies customers @@ -62,8 +65,9 @@ class FixturesTest < ActiveRecord::TestCase end def test_create_fixtures - ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, "parrots") - assert Parrot.find_by_name('Curious George'), 'George is in the database' + fixtures = ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, "parrots") + assert Parrot.find_by_name('Curious George'), 'George is not in the database' + assert fixtures.detect { |f| f.name == 'parrots' }, "no fixtures named 'parrots' in #{fixtures.map(&:name).inspect}" end def test_multiple_clean_fixtures @@ -73,6 +77,13 @@ class FixturesTest < ActiveRecord::TestCase fixtures_array.each { |fixtures| assert_kind_of(ActiveRecord::Fixtures, fixtures) } end + def test_create_symbol_fixtures + fixtures = ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, :collections, :collections => Course) { Course.connection } + + assert Course.find_by_name('Collection'), 'course is not in the database' + assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}" + end + def test_attributes topics = create_fixtures("topics").first assert_equal("The First Topic", topics["first"]["title"]) @@ -93,7 +104,7 @@ class FixturesTest < ActiveRecord::TestCase # Reset cache to make finds on the new table work ActiveRecord::Fixtures.reset_cache - ActiveRecord::Base.connection.create_table :prefix_topics_suffix do |t| + ActiveRecord::Base.connection.create_table :prefix_other_topics_suffix do |t| t.column :title, :string t.column :author_name, :string t.column :author_email_address, :string @@ -115,23 +126,36 @@ class FixturesTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_prefix = 'prefix_' ActiveRecord::Base.table_name_suffix = '_suffix' - topics = create_fixtures("topics") - - first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_topics_suffix WHERE author_name = 'David'") - assert_equal("The First Topic", first_row["title"]) + other_topic_klass = Class.new(ActiveRecord::Base) do + def self.name + "OtherTopic" + end + end - second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_topics_suffix WHERE author_name = 'Mary'") - assert_nil(second_row["author_email_address"]) + topics = [create_fixtures("other_topics")].flatten.first # This checks for a caching problem which causes a bug in the fixtures # class-level configuration helper. assert_not_nil topics, "Fixture data inserted, but fixture objects not returned from create" + + first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'David'") + assert_not_nil first_row, "The prefix_other_topics_suffix table appears to be empty despite create_fixtures: the row with author_name = 'David' was not found" + assert_equal("The First Topic", first_row["title"]) + + second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'Mary'") + assert_nil(second_row["author_email_address"]) + + assert_equal :prefix_other_topics_suffix, topics.table_name.to_sym + # This assertion should preferably be the last in the list, because calling + # other_topic_klass.table_name sets a class-level instance variable + assert_equal :prefix_other_topics_suffix, other_topic_klass.table_name.to_sym + ensure # Restore prefix/suffix to its previous values ActiveRecord::Base.table_name_prefix = old_prefix ActiveRecord::Base.table_name_suffix = old_suffix - ActiveRecord::Base.connection.drop_table :prefix_topics_suffix rescue nil + ActiveRecord::Base.connection.drop_table :prefix_other_topics_suffix rescue nil end end @@ -179,7 +203,7 @@ class FixturesTest < ActiveRecord::TestCase #sanity check to make sure that this file never exists assert Dir[nonexistent_fixture_path+"*"].empty? - assert_raise(FixturesFileNotFound) do + assert_raise(Errno::ENOENT) do ActiveRecord::Fixtures.new( Account.connection, "companies", 'Company', nonexistent_fixture_path) end end @@ -213,7 +237,7 @@ class FixturesTest < ActiveRecord::TestCase def test_binary_in_fixtures data = File.open(ASSETS_ROOT + "/flowers.jpg", 'rb') { |f| f.read } - data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding) + data.force_encoding('ASCII-8BIT') data.freeze assert_equal data, @flowers.data end @@ -573,7 +597,7 @@ class FasterFixturesTest < ActiveRecord::TestCase load_extra_fixture('posts') assert ActiveRecord::Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'posts') - self.class.setup_fixture_accessors('posts') + self.class.setup_fixture_accessors :posts assert_equal 'Welcome to the weblog', posts(:welcome).title end end @@ -731,3 +755,34 @@ class FixtureLoadingTest < ActiveRecord::TestCase ActiveRecord::TestCase.try_to_load_dependency(:works_out_fine) end end + +class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase + ActiveRecord::Fixtures.reset_cache + + set_fixture_class :randomly_named_a9 => + ClassNameThatDoesNotFollowCONVENTIONS, + :'admin/randomly_named_a9' => + Admin::ClassNameThatDoesNotFollowCONVENTIONS, + 'admin/randomly_named_b0' => + Admin::ClassNameThatDoesNotFollowCONVENTIONS + + fixtures :randomly_named_a9, 'admin/randomly_named_a9', + :'admin/randomly_named_b0' + + def test_named_accessor_for_randomly_named_fixture_and_class + assert_kind_of ClassNameThatDoesNotFollowCONVENTIONS, + randomly_named_a9(:first_instance) + end + + def test_named_accessor_for_randomly_named_namespaced_fixture_and_class + assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS, + admin_randomly_named_a9(:first_instance) + assert_kind_of Admin::ClassNameThatDoesNotFollowCONVENTIONS, + admin_randomly_named_b0(:second_instance) + end + + def test_table_name_is_defined_in_the_model + assert_equal :randomly_named_table, ActiveRecord::Fixtures::all_loaded_fixtures["admin/randomly_named_a9"].table_name + assert_equal :randomly_named_table, Admin::ClassNameThatDoesNotFollowCONVENTIONS.table_name + end +end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 6735bc521b..d16cccdaea 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -2,12 +2,14 @@ require File.expand_path('../../../../load_paths', __FILE__) require 'config' -require 'test/unit' +require 'minitest/autorun' require 'stringio' require 'mocha' require 'active_record' +require 'active_record/test_case' require 'active_support/dependencies' +require 'active_support/logger' require 'support/config' require 'support/connection' @@ -68,14 +70,19 @@ module ActiveRecord cattr_accessor :log self.log = [] + attr_reader :ignore + + def initialize(ignore = Regexp.union(self.class.ignored_sql)) + @ignore = ignore + end + def call(name, start, finish, message_id, values) sql = values[:sql] # FIXME: this seems bad. we should probably have a better way to indicate # the query was cached - unless 'CACHE' == values[:name] - self.class.log << sql unless self.class.ignored_sql.any? { |r| sql =~ r } - end + return if 'CACHE' == values[:name] || ignore =~ sql + self.class.log << sql end end @@ -85,7 +92,10 @@ end unless ENV['FIXTURE_DEBUG'] module ActiveRecord::TestFixtures::ClassMethods def try_to_load_dependency_with_silence(*args) - ActiveRecord::Base.logger.silence { try_to_load_dependency_without_silence(*args)} + old = ActiveRecord::Base.logger.level + ActiveRecord::Base.logger.level = ActiveSupport::Logger::ERROR + try_to_load_dependency_without_silence(*args) + ActiveRecord::Base.logger.level = old end alias_method_chain :try_to_load_dependency, :silence diff --git a/activerecord/test/cases/identity_map/middleware_test.rb b/activerecord/test/cases/identity_map/middleware_test.rb index 60dcad4586..5b32a1c6d2 100644 --- a/activerecord/test/cases/identity_map/middleware_test.rb +++ b/activerecord/test/cases/identity_map/middleware_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "rack" module ActiveRecord module IdentityMap @@ -19,6 +20,7 @@ module ActiveRecord called = false mw = Middleware.new lambda { |env| called = true + [200, {}, nil] } mw.call({}) assert called, 'middleware delegated' @@ -27,6 +29,7 @@ module ActiveRecord def test_im_enabled_during_delegation mw = Middleware.new lambda { |env| assert IdentityMap.enabled?, 'identity map should be enabled' + [200, {}, nil] } mw.call({}) end diff --git a/activerecord/test/cases/inclusion_test.rb b/activerecord/test/cases/inclusion_test.rb new file mode 100644 index 0000000000..8726ba5e51 --- /dev/null +++ b/activerecord/test/cases/inclusion_test.rb @@ -0,0 +1,113 @@ +require 'cases/helper' +require 'models/teapot' + +class BasicInclusionModelTest < ActiveRecord::TestCase + def test_basic_model + Teapot.create!(:name => "Ronnie Kemper") + assert_equal "Ronnie Kemper", Teapot.find(1).name + end + + def test_initialization + t = Teapot.new(:name => "Bob") + assert_equal "Bob", t.name + end + + def test_inherited_model + teapot = CoolTeapot.create!(:name => "Bob") + teapot.reload + + assert_equal "Bob", teapot.name + assert_equal "mmm", teapot.aaahhh + end + + def test_generated_feature_methods + assert Teapot < Teapot::GeneratedFeatureMethods + end + + def test_exists + t = Teapot.create!(:name => "Ronnie Kemper") + assert Teapot.exists?(t) + end + + def test_predicate_builder + t = Teapot.create!(:name => "Bob") + assert_equal "Bob", Teapot.where(:id => [t]).first.name + assert_equal "Bob", Teapot.where(:id => t).first.name + end + + def test_nested_model + assert_equal "ceiling_teapots", Ceiling::Teapot.table_name + end +end + +class InclusionUnitTest < ActiveRecord::TestCase + def setup + @klass = Class.new { include ActiveRecord::Model } + end + + def test_non_abstract_class + assert !@klass.abstract_class? + end + + def test_abstract_class + @klass.abstract_class = true + assert @klass.abstract_class? + end + + def test_establish_connection + assert @klass.respond_to?(:establish_connection) + assert ActiveRecord::Model.respond_to?(:establish_connection) + end + + def test_adapter_connection + name = "#{ActiveRecord::Base.connection_config[:adapter]}_connection" + assert @klass.respond_to?(name) + assert ActiveRecord::Model.respond_to?(name) + end + + def test_connection_handler + assert_equal ActiveRecord::Base.connection_handler, @klass.connection_handler + end + + def test_mirrored_configuration + ActiveRecord::Base.time_zone_aware_attributes = true + assert @klass.time_zone_aware_attributes + ActiveRecord::Base.time_zone_aware_attributes = false + assert !@klass.time_zone_aware_attributes + ensure + ActiveRecord::Base.time_zone_aware_attributes = false + end + + # Doesn't really test anything, but this is here to ensure warnings don't occur + def test_included_twice + @klass.send :include, ActiveRecord::Model + end + + def test_deprecation_proxy + assert_equal ActiveRecord::Model.name, ActiveRecord::Model::DeprecationProxy.name + assert_equal ActiveRecord::Base.superclass, assert_deprecated { ActiveRecord::Model::DeprecationProxy.superclass } + + sup, sup2 = nil, nil + ActiveSupport.on_load(:__test_active_record_model_deprecation) do + sup = superclass + sup2 = send(:superclass) + end + assert_deprecated do + ActiveSupport.run_load_hooks(:__test_active_record_model_deprecation, ActiveRecord::Model::DeprecationProxy) + end + assert_equal ActiveRecord::Base.superclass, sup + assert_equal ActiveRecord::Base.superclass, sup2 + end +end + +class InclusionFixturesTest < ActiveRecord::TestCase + fixtures :teapots + + def test_fixtured_record + assert_equal "Bob", teapots(:bob).name + end + + def test_timestamped_fixture + assert_not_nil teapots(:bob).created_at + end +end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index b5d8314541..02df464469 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -65,7 +65,6 @@ class InheritanceTest < ActiveRecord::TestCase end def test_company_descends_from_active_record - assert_raise(NoMethodError) { ActiveRecord::Base.descends_from_active_record? } assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base' assert Company.descends_from_active_record?, 'Company should descend from ActiveRecord::Base' assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base' @@ -236,11 +235,11 @@ class InheritanceTest < ActiveRecord::TestCase c.save end [ Company, Firm, Client].each { |klass| klass.reset_column_information } - Company.set_inheritance_column('ruby_type') + Company.inheritance_column = 'ruby_type' end def switch_to_default_inheritance_column [ Company, Firm, Client].each { |klass| klass.reset_column_information } - Company.set_inheritance_column('type') + Company.inheritance_column = 'type' end end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 3ae7b63dff..8f1cdd47ea 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -88,5 +88,17 @@ module ActiveRecord LegacyMigration.down assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" end + + def test_migrate_down_with_table_name_prefix + ActiveRecord::Base.table_name_prefix = 'p_' + ActiveRecord::Base.table_name_suffix = '_s' + migration = InvertibleMigration.new + migration.migrate(:up) + assert_nothing_raised { migration.migrate(:down) } + assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" + ensure + ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = '' + end + end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index e9bd7f07b6..807274ca67 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -6,12 +6,15 @@ require 'models/reader' require 'models/legacy_thing' require 'models/reference' require 'models/string_key_object' +require 'models/car' +require 'models/engine' +require 'models/wheel' class LockWithoutDefault < ActiveRecord::Base; end class LockWithCustomColumnWithoutDefault < ActiveRecord::Base - set_table_name :lock_without_defaults_cust - set_locking_column :custom_lock_version + self.table_name = :lock_without_defaults_cust + self.locking_column = :custom_lock_version end class ReadonlyFirstNamePerson < Person @@ -224,6 +227,18 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal lock_version, p1.lock_version end end + + def test_polymorphic_destroy_with_dependencies_and_lock_version + car = Car.create! + + assert_difference 'car.wheels.count' do + car.wheels << Wheel.create! + end + assert_difference 'car.wheels.count', -1 do + car.destroy + end + assert car.destroyed? + end end class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase @@ -278,6 +293,8 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase assert_equal true, p1.frozen? assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) } assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) } + ensure + remove_counter_column_from(Person, 'legacy_things_count') end private @@ -289,8 +306,8 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter) end - def remove_counter_column_from(model) - model.connection.remove_column model.table_name, :test_count + def remove_counter_column_from(model, col = :test_count) + model.connection.remove_column model.table_name, col model.reset_column_information end @@ -371,22 +388,32 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? end end + def test_with_lock_commits_transaction + person = Person.find 1 + person.with_lock do + person.first_name = 'fooman' + person.save! + end + assert_equal 'fooman', person.reload.first_name + end + + def test_with_lock_rolls_back_transaction + person = Person.find 1 + old = person.first_name + person.with_lock do + person.first_name = 'fooman' + person.save! + raise 'oops' + end rescue nil + assert_equal old, person.reload.first_name + end + if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) def test_no_locks_no_wait first, second = duel { Person.find 1 } assert first.end > second.end end - # Hit by ruby deadlock detection since connection checkout is mutexed. - if RUBY_VERSION < '1.9.0' - def test_second_lock_waits - assert [0.2, 1, 5].any? { |zzz| - first, second = duel(zzz) { Person.find 1, :lock => true } - second.end > first.end - } - end - end - protected def duel(zzz = 5) t0, t1, t2, t3 = nil, nil, nil, nil diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 9e8475465e..d1f0ace184 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -5,7 +5,7 @@ require "active_support/log_subscriber/test_helper" class LogSubscriberTest < ActiveRecord::TestCase include ActiveSupport::LogSubscriber::TestHelper - include ActiveSupport::BufferedLogger::Severity + include ActiveSupport::Logger::Severity fixtures :posts @@ -13,6 +13,7 @@ class LogSubscriberTest < ActiveRecord::TestCase @old_logger = ActiveRecord::Base.logger @using_identity_map = ActiveRecord::IdentityMap.enabled? ActiveRecord::IdentityMap.enabled = false + Developer.primary_key super ActiveRecord::LogSubscriber.attach_to(:active_record) end diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb index 9fff50edcb..8122857f52 100644 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -50,6 +50,13 @@ module MassAssignmentTestHelpers assert_equal 'm', person.gender assert_equal 'rides a sweet bike', person.comments end + + def with_strict_sanitizer + ActiveRecord::Base.mass_assignment_sanitizer = :strict + yield + ensure + ActiveRecord::Base.mass_assignment_sanitizer = :logger + end end module MassAssignmentRelationTestHelpers @@ -323,6 +330,13 @@ class MassAssignmentSecurityHasOneRelationsTest < ActiveRecord::TestCase assert_all_attributes(best_friend) end + def test_has_one_build_with_strict_sanitizer + with_strict_sanitizer do + best_friend = @person.build_best_friend(attributes_hash.except(:id, :comments)) + assert_equal @person.id, best_friend.best_friend_id + end + end + # create def test_has_one_create_with_attr_protected_attributes @@ -350,6 +364,13 @@ class MassAssignmentSecurityHasOneRelationsTest < ActiveRecord::TestCase assert_all_attributes(best_friend) end + def test_has_one_create_with_strict_sanitizer + with_strict_sanitizer do + best_friend = @person.create_best_friend(attributes_hash.except(:id, :comments)) + assert_equal @person.id, best_friend.best_friend_id + end + end + # create! def test_has_one_create_with_bang_with_attr_protected_attributes @@ -377,6 +398,13 @@ class MassAssignmentSecurityHasOneRelationsTest < ActiveRecord::TestCase assert_all_attributes(best_friend) end + def test_has_one_create_with_bang_with_strict_sanitizer + with_strict_sanitizer do + best_friend = @person.create_best_friend!(attributes_hash.except(:id, :comments)) + assert_equal @person.id, best_friend.best_friend_id + end + end + end @@ -438,6 +466,13 @@ class MassAssignmentSecurityBelongsToRelationsTest < ActiveRecord::TestCase assert_all_attributes(best_friend) end + def test_belongs_to_create_with_strict_sanitizer + with_strict_sanitizer do + best_friend = @person.create_best_friend_of(attributes_hash.except(:id, :comments)) + assert_equal best_friend.id, @person.best_friend_of_id + end + end + # create! def test_belongs_to_create_with_bang_with_attr_protected_attributes @@ -465,6 +500,13 @@ class MassAssignmentSecurityBelongsToRelationsTest < ActiveRecord::TestCase assert_all_attributes(best_friend) end + def test_belongs_to_create_with_bang_with_strict_sanitizer + with_strict_sanitizer do + best_friend = @person.create_best_friend_of!(attributes_hash.except(:id, :comments)) + assert_equal best_friend.id, @person.best_friend_of_id + end + end + end @@ -499,6 +541,13 @@ class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase assert_all_attributes(best_friend) end + def test_has_many_build_with_strict_sanitizer + with_strict_sanitizer do + best_friend = @person.best_friends.build(attributes_hash.except(:id, :comments)) + assert_equal @person.id, best_friend.best_friend_id + end + end + # create def test_has_many_create_with_attr_protected_attributes @@ -526,6 +575,13 @@ class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase assert_all_attributes(best_friend) end + def test_has_many_create_with_strict_sanitizer + with_strict_sanitizer do + best_friend = @person.best_friends.create(attributes_hash.except(:id, :comments)) + assert_equal @person.id, best_friend.best_friend_id + end + end + # create! def test_has_many_create_with_bang_with_attr_protected_attributes @@ -553,6 +609,13 @@ class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase assert_all_attributes(best_friend) end + def test_has_many_create_with_bang_with_strict_sanitizer + with_strict_sanitizer do + best_friend = @person.best_friends.create!(attributes_hash.except(:id, :comments)) + assert_equal @person.id, best_friend.best_friend_id + end + end + end diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index 0ab4f30363..ebf6e26385 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -104,7 +104,7 @@ class MethodScopingTest < ActiveRecord::TestCase def test_scoped_find_include # with the include, will retrieve only developers for the given project scoped_developers = Developer.send(:with_scope, :find => { :include => :projects }) do - Developer.find(:all, :conditions => 'projects.id = 2') + Developer.find(:all, :conditions => { 'projects.id' => 2 }) end assert scoped_developers.include?(developers(:david)) assert !scoped_developers.include?(developers(:jamis)) @@ -204,7 +204,7 @@ class MethodScopingTest < ActiveRecord::TestCase def test_scoped_count_include # with the include, will retrieve only developers for the given project Developer.send(:with_scope, :find => { :include => :projects }) do - assert_equal 1, Developer.count(:conditions => 'projects.id = 2') + assert_equal 1, Developer.count(:conditions => { 'projects.id' => 2 }) end end @@ -339,7 +339,7 @@ class NestedScopingTest < ActiveRecord::TestCase def test_nested_scoped_find_include Developer.send(:with_scope, :find => { :include => :projects }) do - Developer.send(:with_scope, :find => { :conditions => "projects.id = 2" }) do + Developer.send(:with_scope, :find => { :conditions => { 'projects.id' => 2 } }) do assert_nothing_raised { Developer.find(1) } assert_equal('David', Developer.find(:first).name) end @@ -348,7 +348,7 @@ class NestedScopingTest < ActiveRecord::TestCase def test_nested_scoped_find_merged_include # :include's remain unique and don't "double up" when merging - Developer.send(:with_scope, :find => { :include => :projects, :conditions => "projects.id = 2" }) do + Developer.send(:with_scope, :find => { :include => :projects, :conditions => { 'projects.id' => 2 } }) do Developer.send(:with_scope, :find => { :include => :projects }) do assert_equal 1, Developer.scoped.includes_values.uniq.length assert_equal 'David', Developer.find(:first).name @@ -356,7 +356,7 @@ class NestedScopingTest < ActiveRecord::TestCase end # the nested scope doesn't remove the first :include - Developer.send(:with_scope, :find => { :include => :projects, :conditions => "projects.id = 2" }) do + Developer.send(:with_scope, :find => { :include => :projects, :conditions => { 'projects.id' => 2 } }) do Developer.send(:with_scope, :find => { :include => [] }) do assert_equal 1, Developer.scoped.includes_values.uniq.length assert_equal('David', Developer.find(:first).name) @@ -364,7 +364,7 @@ class NestedScopingTest < ActiveRecord::TestCase end # mixing array and symbol include's will merge correctly - Developer.send(:with_scope, :find => { :include => [:projects], :conditions => "projects.id = 2" }) do + Developer.send(:with_scope, :find => { :include => [:projects], :conditions => { 'projects.id' => 2 } }) do Developer.send(:with_scope, :find => { :include => :projects }) do assert_equal 1, Developer.scoped.includes_values.uniq.length assert_equal('David', Developer.find(:first).name) diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb new file mode 100644 index 0000000000..a1ade59e52 --- /dev/null +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -0,0 +1,329 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class ChangeSchemaTest < ActiveRecord::TestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + def teardown + super + connection.drop_table :testings rescue nil + ActiveRecord::Base.primary_key_prefix_type = nil + end + + def test_create_table_without_id + testing_table_with_only_foo_attribute do + assert_equal connection.columns(:testings).size, 1 + end + end + + def test_add_column_with_primary_key_attribute + testing_table_with_only_foo_attribute do + connection.add_column :testings, :id, :primary_key + assert_equal connection.columns(:testings).size, 2 + end + end + + def test_create_table_adds_id + connection.create_table :testings do |t| + t.column :foo, :string + end + + assert_equal %w(foo id), connection.columns(:testings).map(&:name).sort + end + + def test_create_table_with_not_null_column + connection.create_table :testings do |t| + t.column :foo, :string, :null => false + end + + assert_raises(ActiveRecord::StatementInvalid) do + connection.execute "insert into testings (foo) values (NULL)" + end + end + + def test_create_table_with_defaults + # MySQL doesn't allow defaults on TEXT or BLOB columns. + mysql = current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) + + connection.create_table :testings do |t| + t.column :one, :string, :default => "hello" + t.column :two, :boolean, :default => true + t.column :three, :boolean, :default => false + t.column :four, :integer, :default => 1 + t.column :five, :text, :default => "hello" unless mysql + end + + columns = connection.columns(:testings) + one = columns.detect { |c| c.name == "one" } + two = columns.detect { |c| c.name == "two" } + three = columns.detect { |c| c.name == "three" } + four = columns.detect { |c| c.name == "four" } + five = columns.detect { |c| c.name == "five" } unless mysql + + assert_equal "hello", one.default + assert_equal true, two.default + assert_equal false, three.default + assert_equal 1, four.default + assert_equal "hello", five.default unless mysql + end + + def test_create_table_with_limits + connection.create_table :testings do |t| + t.column :foo, :string, :limit => 255 + + t.column :default_int, :integer + + t.column :one_int, :integer, :limit => 1 + t.column :four_int, :integer, :limit => 4 + t.column :eight_int, :integer, :limit => 8 + t.column :eleven_int, :integer, :limit => 11 + end + + columns = connection.columns(:testings) + foo = columns.detect { |c| c.name == "foo" } + assert_equal 255, foo.limit + + default = columns.detect { |c| c.name == "default_int" } + one = columns.detect { |c| c.name == "one_int" } + four = columns.detect { |c| c.name == "four_int" } + eight = columns.detect { |c| c.name == "eight_int" } + eleven = columns.detect { |c| c.name == "eleven_int" } + + if current_adapter?(:PostgreSQLAdapter) + assert_equal 'integer', default.sql_type + assert_equal 'smallint', one.sql_type + assert_equal 'integer', four.sql_type + assert_equal 'bigint', eight.sql_type + assert_equal 'integer', eleven.sql_type + elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) + assert_match 'int(11)', default.sql_type + assert_match 'tinyint', one.sql_type + assert_match 'int', four.sql_type + assert_match 'bigint', eight.sql_type + assert_match 'int(11)', eleven.sql_type + elsif current_adapter?(:OracleAdapter) + assert_equal 'NUMBER(38)', default.sql_type + assert_equal 'NUMBER(1)', one.sql_type + assert_equal 'NUMBER(4)', four.sql_type + assert_equal 'NUMBER(8)', eight.sql_type + end + end + + def test_create_table_with_primary_key_prefix_as_table_name_with_underscore + ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore + + connection.create_table :testings do |t| + t.column :foo, :string + end + + assert_equal %w(foo testing_id), connection.columns(:testings).map(&:name).sort + end + + def test_create_table_with_primary_key_prefix_as_table_name + ActiveRecord::Base.primary_key_prefix_type = :table_name + + connection.create_table :testings do |t| + t.column :foo, :string + end + + assert_equal %w(foo testingid), connection.columns(:testings).map(&:name).sort + end + + def test_create_table_with_timestamps_should_create_datetime_columns + connection.create_table table_name do |t| + t.timestamps + end + created_columns = connection.columns(table_name) + + created_at_column = created_columns.detect {|c| c.name == 'created_at' } + updated_at_column = created_columns.detect {|c| c.name == 'updated_at' } + + assert !created_at_column.null + assert !updated_at_column.null + end + + def test_create_table_with_timestamps_should_create_datetime_columns_with_options + connection.create_table table_name do |t| + t.timestamps :null => false + end + created_columns = connection.columns(table_name) + + created_at_column = created_columns.detect {|c| c.name == 'created_at' } + updated_at_column = created_columns.detect {|c| c.name == 'updated_at' } + + assert !created_at_column.null + assert !updated_at_column.null + end + + def test_create_table_without_a_block + connection.create_table table_name + end + + def test_add_column_not_null_without_default + # Sybase, and SQLite3 will not allow you to add a NOT NULL + # column to a table without a default value. + if current_adapter?(:SybaseAdapter, :SQLite3Adapter) + skip "not supported on #{connection.class}" + end + + + connection.create_table :testings do |t| + t.column :foo, :string + end + connection.add_column :testings, :bar, :string, :null => false + + assert_raise(ActiveRecord::StatementInvalid) do + connection.execute "insert into testings (foo, bar) values ('hello', NULL)" + end + end + + def test_add_column_not_null_with_default + connection.create_table :testings do |t| + t.column :foo, :string + end + + con = connection + connection.enable_identity_insert("testings", true) if current_adapter?(:SybaseAdapter) + connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')" + connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter) + assert_nothing_raised {connection.add_column :testings, :bar, :string, :null => false, :default => "default" } + + assert_raises(ActiveRecord::StatementInvalid) do + unless current_adapter?(:OpenBaseAdapter) + connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)" + else + connection.insert("INSERT INTO testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) VALUES (2, 'hello', NULL)", + "Testing Insert","id",2) + end + end + end + + def test_change_column_quotes_column_names + connection.create_table :testings do |t| + t.column :select, :string + end + + connection.change_column :testings, :select, :string, :limit => 10 + + # Oracle needs primary key value from sequence + if current_adapter?(:OracleAdapter) + connection.execute "insert into testings (id, #{connection.quote_column_name('select')}) values (testings_seq.nextval, '7 chars')" + else + connection.execute "insert into testings (#{connection.quote_column_name('select')}) values ('7 chars')" + end + end + + def test_keeping_default_and_notnull_constaint_on_change + connection.create_table :testings do |t| + t.column :title, :string + end + person_klass = Class.new(ActiveRecord::Base) + person_klass.table_name = 'testings' + + person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99 + person_klass.reset_column_information + assert_equal 99, person_klass.columns_hash["wealth"].default + assert_equal false, person_klass.columns_hash["wealth"].null + # Oracle needs primary key value from sequence + if current_adapter?(:OracleAdapter) + assert_nothing_raised {person_klass.connection.execute("insert into testings (id, title) values (testings_seq.nextval, 'tester')")} + else + assert_nothing_raised {person_klass.connection.execute("insert into testings (title) values ('tester')")} + end + + # change column default to see that column doesn't lose its not null definition + person_klass.connection.change_column_default "testings", "wealth", 100 + person_klass.reset_column_information + assert_equal 100, person_klass.columns_hash["wealth"].default + assert_equal false, person_klass.columns_hash["wealth"].null + + # rename column to see that column doesn't lose its not null and/or default definition + person_klass.connection.rename_column "testings", "wealth", "money" + person_klass.reset_column_information + assert_nil person_klass.columns_hash["wealth"] + assert_equal 100, person_klass.columns_hash["money"].default + assert_equal false, person_klass.columns_hash["money"].null + + # change column + person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000 + person_klass.reset_column_information + assert_equal 1000, person_klass.columns_hash["money"].default + assert_equal false, person_klass.columns_hash["money"].null + + # change column, make it nullable and clear default + person_klass.connection.change_column "testings", "money", :integer, :null => true, :default => nil + person_klass.reset_column_information + assert_nil person_klass.columns_hash["money"].default + assert_equal true, person_klass.columns_hash["money"].null + + # change_column_null, make it not nullable and set null values to a default value + person_klass.connection.execute('UPDATE testings SET money = NULL') + person_klass.connection.change_column_null "testings", "money", false, 2000 + person_klass.reset_column_information + assert_nil person_klass.columns_hash["money"].default + assert_equal false, person_klass.columns_hash["money"].null + assert_equal 2000, connection.select_values("SELECT money FROM testings").first.to_i + end + + def test_column_exists + connection.create_table :testings do |t| + t.column :foo, :string + end + + assert connection.column_exists?(:testings, :foo) + refute connection.column_exists?(:testings, :bar) + end + + def test_column_exists_with_type + connection.create_table :testings do |t| + t.column :foo, :string + t.column :bar, :decimal, :precision => 8, :scale => 2 + end + + assert connection.column_exists?(:testings, :foo, :string) + refute connection.column_exists?(:testings, :foo, :integer) + + assert connection.column_exists?(:testings, :bar, :decimal) + refute connection.column_exists?(:testings, :bar, :integer) + end + + def test_column_exists_with_definition + connection.create_table :testings do |t| + t.column :foo, :string, :limit => 100 + t.column :bar, :decimal, :precision => 8, :scale => 2 + end + + assert connection.column_exists?(:testings, :foo, :string, :limit => 100) + refute connection.column_exists?(:testings, :foo, :string, :limit => 50) + assert connection.column_exists?(:testings, :bar, :decimal, :precision => 8, :scale => 2) + refute connection.column_exists?(:testings, :bar, :decimal, :precision => 10, :scale => 2) + end + + def test_column_exists_on_table_with_no_options_parameter_supplied + connection.create_table :testings do |t| + t.string :foo + end + connection.change_table :testings do |t| + assert t.column_exists?(:foo) + assert !(t.column_exists?(:bar)) + end + end + + private + def testing_table_with_only_foo_attribute + connection.create_table :testings, :id => false do |t| + t.column :foo, :string + end + + yield + end + end + end +end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb new file mode 100644 index 0000000000..040445ef12 --- /dev/null +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -0,0 +1,188 @@ +require "cases/migration/helper" + +module ActiveRecord + class Migration + class ColumnAttributesTest < ActiveRecord::TestCase + include ActiveRecord::Migration::TestHelper + + self.use_transactional_fixtures = false + + def test_add_remove_single_field_using_string_arguments + refute TestModel.column_methods_hash.key?(:last_name) + + add_column 'test_models', 'last_name', :string + + TestModel.reset_column_information + + assert TestModel.column_methods_hash.key?(:last_name) + + remove_column 'test_models', 'last_name' + + TestModel.reset_column_information + refute TestModel.column_methods_hash.key?(:last_name) + end + + def test_add_remove_single_field_using_symbol_arguments + refute TestModel.column_methods_hash.key?(:last_name) + + add_column :test_models, :last_name, :string + + TestModel.reset_column_information + assert TestModel.column_methods_hash.key?(:last_name) + + remove_column :test_models, :last_name + + TestModel.reset_column_information + refute TestModel.column_methods_hash.key?(:last_name) + end + + def test_unabstracted_database_dependent_types + skip "not supported" unless current_adapter?(:MysqlAdapter, :Mysql2Adapter) + + add_column :test_models, :intelligence_quotient, :tinyint + TestModel.reset_column_information + assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type) + end + + # We specifically do a manual INSERT here, and then test only the SELECT + # functionality. This allows us to more easily catch INSERT being broken, + # but SELECT actually working fine. + def test_native_decimal_insert_manual_vs_automatic + correct_value = '0012345678901234567890.0123456789'.to_d + + connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' + + # Do a manual insertion + if current_adapter?(:OracleAdapter) + connection.execute "insert into test_models (id, wealth, created_at, updated_at) values (people_seq.nextval, 12345678901234567890.0123456789, sysdate, sysdate)" + elsif current_adapter?(:OpenBaseAdapter) || (current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003) #before mysql 5.0.3 decimals stored as strings + connection.execute "insert into test_models (wealth, created_at, updated_at) values ('12345678901234567890.0123456789', 0, 0)" + elsif current_adapter?(:PostgreSQLAdapter) + connection.execute "insert into test_models (wealth, created_at, updated_at) values (12345678901234567890.0123456789, now(), now())" + else + connection.execute "insert into test_models (wealth, created_at, updated_at) values (12345678901234567890.0123456789, 0, 0)" + end + + # SELECT + row = TestModel.find(:first) + assert_kind_of BigDecimal, row.wealth + + # If this assert fails, that means the SELECT is broken! + unless current_adapter?(:SQLite3Adapter) + assert_equal correct_value, row.wealth + end + + # Reset to old state + TestModel.delete_all + + # Now use the Rails insertion + TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") + + # SELECT + row = TestModel.find(:first) + assert_kind_of BigDecimal, row.wealth + + # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! + unless current_adapter?(:SQLite3Adapter) + assert_equal correct_value, row.wealth + end + end + + def test_add_column_with_precision_and_scale + connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7 + + wealth_column = TestModel.columns_hash['wealth'] + assert_equal 9, wealth_column.precision + assert_equal 7, wealth_column.scale + end + + def test_change_column_preserve_other_column_precision_and_scale + skip "only on sqlite3" unless current_adapter?(:SQLite3Adapter) + + connection.add_column 'test_models', 'last_name', :string + connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7 + + wealth_column = TestModel.columns_hash['wealth'] + assert_equal 9, wealth_column.precision + assert_equal 7, wealth_column.scale + + connection.change_column 'test_models', 'last_name', :string, :null => false + TestModel.reset_column_information + + wealth_column = TestModel.columns_hash['wealth'] + assert_equal 9, wealth_column.precision + assert_equal 7, wealth_column.scale + end + + def test_native_types + add_column "test_models", "first_name", :string + add_column "test_models", "last_name", :string + add_column "test_models", "bio", :text + add_column "test_models", "age", :integer + add_column "test_models", "height", :float + add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' + add_column "test_models", "birthday", :datetime + add_column "test_models", "favorite_day", :date + add_column "test_models", "moment_of_truth", :datetime + add_column "test_models", "male", :boolean + + TestModel.create :first_name => 'bob', :last_name => 'bobsen', + :bio => "I was born ....", :age => 18, :height => 1.78, + :wealth => BigDecimal.new("12345678901234567890.0123456789"), + :birthday => 18.years.ago, :favorite_day => 10.days.ago, + :moment_of_truth => "1782-10-10 21:40:18", :male => true + + bob = TestModel.find(:first) + assert_equal 'bob', bob.first_name + assert_equal 'bobsen', bob.last_name + assert_equal "I was born ....", bob.bio + assert_equal 18, bob.age + + # Test for 30 significant digits (beyond the 16 of float), 10 of them + # after the decimal place. + + unless current_adapter?(:SQLite3Adapter) + assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth + end + + assert_equal true, bob.male? + + assert_equal String, bob.first_name.class + assert_equal String, bob.last_name.class + assert_equal String, bob.bio.class + assert_equal Fixnum, bob.age.class + assert_equal Time, bob.birthday.class + + if current_adapter?(:OracleAdapter, :SybaseAdapter) + # Sybase, and Oracle don't differentiate between date/time + assert_equal Time, bob.favorite_day.class + else + assert_equal Date, bob.favorite_day.class + end + + # Oracle adapter stores Time or DateTime with timezone value already in _before_type_cast column + # therefore no timezone change is done afterwards when default timezone is changed + unless current_adapter?(:OracleAdapter) + # Test DateTime column and defaults, including timezone. + # FIXME: moment of truth may be Time on 64-bit platforms. + if bob.moment_of_truth.is_a?(DateTime) + + with_env_tz 'US/Eastern' do + bob.reload + assert_equal DateTime.local_offset, bob.moment_of_truth.offset + assert_not_equal 0, bob.moment_of_truth.offset + assert_not_equal "Z", bob.moment_of_truth.zone + # US/Eastern is -5 hours from GMT + assert_equal Rational(-5, 24), bob.moment_of_truth.offset + assert_match(/\A-05:?00\Z/, bob.moment_of_truth.zone) #ruby 1.8.6 uses HH:MM, prior versions use HHMM + assert_equal DateTime::ITALY, bob.moment_of_truth.start + end + end + end + + assert_instance_of TrueClass, bob.male? + assert_kind_of BigDecimal, bob.wealth + end + end + end +end diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb new file mode 100644 index 0000000000..913d935f7c --- /dev/null +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -0,0 +1,60 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class ColumnPositioningTest < ActiveRecord::TestCase + attr_reader :connection, :table_name + alias :conn :connection + + def setup + super + + unless current_adapter?(:MysqlAdapter, :Mysql2Adapter) + skip "not supported on #{connection.class}" + end + + @connection = ActiveRecord::Base.connection + + connection.create_table :testings, :id => false do |t| + t.column :first, :integer + t.column :second, :integer + t.column :third, :integer + end + end + + def teardown + super + connection.drop_table :testings rescue nil + ActiveRecord::Base.primary_key_prefix_type = nil + end + + def test_column_positioning + assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name } + end + + def test_add_column_with_positioning + conn.add_column :testings, :new_col, :integer + assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name } + end + + def test_add_column_with_positioning_first + conn.add_column :testings, :new_col, :integer, :first => true + assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name } + end + + def test_add_column_with_positioning_after + conn.add_column :testings, :new_col, :integer, :after => :first + assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name } + end + + def test_change_column_with_positioning + conn.change_column :testings, :second, :integer, :first => true + assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name } + + conn.change_column :testings, :second, :integer, :after => :third + assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name } + end + + end + end +end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index d108b456f0..8f136bce2b 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -67,6 +67,12 @@ module ActiveRecord assert_equal [:drop_table, [:system_settings]], drop_table end + def test_invert_create_table_with_options + @recorder.record :create_table, [:people_reminders, {:id => false}] + drop_table = @recorder.inverse.first + assert_equal [:drop_table, [:people_reminders]], drop_table + end + def test_invert_rename_table @recorder.record :rename_table, [:old, :new] rename = @recorder.inverse.first diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb new file mode 100644 index 0000000000..fe53510ba2 --- /dev/null +++ b/activerecord/test/cases/migration/helper.rb @@ -0,0 +1,64 @@ +require "cases/helper" + +module ActiveRecord + class Migration + class << self + attr_accessor :message_count + end + + def puts(text="") + ActiveRecord::Migration.message_count ||= 0 + ActiveRecord::Migration.message_count += 1 + end + + module TestHelper + attr_reader :connection, :table_name + + class TestModel < ActiveRecord::Base + self.table_name = 'test_models' + end + + def setup + super + @connection = ActiveRecord::Base.connection + connection.create_table :test_models do |t| + t.timestamps + end + + TestModel.reset_column_information + end + + def teardown + super + TestModel.reset_table_name + TestModel.reset_sequence_name + connection.drop_table :test_models rescue nil + end + + private + def add_column(*args) + connection.add_column(*args) + end + + def remove_column(*args) + connection.remove_column(*args) + end + + def rename_column(*args) + connection.rename_column(*args) + end + + def add_index(*args) + connection.add_index(*args) + end + + def change_column(*args) + connection.change_column(*args) + end + + def rename_table(*args) + connection.rename_table(*args) + end + end + end +end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb new file mode 100644 index 0000000000..26d7aeb148 --- /dev/null +++ b/activerecord/test/cases/migration/index_test.rb @@ -0,0 +1,170 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class IndexTest < ActiveRecord::TestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + + connection.create_table table_name do |t| + t.column :foo, :string, :limit => 100 + t.column :bar, :string, :limit => 100 + + t.string :first_name + t.string :last_name, :limit => 100 + t.string :key, :limit => 100 + t.boolean :administrator + end + end + + def teardown + super + connection.drop_table :testings rescue nil + ActiveRecord::Base.primary_key_prefix_type = nil + end + + def test_rename_index + skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter) + + # keep the names short to make Oracle and similar behave + connection.add_index(table_name, [:foo], :name => 'old_idx') + connection.rename_index(table_name, 'old_idx', 'new_idx') + + # if the adapter doesn't support the indexes call, pick defaults that let the test pass + refute connection.index_name_exists?(table_name, 'old_idx', false) + assert connection.index_name_exists?(table_name, 'new_idx', true) + end + + def test_double_add_index + skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter) + + connection.add_index(table_name, [:foo], :name => 'some_idx') + assert_raises(ArgumentError) { + connection.add_index(table_name, [:foo], :name => 'some_idx') + } + end + + def test_remove_nonexistent_index + skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter) + + # we do this by name, so OpenBase is a wash as noted above + assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") } + end + + def test_add_index_length_limit + good_index_name = 'x' * connection.index_name_length + too_long_index_name = good_index_name + 'x' + + assert_raises(ArgumentError) { + connection.add_index(table_name, "foo", :name => too_long_index_name) + } + + refute connection.index_name_exists?(table_name, too_long_index_name, false) + connection.add_index(table_name, "foo", :name => good_index_name) + + assert connection.index_name_exists?(table_name, good_index_name, false) + connection.remove_index(table_name, :name => good_index_name) + end + + def test_index_symbol_names + connection.add_index table_name, :foo, :name => :symbol_index_name + assert connection.index_exists?(table_name, :foo, :name => :symbol_index_name) + + connection.remove_index table_name, :name => :symbol_index_name + refute connection.index_exists?(table_name, :foo, :name => :symbol_index_name) + end + + def test_index_exists + connection.add_index :testings, :foo + + assert connection.index_exists?(:testings, :foo) + assert !connection.index_exists?(:testings, :bar) + end + + def test_index_exists_on_multiple_columns + connection.add_index :testings, [:foo, :bar] + + assert connection.index_exists?(:testings, [:foo, :bar]) + end + + def test_unique_index_exists + connection.add_index :testings, :foo, :unique => true + + assert connection.index_exists?(:testings, :foo, :unique => true) + end + + def test_named_index_exists + connection.add_index :testings, :foo, :name => "custom_index_name" + + assert connection.index_exists?(:testings, :foo, :name => "custom_index_name") + end + + def test_add_index + connection.add_index("testings", "last_name") + connection.remove_index("testings", "last_name") + + # Orcl nds shrt indx nms. Sybs 2. + # OpenBase does not have named indexes. You must specify a single column name + unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) + connection.add_index("testings", ["last_name", "first_name"]) + connection.remove_index("testings", :column => ["last_name", "first_name"]) + + # Oracle adapter cannot have specified index name larger than 30 characters + # Oracle adapter is shortening index name when just column list is given + unless current_adapter?(:OracleAdapter) + connection.add_index("testings", ["last_name", "first_name"]) + connection.remove_index("testings", :name => :index_testings_on_last_name_and_first_name) + connection.add_index("testings", ["last_name", "first_name"]) + connection.remove_index("testings", "last_name_and_first_name") + end + connection.add_index("testings", ["last_name", "first_name"]) + connection.remove_index("testings", ["last_name", "first_name"]) + + connection.add_index("testings", ["last_name"], :length => 10) + connection.remove_index("testings", "last_name") + + connection.add_index("testings", ["last_name"], :length => {:last_name => 10}) + connection.remove_index("testings", ["last_name"]) + + connection.add_index("testings", ["last_name", "first_name"], :length => 10) + connection.remove_index("testings", ["last_name", "first_name"]) + + connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20}) + connection.remove_index("testings", ["last_name", "first_name"]) + end + + # quoting + # Note: changed index name from "key" to "key_idx" since "key" is a Firebird reserved word + # OpenBase does not have named indexes. You must specify a single column name + unless current_adapter?(:OpenBaseAdapter) + connection.add_index("testings", ["key"], :name => "key_idx", :unique => true) + connection.remove_index("testings", :name => "key_idx", :unique => true) + end + + # Sybase adapter does not support indexes on :boolean columns + # OpenBase does not have named indexes. You must specify a single column + unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) + connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin") + connection.remove_index("testings", :name => "named_admin") + end + + # Selected adapters support index sort order + if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + connection.add_index("testings", ["last_name"], :order => {:last_name => :desc}) + connection.remove_index("testings", ["last_name"]) + connection.add_index("testings", ["last_name", "first_name"], :order => {:last_name => :desc}) + connection.remove_index("testings", ["last_name", "first_name"]) + connection.add_index("testings", ["last_name", "first_name"], :order => {:last_name => :desc, :first_name => :asc}) + connection.remove_index("testings", ["last_name", "first_name"]) + connection.add_index("testings", ["last_name", "first_name"], :order => :desc) + connection.remove_index("testings", ["last_name", "first_name"]) + end + end + + end + end +end diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb new file mode 100644 index 0000000000..ee0c20747e --- /dev/null +++ b/activerecord/test/cases/migration/logger_test.rb @@ -0,0 +1,37 @@ +require "cases/helper" + +module ActiveRecord + class Migration + class LoggerTest < ActiveRecord::TestCase + # mysql can't roll back ddl changes + self.use_transactional_fixtures = false + + Migration = Struct.new(:name, :version) do + def migrate direction + # do nothing + end + end + + def setup + super + ActiveRecord::SchemaMigration.create_table + ActiveRecord::SchemaMigration.delete_all + end + + def teardown + super + ActiveRecord::SchemaMigration.drop_table + end + + def test_migration_should_be_run_without_logger + previous_logger = ActiveRecord::Base.logger + ActiveRecord::Base.logger = nil + migrations = [Migration.new('a', 1), Migration.new('b', 2), Migration.new('c', 3)] + ActiveRecord::Migrator.new(:up, migrations).migrate + ensure + ActiveRecord::Base.logger = previous_logger + end + end + end +end + diff --git a/activerecord/test/cases/migration/rename_column_test.rb b/activerecord/test/cases/migration/rename_column_test.rb new file mode 100644 index 0000000000..2e7e533ed6 --- /dev/null +++ b/activerecord/test/cases/migration/rename_column_test.rb @@ -0,0 +1,195 @@ +require "cases/migration/helper" + +module ActiveRecord + class Migration + class RenameColumnTest < ActiveRecord::TestCase + include ActiveRecord::Migration::TestHelper + + self.use_transactional_fixtures = false + + # FIXME: this is more of an integration test with AR::Base and the + # schema modifications. Maybe we should move this? + def test_add_rename + add_column "test_models", "girlfriend", :string + TestModel.reset_column_information + + TestModel.create :girlfriend => 'bobette' + + rename_column "test_models", "girlfriend", "exgirlfriend" + + TestModel.reset_column_information + bob = TestModel.find(:first) + + assert_equal "bobette", bob.exgirlfriend + end + + # FIXME: another integration test. We should decouple this from the + # AR::Base implementation. + def test_rename_column_using_symbol_arguments + add_column :test_models, :first_name, :string + + TestModel.create :first_name => 'foo' + + rename_column :test_models, :first_name, :nick_name + TestModel.reset_column_information + assert TestModel.column_names.include?("nick_name") + assert_equal ['foo'], TestModel.find(:all).map(&:nick_name) + end + + # FIXME: another integration test. We should decouple this from the + # AR::Base implementation. + def test_rename_column + add_column "test_models", "first_name", "string" + + TestModel.create :first_name => 'foo' + + rename_column "test_models", "first_name", "nick_name" + TestModel.reset_column_information + assert TestModel.column_names.include?("nick_name") + assert_equal ['foo'], TestModel.find(:all).map(&:nick_name) + end + + def test_rename_column_preserves_default_value_not_null + add_column 'test_models', 'salary', :integer, :default => 70000 + + default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default + assert_equal 70000, default_before + + rename_column "test_models", "salary", "anual_salary" + + assert TestModel.column_names.include?("anual_salary") + default_after = connection.columns("test_models").find { |c| c.name == "anual_salary" }.default + assert_equal 70000, default_after + end + + def test_rename_nonexistent_column + exception = if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) + ActiveRecord::StatementInvalid + else + ActiveRecord::ActiveRecordError + end + assert_raise(exception) do + rename_column "test_models", "nonexistent", "should_fail" + end + end + + def test_rename_column_with_sql_reserved_word + add_column 'test_models', 'first_name', :string + rename_column "test_models", "first_name", "group" + + assert TestModel.column_names.include?("group") + end + + def test_rename_column_with_an_index + add_column "test_models", :hat_name, :string + add_index :test_models, :hat_name + + # FIXME: we should test that the index goes away + rename_column "test_models", "hat_name", "name" + end + + def test_remove_column_with_index + add_column "test_models", :hat_name, :string + add_index :test_models, :hat_name + + # FIXME: we should test that the index goes away + remove_column("test_models", "hat_name") + end + + def test_remove_column_with_multi_column_index + add_column "test_models", :hat_size, :integer + add_column "test_models", :hat_style, :string, :limit => 100 + add_index "test_models", ["hat_style", "hat_size"], :unique => true + + # FIXME: we should test that the index goes away + remove_column("test_models", "hat_size") + end + + # FIXME: we need to test that these calls do something + def test_change_type_of_not_null_column + change_column "test_models", "updated_at", :datetime, :null => false + change_column "test_models", "updated_at", :datetime, :null => false + change_column "test_models", "updated_at", :datetime, :null => true + end + + def test_change_column_nullability + add_column "test_models", "funny", :boolean + assert TestModel.columns_hash["funny"].null, "Column 'funny' must initially allow nulls" + + change_column "test_models", "funny", :boolean, :null => false, :default => true + + TestModel.reset_column_information + refute TestModel.columns_hash["funny"].null, "Column 'funny' must *not* allow nulls at this point" + + change_column "test_models", "funny", :boolean, :null => true + TestModel.reset_column_information + assert TestModel.columns_hash["funny"].null, "Column 'funny' must allow nulls again at this point" + end + + def test_change_column + add_column 'test_models', 'age', :integer + add_column 'test_models', 'approved', :boolean, :default => true + + label = "test_change_column Columns" + old_columns = connection.columns(TestModel.table_name, label) + + assert old_columns.find { |c| c.name == 'age' && c.type == :integer } + + change_column "test_models", "age", :string + + new_columns = connection.columns(TestModel.table_name, label) + + refute new_columns.find { |c| c.name == 'age' and c.type == :integer } + assert new_columns.find { |c| c.name == 'age' and c.type == :string } + + old_columns = connection.columns(TestModel.table_name, label) + assert old_columns.find { |c| + c.name == 'approved' && c.type == :boolean && c.default == true + } + + change_column :test_models, :approved, :boolean, :default => false + new_columns = connection.columns(TestModel.table_name, label) + + refute new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true } + assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false } + change_column :test_models, :approved, :boolean, :default => true + end + + def test_change_column_with_nil_default + add_column "test_models", "contributor", :boolean, :default => true + assert TestModel.new.contributor? + + change_column "test_models", "contributor", :boolean, :default => nil + TestModel.reset_column_information + refute TestModel.new.contributor? + assert_nil TestModel.new.contributor + end + + def test_change_column_with_new_default + add_column "test_models", "administrator", :boolean, :default => true + assert TestModel.new.administrator? + + change_column "test_models", "administrator", :boolean, :default => false + TestModel.reset_column_information + refute TestModel.new.administrator? + end + + def test_change_column_default + add_column "test_models", "first_name", :string + connection.change_column_default "test_models", "first_name", "Tester" + + assert_equal "Tester", TestModel.new.first_name + end + + def test_change_column_default_to_null + add_column "test_models", "first_name", :string + connection.change_column_default "test_models", "first_name", nil + assert_nil TestModel.new.first_name + end + + def test_remove_column_no_second_parameter_raises_exception + assert_raise(ArgumentError) { connection.remove_column("funny") } + end + end + end +end diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb new file mode 100644 index 0000000000..d5ff2c607f --- /dev/null +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -0,0 +1,72 @@ +require "cases/migration/helper" + +module ActiveRecord + class Migration + class RenameTableTest < ActiveRecord::TestCase + include ActiveRecord::Migration::TestHelper + + self.use_transactional_fixtures = false + + def setup + super + add_column 'test_models', :url, :string + remove_column 'test_models', :created_at + remove_column 'test_models', :updated_at + end + + def test_rename_table_for_sqlite_should_work_with_reserved_words + renamed = false + + skip "not supported" unless current_adapter?(:SQLite3Adapter) + + add_column :test_models, :url, :string + connection.rename_table :references, :old_references + connection.rename_table :test_models, :references + + renamed = true + + # Using explicit id in insert for compatibility across all databases + con = connection + con.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)" + assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1") + ensure + return unless renamed + connection.rename_table :references, :test_models + connection.rename_table :old_references, :references + end + + def test_rename_table + rename_table :test_models, :octopi + + # Using explicit id in insert for compatibility across all databases + con = connection + con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) + + con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" + + con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) + + assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") + + rename_table :octopi, :test_models + end + + def test_rename_table_with_an_index + add_index :test_models, :url + + rename_table :test_models, :octopi + + # Using explicit id in insert for compatibility across all databases + con = ActiveRecord::Base.connection + con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) + con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" + con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) + + assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") + assert connection.indexes(:octopi).first.columns.include?("url") + + rename_table :octopi, :test_models + end + end + end +end diff --git a/activerecord/test/cases/migration/table_and_index_test.rb b/activerecord/test/cases/migration/table_and_index_test.rb new file mode 100644 index 0000000000..8fd770abd1 --- /dev/null +++ b/activerecord/test/cases/migration/table_and_index_test.rb @@ -0,0 +1,24 @@ +require "cases/helper" + +module ActiveRecord + class Migration + class TableAndIndexTest < ActiveRecord::TestCase + def test_add_schema_info_respects_prefix_and_suffix + conn = ActiveRecord::Base.connection + + conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) + # Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters + ActiveRecord::Base.table_name_prefix = 'p_' + ActiveRecord::Base.table_name_suffix = '_s' + conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) + + conn.initialize_schema_migrations_table + + assert_equal "p_unique_schema_migrations_s", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name] + ensure + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + end + end + end +end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 3e219f2a49..575df2f84b 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "cases/migration/helper" require 'bigdecimal/util' require 'models/person' @@ -6,2209 +7,941 @@ require 'models/topic' require 'models/developer' require MIGRATIONS_ROOT + "/valid/2_we_need_reminders" +require MIGRATIONS_ROOT + "/rename/1_we_need_things" +require MIGRATIONS_ROOT + "/rename/2_rename_things" require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers" -if ActiveRecord::Base.connection.supports_migrations? - class BigNumber < ActiveRecord::Base; end +class BigNumber < ActiveRecord::Base; end - class Reminder < ActiveRecord::Base; end +class Reminder < ActiveRecord::Base; end - class ActiveRecord::Migration - class << self - attr_accessor :message_count - end - - def puts(text="") - ActiveRecord::Migration.message_count ||= 0 - ActiveRecord::Migration.message_count += 1 - end - end - - class MigrationTableAndIndexTest < ActiveRecord::TestCase - def test_add_schema_info_respects_prefix_and_suffix - conn = ActiveRecord::Base.connection +class Thing < ActiveRecord::Base; end - conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) - # Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters - ActiveRecord::Base.table_name_prefix = 'p_' - ActiveRecord::Base.table_name_suffix = '_s' - conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) +class MigrationTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false - conn.initialize_schema_migrations_table + fixtures :people - assert_equal "p_unique_schema_migrations_s", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name] - ensure - ActiveRecord::Base.table_name_prefix = "" - ActiveRecord::Base.table_name_suffix = "" + def setup + super + %w(reminders people_reminders prefix_reminders_suffix).each do |table| + Reminder.connection.drop_table(table) rescue nil end + Reminder.reset_column_information + ActiveRecord::Migration.verbose = true + ActiveRecord::Migration.message_count = 0 end - class MigrationTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false - - fixtures :people - - def setup - ActiveRecord::Migration.verbose = true - ActiveRecord::Migration.message_count = 0 - end - - def teardown - ActiveRecord::Base.connection.initialize_schema_migrations_table - ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}" - - %w(reminders people_reminders prefix_reminders_suffix).each do |table| - Reminder.connection.drop_table(table) rescue nil - end - Reminder.reset_column_information - - %w(last_name key bio age height wealth birthday favorite_day - moment_of_truth male administrator funny).each do |column| - Person.connection.remove_column('people', column) rescue nil - end - Person.connection.remove_column("people", "first_name") rescue nil - Person.connection.remove_column("people", "middle_name") rescue nil - Person.connection.add_column("people", "first_name", :string, :limit => 40) - Person.reset_column_information - end - - def test_add_index - # Limit size of last_name and key columns to support Firebird index limitations - Person.connection.add_column "people", "last_name", :string, :limit => 100 - Person.connection.add_column "people", "key", :string, :limit => 100 - Person.connection.add_column "people", "administrator", :boolean - - assert_nothing_raised { Person.connection.add_index("people", "last_name") } - assert_nothing_raised { Person.connection.remove_index("people", "last_name") } - - # Orcl nds shrt indx nms. Sybs 2. - # OpenBase does not have named indexes. You must specify a single column name - unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) - assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } - assert_nothing_raised { Person.connection.remove_index("people", :column => ["last_name", "first_name"]) } - # Oracle adapter cannot have specified index name larger than 30 characters - # Oracle adapter is shortening index name when just column list is given - unless current_adapter?(:OracleAdapter) - assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } - assert_nothing_raised { Person.connection.remove_index("people", :name => :index_people_on_last_name_and_first_name) } - assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } - assert_nothing_raised { Person.connection.remove_index("people", "last_name_and_first_name") } - end - assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } - assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) } - assert_nothing_raised { Person.connection.add_index("people", ["last_name"], :length => 10) } - assert_nothing_raised { Person.connection.remove_index("people", "last_name") } - assert_nothing_raised { Person.connection.add_index("people", ["last_name"], :length => {:last_name => 10}) } - assert_nothing_raised { Person.connection.remove_index("people", ["last_name"]) } - assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :length => 10) } - assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) } - assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20}) } - assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) } - end - - # quoting - # Note: changed index name from "key" to "key_idx" since "key" is a Firebird reserved word - # OpenBase does not have named indexes. You must specify a single column name - unless current_adapter?(:OpenBaseAdapter) - Person.update_all "#{Person.connection.quote_column_name 'key'}=#{Person.connection.quote_column_name 'id'}" #some databases (including sqlite2 won't add a unique index if existing data non unique) - assert_nothing_raised { Person.connection.add_index("people", ["key"], :name => "key_idx", :unique => true) } - assert_nothing_raised { Person.connection.remove_index("people", :name => "key_idx", :unique => true) } - end - - # Sybase adapter does not support indexes on :boolean columns - # OpenBase does not have named indexes. You must specify a single column - unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) - assert_nothing_raised { Person.connection.add_index("people", %w(last_name first_name administrator), :name => "named_admin") } - assert_nothing_raised { Person.connection.remove_index("people", :name => "named_admin") } - end - - # Selected adapters support index sort order - if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - assert_nothing_raised { Person.connection.add_index("people", ["last_name"], :order => {:last_name => :desc}) } - assert_nothing_raised { Person.connection.remove_index("people", ["last_name"]) } - assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :order => {:last_name => :desc}) } - assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) } - assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :order => {:last_name => :desc, :first_name => :asc}) } - assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) } - assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :order => :desc) } - assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) } - end - end - - def test_index_symbol_names - assert_nothing_raised { Person.connection.add_index :people, :primary_contact_id, :name => :symbol_index_name } - assert Person.connection.index_exists?(:people, :primary_contact_id, :name => :symbol_index_name) - assert_nothing_raised { Person.connection.remove_index :people, :name => :symbol_index_name } - assert !Person.connection.index_exists?(:people, :primary_contact_id, :name => :symbol_index_name) - end - - def test_add_index_length_limit - good_index_name = 'x' * Person.connection.index_name_length - too_long_index_name = good_index_name + 'x' - assert_raise(ArgumentError) { Person.connection.add_index("people", "first_name", :name => too_long_index_name) } - assert !Person.connection.index_name_exists?("people", too_long_index_name, false) - assert_nothing_raised { Person.connection.add_index("people", "first_name", :name => good_index_name) } - assert Person.connection.index_name_exists?("people", good_index_name, false) - Person.connection.remove_index("people", :name => good_index_name) - end - - def test_remove_nonexistent_index - # we do this by name, so OpenBase is a wash as noted above - unless current_adapter?(:OpenBaseAdapter) - assert_raise(ArgumentError) { Person.connection.remove_index("people", "no_such_index") } - end - end - - def test_rename_index - unless current_adapter?(:OpenBaseAdapter) - # keep the names short to make Oracle and similar behave - Person.connection.add_index('people', [:first_name], :name => 'old_idx') - assert_nothing_raised { Person.connection.rename_index('people', 'old_idx', 'new_idx') } - # if the adapter doesn't support the indexes call, pick defaults that let the test pass - assert !Person.connection.index_name_exists?('people', 'old_idx', false) - assert Person.connection.index_name_exists?('people', 'new_idx', true) - end - end - - def test_double_add_index - unless current_adapter?(:OpenBaseAdapter) - Person.connection.add_index('people', [:first_name], :name => 'some_idx') - assert_raise(ArgumentError) { Person.connection.add_index('people', [:first_name], :name => 'some_idx') } - end - end - - def test_index_exists - Person.connection.create_table :testings do |t| - t.column :foo, :string, :limit => 100 - t.column :bar, :string, :limit => 100 - end - Person.connection.add_index :testings, :foo - - assert Person.connection.index_exists?(:testings, :foo) - assert !Person.connection.index_exists?(:testings, :bar) - ensure - Person.connection.drop_table :testings rescue nil - end - - def test_index_exists_on_multiple_columns - Person.connection.create_table :testings do |t| - t.column :foo, :string, :limit => 100 - t.column :bar, :string, :limit => 100 - end - Person.connection.add_index :testings, [:foo, :bar] - - assert Person.connection.index_exists?(:testings, [:foo, :bar]) - ensure - Person.connection.drop_table :testings rescue nil - end - - def test_unique_index_exists - Person.connection.create_table :testings do |t| - t.column :foo, :string, :limit => 100 - end - Person.connection.add_index :testings, :foo, :unique => true - - assert Person.connection.index_exists?(:testings, :foo, :unique => true) - ensure - Person.connection.drop_table :testings rescue nil - end - - def test_named_index_exists - Person.connection.create_table :testings do |t| - t.column :foo, :string, :limit => 100 - end - Person.connection.add_index :testings, :foo, :name => "custom_index_name" - - assert Person.connection.index_exists?(:testings, :foo, :name => "custom_index_name") - ensure - Person.connection.drop_table :testings rescue nil - end - - def testing_table_with_only_foo_attribute - Person.connection.create_table :testings, :id => false do |t| - t.column :foo, :string - end - - yield Person.connection - ensure - Person.connection.drop_table :testings rescue nil - end - protected :testing_table_with_only_foo_attribute + def teardown + ActiveRecord::Base.connection.initialize_schema_migrations_table + ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}" - def test_create_table_without_id - testing_table_with_only_foo_attribute do |connection| - assert_equal connection.columns(:testings).size, 1 - end + %w(things awesome_things prefix_things_suffix prefix_awesome_things_suffix).each do |table| + Thing.connection.drop_table(table) rescue nil end + Thing.reset_column_information - def test_add_column_with_primary_key_attribute - testing_table_with_only_foo_attribute do |connection| - assert_nothing_raised { connection.add_column :testings, :id, :primary_key } - assert_equal connection.columns(:testings).size, 2 - end + %w(reminders people_reminders prefix_reminders_suffix).each do |table| + Reminder.connection.drop_table(table) rescue nil end + Reminder.reset_column_information - def test_create_table_adds_id - Person.connection.create_table :testings do |t| - t.column :foo, :string - end - - assert_equal %w(foo id), - Person.connection.columns(:testings).map { |c| c.name }.sort - ensure - Person.connection.drop_table :testings rescue nil + %w(last_name key bio age height wealth birthday favorite_day + moment_of_truth male administrator funny).each do |column| + Person.connection.remove_column('people', column) rescue nil end + Person.connection.remove_column("people", "first_name") rescue nil + Person.connection.remove_column("people", "middle_name") rescue nil + Person.connection.add_column("people", "first_name", :string, :limit => 40) + Person.reset_column_information + end - def test_create_table_with_not_null_column - assert_nothing_raised do - Person.connection.create_table :testings do |t| - t.column :foo, :string, :null => false - end - end - - assert_raise(ActiveRecord::StatementInvalid) do - Person.connection.execute "insert into testings (foo) values (NULL)" - end - ensure - Person.connection.drop_table :testings rescue nil - end - - def test_create_table_with_defaults - # MySQL doesn't allow defaults on TEXT or BLOB columns. - mysql = current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) - - Person.connection.create_table :testings do |t| - t.column :one, :string, :default => "hello" - t.column :two, :boolean, :default => true - t.column :three, :boolean, :default => false - t.column :four, :integer, :default => 1 - t.column :five, :text, :default => "hello" unless mysql - end - - columns = Person.connection.columns(:testings) - one = columns.detect { |c| c.name == "one" } - two = columns.detect { |c| c.name == "two" } - three = columns.detect { |c| c.name == "three" } - four = columns.detect { |c| c.name == "four" } - five = columns.detect { |c| c.name == "five" } unless mysql - - assert_equal "hello", one.default - assert_equal true, two.default - assert_equal false, three.default - assert_equal 1, four.default - assert_equal "hello", five.default unless mysql - - ensure - Person.connection.drop_table :testings rescue nil + def test_create_table_with_force_true_does_not_drop_nonexisting_table + if Person.connection.table_exists?(:testings2) + Person.connection.drop_table :testings2 end - def test_create_table_with_limits - assert_nothing_raised do - Person.connection.create_table :testings do |t| - t.column :foo, :string, :limit => 255 - - t.column :default_int, :integer + # using a copy as we need the drop_table method to + # continue to work for the ensure block of the test + temp_conn = Person.connection.dup - t.column :one_int, :integer, :limit => 1 - t.column :four_int, :integer, :limit => 4 - t.column :eight_int, :integer, :limit => 8 - t.column :eleven_int, :integer, :limit => 11 - end - end + assert_not_equal temp_conn, Person.connection - columns = Person.connection.columns(:testings) - foo = columns.detect { |c| c.name == "foo" } - assert_equal 255, foo.limit - - default = columns.detect { |c| c.name == "default_int" } - one = columns.detect { |c| c.name == "one_int" } - four = columns.detect { |c| c.name == "four_int" } - eight = columns.detect { |c| c.name == "eight_int" } - eleven = columns.detect { |c| c.name == "eleven_int" } - - if current_adapter?(:PostgreSQLAdapter) - assert_equal 'integer', default.sql_type - assert_equal 'smallint', one.sql_type - assert_equal 'integer', four.sql_type - assert_equal 'bigint', eight.sql_type - assert_equal 'integer', eleven.sql_type - elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) - assert_match 'int(11)', default.sql_type - assert_match 'tinyint', one.sql_type - assert_match 'int', four.sql_type - assert_match 'bigint', eight.sql_type - assert_match 'int(11)', eleven.sql_type - elsif current_adapter?(:OracleAdapter) - assert_equal 'NUMBER(38)', default.sql_type - assert_equal 'NUMBER(1)', one.sql_type - assert_equal 'NUMBER(4)', four.sql_type - assert_equal 'NUMBER(8)', eight.sql_type - end - ensure - Person.connection.drop_table :testings rescue nil + temp_conn.create_table :testings2, :force => true do |t| + t.column :foo, :string end + ensure + Person.connection.drop_table :testings2 rescue nil + end - def test_create_table_with_primary_key_prefix_as_table_name_with_underscore - ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore - - Person.connection.create_table :testings do |t| - t.column :foo, :string - end - - assert_equal %w(foo testing_id), Person.connection.columns(:testings).map { |c| c.name }.sort - ensure - Person.connection.drop_table :testings rescue nil - ActiveRecord::Base.primary_key_prefix_type = nil - end + def connection + ActiveRecord::Base.connection + end - def test_create_table_with_primary_key_prefix_as_table_name - ActiveRecord::Base.primary_key_prefix_type = :table_name + def test_migration_instance_has_connection + migration = Class.new(ActiveRecord::Migration).new + assert_equal connection, migration.connection + end - Person.connection.create_table :testings do |t| - t.column :foo, :string + def test_method_missing_delegates_to_connection + migration = Class.new(ActiveRecord::Migration) { + def connection + Class.new { + def create_table; "hi mom!"; end + }.new end + }.new - assert_equal %w(foo testingid), Person.connection.columns(:testings).map { |c| c.name }.sort - ensure - Person.connection.drop_table :testings rescue nil - ActiveRecord::Base.primary_key_prefix_type = nil - end - - def test_create_table_with_force_true_does_not_drop_nonexisting_table - if Person.connection.table_exists?(:testings2) - Person.connection.drop_table :testings2 - end + assert_equal "hi mom!", migration.method_missing(:create_table) + end - # using a copy as we need the drop_table method to - # continue to work for the ensure block of the test - temp_conn = Person.connection.dup - temp_conn.expects(:drop_table).never - temp_conn.create_table :testings2, :force => true do |t| - t.column :foo, :string - end - ensure - Person.connection.drop_table :testings2 rescue nil - end + def test_add_table_with_decimals + Person.connection.drop_table :big_numbers rescue nil + + assert !BigNumber.table_exists? + GiveMeBigNumbers.up + + assert BigNumber.create( + :bank_balance => 1586.43, + :big_bank_balance => BigDecimal("1000234000567.95"), + :world_population => 6000000000, + :my_house_population => 3, + :value_of_e => BigDecimal("2.7182818284590452353602875") + ) + + b = BigNumber.find(:first) + assert_not_nil b + + assert_not_nil b.bank_balance + assert_not_nil b.big_bank_balance + assert_not_nil b.world_population + assert_not_nil b.my_house_population + assert_not_nil b.value_of_e + + # TODO: set world_population >= 2**62 to cover 64-bit platforms and test + # is_a?(Bignum) + assert_kind_of Integer, b.world_population + assert_equal 6000000000, b.world_population + assert_kind_of Fixnum, b.my_house_population + assert_equal 3, b.my_house_population + assert_kind_of BigDecimal, b.bank_balance + assert_equal BigDecimal("1586.43"), b.bank_balance + assert_kind_of BigDecimal, b.big_bank_balance + assert_equal BigDecimal("1000234000567.95"), b.big_bank_balance + + # This one is fun. The 'value_of_e' field is defined as 'DECIMAL' with + # precision/scale explicitly left out. By the SQL standard, numbers + # assigned to this field should be truncated but that's seldom respected. + if current_adapter?(:PostgreSQLAdapter) + # - PostgreSQL changes the SQL spec on columns declared simply as + # "decimal" to something more useful: instead of being given a scale + # of 0, they take on the compile-time limit for precision and scale, + # so the following should succeed unless you have used really wacky + # compilation options + # - SQLite2 has the default behavior of preserving all data sent in, + # so this happens there too + assert_kind_of BigDecimal, b.value_of_e + assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e + elsif current_adapter?(:SQLite3Adapter) + # - SQLite3 stores a float, in violation of SQL + assert_kind_of BigDecimal, b.value_of_e + assert_in_delta BigDecimal("2.71828182845905"), b.value_of_e, 0.00000000000001 + else + # - SQL standard is an integer + assert_kind_of Fixnum, b.value_of_e + assert_equal 2, b.value_of_e + end + + GiveMeBigNumbers.down + assert_raise(ActiveRecord::StatementInvalid) { BigNumber.find(:first) } + end - def test_create_table_with_timestamps_should_create_datetime_columns - table_name = :testings + def test_filtering_migrations + assert !Person.column_methods_hash.include?(:last_name) + assert !Reminder.table_exists? - Person.connection.create_table table_name do |t| - t.timestamps - end - created_columns = Person.connection.columns(table_name) + name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" } + ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter) - created_at_column = created_columns.detect {|c| c.name == 'created_at' } - updated_at_column = created_columns.detect {|c| c.name == 'updated_at' } + Person.reset_column_information + assert Person.column_methods_hash.include?(:last_name) + assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } - assert !created_at_column.null - assert !updated_at_column.null - ensure - Person.connection.drop_table table_name rescue nil - end + ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter) - def test_create_table_with_timestamps_should_create_datetime_columns_with_options - table_name = :testings - - Person.connection.create_table table_name do |t| - t.timestamps :null => false - end - created_columns = Person.connection.columns(table_name) - - created_at_column = created_columns.detect {|c| c.name == 'created_at' } - updated_at_column = created_columns.detect {|c| c.name == 'updated_at' } + Person.reset_column_information + assert !Person.column_methods_hash.include?(:last_name) + assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + end - assert !created_at_column.null - assert !updated_at_column.null - ensure - Person.connection.drop_table table_name rescue nil + class MockMigration < ActiveRecord::Migration + attr_reader :went_up, :went_down + def initialize + @went_up = false + @went_down = false end - def test_create_table_without_a_block - table_name = :testings - Person.connection.create_table table_name - ensure - Person.connection.drop_table table_name rescue nil + def up + @went_up = true + super end - # Sybase, and SQLite3 will not allow you to add a NOT NULL - # column to a table without a default value. - unless current_adapter?(:SybaseAdapter, :SQLite3Adapter) - def test_add_column_not_null_without_default - Person.connection.create_table :testings do |t| - t.column :foo, :string - end - Person.connection.add_column :testings, :bar, :string, :null => false - - assert_raise(ActiveRecord::StatementInvalid) do - Person.connection.execute "insert into testings (foo, bar) values ('hello', NULL)" - end - ensure - Person.connection.drop_table :testings rescue nil - end + def down + @went_down = true + super end + end - def test_add_column_not_null_with_default - Person.connection.create_table :testings do |t| - t.column :foo, :string - end - - con = Person.connection - Person.connection.enable_identity_insert("testings", true) if current_adapter?(:SybaseAdapter) - Person.connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')" - Person.connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter) - assert_nothing_raised {Person.connection.add_column :testings, :bar, :string, :null => false, :default => "default" } - - assert_raise(ActiveRecord::StatementInvalid) do - unless current_adapter?(:OpenBaseAdapter) - Person.connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)" - else - Person.connection.insert("INSERT INTO testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) VALUES (2, 'hello', NULL)", - "Testing Insert","id",2) - end - end - ensure - Person.connection.drop_table :testings rescue nil - end - - # We specifically do a manual INSERT here, and then test only the SELECT - # functionality. This allows us to more easily catch INSERT being broken, - # but SELECT actually working fine. - def test_native_decimal_insert_manual_vs_automatic - correct_value = '0012345678901234567890.0123456789'.to_d - - Person.delete_all - Person.connection.add_column "people", "wealth", :decimal, :precision => '30', :scale => '10' - Person.reset_column_information - - # Do a manual insertion - if current_adapter?(:OracleAdapter) - Person.connection.execute "insert into people (id, wealth, created_at, updated_at) values (people_seq.nextval, 12345678901234567890.0123456789, sysdate, sysdate)" - elsif current_adapter?(:OpenBaseAdapter) || (current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003) #before mysql 5.0.3 decimals stored as strings - Person.connection.execute "insert into people (wealth, created_at, updated_at) values ('12345678901234567890.0123456789', 0, 0)" - elsif current_adapter?(:PostgreSQLAdapter) - Person.connection.execute "insert into people (wealth, created_at, updated_at) values (12345678901234567890.0123456789, now(), now())" - else - Person.connection.execute "insert into people (wealth, created_at, updated_at) values (12345678901234567890.0123456789, 0, 0)" - end - - # SELECT - row = Person.find(:first) - assert_kind_of BigDecimal, row.wealth - - # If this assert fails, that means the SELECT is broken! - unless current_adapter?(:SQLite3Adapter) - assert_equal correct_value, row.wealth - end - - # Reset to old state - Person.delete_all - - # Now use the Rails insertion - assert_nothing_raised { Person.create :wealth => BigDecimal.new("12345678901234567890.0123456789") } - - # SELECT - row = Person.find(:first) - assert_kind_of BigDecimal, row.wealth + def test_instance_based_migration_up + migration = MockMigration.new + assert !migration.went_up, 'have not gone up' + assert !migration.went_down, 'have not gone down' - # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! - unless current_adapter?(:SQLite3Adapter) - assert_equal correct_value, row.wealth - end + migration.migrate :up + assert migration.went_up, 'have gone up' + assert !migration.went_down, 'have not gone down' + end - # Reset to old state - Person.connection.del_column "people", "wealth" rescue nil - Person.reset_column_information - end + def test_instance_based_migration_down + migration = MockMigration.new + assert !migration.went_up, 'have not gone up' + assert !migration.went_down, 'have not gone down' - def test_add_column_with_precision_and_scale - Person.connection.add_column 'people', 'wealth', :decimal, :precision => 9, :scale => 7 - Person.reset_column_information + migration.migrate :down + assert !migration.went_up, 'have gone up' + assert migration.went_down, 'have not gone down' + end - wealth_column = Person.columns_hash['wealth'] - assert_equal 9, wealth_column.precision - assert_equal 7, wealth_column.scale + def test_migrator_one_up_with_exception_and_rollback + unless ActiveRecord::Base.connection.supports_ddl_transactions? + skip "not supported on #{ActiveRecord::Base.connection.class}" end - # Test SQLite adapter specifically for decimal types with precision and scale - # attributes, since these need to be maintained in schema but aren't actually - # used in SQLite itself - if current_adapter?(:SQLite3Adapter) - def test_change_column_with_new_precision_and_scale - Person.delete_all - Person.connection.add_column 'people', 'wealth', :decimal, :precision => 9, :scale => 7 - Person.reset_column_information - - Person.connection.change_column 'people', 'wealth', :decimal, :precision => 12, :scale => 8 - Person.reset_column_information - - wealth_column = Person.columns_hash['wealth'] - assert_equal 12, wealth_column.precision - assert_equal 8, wealth_column.scale - end - - def test_change_column_preserve_other_column_precision_and_scale - Person.delete_all - Person.connection.add_column 'people', 'last_name', :string - Person.connection.add_column 'people', 'wealth', :decimal, :precision => 9, :scale => 7 - Person.reset_column_information - - wealth_column = Person.columns_hash['wealth'] - assert_equal 9, wealth_column.precision - assert_equal 7, wealth_column.scale - - Person.connection.change_column 'people', 'last_name', :string, :null => false - Person.reset_column_information - - wealth_column = Person.columns_hash['wealth'] - assert_equal 9, wealth_column.precision - assert_equal 7, wealth_column.scale - end - end + refute Person.column_methods_hash.include?(:last_name) - def test_native_types - Person.delete_all - Person.connection.add_column "people", "last_name", :string - Person.connection.add_column "people", "bio", :text - Person.connection.add_column "people", "age", :integer - Person.connection.add_column "people", "height", :float - Person.connection.add_column "people", "wealth", :decimal, :precision => '30', :scale => '10' - Person.connection.add_column "people", "birthday", :datetime - Person.connection.add_column "people", "favorite_day", :date - Person.connection.add_column "people", "moment_of_truth", :datetime - Person.connection.add_column "people", "male", :boolean - Person.reset_column_information - - assert_nothing_raised do - Person.create :first_name => 'bob', :last_name => 'bobsen', - :bio => "I was born ....", :age => 18, :height => 1.78, - :wealth => BigDecimal.new("12345678901234567890.0123456789"), - :birthday => 18.years.ago, :favorite_day => 10.days.ago, - :moment_of_truth => "1782-10-10 21:40:18", :male => true - end + migration = Struct.new(:name, :version) { + def migrate(x); raise 'Something broke'; end + }.new('zomg', 100) - bob = Person.find(:first) - assert_equal 'bob', bob.first_name - assert_equal 'bobsen', bob.last_name - assert_equal "I was born ....", bob.bio - assert_equal 18, bob.age + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - # Test for 30 significant digits (beyond the 16 of float), 10 of them - # after the decimal place. + e = assert_raise(StandardError) { migrator.migrate } - unless current_adapter?(:SQLite3Adapter) - assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth - end + assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message - assert_equal true, bob.male? + Person.reset_column_information + refute Person.column_methods_hash.include?(:last_name) + end - assert_equal String, bob.first_name.class - assert_equal String, bob.last_name.class - assert_equal String, bob.bio.class - assert_equal Fixnum, bob.age.class - assert_equal Time, bob.birthday.class + def test_schema_migrations_table_name + ActiveRecord::Base.table_name_prefix = "prefix_" + ActiveRecord::Base.table_name_suffix = "_suffix" + Reminder.reset_table_name + assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + Reminder.reset_table_name + assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name + ensure + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + end - if current_adapter?(:OracleAdapter, :SybaseAdapter) - # Sybase, and Oracle don't differentiate between date/time - assert_equal Time, bob.favorite_day.class - else - assert_equal Date, bob.favorite_day.class - end + def test_proper_table_name + assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') + assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) + assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder) + Reminder.reset_table_name + assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder) + + # Use the model's own prefix/suffix if a model is given + ActiveRecord::Base.table_name_prefix = "ARprefix_" + ActiveRecord::Base.table_name_suffix = "_ARsuffix" + Reminder.table_name_prefix = 'prefix_' + Reminder.table_name_suffix = '_suffix' + Reminder.reset_table_name + assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder) + Reminder.table_name_prefix = '' + Reminder.table_name_suffix = '' + Reminder.reset_table_name + + # Use AR::Base's prefix/suffix if string or symbol is given + ActiveRecord::Base.table_name_prefix = "prefix_" + ActiveRecord::Base.table_name_suffix = "_suffix" + Reminder.reset_table_name + assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') + assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table) + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + Reminder.reset_table_name + end - # Oracle adapter stores Time or DateTime with timezone value already in _before_type_cast column - # therefore no timezone change is done afterwards when default timezone is changed - unless current_adapter?(:OracleAdapter) - # Test DateTime column and defaults, including timezone. - # FIXME: moment of truth may be Time on 64-bit platforms. - if bob.moment_of_truth.is_a?(DateTime) - - with_env_tz 'US/Eastern' do - bob.reload - assert_equal DateTime.local_offset, bob.moment_of_truth.offset - assert_not_equal 0, bob.moment_of_truth.offset - assert_not_equal "Z", bob.moment_of_truth.zone - # US/Eastern is -5 hours from GMT - assert_equal Rational(-5, 24), bob.moment_of_truth.offset - assert_match(/\A-05:?00\Z/, bob.moment_of_truth.zone) #ruby 1.8.6 uses HH:MM, prior versions use HHMM - assert_equal DateTime::ITALY, bob.moment_of_truth.start - end - end - end + def test_rename_table_with_prefix_and_suffix + assert !Thing.table_exists? + ActiveRecord::Base.table_name_prefix = 'prefix_' + ActiveRecord::Base.table_name_suffix = '_suffix' + Thing.reset_table_name + Thing.reset_sequence_name + WeNeedThings.up + + assert Thing.create("content" => "hello world") + assert_equal "hello world", Thing.find(:first).content + + RenameThings.up + Thing.table_name = "prefix_awesome_things_suffix" + + assert_equal "hello world", Thing.find(:first).content + ensure + ActiveRecord::Base.table_name_prefix = '' + ActiveRecord::Base.table_name_suffix = '' + Thing.reset_table_name + Thing.reset_sequence_name + end - assert_instance_of TrueClass, bob.male? - assert_kind_of BigDecimal, bob.wealth - end + def test_add_drop_table_with_prefix_and_suffix + assert !Reminder.table_exists? + ActiveRecord::Base.table_name_prefix = 'prefix_' + ActiveRecord::Base.table_name_suffix = '_suffix' + Reminder.reset_table_name + Reminder.reset_sequence_name + WeNeedReminders.up + assert Reminder.create("content" => "hello world", "remind_at" => Time.now) + assert_equal "hello world", Reminder.find(:first).content + + WeNeedReminders.down + assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + ensure + ActiveRecord::Base.table_name_prefix = '' + ActiveRecord::Base.table_name_suffix = '' + Reminder.reset_table_name + Reminder.reset_sequence_name + end - if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) - def test_unabstracted_database_dependent_types - Person.delete_all + def test_create_table_with_binary_column + Person.connection.drop_table :binary_testings rescue nil - ActiveRecord::Migration.add_column :people, :intelligence_quotient, :tinyint - Person.reset_column_information - assert_match(/tinyint/, Person.columns_hash['intelligence_quotient'].sql_type) - ensure - ActiveRecord::Migration.remove_column :people, :intelligence_quotient rescue nil + assert_nothing_raised { + Person.connection.create_table :binary_testings do |t| + t.column "data", :binary, :null => false end - end - - def test_add_remove_single_field_using_string_arguments - assert !Person.column_methods_hash.include?(:last_name) - - ActiveRecord::Migration.add_column 'people', 'last_name', :string + } - Person.reset_column_information - assert Person.column_methods_hash.include?(:last_name) - - ActiveRecord::Migration.remove_column 'people', 'last_name' - - Person.reset_column_information - assert !Person.column_methods_hash.include?(:last_name) - end - - def test_add_remove_single_field_using_symbol_arguments - assert !Person.column_methods_hash.include?(:last_name) - - ActiveRecord::Migration.add_column :people, :last_name, :string - - Person.reset_column_information - assert Person.column_methods_hash.include?(:last_name) - - ActiveRecord::Migration.remove_column :people, :last_name - - Person.reset_column_information - assert !Person.column_methods_hash.include?(:last_name) - end + columns = Person.connection.columns(:binary_testings) + data_column = columns.detect { |c| c.name == "data" } if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) - def testing_table_for_positioning - Person.connection.create_table :testings, :id => false do |t| - t.column :first, :integer - t.column :second, :integer - t.column :third, :integer - end - - yield Person.connection - ensure - Person.connection.drop_table :testings rescue nil - end - protected :testing_table_for_positioning - - def test_column_positioning - testing_table_for_positioning do |conn| - assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name } - end - end - - def test_add_column_with_positioning - testing_table_for_positioning do |conn| - conn.add_column :testings, :new_col, :integer - assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name } - end - testing_table_for_positioning do |conn| - conn.add_column :testings, :new_col, :integer, :first => true - assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name } - end - testing_table_for_positioning do |conn| - conn.add_column :testings, :new_col, :integer, :after => :first - assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name } - end - end - - def test_change_column_with_positioning - testing_table_for_positioning do |conn| - conn.change_column :testings, :second, :integer, :first => true - assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name } - end - testing_table_for_positioning do |conn| - conn.change_column :testings, :second, :integer, :after => :third - assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name } - end - end - end - - def test_add_rename - Person.delete_all - - begin - Person.connection.add_column "people", "girlfriend", :string - Person.reset_column_information - Person.create :girlfriend => 'bobette' - - Person.connection.rename_column "people", "girlfriend", "exgirlfriend" - - Person.reset_column_information - bob = Person.find(:first) - - assert_equal "bobette", bob.exgirlfriend - ensure - Person.connection.remove_column("people", "girlfriend") rescue nil - Person.connection.remove_column("people", "exgirlfriend") rescue nil - end - - end - - def test_rename_column_using_symbol_arguments - begin - names_before = Person.find(:all).map(&:first_name) - Person.connection.rename_column :people, :first_name, :nick_name - Person.reset_column_information - assert Person.column_names.include?("nick_name") - assert_equal names_before, Person.find(:all).map(&:nick_name) - ensure - Person.connection.remove_column("people","nick_name") - Person.connection.add_column("people","first_name", :string) - end - end - - def test_rename_column - begin - names_before = Person.find(:all).map(&:first_name) - Person.connection.rename_column "people", "first_name", "nick_name" - Person.reset_column_information - assert Person.column_names.include?("nick_name") - assert_equal names_before, Person.find(:all).map(&:nick_name) - ensure - Person.connection.remove_column("people","nick_name") - Person.connection.add_column("people","first_name", :string) - end + assert_equal '', data_column.default + else + assert_nil data_column.default end - def test_rename_column_preserves_default_value_not_null - begin - default_before = Developer.connection.columns("developers").find { |c| c.name == "salary" }.default - assert_equal 70000, default_before - Developer.connection.rename_column "developers", "salary", "anual_salary" - Developer.reset_column_information - assert Developer.column_names.include?("anual_salary") - default_after = Developer.connection.columns("developers").find { |c| c.name == "anual_salary" }.default - assert_equal 70000, default_after - ensure - Developer.connection.rename_column "developers", "anual_salary", "salary" - Developer.reset_column_information - end - end - - def test_rename_nonexistent_column - ActiveRecord::Base.connection.create_table(:hats) do |table| - table.column :hat_name, :string, :default => nil - end - exception = if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) - ActiveRecord::StatementInvalid - else - ActiveRecord::ActiveRecordError - end - assert_raise(exception) do - Person.connection.rename_column "hats", "nonexistent", "should_fail" - end - ensure - ActiveRecord::Base.connection.drop_table(:hats) - end - - def test_rename_column_with_sql_reserved_word - begin - assert_nothing_raised { Person.connection.rename_column "people", "first_name", "group" } - Person.reset_column_information - assert Person.column_names.include?("group") - ensure - Person.connection.remove_column("people", "group") rescue nil - Person.connection.add_column("people", "first_name", :string) rescue nil - end - end - - def test_rename_column_with_an_index - ActiveRecord::Base.connection.create_table(:hats) do |table| - table.column :hat_name, :string, :limit => 100 - table.column :hat_size, :integer - end - Person.connection.add_index :hats, :hat_name - assert_nothing_raised do - Person.connection.rename_column "hats", "hat_name", "name" - end - ensure - ActiveRecord::Base.connection.drop_table(:hats) - end - - def test_remove_column_with_index - ActiveRecord::Base.connection.create_table(:hats) do |table| - table.column :hat_name, :string, :limit => 100 - table.column :hat_size, :integer - end - ActiveRecord::Base.connection.add_index "hats", "hat_size" - - assert_nothing_raised { Person.connection.remove_column("hats", "hat_size") } - ensure - ActiveRecord::Base.connection.drop_table(:hats) - end - - def test_remove_column_with_multi_column_index - ActiveRecord::Base.connection.create_table(:hats) do |table| - table.column :hat_name, :string, :limit => 100 - table.column :hat_size, :integer - table.column :hat_style, :string, :limit => 100 - end - ActiveRecord::Base.connection.add_index "hats", ["hat_style", "hat_size"], :unique => true - - assert_nothing_raised { Person.connection.remove_column("hats", "hat_size") } - ensure - ActiveRecord::Base.connection.drop_table(:hats) - end - - def test_remove_column_no_second_parameter_raises_exception - assert_raise(ArgumentError) { Person.connection.remove_column("funny") } - end - - def test_change_type_of_not_null_column - assert_nothing_raised do - Topic.connection.change_column "topics", "written_on", :datetime, :null => false - Topic.reset_column_information - - Topic.connection.change_column "topics", "written_on", :datetime, :null => false - Topic.reset_column_information - - Topic.connection.change_column "topics", "written_on", :datetime, :null => true - Topic.reset_column_information - end - end + Person.connection.drop_table :binary_testings rescue nil + end - if current_adapter?(:SQLite3Adapter) - def test_rename_table_for_sqlite_should_work_with_reserved_words - begin - assert_nothing_raised do - ActiveRecord::Base.connection.rename_table :references, :old_references - ActiveRecord::Base.connection.create_table :octopuses do |t| - t.column :url, :string - end - end - - assert_nothing_raised { ActiveRecord::Base.connection.rename_table :octopuses, :references } - - # Using explicit id in insert for compatibility across all databases - con = ActiveRecord::Base.connection - assert_nothing_raised do - con.execute "INSERT INTO 'references' (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://rubyonrails.com')" - end - assert_equal 'http://rubyonrails.com', ActiveRecord::Base.connection.select_value("SELECT url FROM 'references' WHERE id=1") - - ensure - ActiveRecord::Base.connection.drop_table :references - ActiveRecord::Base.connection.rename_table :old_references, :references - end - end - end + def test_create_table_with_custom_sequence_name + skip "not supported" unless current_adapter? :OracleAdapter - def test_rename_table + # table name is 29 chars, the standard sequence name will + # be 33 chars and should be shortened + assert_nothing_raised do begin - ActiveRecord::Base.connection.create_table :octopuses do |t| - t.column :url, :string + Person.connection.create_table :table_with_name_thats_just_ok do |t| + t.column :foo, :string, :null => false end - ActiveRecord::Base.connection.rename_table :octopuses, :octopi - - # Using explicit id in insert for compatibility across all databases - con = ActiveRecord::Base.connection - con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) - assert_nothing_raised { con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" } - con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) - - assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', ActiveRecord::Base.connection.select_value("SELECT url FROM octopi WHERE id=1") - ensure - ActiveRecord::Base.connection.drop_table :octopuses rescue nil - ActiveRecord::Base.connection.drop_table :octopi rescue nil + Person.connection.drop_table :table_with_name_thats_just_ok rescue nil end end - def test_change_column_nullability - Person.delete_all - Person.connection.add_column "people", "funny", :boolean - Person.reset_column_information - assert Person.columns_hash["funny"].null, "Column 'funny' must initially allow nulls" - Person.connection.change_column "people", "funny", :boolean, :null => false, :default => true - Person.reset_column_information - assert !Person.columns_hash["funny"].null, "Column 'funny' must *not* allow nulls at this point" - Person.connection.change_column "people", "funny", :boolean, :null => true - Person.reset_column_information - assert Person.columns_hash["funny"].null, "Column 'funny' must allow nulls again at this point" - end - - def test_rename_table_with_an_index + # should be all good w/ a custom sequence name + assert_nothing_raised do begin - ActiveRecord::Base.connection.create_table :octopuses do |t| - t.column :url, :string + Person.connection.create_table :table_with_name_thats_just_ok, + :sequence_name => 'suitably_short_seq' do |t| + t.column :foo, :string, :null => false end - ActiveRecord::Base.connection.add_index :octopuses, :url - ActiveRecord::Base.connection.rename_table :octopuses, :octopi - - # Using explicit id in insert for compatibility across all databases - con = ActiveRecord::Base.connection - con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) - assert_nothing_raised { con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" } - con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) + Person.connection.execute("select suitably_short_seq.nextval from dual") - assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', ActiveRecord::Base.connection.select_value("SELECT url FROM octopi WHERE id=1") - assert ActiveRecord::Base.connection.indexes(:octopi).first.columns.include?("url") ensure - ActiveRecord::Base.connection.drop_table :octopuses rescue nil - ActiveRecord::Base.connection.drop_table :octopi rescue nil - end - end - - def test_change_column - Person.connection.add_column 'people', 'age', :integer - label = "test_change_column Columns" - old_columns = Person.connection.columns(Person.table_name, label) - assert old_columns.find { |c| c.name == 'age' and c.type == :integer } - - assert_nothing_raised { Person.connection.change_column "people", "age", :string } - - new_columns = Person.connection.columns(Person.table_name, label) - assert_nil new_columns.find { |c| c.name == 'age' and c.type == :integer } - assert new_columns.find { |c| c.name == 'age' and c.type == :string } - - old_columns = Topic.connection.columns(Topic.table_name, label) - assert old_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true } - assert_nothing_raised { Topic.connection.change_column :topics, :approved, :boolean, :default => false } - new_columns = Topic.connection.columns(Topic.table_name, label) - assert_nil new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true } - assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false } - assert_nothing_raised { Topic.connection.change_column :topics, :approved, :boolean, :default => true } - end - - def test_change_column_with_nil_default - Person.connection.add_column "people", "contributor", :boolean, :default => true - Person.reset_column_information - assert Person.new.contributor? - - assert_nothing_raised { Person.connection.change_column "people", "contributor", :boolean, :default => nil } - Person.reset_column_information - assert !Person.new.contributor? - assert_nil Person.new.contributor - ensure - Person.connection.remove_column("people", "contributor") rescue nil - end - - def test_change_column_with_new_default - Person.connection.add_column "people", "administrator", :boolean, :default => true - Person.reset_column_information - assert Person.new.administrator? - - assert_nothing_raised { Person.connection.change_column "people", "administrator", :boolean, :default => false } - Person.reset_column_information - assert !Person.new.administrator? - ensure - Person.connection.remove_column("people", "administrator") rescue nil - end - - def test_change_column_default - Person.connection.change_column_default "people", "first_name", "Tester" - Person.reset_column_information - assert_equal "Tester", Person.new.first_name - end - - def test_change_column_quotes_column_names - Person.connection.create_table :testings do |t| - t.column :select, :string - end - - assert_nothing_raised { Person.connection.change_column :testings, :select, :string, :limit => 10 } - - # Oracle needs primary key value from sequence - if current_adapter?(:OracleAdapter) - assert_nothing_raised { Person.connection.execute "insert into testings (id, #{Person.connection.quote_column_name('select')}) values (testings_seq.nextval, '7 chars')" } - else - assert_nothing_raised { Person.connection.execute "insert into testings (#{Person.connection.quote_column_name('select')}) values ('7 chars')" } - end - ensure - Person.connection.drop_table :testings rescue nil - end - - def test_keeping_default_and_notnull_constaint_on_change - Person.connection.create_table :testings do |t| - t.column :title, :string - end - person_klass = Class.new(Person) - person_klass.set_table_name 'testings' - - person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99 - person_klass.reset_column_information - assert_equal 99, person_klass.columns_hash["wealth"].default - assert_equal false, person_klass.columns_hash["wealth"].null - # Oracle needs primary key value from sequence - if current_adapter?(:OracleAdapter) - assert_nothing_raised {person_klass.connection.execute("insert into testings (id, title) values (testings_seq.nextval, 'tester')")} - else - assert_nothing_raised {person_klass.connection.execute("insert into testings (title) values ('tester')")} - end - - # change column default to see that column doesn't lose its not null definition - person_klass.connection.change_column_default "testings", "wealth", 100 - person_klass.reset_column_information - assert_equal 100, person_klass.columns_hash["wealth"].default - assert_equal false, person_klass.columns_hash["wealth"].null - - # rename column to see that column doesn't lose its not null and/or default definition - person_klass.connection.rename_column "testings", "wealth", "money" - person_klass.reset_column_information - assert_nil person_klass.columns_hash["wealth"] - assert_equal 100, person_klass.columns_hash["money"].default - assert_equal false, person_klass.columns_hash["money"].null - - # change column - person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000 - person_klass.reset_column_information - assert_equal 1000, person_klass.columns_hash["money"].default - assert_equal false, person_klass.columns_hash["money"].null - - # change column, make it nullable and clear default - person_klass.connection.change_column "testings", "money", :integer, :null => true, :default => nil - person_klass.reset_column_information - assert_nil person_klass.columns_hash["money"].default - assert_equal true, person_klass.columns_hash["money"].null - - # change_column_null, make it not nullable and set null values to a default value - person_klass.connection.execute('UPDATE testings SET money = NULL') - person_klass.connection.change_column_null "testings", "money", false, 2000 - person_klass.reset_column_information - assert_nil person_klass.columns_hash["money"].default - assert_equal false, person_klass.columns_hash["money"].null - assert_equal [2000], Person.connection.select_values("SELECT money FROM testings").map { |s| s.to_i }.sort - ensure - Person.connection.drop_table :testings rescue nil - end - - def test_change_column_default_to_null - Person.connection.change_column_default "people", "first_name", nil - Person.reset_column_information - assert_nil Person.new.first_name - end - - def test_column_exists - Person.connection.create_table :testings do |t| - t.column :foo, :string + Person.connection.drop_table :table_with_name_thats_just_ok, + :sequence_name => 'suitably_short_seq' rescue nil end - - assert Person.connection.column_exists?(:testings, :foo) - assert !Person.connection.column_exists?(:testings, :bar) - ensure - Person.connection.drop_table :testings rescue nil - end - - def test_column_exists_with_type - Person.connection.create_table :testings do |t| - t.column :foo, :string - t.column :bar, :decimal, :precision => 8, :scale => 2 - end - - assert Person.connection.column_exists?(:testings, :foo, :string) - assert !Person.connection.column_exists?(:testings, :foo, :integer) - assert Person.connection.column_exists?(:testings, :bar, :decimal) - assert !Person.connection.column_exists?(:testings, :bar, :integer) - ensure - Person.connection.drop_table :testings rescue nil end - def test_column_exists_with_definition - Person.connection.create_table :testings do |t| - t.column :foo, :string, :limit => 100 - t.column :bar, :decimal, :precision => 8, :scale => 2 - end - - assert Person.connection.column_exists?(:testings, :foo, :string, :limit => 100) - assert !Person.connection.column_exists?(:testings, :foo, :string, :limit => 50) - assert Person.connection.column_exists?(:testings, :bar, :decimal, :precision => 8, :scale => 2) - assert !Person.connection.column_exists?(:testings, :bar, :decimal, :precision => 10, :scale => 2) - ensure - Person.connection.drop_table :testings rescue nil + # confirm the custom sequence got dropped + assert_raise(ActiveRecord::StatementInvalid) do + Person.connection.execute("select suitably_short_seq.nextval from dual") end + end - def test_column_exists_on_table_with_no_options_parameter_supplied - Person.connection.create_table :testings do |t| - t.string :foo - end - Person.connection.change_table :testings do |t| - assert t.column_exists?(:foo) - assert !(t.column_exists?(:bar)) - end + protected + def with_env_tz(new_tz = 'US/Eastern') + old_tz, ENV['TZ'] = ENV['TZ'], new_tz + yield ensure - Person.connection.drop_table :testings rescue nil - end - - def test_add_table - assert !Reminder.table_exists? - - WeNeedReminders.up - - assert Reminder.create("content" => "hello world", "remind_at" => Time.now) - assert_equal "hello world", Reminder.find(:first).content - - WeNeedReminders.down - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } - end - - def test_add_table_with_decimals - Person.connection.drop_table :big_numbers rescue nil - - assert !BigNumber.table_exists? - GiveMeBigNumbers.up - - assert BigNumber.create( - :bank_balance => 1586.43, - :big_bank_balance => BigDecimal("1000234000567.95"), - :world_population => 6000000000, - :my_house_population => 3, - :value_of_e => BigDecimal("2.7182818284590452353602875") - ) - - b = BigNumber.find(:first) - assert_not_nil b - - assert_not_nil b.bank_balance - assert_not_nil b.big_bank_balance - assert_not_nil b.world_population - assert_not_nil b.my_house_population - assert_not_nil b.value_of_e - - # TODO: set world_population >= 2**62 to cover 64-bit platforms and test - # is_a?(Bignum) - assert_kind_of Integer, b.world_population - assert_equal 6000000000, b.world_population - assert_kind_of Fixnum, b.my_house_population - assert_equal 3, b.my_house_population - assert_kind_of BigDecimal, b.bank_balance - assert_equal BigDecimal("1586.43"), b.bank_balance - assert_kind_of BigDecimal, b.big_bank_balance - assert_equal BigDecimal("1000234000567.95"), b.big_bank_balance - - # This one is fun. The 'value_of_e' field is defined as 'DECIMAL' with - # precision/scale explicitly left out. By the SQL standard, numbers - # assigned to this field should be truncated but that's seldom respected. - if current_adapter?(:PostgreSQLAdapter) - # - PostgreSQL changes the SQL spec on columns declared simply as - # "decimal" to something more useful: instead of being given a scale - # of 0, they take on the compile-time limit for precision and scale, - # so the following should succeed unless you have used really wacky - # compilation options - # - SQLite2 has the default behavior of preserving all data sent in, - # so this happens there too - assert_kind_of BigDecimal, b.value_of_e - assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e - elsif current_adapter?(:SQLite3Adapter) - # - SQLite3 stores a float, in violation of SQL - assert_kind_of BigDecimal, b.value_of_e - assert_in_delta BigDecimal("2.71828182845905"), b.value_of_e, 0.00000000000001 - else - # - SQL standard is an integer - assert_kind_of Fixnum, b.value_of_e - assert_equal 2, b.value_of_e - end - - GiveMeBigNumbers.down - assert_raise(ActiveRecord::StatementInvalid) { BigNumber.find(:first) } + old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') end +end - def test_migrator - assert !Person.column_methods_hash.include?(:last_name) - assert !Reminder.table_exists? - - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid") - - assert_equal 3, ActiveRecord::Migrator.current_version - Person.reset_column_information - assert Person.column_methods_hash.include?(:last_name) - assert Reminder.create("content" => "hello world", "remind_at" => Time.now) - assert_equal "hello world", Reminder.find(:first).content - - ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid") - - assert_equal 0, ActiveRecord::Migrator.current_version - Person.reset_column_information - assert !Person.column_methods_hash.include?(:last_name) - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } +class ReservedWordsMigrationTest < ActiveRecord::TestCase + def test_drop_index_from_table_named_values + connection = Person.connection + connection.create_table :values, :force => true do |t| + t.integer :value end - class MockMigration < ActiveRecord::Migration - attr_reader :went_up, :went_down - def initialize - @went_up = false - @went_down = false - end - - def up - @went_up = true - super - end - - def down - @went_down = true - super - end + assert_nothing_raised do + connection.add_index :values, :value + connection.remove_index :values, :column => :value end - def test_instance_based_migration_up - migration = MockMigration.new - assert !migration.went_up, 'have not gone up' - assert !migration.went_down, 'have not gone down' - - migration.migrate :up - assert migration.went_up, 'have gone up' - assert !migration.went_down, 'have not gone down' - end + connection.drop_table :values rescue nil + end +end - def test_instance_based_migration_down - migration = MockMigration.new - assert !migration.went_up, 'have not gone up' - assert !migration.went_down, 'have not gone down' - migration.migrate :down - assert !migration.went_up, 'have gone up' - assert migration.went_down, 'have not gone down' +class ChangeTableMigrationsTest < ActiveRecord::TestCase + def setup + @connection = Person.connection + @connection.create_table :delete_me, :force => true do |t| end + end - def test_migrator_one_up - assert !Person.column_methods_hash.include?(:last_name) - assert !Reminder.table_exists? - - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) - - Person.reset_column_information - assert Person.column_methods_hash.include?(:last_name) - assert !Reminder.table_exists? - - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 2) + def teardown + Person.connection.drop_table :delete_me rescue nil + end - assert Reminder.create("content" => "hello world", "remind_at" => Time.now) - assert_equal "hello world", Reminder.find(:first).content + def test_references_column_type_adds_id + with_change_table do |t| + @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {}) + t.references :customer end + end - def test_migrator_one_down - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid") - - ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 1) - - Person.reset_column_information - assert Person.column_methods_hash.include?(:last_name) - assert !Reminder.table_exists? + def test_remove_references_column_type_removes_id + with_change_table do |t| + @connection.expects(:remove_column).with(:delete_me, 'customer_id') + t.remove_references :customer end + end - def test_migrator_one_up_one_down - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) - ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 0) - - assert !Person.column_methods_hash.include?(:last_name) - assert !Reminder.table_exists? + def test_add_belongs_to_works_like_add_references + with_change_table do |t| + @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {}) + t.belongs_to :customer end + end - def test_migrator_double_up - assert_equal(0, ActiveRecord::Migrator.current_version) - ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 1) - assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 1) } - assert_equal(1, ActiveRecord::Migrator.current_version) + def test_remove_belongs_to_works_like_remove_references + with_change_table do |t| + @connection.expects(:remove_column).with(:delete_me, 'customer_id') + t.remove_belongs_to :customer end + end - def test_migrator_double_down - assert_equal(0, ActiveRecord::Migrator.current_version) - ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 1) - ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT + "/valid", 1) - assert_nothing_raised { ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT + "/valid", 1) } - assert_equal(0, ActiveRecord::Migrator.current_version) + def test_references_column_type_with_polymorphic_adds_type + with_change_table do |t| + @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {}) + @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {}) + t.references :taggable, :polymorphic => true end + end - if ActiveRecord::Base.connection.supports_ddl_transactions? - def test_migrator_one_up_with_exception_and_rollback - assert !Person.column_methods_hash.include?(:last_name) - - e = assert_raise(StandardError) do - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/broken", 100) - end - - assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message - - Person.reset_column_information - assert !Person.column_methods_hash.include?(:last_name) - end + def test_remove_references_column_type_with_polymorphic_removes_type + with_change_table do |t| + @connection.expects(:remove_column).with(:delete_me, 'taggable_type') + @connection.expects(:remove_column).with(:delete_me, 'taggable_id') + t.remove_references :taggable, :polymorphic => true end + end - def test_finds_migrations - migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations - - [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i| - assert_equal migrations[i].version, pair.first - assert_equal migrations[i].name, pair.last - end + def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag + with_change_table do |t| + @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {:null => false}) + @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {:null => false}) + t.references :taggable, :polymorphic => true, :null => false end + end - def test_finds_migrations_from_two_directories - directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps'] - migrations = ActiveRecord::Migrator.new(:up, directories).migrations - - [[20090101010101, "PeopleHaveHobbies"], - [20090101010202, "PeopleHaveDescriptions"], - [20100101010101, "ValidWithTimestampsPeopleHaveLastNames"], - [20100201010101, "ValidWithTimestampsWeNeedReminders"], - [20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i| - assert_equal pair.first, migrations[i].version - assert_equal pair.last, migrations[i].name - end + def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag + with_change_table do |t| + @connection.expects(:remove_column).with(:delete_me, 'taggable_type') + @connection.expects(:remove_column).with(:delete_me, 'taggable_id') + t.remove_references :taggable, :polymorphic => true, :null => false end + end - def test_dump_schema_information_outputs_lexically_ordered_versions - migration_path = MIGRATIONS_ROOT + '/valid_with_timestamps' - ActiveRecord::Migrator.run(:up, migration_path, 20100301010101) - ActiveRecord::Migrator.run(:up, migration_path, 20100201010101) - - schema_info = ActiveRecord::Base.connection.dump_schema_information - assert_match(/20100201010101.*20100301010101/m, schema_info) + def test_timestamps_creates_updated_at_and_created_at + with_change_table do |t| + @connection.expects(:add_timestamps).with(:delete_me) + t.timestamps end + end - def test_finds_pending_migrations - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2", 1) - migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/interleaved/pass_2").pending_migrations - - assert_equal 1, migrations.size - assert_equal migrations[0].version, 3 - assert_equal migrations[0].name, 'InterleavedInnocentJointable' + def test_remove_timestamps_creates_updated_at_and_created_at + with_change_table do |t| + @connection.expects(:remove_timestamps).with(:delete_me) + t.remove_timestamps end + end - def test_relative_migrations - list = Dir.chdir(MIGRATIONS_ROOT) do - ActiveRecord::Migrator.up("valid/", 1) - end - - migration_proxy = list.find { |item| - item.name == 'ValidPeopleHaveLastNames' - } - assert migration_proxy, 'should find pending migration' + def string_column + if current_adapter?(:PostgreSQLAdapter) + "character varying(255)" + elsif current_adapter?(:OracleAdapter) + 'VARCHAR2(255)' + else + 'varchar(255)' end + end - def test_only_loads_pending_migrations - # migrate up to 1 - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) - - proxies = ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", nil) - - names = proxies.map(&:name) - assert !names.include?('ValidPeopleHaveLastNames') - assert names.include?('WeNeedReminders') - assert names.include?('InnocentJointable') + def integer_column + if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) + 'int(11)' + elsif current_adapter?(:OracleAdapter) + 'NUMBER(38)' + else + 'integer' end + end - def test_target_version_zero_should_run_only_once - # migrate up to 1 - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1) - - # migrate down to 0 - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0) - - # migrate down to 0 again - proxies = ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0) - assert_equal [], proxies + def test_integer_creates_integer_column + with_change_table do |t| + @connection.expects(:add_column).with(:delete_me, :foo, integer_column, {}) + @connection.expects(:add_column).with(:delete_me, :bar, integer_column, {}) + t.integer :foo, :bar end + end - def test_migrator_db_has_no_schema_migrations_table - # Oracle adapter raises error if semicolon is present as last character - if current_adapter?(:OracleAdapter) - ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations") - else - ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations;") - end - assert_nothing_raised do - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1) - end - end - - def test_migrator_verbosity - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) - assert_not_equal 0, ActiveRecord::Migration.message_count - ActiveRecord::Migration.message_count = 0 - - ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 0) - assert_not_equal 0, ActiveRecord::Migration.message_count - ActiveRecord::Migration.message_count = 0 + def test_string_creates_string_column + with_change_table do |t| + @connection.expects(:add_column).with(:delete_me, :foo, string_column, {}) + @connection.expects(:add_column).with(:delete_me, :bar, string_column, {}) + t.string :foo, :bar end + end - def test_migrator_verbosity_off - ActiveRecord::Migration.verbose = false - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) - assert_equal 0, ActiveRecord::Migration.message_count - ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 0) - assert_equal 0, ActiveRecord::Migration.message_count + def test_column_creates_column + with_change_table do |t| + @connection.expects(:add_column).with(:delete_me, :bar, :integer, {}) + t.column :bar, :integer end + end - def test_migrator_going_down_due_to_version_target - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0) - - assert !Person.column_methods_hash.include?(:last_name) - assert !Reminder.table_exists? - - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid") - - Person.reset_column_information - assert Person.column_methods_hash.include?(:last_name) - assert Reminder.create("content" => "hello world", "remind_at" => Time.now) - assert_equal "hello world", Reminder.find(:first).content + def test_column_creates_column_with_options + with_change_table do |t| + @connection.expects(:add_column).with(:delete_me, :bar, :integer, {:null => false}) + t.column :bar, :integer, :null => false end + end - def test_migrator_rollback - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid") - assert_equal(3, ActiveRecord::Migrator.current_version) - - ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") - assert_equal(2, ActiveRecord::Migrator.current_version) - - ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") - assert_equal(1, ActiveRecord::Migrator.current_version) - - ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") - assert_equal(0, ActiveRecord::Migrator.current_version) - - ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") - assert_equal(0, ActiveRecord::Migrator.current_version) + def test_index_creates_index + with_change_table do |t| + @connection.expects(:add_index).with(:delete_me, :bar, {}) + t.index :bar end + end - def test_migrator_forward - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1) - assert_equal(1, ActiveRecord::Migrator.current_version) - - ActiveRecord::Migrator.forward(MIGRATIONS_ROOT + "/valid", 2) - assert_equal(3, ActiveRecord::Migrator.current_version) - - ActiveRecord::Migrator.forward(MIGRATIONS_ROOT + "/valid") - assert_equal(3, ActiveRecord::Migrator.current_version) + def test_index_creates_index_with_options + with_change_table do |t| + @connection.expects(:add_index).with(:delete_me, :bar, {:unique => true}) + t.index :bar, :unique => true end + end - def test_get_all_versions - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid") - assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions) - - ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") - assert_equal([1,2], ActiveRecord::Migrator.get_all_versions) - - ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") - assert_equal([1], ActiveRecord::Migrator.get_all_versions) - - ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") - assert_equal([], ActiveRecord::Migrator.get_all_versions) + def test_index_exists + with_change_table do |t| + @connection.expects(:index_exists?).with(:delete_me, :bar, {}) + t.index_exists?(:bar) end + end - def test_schema_migrations_table_name - ActiveRecord::Base.table_name_prefix = "prefix_" - ActiveRecord::Base.table_name_suffix = "_suffix" - Reminder.reset_table_name - assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name - ActiveRecord::Base.table_name_prefix = "" - ActiveRecord::Base.table_name_suffix = "" - Reminder.reset_table_name - assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name - ensure - ActiveRecord::Base.table_name_prefix = "" - ActiveRecord::Base.table_name_suffix = "" - end - - def test_proper_table_name - assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') - assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) - assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder) - Reminder.reset_table_name - assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder) - - # Use the model's own prefix/suffix if a model is given - ActiveRecord::Base.table_name_prefix = "ARprefix_" - ActiveRecord::Base.table_name_suffix = "_ARsuffix" - Reminder.table_name_prefix = 'prefix_' - Reminder.table_name_suffix = '_suffix' - Reminder.reset_table_name - assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder) - Reminder.table_name_prefix = '' - Reminder.table_name_suffix = '' - Reminder.reset_table_name - - # Use AR::Base's prefix/suffix if string or symbol is given - ActiveRecord::Base.table_name_prefix = "prefix_" - ActiveRecord::Base.table_name_suffix = "_suffix" - Reminder.reset_table_name - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table) - ActiveRecord::Base.table_name_prefix = "" - ActiveRecord::Base.table_name_suffix = "" - Reminder.reset_table_name - end - - def test_add_drop_table_with_prefix_and_suffix - assert !Reminder.table_exists? - ActiveRecord::Base.table_name_prefix = 'prefix_' - ActiveRecord::Base.table_name_suffix = '_suffix' - Reminder.reset_table_name - Reminder.reset_sequence_name - WeNeedReminders.up - assert Reminder.create("content" => "hello world", "remind_at" => Time.now) - assert_equal "hello world", Reminder.find(:first).content - - WeNeedReminders.down - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } - ensure - ActiveRecord::Base.table_name_prefix = '' - ActiveRecord::Base.table_name_suffix = '' - Reminder.reset_table_name - Reminder.reset_sequence_name + def test_index_exists_with_options + with_change_table do |t| + @connection.expects(:index_exists?).with(:delete_me, :bar, {:unique => true}) + t.index_exists?(:bar, :unique => true) end + end - def test_create_table_with_binary_column - Person.connection.drop_table :binary_testings rescue nil - - assert_nothing_raised { - Person.connection.create_table :binary_testings do |t| - t.column "data", :binary, :null => false - end - } - - columns = Person.connection.columns(:binary_testings) - data_column = columns.detect { |c| c.name == "data" } - - if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) - assert_equal '', data_column.default - else - assert_nil data_column.default - end - - Person.connection.drop_table :binary_testings rescue nil + def test_change_changes_column + with_change_table do |t| + @connection.expects(:change_column).with(:delete_me, :bar, :string, {}) + t.change :bar, :string end + end - def test_migrator_with_duplicates - assert_raise(ActiveRecord::DuplicateMigrationVersionError) do - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/duplicate", nil) - end + def test_change_changes_column_with_options + with_change_table do |t| + @connection.expects(:change_column).with(:delete_me, :bar, :string, {:null => true}) + t.change :bar, :string, :null => true end + end - def test_migrator_with_duplicate_names - assert_raise(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/duplicate_names", nil) - end + def test_change_default_changes_column + with_change_table do |t| + @connection.expects(:change_column_default).with(:delete_me, :bar, :string) + t.change_default :bar, :string end + end - def test_migrator_with_missing_version_numbers - assert_raise(ActiveRecord::UnknownMigrationVersionError) do - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/missing", 500) - end + def test_remove_drops_single_column + with_change_table do |t| + @connection.expects(:remove_column).with(:delete_me, [:bar]) + t.remove :bar end + end - def test_create_table_with_custom_sequence_name - return unless current_adapter? :OracleAdapter - - # table name is 29 chars, the standard sequence name will - # be 33 chars and should be shortened - assert_nothing_raised do - begin - Person.connection.create_table :table_with_name_thats_just_ok do |t| - t.column :foo, :string, :null => false - end - ensure - Person.connection.drop_table :table_with_name_thats_just_ok rescue nil - end - end - - # should be all good w/ a custom sequence name - assert_nothing_raised do - begin - Person.connection.create_table :table_with_name_thats_just_ok, - :sequence_name => 'suitably_short_seq' do |t| - t.column :foo, :string, :null => false - end - - Person.connection.execute("select suitably_short_seq.nextval from dual") - - ensure - Person.connection.drop_table :table_with_name_thats_just_ok, - :sequence_name => 'suitably_short_seq' rescue nil - end - end - - # confirm the custom sequence got dropped - assert_raise(ActiveRecord::StatementInvalid) do - Person.connection.execute("select suitably_short_seq.nextval from dual") - end + def test_remove_drops_multiple_columns + with_change_table do |t| + @connection.expects(:remove_column).with(:delete_me, [:bar, :baz]) + t.remove :bar, :baz end - - protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end - end - class MigrationLoggerTest < ActiveRecord::TestCase - def test_migration_should_be_run_without_logger - previous_logger = ActiveRecord::Base.logger - ActiveRecord::Base.logger = nil - assert_nothing_raised do - ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid") - end - ensure - ActiveRecord::Base.logger = previous_logger + def test_remove_index_removes_index_with_options + with_change_table do |t| + @connection.expects(:remove_index).with(:delete_me, {:unique => true}) + t.remove_index :unique => true end end - class InterleavedMigrationsTest < ActiveRecord::TestCase - def test_migrator_interleaved_migrations - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_1") - - assert_nothing_raised do - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2") - end - - Person.reset_column_information - assert Person.column_methods_hash.include?(:last_name) - - assert_nothing_raised do - proxies = ActiveRecord::Migrator.down( - MIGRATIONS_ROOT + "/interleaved/pass_3") - names = proxies.map(&:name) - assert names.include?('InterleavedPeopleHaveLastNames') - assert names.include?('InterleavedInnocentJointable') - end + def test_rename_renames_column + with_change_table do |t| + @connection.expects(:rename_column).with(:delete_me, :bar, :baz) + t.rename :bar, :baz end end - class ReservedWordsMigrationTest < ActiveRecord::TestCase - def test_drop_index_from_table_named_values - connection = Person.connection - connection.create_table :values, :force => true do |t| - t.integer :value - end - - assert_nothing_raised do - connection.add_index :values, :value - connection.remove_index :values, :column => :value - end - - connection.drop_table :values rescue nil + protected + def with_change_table + Person.connection.change_table :delete_me do |t| + yield t end end +end - - class ChangeTableMigrationsTest < ActiveRecord::TestCase +if ActiveRecord::Base.connection.supports_bulk_alter? + class BulkAlterTableMigrationsTest < ActiveRecord::TestCase def setup @connection = Person.connection - @connection.create_table :delete_me, :force => true do |t| - end + @connection.create_table(:delete_me, :force => true) {|t| } end def teardown - Person.connection.drop_table :delete_me rescue nil - end - - def test_references_column_type_adds_id - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {}) - t.references :customer - end + Person.connection.drop_table(:delete_me) rescue nil end - def test_remove_references_column_type_removes_id - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'customer_id') - t.remove_references :customer + def test_adding_multiple_columns + assert_queries(1) do + with_bulk_change_table do |t| + t.column :name, :string + t.string :qualification, :experience + t.integer :age, :default => 0 + t.date :birthdate + t.timestamps + end end - end - def test_add_belongs_to_works_like_add_references - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {}) - t.belongs_to :customer - end + assert_equal 8, columns.size + [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type } + assert_equal 0, column(:age).default end - def test_remove_belongs_to_works_like_remove_references - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'customer_id') - t.remove_belongs_to :customer + def test_removing_columns + with_bulk_change_table do |t| + t.string :qualification, :experience end - end - def test_references_column_type_with_polymorphic_adds_type - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {}) - @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {}) - t.references :taggable, :polymorphic => true - end - end + [:qualification, :experience].each {|c| assert column(c) } - def test_remove_references_column_type_with_polymorphic_removes_type - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'taggable_type') - @connection.expects(:remove_column).with(:delete_me, 'taggable_id') - t.remove_references :taggable, :polymorphic => true + assert_queries(1) do + with_bulk_change_table do |t| + t.remove :qualification, :experience + t.string :qualification_experience + end end - end - def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {:null => false}) - @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {:null => false}) - t.references :taggable, :polymorphic => true, :null => false - end + [:qualification, :experience].each {|c| assert ! column(c) } + assert column(:qualification_experience) end - def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'taggable_type') - @connection.expects(:remove_column).with(:delete_me, 'taggable_id') - t.remove_references :taggable, :polymorphic => true, :null => false + def test_adding_indexes + with_bulk_change_table do |t| + t.string :username + t.string :name + t.integer :age end - end - def test_timestamps_creates_updated_at_and_created_at - with_change_table do |t| - @connection.expects(:add_timestamps).with(:delete_me) - t.timestamps + # Adding an index fires a query every time to check if an index already exists or not + assert_queries(3) do + with_bulk_change_table do |t| + t.index :username, :unique => true, :name => :awesome_username_index + t.index [:name, :age] + end end - end - def test_remove_timestamps_creates_updated_at_and_created_at - with_change_table do |t| - @connection.expects(:remove_timestamps).with(:delete_me) - t.remove_timestamps - end - end + assert_equal 2, indexes.size - def string_column - if current_adapter?(:PostgreSQLAdapter) - "character varying(255)" - elsif current_adapter?(:OracleAdapter) - 'VARCHAR2(255)' - else - 'varchar(255)' - end - end + name_age_index = index(:index_delete_me_on_name_and_age) + assert_equal ['name', 'age'].sort, name_age_index.columns.sort + assert ! name_age_index.unique - def integer_column - if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) - 'int(11)' - elsif current_adapter?(:OracleAdapter) - 'NUMBER(38)' - else - 'integer' - end + assert index(:awesome_username_index).unique end - def test_integer_creates_integer_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :foo, integer_column, {}) - @connection.expects(:add_column).with(:delete_me, :bar, integer_column, {}) - t.integer :foo, :bar + def test_removing_index + with_bulk_change_table do |t| + t.string :name + t.index :name end - end - def test_string_creates_string_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :foo, string_column, {}) - @connection.expects(:add_column).with(:delete_me, :bar, string_column, {}) - t.string :foo, :bar - end - end + assert index(:index_delete_me_on_name) - def test_column_creates_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :bar, :integer, {}) - t.column :bar, :integer + assert_queries(3) do + with_bulk_change_table do |t| + t.remove_index :name + t.index :name, :name => :new_name_index, :unique => true + end end - end - def test_column_creates_column_with_options - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :bar, :integer, {:null => false}) - t.column :bar, :integer, :null => false - end - end + assert ! index(:index_delete_me_on_name) - def test_index_creates_index - with_change_table do |t| - @connection.expects(:add_index).with(:delete_me, :bar, {}) - t.index :bar - end + new_name_index = index(:new_name_index) + assert new_name_index.unique end - def test_index_creates_index_with_options - with_change_table do |t| - @connection.expects(:add_index).with(:delete_me, :bar, {:unique => true}) - t.index :bar, :unique => true + def test_changing_columns + with_bulk_change_table do |t| + t.string :name + t.date :birthdate end - end - def test_index_exists - with_change_table do |t| - @connection.expects(:index_exists?).with(:delete_me, :bar, {}) - t.index_exists?(:bar) - end - end + assert ! column(:name).default + assert_equal :date, column(:birthdate).type - def test_index_exists_with_options - with_change_table do |t| - @connection.expects(:index_exists?).with(:delete_me, :bar, {:unique => true}) - t.index_exists?(:bar, :unique => true) + # One query for columns (delete_me table) + # One query for primary key (delete_me table) + # One query to do the bulk change + assert_queries(3) do + with_bulk_change_table do |t| + t.change :name, :string, :default => 'NONAME' + t.change :birthdate, :datetime + end end - end - def test_change_changes_column - with_change_table do |t| - @connection.expects(:change_column).with(:delete_me, :bar, :string, {}) - t.change :bar, :string - end + assert_equal 'NONAME', column(:name).default + assert_equal :datetime, column(:birthdate).type end - def test_change_changes_column_with_options - with_change_table do |t| - @connection.expects(:change_column).with(:delete_me, :bar, :string, {:null => true}) - t.change :bar, :string, :null => true - end - end + protected - def test_change_default_changes_column - with_change_table do |t| - @connection.expects(:change_column_default).with(:delete_me, :bar, :string) - t.change_default :bar, :string - end - end + def with_bulk_change_table + # Reset columns/indexes cache as we're changing the table + @columns = @indexes = nil - def test_remove_drops_single_column - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, [:bar]) - t.remove :bar + Person.connection.change_table(:delete_me, :bulk => true) do |t| + yield t end end - def test_remove_drops_multiple_columns - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, [:bar, :baz]) - t.remove :bar, :baz - end + def column(name) + columns.detect {|c| c.name == name.to_s } end - def test_remove_index_removes_index_with_options - with_change_table do |t| - @connection.expects(:remove_index).with(:delete_me, {:unique => true}) - t.remove_index :unique => true - end + def columns + @columns ||= Person.connection.columns('delete_me') end - def test_rename_renames_column - with_change_table do |t| - @connection.expects(:rename_column).with(:delete_me, :bar, :baz) - t.rename :bar, :baz - end + def index(name) + indexes.detect {|i| i.name == name.to_s } end - protected - def with_change_table - Person.connection.change_table :delete_me do |t| - yield t - end + def indexes + @indexes ||= Person.connection.indexes('delete_me') end - end - - if ActiveRecord::Base.connection.supports_bulk_alter? - class BulkAlterTableMigrationsTest < ActiveRecord::TestCase - def setup - @connection = Person.connection - @connection.create_table(:delete_me, :force => true) {|t| } - end - - def teardown - Person.connection.drop_table(:delete_me) rescue nil - end - - def test_adding_multiple_columns - assert_queries(1) do - with_bulk_change_table do |t| - t.column :name, :string - t.string :qualification, :experience - t.integer :age, :default => 0 - t.date :birthdate - t.timestamps - end - end - - assert_equal 8, columns.size - [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type } - assert_equal 0, column(:age).default - end - - def test_removing_columns - with_bulk_change_table do |t| - t.string :qualification, :experience - end + end # AlterTableMigrationsTest - [:qualification, :experience].each {|c| assert column(c) } - - assert_queries(1) do - with_bulk_change_table do |t| - t.remove :qualification, :experience - t.string :qualification_experience - end - end - - [:qualification, :experience].each {|c| assert ! column(c) } - assert column(:qualification_experience) - end - - def test_adding_indexes - with_bulk_change_table do |t| - t.string :username - t.string :name - t.integer :age - end - - # Adding an index fires a query every time to check if an index already exists or not - assert_queries(3) do - with_bulk_change_table do |t| - t.index :username, :unique => true, :name => :awesome_username_index - t.index [:name, :age] - end - end - - assert_equal 2, indexes.size - - name_age_index = index(:index_delete_me_on_name_and_age) - assert_equal ['name', 'age'].sort, name_age_index.columns.sort - assert ! name_age_index.unique - - assert index(:awesome_username_index).unique - end - - def test_removing_index - with_bulk_change_table do |t| - t.string :name - t.index :name - end - - assert index(:index_delete_me_on_name) - - assert_queries(3) do - with_bulk_change_table do |t| - t.remove_index :name - t.index :name, :name => :new_name_index, :unique => true - end - end - - assert ! index(:index_delete_me_on_name) - - new_name_index = index(:new_name_index) - assert new_name_index.unique - end - - def test_changing_columns - with_bulk_change_table do |t| - t.string :name - t.date :birthdate - end - - assert ! column(:name).default - assert_equal :date, column(:birthdate).type - - # One query for columns (delete_me table) - # One query for primary key (delete_me table) - # One query to do the bulk change - assert_queries(3) do - with_bulk_change_table do |t| - t.change :name, :string, :default => 'NONAME' - t.change :birthdate, :datetime - end - end - - assert_equal 'NONAME', column(:name).default - assert_equal :datetime, column(:birthdate).type - end - - protected - - def with_bulk_change_table - # Reset columns/indexes cache as we're changing the table - @columns = @indexes = nil - - Person.connection.change_table(:delete_me, :bulk => true) do |t| - yield t - end - end - - def column(name) - columns.detect {|c| c.name == name.to_s } - end - - def columns - @columns ||= Person.connection.columns('delete_me') - end - - def index(name) - indexes.detect {|i| i.name == name.to_s } - end +end - def indexes - @indexes ||= Person.connection.indexes('delete_me') - end - end # AlterTableMigrationsTest +class CopyMigrationsTest < ActiveRecord::TestCase + def setup + end + def clear + ActiveRecord::Base.timestamped_migrations = true + to_delete = Dir[@migrations_path + "/*.rb"] - @existing_migrations + File.delete(*to_delete) end - class CopyMigrationsTest < ActiveRecord::TestCase - def setup - end + def test_copying_migrations_without_timestamps + ActiveRecord::Base.timestamped_migrations = false + @migrations_path = MIGRATIONS_ROOT + "/valid" + @existing_migrations = Dir[@migrations_path + "/*.rb"] + + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"}) + assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") + assert_equal [@migrations_path + "/4_people_have_hobbies.bukkits.rb", @migrations_path + "/5_people_have_descriptions.bukkits.rb"], copied.map(&:filename) + + expected = "# This migration comes from bukkits (originally 1)" + assert_equal expected, IO.readlines(@migrations_path + "/4_people_have_hobbies.bukkits.rb")[0].chomp + + files_count = Dir[@migrations_path + "/*.rb"].length + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"}) + assert_equal files_count, Dir[@migrations_path + "/*.rb"].length + assert copied.empty? + ensure + clear + end - def clear - ActiveRecord::Base.timestamped_migrations = true - to_delete = Dir[@migrations_path + "/*.rb"] - @existing_migrations - File.delete(*to_delete) - end + def test_copying_migrations_without_timestamps_from_2_sources + ActiveRecord::Base.timestamped_migrations = false + @migrations_path = MIGRATIONS_ROOT + "/valid" + @existing_migrations = Dir[@migrations_path + "/*.rb"] + + sources = ActiveSupport::OrderedHash.new + sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy" + sources[:omg] = MIGRATIONS_ROOT + "/to_copy2" + ActiveRecord::Migration.copy(@migrations_path, sources) + assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") + assert File.exists?(@migrations_path + "/6_create_articles.omg.rb") + assert File.exists?(@migrations_path + "/7_create_comments.omg.rb") + + files_count = Dir[@migrations_path + "/*.rb"].length + ActiveRecord::Migration.copy(@migrations_path, sources) + assert_equal files_count, Dir[@migrations_path + "/*.rb"].length + ensure + clear + end - def test_copying_migrations_without_timestamps - ActiveRecord::Base.timestamped_migrations = false - @migrations_path = MIGRATIONS_ROOT + "/valid" - @existing_migrations = Dir[@migrations_path + "/*.rb"] + def test_copying_migrations_with_timestamps + @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" + @existing_migrations = Dir[@migrations_path + "/*.rb"] - copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"}) - assert File.exists?(@migrations_path + "/4_people_have_hobbies.rb") - assert File.exists?(@migrations_path + "/5_people_have_descriptions.rb") - assert_equal [@migrations_path + "/4_people_have_hobbies.rb", @migrations_path + "/5_people_have_descriptions.rb"], copied.map(&:filename) + Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) + assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + expected = [@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb", + @migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb"] + assert_equal expected, copied.map(&:filename) files_count = Dir[@migrations_path + "/*.rb"].length - copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"}) + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) assert_equal files_count, Dir[@migrations_path + "/*.rb"].length assert copied.empty? - ensure - clear end + ensure + clear + end - def test_copying_migrations_without_timestamps_from_2_sources - ActiveRecord::Base.timestamped_migrations = false - @migrations_path = MIGRATIONS_ROOT + "/valid" - @existing_migrations = Dir[@migrations_path + "/*.rb"] + def test_copying_migrations_with_timestamps_from_2_sources + @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" + @existing_migrations = Dir[@migrations_path + "/*.rb"] - sources = ActiveSupport::OrderedHash.new - sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy" - sources[:omg] = MIGRATIONS_ROOT + "/to_copy2" - ActiveRecord::Migration.copy(@migrations_path, sources) - assert File.exists?(@migrations_path + "/4_people_have_hobbies.rb") - assert File.exists?(@migrations_path + "/5_people_have_descriptions.rb") - assert File.exists?(@migrations_path + "/6_create_articles.rb") - assert File.exists?(@migrations_path + "/7_create_comments.rb") + sources = ActiveSupport::OrderedHash.new + sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" + sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps2" + + Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + copied = ActiveRecord::Migration.copy(@migrations_path, sources) + assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exists?(@migrations_path + "/20100726101012_create_articles.omg.rb") + assert File.exists?(@migrations_path + "/20100726101013_create_comments.omg.rb") + assert_equal 4, copied.length files_count = Dir[@migrations_path + "/*.rb"].length ActiveRecord::Migration.copy(@migrations_path, sources) assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - ensure - clear end + ensure + clear + end - def test_copying_migrations_with_timestamps - @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" - @existing_migrations = Dir[@migrations_path + "/*.rb"] + def test_copying_migrations_with_timestamps_to_destination_with_timestamps_in_future + @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" + @existing_migrations = Dir[@migrations_path + "/*.rb"] - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do - copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb") - expected = [@migrations_path + "/20100726101010_people_have_hobbies.rb", - @migrations_path + "/20100726101011_people_have_descriptions.rb"] - assert_equal expected, copied.map(&:filename) + Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do + ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) + assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb") - files_count = Dir[@migrations_path + "/*.rb"].length - copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - assert copied.empty? - end - ensure - clear + files_count = Dir[@migrations_path + "/*.rb"].length + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) + assert_equal files_count, Dir[@migrations_path + "/*.rb"].length + assert copied.empty? end + ensure + clear + end - def test_copying_migrations_with_timestamps_from_2_sources - @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" - @existing_migrations = Dir[@migrations_path + "/*.rb"] + def test_skipping_migrations + @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" + @existing_migrations = Dir[@migrations_path + "/*.rb"] - sources = ActiveSupport::OrderedHash.new - sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" - sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps2" + sources = ActiveSupport::OrderedHash.new + sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" + sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_name_collision" - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do - copied = ActiveRecord::Migration.copy(@migrations_path, sources) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb") - assert File.exists?(@migrations_path + "/20100726101012_create_articles.rb") - assert File.exists?(@migrations_path + "/20100726101013_create_comments.rb") - assert_equal 4, copied.length + skipped = [] + on_skip = Proc.new { |name, migration| skipped << "#{name} #{migration.name}" } + copied = ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip) + assert_equal 2, copied.length - files_count = Dir[@migrations_path + "/*.rb"].length - ActiveRecord::Migration.copy(@migrations_path, sources) - assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - end - ensure - clear - end + assert_equal 1, skipped.length + assert_equal ["omg PeopleHaveHobbies"], skipped + ensure + clear + end - def test_copying_migrations_with_timestamps_to_destination_with_timestamps_in_future - @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" - @existing_migrations = Dir[@migrations_path + "/*.rb"] + def test_skip_is_not_called_if_migrations_are_from_the_same_plugin + @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" + @existing_migrations = Dir[@migrations_path + "/*.rb"] - Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do - ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.rb") - assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.rb") + sources = ActiveSupport::OrderedHash.new + sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" - files_count = Dir[@migrations_path + "/*.rb"].length - copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert_equal files_count, Dir[@migrations_path + "/*.rb"].length - assert copied.empty? - end - ensure - clear - end + skipped = [] + on_skip = Proc.new { |name, migration| skipped << "#{name} #{migration.name}" } + copied = ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip) + ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip) - def test_skipping_migrations - @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" - @existing_migrations = Dir[@migrations_path + "/*.rb"] + assert_equal 2, copied.length + assert_equal 0, skipped.length + ensure + clear + end - sources = ActiveSupport::OrderedHash.new - sources[:bukkits] = sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" + def test_copying_migrations_to_non_existing_directory + @migrations_path = MIGRATIONS_ROOT + "/non_existing" + @existing_migrations = [] - skipped = [] - on_skip = Proc.new { |name, migration| skipped << "#{name} #{migration.name}" } - copied = ActiveRecord::Migration.copy(@migrations_path, sources, :on_skip => on_skip) + Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) + assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") assert_equal 2, copied.length - - assert_equal 2, skipped.length - assert_equal ["bukkits PeopleHaveHobbies", "bukkits PeopleHaveDescriptions"], skipped - ensure - clear - end - - def test_copying_migrations_to_non_existing_directory - @migrations_path = MIGRATIONS_ROOT + "/non_existing" - @existing_migrations = [] - - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do - copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb") - assert_equal 2, copied.length - end - ensure - clear - Dir.delete(@migrations_path) end + ensure + clear + Dir.delete(@migrations_path) + end - def test_copying_migrations_to_empty_directory - @migrations_path = MIGRATIONS_ROOT + "/empty" - @existing_migrations = [] + def test_copying_migrations_to_empty_directory + @migrations_path = MIGRATIONS_ROOT + "/empty" + @existing_migrations = [] - Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do - copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.rb") - assert_equal 2, copied.length - end - ensure - clear + Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) + assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert_equal 2, copied.length end + ensure + clear end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb new file mode 100644 index 0000000000..1e16addcf3 --- /dev/null +++ b/activerecord/test/cases/migrator_test.rb @@ -0,0 +1,377 @@ +require "cases/helper" +require "cases/migration/helper" + +module ActiveRecord + class MigratorTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + # Use this class to sense if migrations have gone + # up or down. + class Sensor < ActiveRecord::Migration + attr_reader :went_up, :went_down + + def initialize name = self.class.name, version = nil + super + @went_up = false + @went_down = false + end + + def up; @went_up = true; end + def down; @went_down = true; end + end + + def setup + super + ActiveRecord::SchemaMigration.create_table + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + def teardown + super + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + def test_migrator_with_duplicate_names + assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do + list = [Migration.new('Chunky'), Migration.new('Chunky')] + ActiveRecord::Migrator.new(:up, list) + end + end + + def test_migrator_with_duplicate_versions + assert_raises(ActiveRecord::DuplicateMigrationVersionError) do + list = [Migration.new('Foo', 1), Migration.new('Bar', 1)] + ActiveRecord::Migrator.new(:up, list) + end + end + + def test_migrator_with_missing_version_numbers + assert_raises(ActiveRecord::UnknownMigrationVersionError) do + list = [Migration.new('Foo', 1), Migration.new('Bar', 2)] + ActiveRecord::Migrator.new(:up, list, 3).run + end + end + + def test_finds_migrations + migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid") + + [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i| + assert_equal migrations[i].version, pair.first + assert_equal migrations[i].name, pair.last + end + end + + def test_finds_migrations_in_subdirectories + migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories") + + [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i| + assert_equal migrations[i].version, pair.first + assert_equal migrations[i].name, pair.last + end + end + + def test_finds_migrations_from_two_directories + directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps'] + migrations = ActiveRecord::Migrator.migrations directories + + [[20090101010101, "PeopleHaveHobbies"], + [20090101010202, "PeopleHaveDescriptions"], + [20100101010101, "ValidWithTimestampsPeopleHaveLastNames"], + [20100201010101, "ValidWithTimestampsWeNeedReminders"], + [20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i| + assert_equal pair.first, migrations[i].version + assert_equal pair.last, migrations[i].name + end + end + + def test_deprecated_constructor + assert_deprecated do + ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid") + end + end + + def test_relative_migrations + list = Dir.chdir(MIGRATIONS_ROOT) do + ActiveRecord::Migrator.migrations("valid/") + end + + migration_proxy = list.find { |item| + item.name == 'ValidPeopleHaveLastNames' + } + assert migration_proxy, 'should find pending migration' + end + + def test_finds_pending_migrations + ActiveRecord::SchemaMigration.create!(:version => '1') + migration_list = [ Migration.new('foo', 1), Migration.new('bar', 3) ] + migrations = ActiveRecord::Migrator.new(:up, migration_list).pending_migrations + + assert_equal 1, migrations.size + assert_equal migration_list.last, migrations.first + end + + def test_migrator_interleaved_migrations + pass_one = [Sensor.new('One', 1)] + + ActiveRecord::Migrator.new(:up, pass_one).migrate + assert pass_one.first.went_up + refute pass_one.first.went_down + + pass_two = [Sensor.new('One', 1), Sensor.new('Three', 3)] + ActiveRecord::Migrator.new(:up, pass_two).migrate + refute pass_two[0].went_up + assert pass_two[1].went_up + assert pass_two.all? { |x| !x.went_down } + + pass_three = [Sensor.new('One', 1), + Sensor.new('Two', 2), + Sensor.new('Three', 3)] + + ActiveRecord::Migrator.new(:down, pass_three).migrate + assert pass_three[0].went_down + refute pass_three[1].went_down + assert pass_three[2].went_down + end + + def test_up_calls_up + migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] + ActiveRecord::Migrator.new(:up, migrations).migrate + assert migrations.all? { |m| m.went_up } + assert migrations.all? { |m| !m.went_down } + assert_equal 2, ActiveRecord::Migrator.current_version + end + + def test_down_calls_down + test_up_calls_up + + migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] + ActiveRecord::Migrator.new(:down, migrations).migrate + assert migrations.all? { |m| !m.went_up } + assert migrations.all? { |m| m.went_down } + assert_equal 0, ActiveRecord::Migrator.current_version + end + + def test_current_version + ActiveRecord::SchemaMigration.create!(:version => '1000') + assert_equal 1000, ActiveRecord::Migrator.current_version + end + + def test_migrator_one_up + calls, migrations = sensors(3) + + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal [[:up, 1]], calls + calls.clear + + ActiveRecord::Migrator.new(:up, migrations, 2).migrate + assert_equal [[:up, 2]], calls + end + + def test_migrator_one_down + calls, migrations = sensors(3) + + ActiveRecord::Migrator.new(:up, migrations).migrate + assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls + calls.clear + + ActiveRecord::Migrator.new(:down, migrations, 1).migrate + + assert_equal [[:down, 3], [:down, 2]], calls + end + + def test_migrator_one_up_one_down + calls, migrations = sensors(3) + + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal [[:up, 1]], calls + calls.clear + + ActiveRecord::Migrator.new(:down, migrations, 0).migrate + assert_equal [[:down, 1]], calls + end + + def test_migrator_double_up + calls, migrations = sensors(3) + assert_equal(0, ActiveRecord::Migrator.current_version) + + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal [[:up, 1]], calls + calls.clear + + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal [], calls + end + + def test_migrator_double_down + calls, migrations = sensors(3) + + assert_equal(0, ActiveRecord::Migrator.current_version) + + ActiveRecord::Migrator.new(:up, migrations, 1).run + assert_equal [[:up, 1]], calls + calls.clear + + ActiveRecord::Migrator.new(:down, migrations, 1).run + assert_equal [[:down, 1]], calls + calls.clear + + ActiveRecord::Migrator.new(:down, migrations, 1).run + assert_equal [], calls + + assert_equal(0, ActiveRecord::Migrator.current_version) + end + + def test_migrator_verbosity + _, migrations = sensors(3) + + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_not_equal 0, ActiveRecord::Migration.message_count + + ActiveRecord::Migration.message_count = 0 + + ActiveRecord::Migrator.new(:down, migrations, 0).migrate + assert_not_equal 0, ActiveRecord::Migration.message_count + ActiveRecord::Migration.message_count = 0 + end + + def test_migrator_verbosity_off + _, migrations = sensors(3) + + ActiveRecord::Migration.message_count = 0 + ActiveRecord::Migration.verbose = false + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal 0, ActiveRecord::Migration.message_count + ActiveRecord::Migrator.new(:down, migrations, 0).migrate + assert_equal 0, ActiveRecord::Migration.message_count + end + + def test_target_version_zero_should_run_only_once + calls, migrations = sensors(3) + + # migrate up to 1 + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal [[:up, 1]], calls + calls.clear + + # migrate down to 0 + ActiveRecord::Migrator.new(:down, migrations, 0).migrate + assert_equal [[:down, 1]], calls + calls.clear + + # migrate down to 0 again + ActiveRecord::Migrator.new(:down, migrations, 0).migrate + assert_equal [], calls + end + + def test_migrator_going_down_due_to_version_target + calls, migrator = migrator_class(3) + + migrator.up("valid", 1) + assert_equal [[:up, 1]], calls + calls.clear + + migrator.migrate("valid", 0) + assert_equal [[:down, 1]], calls + calls.clear + + migrator.migrate("valid") + assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls + end + + def test_migrator_rollback + _, migrator = migrator_class(3) + + migrator.migrate("valid") + assert_equal(3, ActiveRecord::Migrator.current_version) + + migrator.rollback("valid") + assert_equal(2, ActiveRecord::Migrator.current_version) + + migrator.rollback("valid") + assert_equal(1, ActiveRecord::Migrator.current_version) + + migrator.rollback("valid") + assert_equal(0, ActiveRecord::Migrator.current_version) + + migrator.rollback("valid") + assert_equal(0, ActiveRecord::Migrator.current_version) + end + + def test_migrator_db_has_no_schema_migrations_table + _, migrator = migrator_class(3) + + ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations") + refute ActiveRecord::Base.connection.table_exists?('schema_migrations') + migrator.migrate("valid", 1) + assert ActiveRecord::Base.connection.table_exists?('schema_migrations') + end + + def test_migrator_forward + _, migrator = migrator_class(3) + migrator.migrate("/valid", 1) + assert_equal(1, ActiveRecord::Migrator.current_version) + + migrator.forward("/valid", 2) + assert_equal(3, ActiveRecord::Migrator.current_version) + + migrator.forward("/valid") + assert_equal(3, ActiveRecord::Migrator.current_version) + end + + def test_only_loads_pending_migrations + # migrate up to 1 + ActiveRecord::SchemaMigration.create!(:version => '1') + + calls, migrator = migrator_class(3) + migrator.migrate("valid", nil) + + assert_equal [[:up, 2], [:up, 3]], calls + end + + def test_get_all_versions + _, migrator = migrator_class(3) + + migrator.migrate("valid") + assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions) + + migrator.rollback("valid") + assert_equal([1,2], ActiveRecord::Migrator.get_all_versions) + + migrator.rollback("valid") + assert_equal([1], ActiveRecord::Migrator.get_all_versions) + + migrator.rollback("valid") + assert_equal([], ActiveRecord::Migrator.get_all_versions) + end + + private + def m(name, version, &block) + x = Sensor.new name, version + x.extend(Module.new { + define_method(:up) { block.call(:up, x); super() } + define_method(:down) { block.call(:down, x); super() } + }) if block_given? + end + + def sensors(count) + calls = [] + migrations = count.times.map { |i| + m(nil, i + 1) { |c,migration| + calls << [c, migration.version] + } + } + [calls, migrations] + end + + def migrator_class(count) + calls, migrations = sensors(count) + + migrator = Class.new(Migrator).extend(Module.new { + define_method(:migrations) { |paths| + migrations + } + }) + [calls, migrator] + end + end +end diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index a2041af16a..f7a5d05582 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -72,7 +72,7 @@ class ModulesTest < ActiveRecord::TestCase clients = [] assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do - clients << MyApplication::Business::Client.find(3, :include => {:firm => :account}, :conditions => 'accounts.id IS NOT NULL') + clients << MyApplication::Business::Client.find(3, :include => {:firm => :account}, :conditions => 'accounts.id IS NOT NULL', :references => :accounts) clients << MyApplication::Business::Client.find(3, :include => {:firm => :account}) end diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index bd51388e05..e704322b5d 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -85,7 +85,6 @@ class MultipleDbTest < ActiveRecord::TestCase end def test_arel_table_engines - assert_not_equal Entrant.arel_engine, Course.arel_engine assert_equal Entrant.arel_engine, Bird.arel_engine end end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 4a09a87322..e17ba76437 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'active_support/core_ext/array/random_access' require 'models/post' require 'models/topic' require 'models/comment' @@ -337,6 +336,11 @@ class NamedScopeTest < ActiveRecord::TestCase end end + def test_should_not_duplicates_where_values + where_values = Topic.where("1=1").scope_with_lambda.where_values + assert_equal ["1=1"], where_values + end + def test_chaining_with_duplicate_joins join = "INNER JOIN comments ON comments.post_id = posts.id" post = Post.find(1) diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index bc3dfb1078..fba3006ebe 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -7,17 +7,17 @@ class PooledConnectionsTest < ActiveRecord::TestCase def setup @per_test_teardown = [] - @connection = ActiveRecord::Base.remove_connection + @connection = ActiveRecord::Model.remove_connection end def teardown - ActiveRecord::Base.clear_all_connections! - ActiveRecord::Base.establish_connection(@connection) + ActiveRecord::Model.clear_all_connections! + ActiveRecord::Model.establish_connection(@connection) @per_test_teardown.each {|td| td.call } end def checkout_connections - ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :wait_timeout => 0.3})) + ActiveRecord::Model.establish_connection(@connection.merge({:pool => 2, :wait_timeout => 0.3})) @connections = [] @timed_out = 0 @@ -33,24 +33,16 @@ class PooledConnectionsTest < ActiveRecord::TestCase end # Will deadlock due to lack of Monitor timeouts in 1.9 - if RUBY_VERSION < '1.9' - def test_pooled_connection_checkout - checkout_connections - assert_equal 2, @connections.length - assert_equal 2, @timed_out - end - end - def checkout_checkin_connections(pool_size, threads) - ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :wait_timeout => 0.5})) + ActiveRecord::Model.establish_connection(@connection.merge({:pool => pool_size, :wait_timeout => 0.5})) @connection_count = 0 @timed_out = 0 threads.times do Thread.new do begin - conn = ActiveRecord::Base.connection_pool.checkout + conn = ActiveRecord::Model.connection_pool.checkout sleep 0.1 - ActiveRecord::Base.connection_pool.checkin conn + ActiveRecord::Model.connection_pool.checkin conn @connection_count += 1 rescue ActiveRecord::ConnectionTimeoutError @timed_out += 1 @@ -63,13 +55,13 @@ class PooledConnectionsTest < ActiveRecord::TestCase checkout_checkin_connections 1, 2 assert_equal 2, @connection_count assert_equal 0, @timed_out - assert_equal 1, ActiveRecord::Base.connection_pool.connections.size + assert_equal 1, ActiveRecord::Model.connection_pool.connections.size end private def add_record(name) - ActiveRecord::Base.connection_pool.with_connection { Project.create! :name => name } + ActiveRecord::Model.connection_pool.with_connection { Project.create! :name => name } end end unless current_adapter?(:FrontBase) || in_memory_db? diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 4bb5752096..b4d1a631fa 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -23,6 +23,11 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal keyboard.to_key, [keyboard.id] end + def test_read_attribute_with_custom_primary_key + keyboard = Keyboard.create! + assert_equal keyboard.key_number, keyboard.read_attribute(:id) + end + def test_to_key_with_primary_key_after_destroy topic = Topic.find(1) topic.destroy @@ -142,8 +147,38 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal k.connection.quote_column_name("id"), k.quoted_primary_key k.primary_key = "foo" assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key - k.set_primary_key "bar" - assert_equal k.connection.quote_column_name("bar"), k.quoted_primary_key + end + + def test_two_models_with_same_table_but_different_primary_key + k1 = Class.new(ActiveRecord::Base) + k1.table_name = 'posts' + k1.primary_key = 'id' + + k2 = Class.new(ActiveRecord::Base) + k2.table_name = 'posts' + k2.primary_key = 'title' + + assert k1.columns.find { |c| c.name == 'id' }.primary + assert !k1.columns.find { |c| c.name == 'title' }.primary + assert k1.columns_hash['id'].primary + assert !k1.columns_hash['title'].primary + + assert !k2.columns.find { |c| c.name == 'id' }.primary + assert k2.columns.find { |c| c.name == 'title' }.primary + assert !k2.columns_hash['id'].primary + assert k2.columns_hash['title'].primary + end + + def test_models_with_same_table_have_different_columns + k1 = Class.new(ActiveRecord::Base) + k1.table_name = 'posts' + + k2 = Class.new(ActiveRecord::Base) + k2.table_name = 'posts' + + k1.columns.zip(k2.columns).each do |col1, col2| + assert !col1.equal?(col2) + end end end @@ -153,15 +188,14 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase def test_set_primary_key_with_no_connection return skip("disconnect wipes in-memory db") if in_memory_db? - connection = ActiveRecord::Base.remove_connection + connection = ActiveRecord::Model.remove_connection - model = Class.new(ActiveRecord::Base) do - set_primary_key 'foo' - end + model = Class.new(ActiveRecord::Base) + model.primary_key = 'foo' assert_equal 'foo', model.primary_key - ActiveRecord::Base.establish_connection(connection) + ActiveRecord::Model.establish_connection(connection) assert_equal 'foo', model.primary_key end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 9554386dcf..f36f4237b1 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -3,7 +3,7 @@ require 'models/topic' require 'models/task' require 'models/category' require 'models/post' - +require 'rack' class QueryCacheTest < ActiveRecord::TestCase fixtures :tasks, :topics, :categories, :posts, :categories_posts @@ -43,6 +43,7 @@ class QueryCacheTest < ActiveRecord::TestCase called = false mw = ActiveRecord::QueryCache.new lambda { |env| called = true + [200, {}, nil] } mw.call({}) assert called, 'middleware should delegate' @@ -53,6 +54,7 @@ class QueryCacheTest < ActiveRecord::TestCase Task.find 1 Task.find 1 assert_equal 1, ActiveRecord::Base.connection.query_cache.length + [200, {}, nil] } mw.call({}) end @@ -62,6 +64,7 @@ class QueryCacheTest < ActiveRecord::TestCase mw = ActiveRecord::QueryCache.new lambda { |env| assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on' + [200, {}, nil] } mw.call({}) end @@ -83,7 +86,7 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_cache_off_after_close - mw = ActiveRecord::QueryCache.new lambda { |env| } + mw = ActiveRecord::QueryCache.new lambda { |env| [200, {}, nil] } body = mw.call({}).last assert ActiveRecord::Base.connection.query_cache_enabled, 'cache enabled' @@ -94,6 +97,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_clear_after_close mw = ActiveRecord::QueryCache.new lambda { |env| Post.find(:first) + [200, {}, nil] } body = mw.call({}).last @@ -244,14 +248,3 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase end end end - -class QueryCacheBodyProxyTest < ActiveRecord::TestCase - - test "is polite to it's body and responds to it" do - body = Class.new(String) { def to_path; "/path"; end }.new - proxy = ActiveRecord::QueryCache::BodyProxy.new(nil, body, ActiveRecord::Base.connection_id) - assert proxy.respond_to?(:to_path) - assert_equal proxy.to_path, "/path" - end - -end diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb new file mode 100644 index 0000000000..576ab60090 --- /dev/null +++ b/activerecord/test/cases/reaper_test.rb @@ -0,0 +1,81 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class ReaperTest < ActiveRecord::TestCase + attr_reader :pool + + def setup + super + @pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec + end + + def teardown + super + @pool.connections.each(&:close) + end + + class FakePool + attr_reader :reaped + + def initialize + @reaped = false + end + + def reap + @reaped = true + end + end + + # A reaper with nil time should never reap connections + def test_nil_time + fp = FakePool.new + assert !fp.reaped + reaper = ConnectionPool::Reaper.new(fp, nil) + reaper.run + assert !fp.reaped + end + + def test_some_time + fp = FakePool.new + assert !fp.reaped + + reaper = ConnectionPool::Reaper.new(fp, 0.0001) + reaper.run + until fp.reaped + Thread.pass + end + assert fp.reaped + end + + def test_pool_has_reaper + assert pool.reaper + end + + def test_reaping_frequency_configuration + spec = ActiveRecord::Base.connection_pool.spec.dup + spec.config[:reaping_frequency] = 100 + pool = ConnectionPool.new spec + assert_equal 100, pool.reaper.frequency + end + + def test_connection_pool_starts_reaper + spec = ActiveRecord::Base.connection_pool.spec.dup + spec.config[:reaping_frequency] = 0.0001 + + pool = ConnectionPool.new spec + pool.timeout = 0 + + conn = pool.checkout + count = pool.connections.length + + conn.extend(Module.new { def active?; false; end; }) + + while count == pool.connections.length + Thread.pass + end + assert_equal(count - 1, pool.connections.length) + end + end + end +end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index b30db542a7..297fb56570 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -36,25 +36,25 @@ class ReflectionTest < ActiveRecord::TestCase def test_read_attribute_names assert_equal( - %w( id title author_name author_email_address bonus_time written_on last_read content group approved replies_count parent_id parent_title type created_at updated_at ).sort, + %w( id title author_name author_email_address bonus_time written_on last_read content important group approved replies_count parent_id parent_title type created_at updated_at ).sort, @first.attribute_names.sort ) end def test_columns - assert_equal 16, Topic.columns.length + assert_equal 17, Topic.columns.length end def test_columns_are_returned_in_the_order_they_were_declared column_names = Topic.columns.map { |column| column.name } - assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id parent_title type group created_at updated_at), column_names + assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content important approved replies_count parent_id parent_title type group created_at updated_at), column_names end def test_content_columns content_columns = Topic.content_columns content_column_names = content_columns.map {|column| column.name} - assert_equal 12, content_columns.length - assert_equal %w(title author_name author_email_address written_on bonus_time last_read content group approved parent_title created_at updated_at).sort, content_column_names.sort + assert_equal 13, content_columns.length + assert_equal %w(title author_name author_email_address written_on bonus_time last_read content important group approved parent_title created_at updated_at).sort, content_column_names.sort end def test_column_string_type_and_limit @@ -189,8 +189,8 @@ class ReflectionTest < ActiveRecord::TestCase def test_reflection_of_all_associations # FIXME these assertions bust a lot - assert_equal 37, Firm.reflect_on_all_associations.size - assert_equal 27, Firm.reflect_on_all_associations(:has_many).size + assert_equal 38, Firm.reflect_on_all_associations.size + assert_equal 28, Firm.reflect_on_all_associations(:has_many).size assert_equal 10, Firm.reflect_on_all_associations(:has_one).size assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 1e2093273e..edf38cb7a3 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -106,7 +106,7 @@ class RelationScopingTest < ActiveRecord::TestCase def test_scoped_find_include # with the include, will retrieve only developers for the given project scoped_developers = Developer.includes(:projects).scoping do - Developer.where('projects.id = 2').all + Developer.where('projects.id' => 2).all end assert scoped_developers.include?(developers(:david)) assert !scoped_developers.include?(developers(:jamis)) @@ -421,6 +421,12 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end + def test_order_after_reorder_combines_orders + expected = Developer.order('name DESC, id DESC').collect { |dev| [dev.name, dev.id] } + received = Developer.order('name ASC').reorder('name DESC').order('id DESC').collect { |dev| [dev.name, dev.id] } + assert_equal expected, received + end + def test_nested_exclusive_scope expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 715a378431..ac6dee3c6a 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -20,7 +20,7 @@ module ActiveRecord end def test_single_values - assert_equal [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order, :uniq].map(&:to_s).sort, + assert_equal [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :uniq].map(&:to_s).sort, Relation::SINGLE_VALUE_METHODS.map(&:to_s).sort end @@ -44,7 +44,7 @@ module ActiveRecord end def test_multi_value_methods - assert_equal [:select, :group, :order, :joins, :where, :having, :bind].map(&:to_s).sort, + assert_equal [:select, :group, :order, :joins, :where, :having, :bind, :references].map(&:to_s).sort, Relation::MULTI_VALUE_METHODS.map(&:to_s).sort end @@ -135,5 +135,24 @@ module ActiveRecord relation.eager_load_values << :b assert relation.eager_loading? end + + def test_references_values + relation = Relation.new :a, :b + assert_equal [], relation.references_values + relation = relation.references(:foo).references(:omg, :lol) + assert_equal ['foo', 'omg', 'lol'], relation.references_values + end + + def test_references_values_dont_duplicate + relation = Relation.new :a, :b + relation = relation.references(:foo).references(:foo) + assert_equal ['foo'], relation.references_values + end + + def test_apply_finder_options_takes_references + relation = Relation.new :a, :b + relation = relation.apply_finder_options(:references => :foo) + assert_equal ['foo'], relation.references_values + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index bf1eb6386a..5e19465253 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -184,12 +184,12 @@ class RelationTest < ActiveRecord::TestCase end def test_finding_with_complex_order_and_limit - tags = Tag.includes(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a + tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a assert_equal 1, tags.length end def test_finding_with_complex_order - tags = Tag.includes(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a + tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a assert_equal 3, tags.length end @@ -1104,7 +1104,9 @@ class RelationTest < ActiveRecord::TestCase ) ) - assert scope.eager_loading? + assert_deprecated do + assert scope.eager_loading? + end end def test_ordering_with_extra_spaces @@ -1164,4 +1166,49 @@ class RelationTest < ActiveRecord::TestCase end assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) end + + def test_references_triggers_eager_loading + scope = Post.includes(:comments) + assert !scope.eager_loading? + assert scope.references(:comments).eager_loading? + end + + def test_references_doesnt_trigger_eager_loading_if_reference_not_included + scope = Post.references(:comments) + assert !scope.eager_loading? + end + + def test_automatically_added_where_references + scope = Post.where(:comments => { :body => "Bla" }) + assert_equal ['comments'], scope.references_values + + scope = Post.where('comments.body' => 'Bla') + assert_equal ['comments'], scope.references_values + end + + def test_automatically_added_having_references + scope = Post.having(:comments => { :body => "Bla" }) + assert_equal ['comments'], scope.references_values + + scope = Post.having('comments.body' => 'Bla') + assert_equal ['comments'], scope.references_values + end + + def test_automatically_added_order_references + scope = Post.order('comments.body') + assert_equal ['comments'], scope.references_values + + scope = Post.order('comments.body', 'yaks.body') + assert_equal ['comments', 'yaks'], scope.references_values + + # Don't infer yaks, let's not go down that road again... + scope = Post.order('comments.body, yaks.body') + assert_equal ['comments'], scope.references_values + + scope = Post.order('comments.body asc') + assert_equal ['comments'], scope.references_values + + scope = Post.order('foo(comments.body)') + assert_equal [], scope.references_values + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 5c3a78688e..abeb56fd3f 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -2,7 +2,13 @@ require "cases/helper" class SchemaDumperTest < ActiveRecord::TestCase + def initialize(*) + super + ActiveRecord::SchemaMigration.create_table + end + def setup + super @stream = StringIO.new end @@ -13,10 +19,18 @@ class SchemaDumperTest < ActiveRecord::TestCase @stream.string end - if "string".encoding_aware? - def test_magic_comment - assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump + def test_dump_schema_information_outputs_lexically_ordered_versions + versions = %w{ 20100101010101 20100201010101 20100301010101 } + versions.reverse.each do |v| + ActiveRecord::SchemaMigration.create!(:version => v) end + + schema_info = ActiveRecord::Base.connection.dump_schema_information + assert_match(/20100201010101.*20100301010101/m, schema_info) + end + + def test_magic_comment + assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump end def test_schema_dump diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb index 258cee7aba..a3b8ab74d9 100644 --- a/activerecord/test/cases/session_store/session_test.rb +++ b/activerecord/test/cases/session_store/session_test.rb @@ -5,11 +5,15 @@ require 'active_record/session_store' module ActiveRecord class SessionStore class SessionTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? && ActiveRecord::Base.connection.supports_ddl_transactions? + self.use_transactional_fixtures = false + + attr_reader :session_klass def setup super + ActiveRecord::Base.connection.schema_cache.clear! Session.drop_table! if Session.table_exists? + @session_klass = Class.new(Session) end def test_data_column_name @@ -60,8 +64,8 @@ module ActiveRecord def test_find_by_session_id Session.create_table! session_id = "10" - s = Session.create!(:data => 'world', :session_id => session_id) - t = Session.find_by_session_id(session_id) + s = session_klass.create!(:data => 'world', :session_id => session_id) + t = session_klass.find_by_session_id(session_id) assert_equal s, t assert_equal s.data, t.data Session.drop_table! diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 074fd39e65..5a3f9a9711 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -24,9 +24,9 @@ class StoreTest < ActiveRecord::TestCase @john.settings[:icecream] = 'graeters' @john.save - assert 'graeters', @john.reload.settings[:icecream] + assert_equal 'graeters', @john.reload.settings[:icecream] end - + test "updating the store will mark it as changed" do @john.color = 'red' assert @john.settings_changed? diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 85f222bca2..f8b3e01a49 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -6,7 +6,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase fixtures :topics class TopicWithCallbacks < ActiveRecord::Base - set_table_name :topics + self.table_name = :topics after_commit{|record| record.send(:do_after_commit, nil)} after_commit(:on => :create){|record| record.send(:do_after_commit, :create)} @@ -252,7 +252,7 @@ class TransactionObserverCallbacksTest < ActiveRecord::TestCase fixtures :topics class TopicWithObserverAttached < ActiveRecord::Base - set_table_name :topics + self.table_name = :topics def history @history ||= [] end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 110a18772f..203dd054f1 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -563,6 +563,7 @@ if current_adapter?(:PostgreSQLAdapter) topic.approved = !topic.approved? topic.save! end + Topic.connection.close end end @@ -598,6 +599,7 @@ if current_adapter?(:PostgreSQLAdapter) dev = Developer.find(1) assert_equal original_salary, dev.salary end + Developer.connection.close end end @@ -610,6 +612,7 @@ if current_adapter?(:PostgreSQLAdapter) assert_equal original_salary, Developer.find(1).salary end end + Developer.connection.close end threads.each { |t| t.join } diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index e82ca3f93d..5a69054445 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -7,13 +7,13 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase self.use_transactional_fixtures = false def setup - @underlying = ActiveRecord::Base.connection - @specification = ActiveRecord::Base.remove_connection + @underlying = ActiveRecord::Model.connection + @specification = ActiveRecord::Model.remove_connection end def teardown @underlying = nil - ActiveRecord::Base.establish_connection(@specification) + ActiveRecord::Model.establish_connection(@specification) load_schema if in_memory_db? end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index 56e345990f..c72b7b35cd 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -61,6 +61,16 @@ class AssociationValidationTest < ActiveRecord::TestCase assert r.valid? end + def test_validates_associated_marked_for_destruction + Topic.validates_associated(:replies) + Reply.validates_presence_of(:content) + t = Topic.new + t.replies << Reply.new + assert t.invalid? + t.replies.first.mark_for_destruction + assert t.valid? + end + def test_validates_associated_with_custom_message_using_quotes Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" Topic.validates_presence_of :content @@ -81,14 +91,12 @@ class AssociationValidationTest < ActiveRecord::TestCase end def test_validates_size_of_association_utf8 - with_kcode('UTF8') do - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'あいうえおかきくけこ') - assert !o.save - assert o.errors[:pets].any? - o.pets.build('name' => 'あいうえおかきくけこ') - assert o.valid? - end + assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } + o = Owner.new('name' => 'あいうえおかきくけこ') + assert !o.save + assert o.errors[:pets].any? + o.pets.build('name' => 'あいうえおかきくけこ') + assert o.valid? end def test_validates_presence_of_belongs_to_association__parent_is_new_record @@ -110,4 +118,21 @@ class AssociationValidationTest < ActiveRecord::TestCase end end + def test_validates_associated_models_in_the_same_context + Topic.validates_presence_of :title, :on => :custom_context + Topic.validates_associated :replies + Reply.validates_presence_of :title, :on => :custom_context + + t = Topic.new('title' => '') + r = t.replies.new('title' => '') + + assert t.valid? + assert !t.valid?(:custom_context) + + t.title = "Longer" + assert !t.valid?(:custom_context), "Should NOT be valid if the associated object is not valid in the same context." + + r.title = "Longer" + assert t.valid?(:custom_context), "Should be valid if the associated object is not valid in the same context." + end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 0f1b3667cc..79442d68b0 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -45,6 +45,18 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t2.save, "Should now save t2 as unique" end + def test_validates_uniqueness_with_nil_value + Topic.validates_uniqueness_of(:title) + + t = Topic.new("title" => nil) + assert t.save, "Should save t as unique" + + t2 = Topic.new("title" => nil) + assert !t2.valid?, "Shouldn't be valid" + assert !t2.save, "Shouldn't save t2 as unique" + assert_equal ["has already been taken"], t2.errors[:title] + end + def test_validates_uniqueness_with_validates Topic.validates :title, :uniqueness => true Topic.create!('title' => 'abc') @@ -80,6 +92,30 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert r3.valid?, "Saving r3" end + def test_validate_uniqueness_with_object_scope + Reply.validates_uniqueness_of(:content, :scope => :topic) + + t = Topic.create("title" => "I'm unique!") + + r1 = t.replies.create "title" => "r1", "content" => "hello world" + assert r1.valid?, "Saving r1" + + r2 = t.replies.create "title" => "r2", "content" => "hello world" + assert !r2.valid?, "Saving r2 first time" + end + + def test_validate_uniqueness_with_object_arg + Reply.validates_uniqueness_of(:topic) + + t = Topic.create("title" => "I'm unique!") + + r1 = t.replies.create "title" => "r1", "content" => "hello world" + assert r1.valid?, "Saving r1" + + r2 = t.replies.create "title" => "r2", "content" => "hello world" + assert !r2.valid?, "Saving r2 first time" + end + def test_validate_uniqueness_scoped_to_defining_class t = Topic.create("title" => "What, me worry?") @@ -149,16 +185,14 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t2.valid?, "should validate with nil" assert t2.save, "should save with nil" - with_kcode('UTF8') do - t_utf8 = Topic.new("title" => "Я тоже уникальный!") - assert t_utf8.save, "Should save t_utf8 as unique" + t_utf8 = Topic.new("title" => "Я тоже уникальный!") + assert t_utf8.save, "Should save t_utf8 as unique" - # If database hasn't UTF-8 character set, this test fails - if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!" - t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!") - assert !t2_utf8.valid?, "Shouldn't be valid" - assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique" - end + # If database hasn't UTF-8 character set, this test fails + if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!" + t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!") + assert !t2_utf8.valid?, "Shouldn't be valid" + assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique" end end @@ -256,13 +290,11 @@ class UniquenessValidationTest < ActiveRecord::TestCase end def test_validate_uniqueness_with_limit_and_utf8 - with_kcode('UTF8') do - # Event.title is limited to 5 characters - e1 = Event.create(:title => "一二三四五") - assert e1.valid?, "Could not create an event with a unique, 5 character title" - e2 = Event.create(:title => "一二三四五六七八") - assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique" - end + # Event.title is limited to 5 characters + e1 = Event.create(:title => "一二三四五") + assert e1.valid?, "Could not create an event with a unique, 5 character title" + e2 = Event.create(:title => "一二三四五六七八") + assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique" end def test_validate_straight_inheritance_uniqueness diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index c3e494866b..e575a98170 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -8,7 +8,7 @@ require 'models/parrot' require 'models/company' class ProtectedPerson < ActiveRecord::Base - set_table_name 'people' + self.table_name = 'people' attr_accessor :addon attr_protected :first_name end diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 0b54c309d1..302913e095 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -1,14 +1,20 @@ -require "cases/helper" +require 'cases/helper' require 'models/topic' class YamlSerializationTest < ActiveRecord::TestCase fixtures :topics def test_to_yaml_with_time_with_zone_should_not_raise_exception + tz = Time.zone Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"] ActiveRecord::Base.time_zone_aware_attributes = true + topic = Topic.new(:written_on => DateTime.now) assert_nothing_raised { topic.to_yaml } + + ensure + Time.zone = tz + ActiveRecord::Base.time_zone_aware_attributes = false end def test_roundtrip @@ -18,6 +24,11 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal topic, t end + def test_roundtrip_serialized_column + topic = Topic.new(:content => {:omg=>:lol}) + assert_equal({:omg=>:lol}, YAML.load(YAML.dump(topic)).content) + end + def test_encode_with_coder topic = Topic.first coder = {} @@ -25,22 +36,17 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal({'attributes' => topic.attributes}, coder) end - begin - require 'psych' - - def test_psych_roundtrip - topic = Topic.first - assert topic - t = Psych.load Psych.dump topic - assert_equal topic, t - end - - def test_psych_roundtrip_new_object - topic = Topic.new - assert topic - t = Psych.load Psych.dump topic - assert_equal topic.attributes, t.attributes - end - rescue LoadError + def test_psych_roundtrip + topic = Topic.first + assert topic + t = Psych.load Psych.dump topic + assert_equal topic, t + end + + def test_psych_roundtrip_new_object + topic = Topic.new + assert topic + t = Psych.load Psych.dump topic + assert_equal topic.attributes, t.attributes end end diff --git a/activerecord/test/fixtures/admin/randomly_named_a9.yml b/activerecord/test/fixtures/admin/randomly_named_a9.yml new file mode 100644 index 0000000000..bc51c83112 --- /dev/null +++ b/activerecord/test/fixtures/admin/randomly_named_a9.yml @@ -0,0 +1,7 @@ +first_instance:
+ some_attribute: AAA
+ another_attribute: 000
+
+second_instance:
+ some_attribute: BBB
+ another_attribute: 999
diff --git a/activerecord/test/fixtures/admin/randomly_named_b0.yml b/activerecord/test/fixtures/admin/randomly_named_b0.yml new file mode 100644 index 0000000000..bc51c83112 --- /dev/null +++ b/activerecord/test/fixtures/admin/randomly_named_b0.yml @@ -0,0 +1,7 @@ +first_instance:
+ some_attribute: AAA
+ another_attribute: 000
+
+second_instance:
+ some_attribute: BBB
+ another_attribute: 999
diff --git a/activerecord/test/fixtures/dog_lovers.yml b/activerecord/test/fixtures/dog_lovers.yml new file mode 100644 index 0000000000..d3e5e4a1aa --- /dev/null +++ b/activerecord/test/fixtures/dog_lovers.yml @@ -0,0 +1,4 @@ +david: + id: 1 + bred_dogs_count: 0 + trained_dogs_count: 1 diff --git a/activerecord/test/fixtures/dogs.yml b/activerecord/test/fixtures/dogs.yml new file mode 100644 index 0000000000..16d19be2c5 --- /dev/null +++ b/activerecord/test/fixtures/dogs.yml @@ -0,0 +1,3 @@ +sophie: + id: 1 + trainer_id: 1 diff --git a/activerecord/test/fixtures/other_topics.yml b/activerecord/test/fixtures/other_topics.yml new file mode 100644 index 0000000000..93f48aedc4 --- /dev/null +++ b/activerecord/test/fixtures/other_topics.yml @@ -0,0 +1,42 @@ +first: + id: 1 + title: The First Topic + author_name: David + author_email_address: david@loudthinking.com + written_on: 2003-07-16t15:28:11.2233+01:00 + last_read: 2004-04-15 + bonus_time: 2005-01-30t15:28:00.00+01:00 + content: Have a nice day + approved: false + replies_count: 1 + +second: + id: 2 + title: The Second Topic of the day + author_name: Mary + written_on: 2004-07-15t15:28:00.0099+01:00 + content: Have a nice day + approved: true + replies_count: 0 + parent_id: 1 + type: Reply + +third: + id: 3 + title: The Third Topic of the day + author_name: Carl + written_on: 2005-07-15t15:28:00.0099+01:00 + content: I'm a troll + approved: true + replies_count: 1 + +fourth: + id: 4 + title: The Fourth Topic of the day + author_name: Carl + written_on: 2006-07-15t15:28:00.0099+01:00 + content: Why not? + approved: true + type: Reply + parent_id: 3 + diff --git a/activerecord/test/fixtures/randomly_named_a9.yml b/activerecord/test/fixtures/randomly_named_a9.yml new file mode 100644 index 0000000000..bc51c83112 --- /dev/null +++ b/activerecord/test/fixtures/randomly_named_a9.yml @@ -0,0 +1,7 @@ +first_instance:
+ some_attribute: AAA
+ another_attribute: 000
+
+second_instance:
+ some_attribute: BBB
+ another_attribute: 999
diff --git a/activerecord/test/fixtures/teapots.yml b/activerecord/test/fixtures/teapots.yml new file mode 100644 index 0000000000..ff515beb45 --- /dev/null +++ b/activerecord/test/fixtures/teapots.yml @@ -0,0 +1,3 @@ +bob: + id: 1 + name: Bob diff --git a/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb b/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb deleted file mode 100644 index ffb224dad9..0000000000 --- a/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb +++ /dev/null @@ -1,10 +0,0 @@ -class MigrationThatRaisesException < ActiveRecord::Migration - def self.up - add_column "people", "last_name", :string - raise 'Something broke' - end - - def self.down - remove_column "people", "last_name" - end -end diff --git a/activerecord/test/migrations/duplicate/3_foo.rb b/activerecord/test/migrations/duplicate/3_foo.rb deleted file mode 100644 index 916fe580fa..0000000000 --- a/activerecord/test/migrations/duplicate/3_foo.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Foo < ActiveRecord::Migration - def self.up - end - - def self.down - end -end
\ No newline at end of file diff --git a/activerecord/test/migrations/duplicate_names/20080507052938_chunky.rb b/activerecord/test/migrations/duplicate_names/20080507052938_chunky.rb deleted file mode 100644 index 5fe5089e18..0000000000 --- a/activerecord/test/migrations/duplicate_names/20080507052938_chunky.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Chunky < ActiveRecord::Migration - def self.up - end - - def self.down - end -end diff --git a/activerecord/test/migrations/duplicate_names/20080507053028_chunky.rb b/activerecord/test/migrations/duplicate_names/20080507053028_chunky.rb deleted file mode 100644 index 5fe5089e18..0000000000 --- a/activerecord/test/migrations/duplicate_names/20080507053028_chunky.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Chunky < ActiveRecord::Migration - def self.up - end - - def self.down - end -end diff --git a/activerecord/test/migrations/interleaved/pass_1/3_interleaved_innocent_jointable.rb b/activerecord/test/migrations/interleaved/pass_1/3_interleaved_innocent_jointable.rb deleted file mode 100644 index bf912fbfc8..0000000000 --- a/activerecord/test/migrations/interleaved/pass_1/3_interleaved_innocent_jointable.rb +++ /dev/null @@ -1,12 +0,0 @@ -class InterleavedInnocentJointable < ActiveRecord::Migration - def self.up - create_table("people_reminders", :id => false) do |t| - t.column :reminder_id, :integer - t.column :person_id, :integer - end - end - - def self.down - drop_table "people_reminders" - end -end diff --git a/activerecord/test/migrations/interleaved/pass_2/1_interleaved_people_have_last_names.rb b/activerecord/test/migrations/interleaved/pass_2/1_interleaved_people_have_last_names.rb deleted file mode 100644 index c6c94213a0..0000000000 --- a/activerecord/test/migrations/interleaved/pass_2/1_interleaved_people_have_last_names.rb +++ /dev/null @@ -1,9 +0,0 @@ -class InterleavedPeopleHaveLastNames < ActiveRecord::Migration - def self.up - add_column "people", "last_name", :string - end - - def self.down - remove_column "people", "last_name" - end -end diff --git a/activerecord/test/migrations/interleaved/pass_2/3_interleaved_innocent_jointable.rb b/activerecord/test/migrations/interleaved/pass_2/3_interleaved_innocent_jointable.rb deleted file mode 100644 index bf912fbfc8..0000000000 --- a/activerecord/test/migrations/interleaved/pass_2/3_interleaved_innocent_jointable.rb +++ /dev/null @@ -1,12 +0,0 @@ -class InterleavedInnocentJointable < ActiveRecord::Migration - def self.up - create_table("people_reminders", :id => false) do |t| - t.column :reminder_id, :integer - t.column :person_id, :integer - end - end - - def self.down - drop_table "people_reminders" - end -end diff --git a/activerecord/test/migrations/interleaved/pass_3/1_interleaved_people_have_last_names.rb b/activerecord/test/migrations/interleaved/pass_3/1_interleaved_people_have_last_names.rb deleted file mode 100644 index c6c94213a0..0000000000 --- a/activerecord/test/migrations/interleaved/pass_3/1_interleaved_people_have_last_names.rb +++ /dev/null @@ -1,9 +0,0 @@ -class InterleavedPeopleHaveLastNames < ActiveRecord::Migration - def self.up - add_column "people", "last_name", :string - end - - def self.down - remove_column "people", "last_name" - end -end diff --git a/activerecord/test/migrations/interleaved/pass_3/2_interleaved_i_raise_on_down.rb b/activerecord/test/migrations/interleaved/pass_3/2_interleaved_i_raise_on_down.rb deleted file mode 100644 index 6849995f5e..0000000000 --- a/activerecord/test/migrations/interleaved/pass_3/2_interleaved_i_raise_on_down.rb +++ /dev/null @@ -1,8 +0,0 @@ -class InterleavedIRaiseOnDown < ActiveRecord::Migration - def self.up - end - - def self.down - raise - end -end diff --git a/activerecord/test/migrations/interleaved/pass_3/3_interleaved_innocent_jointable.rb b/activerecord/test/migrations/interleaved/pass_3/3_interleaved_innocent_jointable.rb deleted file mode 100644 index bf912fbfc8..0000000000 --- a/activerecord/test/migrations/interleaved/pass_3/3_interleaved_innocent_jointable.rb +++ /dev/null @@ -1,12 +0,0 @@ -class InterleavedInnocentJointable < ActiveRecord::Migration - def self.up - create_table("people_reminders", :id => false) do |t| - t.column :reminder_id, :integer - t.column :person_id, :integer - end - end - - def self.down - drop_table "people_reminders" - end -end diff --git a/activerecord/test/migrations/rename/1_we_need_things.rb b/activerecord/test/migrations/rename/1_we_need_things.rb new file mode 100644 index 0000000000..cdbe0b1679 --- /dev/null +++ b/activerecord/test/migrations/rename/1_we_need_things.rb @@ -0,0 +1,11 @@ +class WeNeedThings < ActiveRecord::Migration + def self.up + create_table("things") do |t| + t.column :content, :text + end + end + + def self.down + drop_table "things" + end +end
\ No newline at end of file diff --git a/activerecord/test/migrations/rename/2_rename_things.rb b/activerecord/test/migrations/rename/2_rename_things.rb new file mode 100644 index 0000000000..d441b71fc9 --- /dev/null +++ b/activerecord/test/migrations/rename/2_rename_things.rb @@ -0,0 +1,9 @@ +class RenameThings < ActiveRecord::Migration + def self.up + rename_table "things", "awesome_things" + end + + def self.down + rename_table "awesome_things", "things" + end +end
\ No newline at end of file diff --git a/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb new file mode 100644 index 0000000000..e438cf5999 --- /dev/null +++ b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb @@ -0,0 +1,9 @@ +class PeopleHaveLastNames < ActiveRecord::Migration + def self.up + add_column "people", "hobbies", :string + end + + def self.down + remove_column "people", "hobbies" + end +end diff --git a/activerecord/test/migrations/duplicate/1_people_have_last_names.rb b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb index 81af5fef5e..06cb911117 100644 --- a/activerecord/test/migrations/duplicate/1_people_have_last_names.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class ValidPeopleHaveLastNames < ActiveRecord::Migration def self.up add_column "people", "last_name", :string end @@ -6,4 +6,4 @@ class PeopleHaveLastNames < ActiveRecord::Migration def self.down remove_column "people", "last_name" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/duplicate/2_we_need_reminders.rb b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb index d5e71ce8ef..d5e71ce8ef 100644 --- a/activerecord/test/migrations/duplicate/2_we_need_reminders.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb diff --git a/activerecord/test/migrations/duplicate/3_innocent_jointable.rb b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb index 21c9ca5328..21c9ca5328 100644 --- a/activerecord/test/migrations/duplicate/3_innocent_jointable.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb diff --git a/activerecord/test/models/admin/randomly_named_c1.rb b/activerecord/test/models/admin/randomly_named_c1.rb new file mode 100644 index 0000000000..2f81d5b831 --- /dev/null +++ b/activerecord/test/models/admin/randomly_named_c1.rb @@ -0,0 +1,3 @@ +class Admin::ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
+ self.table_name = :randomly_named_table
+end
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 23db5650d4..d50e11d6c9 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -49,12 +49,12 @@ class Author < ActiveRecord::Base has_many :sti_post_comments, :through => :sti_posts, :source => :comments has_many :special_nonexistant_posts, :class_name => "SpecialPost", :conditions => "posts.body = 'nonexistant'" - has_many :special_nonexistant_post_comments, :through => :special_nonexistant_posts, :source => :comments, :conditions => "comments.post_id = 0" + has_many :special_nonexistant_post_comments, :through => :special_nonexistant_posts, :source => :comments, :conditions => { 'comments.post_id' => 0 } has_many :nonexistant_comments, :through => :posts has_many :hello_posts, :class_name => "Post", :conditions => "posts.body = 'hello'" has_many :hello_post_comments, :through => :hello_posts, :source => :comments - has_many :posts_with_no_comments, :class_name => 'Post', :conditions => 'comments.id is null', :include => :comments + has_many :posts_with_no_comments, :class_name => 'Post', :conditions => { 'comments.id' => nil }, :include => :comments has_many :hello_posts_with_hash_conditions, :class_name => "Post", :conditions => {:body => 'hello'} @@ -128,7 +128,6 @@ class Author < ActiveRecord::Base belongs_to :author_address, :dependent => :destroy belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress" - has_many :post_categories, :through => :posts, :source => :categories has_many :category_post_comments, :through => :categories, :source => :post_comments has_many :misc_posts, :class_name => 'Post', diff --git a/activerecord/test/models/bird.rb b/activerecord/test/models/bird.rb index e61d48e6a5..dff099c1fb 100644 --- a/activerecord/test/models/bird.rb +++ b/activerecord/test/models/bird.rb @@ -1,9 +1,12 @@ class Bird < ActiveRecord::Base + belongs_to :pirate validates_presence_of :name + accepts_nested_attributes_for :pirate + attr_accessor :cancel_save_from_callback before_save :cancel_save_callback_method, :if => :cancel_save_from_callback def cancel_save_callback_method false end -end
\ No newline at end of file +end diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index b9c2e8ec9a..6ff1329d8e 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -9,7 +9,7 @@ class Car < ActiveRecord::Base has_many :tyres has_many :engines, :dependent => :destroy - has_many :wheels, :as => :wheelable + has_many :wheels, :as => :wheelable, :dependent => :destroy scope :incl_tyres, includes(:tyres) scope :incl_engines, includes(:engines) diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 78eb4c57ac..d1a8a82786 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -4,7 +4,7 @@ end class Company < AbstractCompany attr_protected :rating - set_sequence_name :companies_nonstd_seq + self.sequence_name = :companies_nonstd_seq validates_presence_of :name @@ -88,6 +88,8 @@ class Firm < Company has_many :accounts has_many :unautosaved_accounts, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false + has_many :association_with_references, :class_name => 'Client', :references => :foo + def log @log ||= [] end diff --git a/activerecord/test/models/country.rb b/activerecord/test/models/country.rb index 15e3a1de0b..7db9a4e731 100644 --- a/activerecord/test/models/country.rb +++ b/activerecord/test/models/country.rb @@ -1,6 +1,6 @@ class Country < ActiveRecord::Base - set_primary_key :country_id + self.primary_key = :country_id has_and_belongs_to_many :treaties diff --git a/activerecord/test/models/dashboard.rb b/activerecord/test/models/dashboard.rb index a8a25834b1..1b3b54545f 100644 --- a/activerecord/test/models/dashboard.rb +++ b/activerecord/test/models/dashboard.rb @@ -1,3 +1,3 @@ class Dashboard < ActiveRecord::Base - set_primary_key :dashboard_id -end
\ No newline at end of file + self.primary_key = :dashboard_id +end diff --git a/activerecord/test/models/dog.rb b/activerecord/test/models/dog.rb new file mode 100644 index 0000000000..72b7d33a86 --- /dev/null +++ b/activerecord/test/models/dog.rb @@ -0,0 +1,4 @@ +class Dog < ActiveRecord::Base + belongs_to :breeder, :class_name => "DogLover", :counter_cache => :bred_dogs_count + belongs_to :trainer, :class_name => "DogLover", :counter_cache => :trained_dogs_count +end diff --git a/activerecord/test/models/dog_lover.rb b/activerecord/test/models/dog_lover.rb new file mode 100644 index 0000000000..a33dc575c5 --- /dev/null +++ b/activerecord/test/models/dog_lover.rb @@ -0,0 +1,4 @@ +class DogLover < ActiveRecord::Base + has_many :trained_dogs, :class_name => "Dog", :foreign_key => :trainer_id + has_many :bred_dogs, :class_name => "Dog", :foreign_key => :breeder_id +end diff --git a/activerecord/test/models/joke.rb b/activerecord/test/models/joke.rb index d7f01e59e6..edda4655dc 100644 --- a/activerecord/test/models/joke.rb +++ b/activerecord/test/models/joke.rb @@ -1,7 +1,7 @@ class Joke < ActiveRecord::Base - set_table_name 'funny_jokes' + self.table_name = 'funny_jokes' end class GoodJoke < ActiveRecord::Base - set_table_name 'funny_jokes' + self.table_name = 'funny_jokes' end diff --git a/activerecord/test/models/keyboard.rb b/activerecord/test/models/keyboard.rb index 32a4a7fad0..39347e274e 100644 --- a/activerecord/test/models/keyboard.rb +++ b/activerecord/test/models/keyboard.rb @@ -1,3 +1,3 @@ class Keyboard < ActiveRecord::Base - set_primary_key 'key_number' + self.primary_key = 'key_number' end diff --git a/activerecord/test/models/legacy_thing.rb b/activerecord/test/models/legacy_thing.rb index eaeb642d12..eead181a0e 100644 --- a/activerecord/test/models/legacy_thing.rb +++ b/activerecord/test/models/legacy_thing.rb @@ -1,3 +1,3 @@ class LegacyThing < ActiveRecord::Base - set_locking_column :version + self.locking_column = :version end diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb index b96c054f6c..3fcd5e4b69 100644 --- a/activerecord/test/models/liquid.rb +++ b/activerecord/test/models/liquid.rb @@ -1,5 +1,5 @@ class Liquid < ActiveRecord::Base - set_table_name :liquid + self.table_name = :liquid has_many :molecules, :uniq => true end diff --git a/activerecord/test/models/minivan.rb b/activerecord/test/models/minivan.rb index 830cdb5796..4fe79720ad 100644 --- a/activerecord/test/models/minivan.rb +++ b/activerecord/test/models/minivan.rb @@ -1,5 +1,5 @@ class Minivan < ActiveRecord::Base - set_primary_key :minivan_id + self.primary_key = :minivan_id belongs_to :speedometer has_one :dashboard, :through => :speedometer diff --git a/activerecord/test/models/mixed_case_monkey.rb b/activerecord/test/models/mixed_case_monkey.rb index 853f2682b3..763baefd91 100644 --- a/activerecord/test/models/mixed_case_monkey.rb +++ b/activerecord/test/models/mixed_case_monkey.rb @@ -1,3 +1,3 @@ class MixedCaseMonkey < ActiveRecord::Base - set_primary_key 'monkeyID' + self.primary_key = 'monkeyID' end diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index 5760b991ec..fea55f4535 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -1,5 +1,5 @@ class Owner < ActiveRecord::Base - set_primary_key :owner_id + self.primary_key = :owner_id has_many :pets has_many :toys, :through => :pets end diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index 737ef9131b..c4ee2bd19d 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -1,5 +1,6 @@ class Parrot < ActiveRecord::Base - set_inheritance_column :parrot_sti_class + self.inheritance_column = :parrot_sti_class + has_and_belongs_to_many :pirates has_and_belongs_to_many :treasures has_many :loots, :as => :looter diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index 967a3625aa..d2a0c6b40c 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -3,7 +3,8 @@ class Person < ActiveRecord::Base has_one :reader has_many :posts, :through => :readers - has_many :posts_with_no_comments, :through => :readers, :source => :post, :include => :comments, :conditions => 'comments.id is null' + has_many :posts_with_no_comments, :through => :readers, :source => :post, :include => :comments, + :conditions => 'comments.id is null', :references => :comments has_many :references has_many :bad_references @@ -54,7 +55,7 @@ class LoosePerson < ActiveRecord::Base self.table_name = 'people' self.abstract_class = true - attr_protected :comments + attr_protected :comments, :best_friend_id, :best_friend_of_id attr_protected :as => :admin has_one :best_friend, :class_name => 'LoosePerson', :foreign_key => :best_friend_id @@ -81,4 +82,4 @@ class TightPerson < ActiveRecord::Base accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends end -class TightDescendant < TightPerson; end
\ No newline at end of file +class TightDescendant < TightPerson; end diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb index 113826756a..3cd5bceed5 100644 --- a/activerecord/test/models/pet.rb +++ b/activerecord/test/models/pet.rb @@ -2,7 +2,7 @@ class Pet < ActiveRecord::Base attr_accessor :current_user - set_primary_key :pet_id + self.primary_key = :pet_id belongs_to :owner, :touch => true has_many :toys diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 198a963cbc..1cab78d8c7 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -24,6 +24,10 @@ class Post < ActiveRecord::Base belongs_to :author_with_posts, :class_name => "Author", :foreign_key => :author_id, :include => :posts belongs_to :author_with_address, :class_name => "Author", :foreign_key => :author_id, :include => :author_address + def first_comment + super.body + end + has_one :first_comment, :class_name => 'Comment', :order => 'id ASC' has_one :last_comment, :class_name => 'Comment', :order => 'id desc' scope :with_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'SpecialComment'} } @@ -40,6 +44,10 @@ class Post < ActiveRecord::Base def newest created.last end + + def the_association + proxy_association + end end has_many :author_favorites, :through => :author @@ -181,4 +189,4 @@ end class SpecialPostWithDefaultScope < ActiveRecord::Base self.table_name = 'posts' default_scope where(:id => [1, 5,6]) -end
\ No newline at end of file +end diff --git a/activerecord/test/models/randomly_named_c1.rb b/activerecord/test/models/randomly_named_c1.rb new file mode 100644 index 0000000000..18a86c4989 --- /dev/null +++ b/activerecord/test/models/randomly_named_c1.rb @@ -0,0 +1,3 @@ +class ClassNameThatDoesNotFollowCONVENTIONS < ActiveRecord::Base
+ self.table_name = :randomly_named_table
+end
diff --git a/activerecord/test/models/speedometer.rb b/activerecord/test/models/speedometer.rb index 94743eff8e..0a7d38d8ec 100644 --- a/activerecord/test/models/speedometer.rb +++ b/activerecord/test/models/speedometer.rb @@ -1,4 +1,4 @@ class Speedometer < ActiveRecord::Base - set_primary_key :speedometer_id + self.primary_key = :speedometer_id belongs_to :dashboard -end
\ No newline at end of file +end diff --git a/activerecord/test/models/string_key_object.rb b/activerecord/test/models/string_key_object.rb index f8d4c6e0e4..f084ec1bdc 100644 --- a/activerecord/test/models/string_key_object.rb +++ b/activerecord/test/models/string_key_object.rb @@ -1,3 +1,3 @@ class StringKeyObject < ActiveRecord::Base - set_primary_key :id + self.primary_key = :id end diff --git a/activerecord/test/models/subscriber.rb b/activerecord/test/models/subscriber.rb index 5b78014e6f..76e85a0cd3 100644 --- a/activerecord/test/models/subscriber.rb +++ b/activerecord/test/models/subscriber.rb @@ -1,5 +1,5 @@ class Subscriber < ActiveRecord::Base - set_primary_key 'nick' + self.primary_key = 'nick' has_many :subscriptions has_many :books, :through => :subscriptions end diff --git a/activerecord/test/models/teapot.rb b/activerecord/test/models/teapot.rb new file mode 100644 index 0000000000..b035b18c1b --- /dev/null +++ b/activerecord/test/models/teapot.rb @@ -0,0 +1,35 @@ +class Teapot + # I'm a little teapot, + # Short and stout, + # Here is my handle + # Here is my spout + # When I get all steamed up, + # Hear me shout, + # Tip me over and pour me out! + # + # HELL YEAH TEAPOT SONG + + include ActiveRecord::Model +end + +class OtherTeapot < Teapot +end + +class OMFGIMATEAPOT + def aaahhh + "mmm" + end +end + +class CoolTeapot < OMFGIMATEAPOT + include ActiveRecord::Model + self.table_name = "teapots" +end + +class Ceiling + include ActiveRecord::Model + + class Teapot + include ActiveRecord::Model + end +end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index fe424e61b2..8bcb9df8a8 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -8,6 +8,8 @@ class Topic < ActiveRecord::Base scope :approved, :conditions => {:approved => true} scope :rejected, :conditions => {:approved => false} + scope :scope_with_lambda, lambda { scoped } + scope :by_lifo, :conditions => {:author_name => 'lifo'} scope :approved_as_hash_condition, :conditions => {:topics => {:approved => true}} @@ -78,6 +80,11 @@ class Topic < ActiveRecord::Base after_initialize :set_email_address + class_attribute :after_initialize_called + after_initialize do + self.class.after_initialize_called = true + end + def approved=(val) @custom_approved = val write_attribute(:approved, val) @@ -106,6 +113,10 @@ class Topic < ActiveRecord::Base def after_create_for_transaction; end end +class ImportantTopic < Topic + serialize :important, Hash +end + module Web class Topic < ActiveRecord::Base has_many :replies, :dependent => :destroy, :foreign_key => "parent_id", :class_name => 'Web::Reply' diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb index 6c45e99671..0377e50011 100644 --- a/activerecord/test/models/toy.rb +++ b/activerecord/test/models/toy.rb @@ -1,5 +1,5 @@ class Toy < ActiveRecord::Base - set_primary_key :toy_id + self.primary_key = :toy_id belongs_to :pet scope :with_pet, joins(:pet) diff --git a/activerecord/test/models/treaty.rb b/activerecord/test/models/treaty.rb index b46537f0d2..41fd1350f3 100644 --- a/activerecord/test/models/treaty.rb +++ b/activerecord/test/models/treaty.rb @@ -1,6 +1,6 @@ class Treaty < ActiveRecord::Base - set_primary_key :treaty_id + self.primary_key = :treaty_id has_and_belongs_to_many :countries diff --git a/activerecord/test/models/warehouse_thing.rb b/activerecord/test/models/warehouse_thing.rb index 6ace1183cc..f20bd1a245 100644 --- a/activerecord/test/models/warehouse_thing.rb +++ b/activerecord/test/models/warehouse_thing.rb @@ -1,5 +1,5 @@ class WarehouseThing < ActiveRecord::Base - set_table_name "warehouse-things" + self.table_name = "warehouse-things" validates_uniqueness_of :value -end
\ No newline at end of file +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index bb08f5c181..e2cd7ce9e4 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -107,6 +107,7 @@ ActiveRecord::Schema.define do t.string :name t.integer :engines_count t.integer :wheels_count + t.column :lock_version, :integer, :null => false, :default => 0 end create_table :categories, :force => true do |t| @@ -212,6 +213,16 @@ ActiveRecord::Schema.define do t.integer :access_level, :default => 1 end + create_table :dog_lovers, :force => true do |t| + t.integer :trained_dogs_count, :default => 0 + t.integer :bred_dogs_count, :default => 0 + end + + create_table :dogs, :force => true do |t| + t.integer :trainer_id + t.integer :breeder_id + end + create_table :edges, :force => true, :id => false do |t| t.column :source_id, :integer, :null => false t.column :sink_id, :integer, :null => false @@ -505,6 +516,11 @@ ActiveRecord::Schema.define do t.string :type end + create_table :randomly_named_table, :force => true do |t| + t.string :some_attribute + t.integer :another_attribute + end + create_table :ratings, :force => true do |t| t.integer :comment_id t.integer :value @@ -596,6 +612,12 @@ ActiveRecord::Schema.define do t.datetime :ending end + create_table :teapots, :force => true do |t| + t.string :name + t.string :type + t.timestamps + end + create_table :topics, :force => true do |t| t.string :title t.string :author_name @@ -607,8 +629,10 @@ ActiveRecord::Schema.define do # Oracle SELECT WHERE clause which causes many unit test failures if current_adapter?(:OracleAdapter) t.string :content, :limit => 4000 + t.string :important, :limit => 4000 else t.text :content + t.text :important end t.boolean :approved, :default => true t.integer :replies_count, :default => 0 diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index a39794fa39..60fea46fd3 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -1,4 +1,4 @@ -require 'logger' +require 'active_support/logger' require_dependency 'models/course' module ARTest @@ -12,9 +12,9 @@ module ARTest def self.connect puts "Using #{connection_name} with Identity Map #{ActiveRecord::IdentityMap.enabled? ? 'on' : 'off'}" - ActiveRecord::Base.logger = Logger.new("debug.log") - ActiveRecord::Base.configurations = connection_config - ActiveRecord::Base.establish_connection 'arunit' + ActiveRecord::Model.logger = ActiveSupport::Logger.new("debug.log") + ActiveRecord::Model.configurations = connection_config + ActiveRecord::Model.establish_connection 'arunit' Course.establish_connection 'arunit2' end end |