diff options
Diffstat (limited to 'activerecord')
119 files changed, 3753 insertions, 1778 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 8f1f315e42..10bfe1945a 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,82 @@ ## Rails 4.0.0 (unreleased) ## +* Added `stored_attributes` hash which contains the attributes stored using + ActiveRecord::Store. This allows you to retrieve the list of attributes + you've defined. + + class User < ActiveRecord::Base + store :settings, accessors: [:color, :homepage] + end + + User.stored_attributes[:settings] # [:color, :homepage] + + *Joost Baaij & Carlos Antonio da Silva* + +* `composed_of` was removed. You'll have to write your own accessor + and mutator methods if you'd like to use value objects to represent some + portion of your models. So, instead of: + + class Person < ActiveRecord::Base + composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] + end + + you could write something like this: + + def address + @address ||= Address.new(address_street, address_city) + end + + def address=(address) + self[:address_street] = @address.street + self[:address_city] = @address.city + + @address = address + end + + *Steve Klabnik* + +* PostgreSQL default log level is now 'warning', to bypass the noisy notice + messages. You can change the log level using the `min_messages` option + available in your config/database.yml. + + *kennyj* + +* Add uuid datatype support to PostgreSQL adapter. *Konstantin Shabanov* + +* `update_attribute` has been removed. Use `update_column` if + you want to bypass mass-assignment protection, validations, callbacks, + and touching of updated_at. Otherwise please use `update_attributes`. + + *Steve Klabnik* + +* Added `ActiveRecord::Migration.check_pending!` that raises an error if + migrations are pending. *Richard Schneeman* + +* Added `#destroy!` which acts like `#destroy` but will raise an + `ActiveRecord::RecordNotDestroyed` exception instead of returning `false`. + + *Marc-André Lafortune* + +* Allow blocks for `count` with `ActiveRecord::Relation`, to work similar as + `Array#count`: + + Person.where("age > 26").count { |person| person.gender == 'female' } + + *Chris Finne & Carlos Antonio da Silva* + +* Added support to `CollectionAssociation#delete` for passing `fixnum` + or `string` values as record ids. This finds the records responding + to the `id` and executes delete on them. + + class Person < ActiveRecord::Base + has_many :pets + end + + person.pets.delete("1") # => [#<Pet id: 1>] + person.pets.delete(2, 3) # => [#<Pet id: 2>, #<Pet id: 3>] + + *Francesco Rodriguez* + * Deprecated most of the 'dynamic finder' methods. All dynamic methods except for `find_by_...` and `find_by_...!` are deprecated. Here's how you can rewrite the code: @@ -324,6 +401,33 @@ * PostgreSQL hstore types are automatically deserialized from the database. +## Rails 3.2.5 (Jun 1, 2012) ## + +* Restore behavior of Active Record 3.2.3 scopes. + A series of commits relating to preloading and scopes caused a regression. + + *Andrew White* + + +## Rails 3.2.4 (May 31, 2012) ## + +* Perf fix: Don't load the records when doing assoc.delete_all. + GH #6289. *Jon Leighton* + +* Association preloading shouldn't be affected by the current scoping. + This could cause infinite recursion and potentially other problems. + See GH #5667. *Jon Leighton* + +* Datetime attributes are forced to be changed. GH #3965 + +* Fix attribute casting. GH #5549 + +* Fix #5667. Preloading should ignore scoping. + +* Predicate builder should not recurse for determining where columns. + Thanks to Ben Murphy for reporting this! CVE-2012-2661 + + ## Rails 3.2.3 (March 30, 2012) ## * Added find_or_create_by_{attribute}! dynamic method. *Andrew White* @@ -1159,8 +1263,6 @@ ## Rails 3.0.0 (August 29, 2010) ## -* Changed update_attribute to not run callbacks and update the record directly in the database *Neeraj Singh* - * Add scoping and unscoped as the syntax to replace the old with_scope and with_exclusive_scope *José Valim* * New rake task, db:migrate:status, displays status of migrations #4947 *Kevin Skoglund* diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 30a66ff5f0..60965590a1 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -46,25 +46,13 @@ A short rundown of some of the major features: {Learn more}[link:classes/ActiveRecord/Associations/ClassMethods.html] -* Aggregations of value objects. - - class Account < ActiveRecord::Base - composed_of :balance, :class_name => "Money", - :mapping => %w(balance amount) - composed_of :address, - :mapping => [%w(address_street street), %w(address_city city)] - end - - {Learn more}[link:classes/ActiveRecord/Aggregations/ClassMethods.html] - - * Validation rules that can differ for new or existing objects. class Account < ActiveRecord::Base - validates_presence_of :subdomain, :name, :email_address, :password - validates_uniqueness_of :subdomain - validates_acceptance_of :terms_of_service, :on => :create - validates_confirmation_of :password, :email_address, :on => :create + validates :subdomain, :name, :email_address, :password, presence: true + validates :subdomain, uniqueness: true + validates :terms_of_service, acceptance: true, on: :create + validates :password, :email_address, confirmation: true, on: :create end {Learn more}[link:classes/ActiveRecord/Validations.html] diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 7feb0b75a0..a29d7b0e99 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -114,6 +114,16 @@ namespace :postgresql do config = ARTest.config['connections']['postgresql'] %x( createdb -E UTF8 #{config['arunit']['database']} ) %x( createdb -E UTF8 #{config['arunit2']['database']} ) + + # prepare hstore + version = %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") + %w(arunit arunit2).each do |db| + if version < "9.1.0" + puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html" + else + %x( psql #{config[db]['database']} -c "CREATE EXTENSION hstore;" ) + end + end end desc 'Drop the PostgreSQL test databases' diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index e8e5f4adfe..dca7f13fd2 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -8,6 +8,7 @@ Gem::Specification.new do |s| 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.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 210820062b..a894d83ea7 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -36,7 +36,6 @@ module ActiveRecord autoload :ConnectionNotEstablished, 'active_record/errors' autoload :ConnectionAdapters, 'active_record/connection_adapters/abstract_adapter' - autoload :Aggregations autoload :Associations autoload :AttributeMethods autoload :AttributeAssignment @@ -80,6 +79,7 @@ module ActiveRecord autoload :Sanitization autoload :Schema autoload :SchemaDumper + autoload :SchemaMigration autoload :Scoping autoload :Serialization autoload :SessionStore @@ -136,6 +136,16 @@ module ActiveRecord end end + module Tasks + extend ActiveSupport::Autoload + + autoload :DatabaseTasks + autoload :SQLiteDatabaseTasks, 'active_record/tasks/sqlite_database_tasks' + autoload :MySQLDatabaseTasks, 'active_record/tasks/mysql_database_tasks' + autoload :PostgreSQLDatabaseTasks, + 'active_record/tasks/postgresql_database_tasks' + end + autoload :TestCase autoload :TestFixtures, 'active_record/fixtures' end diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb deleted file mode 100644 index 5de10a8dd6..0000000000 --- a/activerecord/lib/active_record/aggregations.rb +++ /dev/null @@ -1,262 +0,0 @@ -module ActiveRecord - # = Active Record Aggregations - module Aggregations # :nodoc: - extend ActiveSupport::Concern - - def clear_aggregation_cache #:nodoc: - @aggregation_cache.clear if persisted? - end - - # Active Record implements aggregation through a macro-like class method called +composed_of+ - # for representing attributes as value objects. It expresses relationships like "Account [is] - # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call - # to the macro adds a description of how the value objects are created from the attributes of - # the entity object (when the entity is initialized either as a new object or from finding an - # existing object) and how it can be turned back into attributes (when the entity is saved to - # the database). - # - # class Customer < ActiveRecord::Base - # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount) - # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] - # end - # - # The customer class now has the following methods to manipulate the value objects: - # * <tt>Customer#balance, Customer#balance=(money)</tt> - # * <tt>Customer#address, Customer#address=(address)</tt> - # - # These methods will operate with value objects like the ones described below: - # - # class Money - # include Comparable - # attr_reader :amount, :currency - # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } - # - # def initialize(amount, currency = "USD") - # @amount, @currency = amount, currency - # end - # - # def exchange_to(other_currency) - # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor - # Money.new(exchanged_amount, other_currency) - # end - # - # def ==(other_money) - # amount == other_money.amount && currency == other_money.currency - # end - # - # def <=>(other_money) - # if currency == other_money.currency - # amount <=> other_money.amount - # else - # amount <=> other_money.exchange_to(currency).amount - # end - # end - # end - # - # class Address - # attr_reader :street, :city - # def initialize(street, city) - # @street, @city = street, city - # end - # - # def close_to?(other_address) - # city == other_address.city - # end - # - # def ==(other_address) - # city == other_address.city && street == other_address.street - # end - # end - # - # Now it's possible to access attributes from the database through the value objects instead. If - # you choose to name the composition the same as the attribute's name, it will be the only way to - # access that attribute. That's the case with our +balance+ attribute. You interact with the value - # objects just like you would with any other attribute: - # - # customer.balance = Money.new(20) # sets the Money value object and the attribute - # customer.balance # => Money value object - # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK") - # customer.balance > Money.new(10) # => true - # customer.balance == Money.new(20) # => true - # customer.balance < Money.new(5) # => false - # - # Value objects can also be composed of multiple attributes, such as the case of Address. The order - # of the mappings will determine the order of the parameters. - # - # customer.address_street = "Hyancintvej" - # customer.address_city = "Copenhagen" - # customer.address # => Address.new("Hyancintvej", "Copenhagen") - # - # customer.address_street = "Vesterbrogade" - # customer.address # => Address.new("Hyancintvej", "Copenhagen") - # customer.clear_aggregation_cache - # customer.address # => Address.new("Vesterbrogade", "Copenhagen") - # - # customer.address = Address.new("May Street", "Chicago") - # customer.address_street # => "May Street" - # customer.address_city # => "Chicago" - # - # == Writing value objects - # - # Value objects are immutable and interchangeable objects that represent a given value, such as - # a Money object representing $5. Two Money objects both representing $5 should be equal (through - # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is - # unlike entity objects where equality is determined by identity. An entity class such as Customer can - # easily have two different objects that both have an address on Hyancintvej. Entity identity is - # determined by object or relational unique identifiers (such as primary keys). Normal - # ActiveRecord::Base classes are entity objects. - # - # It's also important to treat the value objects as immutable. Don't allow the Money object to have - # its amount changed after creation. Create a new Money object with the new value instead. The - # Money#exchange_to method is an example of this. It returns a new value object instead of changing - # its own values. Active Record won't persist value objects that have been changed through means - # other than the writer method. - # - # The immutable requirement is enforced by Active Record by freezing any object assigned as a value - # object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError. - # - # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not - # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable - # - # == Custom constructors and converters - # - # By default value objects are initialized by calling the <tt>new</tt> constructor of the value - # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt> - # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows - # a custom constructor to be specified. - # - # When a new value is assigned to the value object, the default assumption is that the new value - # is an instance of the value class. Specifying a custom converter allows the new value to be automatically - # converted to an instance of value class if necessary. - # - # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that - # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor - # for the value class is called +create+ and it expects a CIDR address string as a parameter. New - # values can be assigned to the value object using either another NetAddr::CIDR object, a string - # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet - # these requirements: - # - # class NetworkResource < ActiveRecord::Base - # composed_of :cidr, - # :class_name => 'NetAddr::CIDR', - # :mapping => [ %w(network_address network), %w(cidr_range bits) ], - # :allow_nil => true, - # :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") }, - # :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) } - # end - # - # # This calls the :constructor - # network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24) - # - # # These assignments will both use the :converter - # network_resource.cidr = [ '192.168.2.1', 8 ] - # network_resource.cidr = '192.168.0.1/24' - # - # # This assignment won't use the :converter as the value is already an instance of the value class - # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8') - # - # # Saving and then reloading will use the :constructor on reload - # network_resource.save - # network_resource.reload - # - # == Finding records by a value object - # - # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database - # by specifying an instance of the value object in the conditions hash. The following example - # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": - # - # Customer.where(:balance => Money.new(20, "USD")).all - # - module ClassMethods - # Adds reader and writer methods for manipulating a value object: - # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods. - # - # Options are: - # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name - # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked - # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it - # with this option. - # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value - # object. Each mapping is represented as an array where the first item is the name of the - # entity attribute and the second item is the name of the attribute in the value object. The - # order in which mappings are defined determines the order in which attributes are sent to the - # value class constructor. - # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped - # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all - # mapped attributes. - # This defaults to +false+. - # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that - # is called to initialize the value object. The constructor is passed all of the mapped attributes, - # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them - # to instantiate a <tt>:class_name</tt> object. - # The default is <tt>:new</tt>. - # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> - # or a Proc that is called when a new value is assigned to the value object. The converter is - # passed the single value that is used in the assignment and is only called if the new value is - # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter - # can return nil to skip the assignment. - # - # Option examples: - # composed_of :temperature, :mapping => %w(reading celsius) - # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), - # :converter => Proc.new { |balance| balance.to_money } - # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] - # composed_of :gps_location - # composed_of :gps_location, :allow_nil => true - # composed_of :ip_address, - # :class_name => 'IPAddr', - # :mapping => %w(ip to_i), - # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) }, - # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) } - # - def composed_of(part_id, options = {}) - options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) - - name = part_id.id2name - class_name = options[:class_name] || name.camelize - mapping = options[:mapping] || [ name, name ] - mapping = [ mapping ] unless mapping.first.is_a?(Array) - allow_nil = options[:allow_nil] || false - constructor = options[:constructor] || :new - converter = options[:converter] - - reader_method(name, class_name, mapping, allow_nil, constructor) - writer_method(name, class_name, mapping, allow_nil, converter) - - create_reflection(:composed_of, part_id, options, self) - end - - private - def reader_method(name, class_name, mapping, allow_nil, constructor) - define_method(name) do - if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) - attrs = mapping.collect {|pair| read_attribute(pair.first)} - object = constructor.respond_to?(:call) ? - constructor.call(*attrs) : - class_name.constantize.send(constructor, *attrs) - @aggregation_cache[name] = object - end - @aggregation_cache[name] - end - end - - def writer_method(name, class_name, mapping, allow_nil, converter) - define_method("#{name}=") do |part| - unless part.is_a?(class_name.constantize) || converter.nil? || part.nil? - part = converter.respond_to?(:call) ? - converter.call(part) : - class_name.constantize.send(converter, part) - end - - if part.nil? && allow_nil - mapping.each { |pair| self[pair.first] = nil } - @aggregation_cache[name] = nil - else - mapping.each { |pair| self[pair.first] = part.send(pair.last) } - @aggregation_cache[name] = part.freeze - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 5a44d3a156..89a626693d 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -96,7 +96,7 @@ module ActiveRecord conditions.each do |condition| if options[:through] && condition.is_a?(Hash) - condition = { table.name => condition } + condition = disambiguate_condition(table, condition) end scope = scope.where(interpolate(condition)) @@ -113,7 +113,7 @@ module ActiveRecord conditions.each do |condition| condition = interpolate(condition) - condition = { (table.table_alias || table.name) => condition } unless i == 0 + condition = disambiguate_condition(table, condition) unless i == 0 scope = scope.where(condition) end @@ -138,6 +138,21 @@ module ActiveRecord end end + def disambiguate_condition(table, condition) + if condition.is_a?(Hash) + Hash[ + condition.map do |k, v| + if v.is_a?(Hash) + [k, v] + else + [table.table_alias || table.name, { k => v }] + end + end + ] + else + condition + end + end end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 4ec176e641..2447914640 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -50,18 +50,7 @@ module ActiveRecord end else column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - relation = scoped - - 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.pluck(column) + scoped.pluck(column) end end @@ -230,6 +219,7 @@ module ActiveRecord delete_records(:all, dependent) end else + records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } delete_or_destroy(records, dependent) end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index fa316a8c9d..2fb80fdc4c 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -37,9 +37,107 @@ module ActiveRecord delegate :target, :load_target, :loaded?, :to => :@association ## + # :method: select + # + # :call-seq: + # select(select = nil) + # select(&block) + # + # Works in two ways. + # + # *First:* Specify a subset of fields to be selected from the result set. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.select(:name) + # # => [ + # # #<Pet id: nil, name: "Fancy-Fancy">, + # # #<Pet id: nil, name: "Spook">, + # # #<Pet id: nil, name: "Choo-Choo"> + # # ] + # + # person.pets.select([:id, :name]) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy">, + # # #<Pet id: 2, name: "Spook">, + # # #<Pet id: 3, name: "Choo-Choo"> + # # ] + # + # Be careful because this also means you’re initializing a model + # object with only the fields that you’ve selected. If you attempt + # to access a field that is not in the initialized record you’ll + # receive: + # + # person.pets.select(:name).first.person_id + # # => ActiveModel::MissingAttributeError: missing attribute: person_id + # + # *Second:* You can pass a block so it can be used just like Array#select. + # This build an array of objects from the database for the scope, + # converting them into an array and iterating through them using + # Array#select. + # + # person.pets.select { |pet| pet.name =~ /oo/ } + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.select(:name) { |pet| pet.name =~ /oo/ } + # # => [ + # # #<Pet id: 2, name: "Spook">, + # # #<Pet id: 3, name: "Choo-Choo"> + # # ] + + ## + # :method: find + # + # :call-seq: + # find(*args, &block) + # + # Finds an object in the collection responding to the +id+. Uses the same + # rules as +ActiveRecord::Base.find+. Returns +ActiveRecord::RecordNotFound++ + # error if the object can not be found. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4 + # + # person.pets.find(2) { |pet| pet.name.downcase! } + # # => #<Pet id: 2, name: "fancy-fancy", person_id: 1> + # + # person.pets.find(2, 3) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + + ## # :method: first + # + # :call-seq: + # first(limit = nil) + # # Returns the first record, or the first +n+ records, from the collection. - # If the collection is empty, the first form returns nil, and the second + # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. # # class Person < ActiveRecord::Base @@ -67,8 +165,12 @@ module ActiveRecord ## # :method: last + # + # :call-seq: + # last(limit = nil) + # # Returns the last record, or the last +n+ records, from the collection. - # If the collection is empty, the first form returns nil, and the second + # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. # # class Person < ActiveRecord::Base @@ -95,7 +197,95 @@ module ActiveRecord # another_person_without.pets.last(3) # => [] ## + # :method: build + # + # :call-seq: + # build(attributes = {}, options = {}, &block) + # + # Returns a new object of the collection type that has been instantiated + # with +attributes+ and linked to this object, but have not yet been saved. + # You can pass an array of attributes hashes, this will return an array + # with the new objects. + # + # class Person + # has_many :pets + # end + # + # person.pets.build + # # => #<Pet id: nil, name: nil, person_id: 1> + # + # person.pets.build(name: 'Fancy-Fancy') + # # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}]) + # # => [ + # # #<Pet id: nil, name: "Spook", person_id: 1>, + # # #<Pet id: nil, name: "Choo-Choo", person_id: 1>, + # # #<Pet id: nil, name: "Brain", person_id: 1> + # # ] + # + # person.pets.size # => 5 # size of the collection + # person.pets.count # => 0 # count from database + + ## + # :method: create + # + # :call-seq: + # create(attributes = {}, options = {}, &block) + # + # Returns a new object of the collection type that has been instantiated with + # attributes, linked to this object and that has already been saved (if it + # passes the validations). + # + # class Person + # has_many :pets + # end + # + # person.pets.create(name: 'Fancy-Fancy') + # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.create([{name: 'Spook'}, {name: 'Choo-Choo'}]) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 3 + # person.pets.count # => 3 + # + # person.pets.find(1, 2, 3) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + + ## + # :method: create! + # + # :call-seq: + # create!(attributes = {}, options = {}, &block) + # + # Like +create+, except that if the record is invalid, raises an exception. + # + # class Person + # has_many :pets + # end + # + # class Pet + # attr_accessible :name + # validates :name, presence: true + # end + # + # person.pets.create!(name: nil) + # # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank + + ## # :method: concat + # + # :call-seq: + # concat(*records) + # # Add one or more records to the collection by setting their foreign keys # to the association's primary key. Since << flattens its argument list and # inserts each record, +push+ and +concat+ behave identically. Returns +self+ @@ -123,6 +313,10 @@ module ActiveRecord ## # :method: replace + # + # :call-seq: + # replace(other_array) + # # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. # @@ -147,23 +341,425 @@ module ActiveRecord # # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String ## + # :method: delete_all + # + # :call-seq: + # delete_all() + # + # Deletes all the records from the collection. For +has_many+ asssociations, + # the deletion is done according to the strategy specified by the <tt>:dependent</tt> + # option. Returns an array with the deleted records. + # + # If no <tt>:dependent</tt> option is given, then it will follow the + # default strategy. The default strategy is <tt>:nullify</tt>. This + # sets the foreign keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, + # the default strategy is +delete_all+. + # + # class Person < ActiveRecord::Base + # has_many :pets # dependent: :nullify option by default + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1, 2, 3) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>, + # # #<Pet id: 2, name: "Spook", person_id: nil>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: nil> + # # ] + # + # If it is set to <tt>:destroy</tt> all the objects from the collection + # are removed by calling their +destroy+ method. See +destroy+ for more + # information. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :destroy + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1, 2, 3) + # # => ActiveRecord::RecordNotFound + # + # If it is set to <tt>:delete_all</tt>, all the objects are deleted + # *without* calling their +destroy+ method. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :delete_all + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1, 2, 3) + # # => ActiveRecord::RecordNotFound + + ## # :method: destroy_all - # Destroy all the records from this association. + # + # :call-seq: + # destroy_all() + # + # Deletes the records of the collection directly from the database. + # This will _always_ remove the records ignoring the +:dependent+ + # option. # # class Person < ActiveRecord::Base # has_many :pets # end # # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] # # person.pets.destroy_all # # person.pets.size # => 0 # person.pets # => [] + # + # Pet.find(1) # => Couldn't find Pet with id=1 + + ## + # :method: delete + # + # :call-seq: + # delete(*records) + # delete(*fixnum_ids) + # delete(*string_ids) + # + # Deletes the +records+ supplied and removes them from the collection. For + # +has_many+ associations, the deletion is done according to the strategy + # specified by the <tt>:dependent</tt> option. Returns an array with the + # deleted records. + # + # If no <tt>:dependent</tt> option is given, then it will follow the default + # strategy. The default strategy is <tt>:nullify</tt>. This sets the foreign + # keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, the default + # strategy is +delete_all+. + # + # class Person < ActiveRecord::Base + # has_many :pets # dependent: :nullify option by default + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete(Pet.find(1)) + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1) + # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: nil> + # + # If it is set to <tt>:destroy</tt> all the +records+ are removed by calling + # their +destroy+ method. See +destroy+ for more information. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :destroy + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete(Pet.find(1), Pet.find(3)) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 1 + # person.pets + # # => [#<Pet id: 2, name: "Spook", person_id: 1>] + # + # Pet.find(1, 3) + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3) + # + # If it is set to <tt>:delete_all</tt>, all the +records+ are deleted + # *without* calling their +destroy+ method. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :delete_all + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete(Pet.find(1)) + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1) + # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1 + # + # You can pass +Fixnum+ or +String+ values, it finds the records + # responding to the +id+ and executes delete on them. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete("1") + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.delete(2, 3) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + + ## + # :method: destroy + # + # :call-seq: + # destroy(*records) + # + # Destroys the +records+ supplied and removes them from the collection. + # This method will _always_ remove record from the database ignoring + # the +:dependent+ option. Returns an array with the removed records. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy(Pet.find(1)) + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy(Pet.find(2), Pet.find(3)) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3) + # + # You can pass +Fixnum+ or +String+ values, it finds the records + # responding to the +id+ and then deletes them from the database. + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.destroy("4") + # # => #<Pet id: 4, name: "Benny", person_id: 1> + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.destroy(5, 6) + # # => [ + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6) + + ## + # :method: uniq + # + # :call-seq: + # uniq() + # + # Specifies whether the records should be unique or not. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.select(:name) + # # => [ + # # #<Pet name: "Fancy-Fancy">, + # # #<Pet name: "Fancy-Fancy"> + # # ] + # + # person.pets.select(:name).uniq + # # => [#<Pet name: "Fancy-Fancy">] + + ## + # :method: count + # + # :call-seq: + # count() + # + # Count all records using SQL. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + + ## + # :method: size + # + # :call-seq: + # size() + # + # Returns the size of the collection. If the collection hasn't been loaded, + # it executes a <tt>SELECT COUNT(*)</tt> query. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1 + # + # person.pets # This will execute a SELECT * FROM query + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 3 + # # Because the collection is already loaded, this will behave like + # # collection.size and no SQL count query is executed. + + ## + # :method: length + # + # :call-seq: + # length() + # + # Returns the size of the collection calling +size+ on the target. + # If the collection has been already loaded, +length+ and +size+ are + # equivalent. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.length # => 3 + # # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1 + # + # # Because the collection is loaded, you can + # # call the collection with no additional queries: + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] ## # :method: empty? - # Returns true if the collection is empty. + # + # Returns +true+ if the collection is empty. # # class Person < ActiveRecord::Base # has_many :pets @@ -179,7 +775,12 @@ module ActiveRecord ## # :method: any? - # Returns true if the collection is not empty. + # + # :call-seq: + # any? + # any?{|item| block} + # + # Returns +true+ if the collection is not empty. # # class Person < ActiveRecord::Base # has_many :pets @@ -211,8 +812,13 @@ module ActiveRecord ## # :method: many? + # + # :call-seq: + # many? + # many?{|item| block} + # # Returns true if the collection has more than one record. - # Equivalent to +collection.size > 1+. + # Equivalent to <tt>collection.size > 1</tt>. # # class Person < ActiveRecord::Base # has_many :pets @@ -248,7 +854,11 @@ module ActiveRecord ## # :method: include? - # Returns true if the given object is present in the collection. + # + # :call-seq: + # include?(record) + # + # Returns +true+ if the given object is present in the collection. # # class Person < ActiveRecord::Base # has_many :pets @@ -265,7 +875,7 @@ module ActiveRecord :any?, :many?, :include?, :to => :@association - def initialize(association) + def initialize(association) #:nodoc: @association = association super association.klass, association.klass.arel_table merge! association.scoped @@ -297,10 +907,67 @@ module ActiveRecord end end + # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays + # contain the same number of elements and if each element is equal + # to the corresponding element in the other array, otherwise returns + # +false+. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1> + # # ] + # + # other = person.pets.to_ary + # + # person.pets == other + # # => true + # + # other = [Pet.new(id: 1), Pet.new(id: 2)] + # + # person.pets == other + # # => false def ==(other) load_target == other end + # Returns a new array of objects from the collection. If the collection + # hasn't been loaded, it fetches the records from the database. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # other_pets = person.pets.to_ary + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # other_pets.replace([Pet.new(name: 'BooGoo')]) + # + # other_pets + # # => [#<Pet id: nil, name: "BooGoo", person_id: 1>] + # + # person.pets + # # This is not affected by replace + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] def to_ary load_target.dup end @@ -331,37 +998,32 @@ module ActiveRecord end alias_method :push, :<< - # Removes every object from the collection. This does not destroy - # the objects, it sets their foreign keys to +NULL+. Returns +self+ - # so methods can be chained. + # Equivalent to +delete_all+. The difference is that returns +self+, instead + # of an array with the deleted objects, so methods can be chained. See + # +delete_all+ for more information. + def clear + delete_all + self + end + + # Reloads the collection from the database. Returns +self+. + # Equivalent to <tt>collection(true)</tt>. # # class Person < ActiveRecord::Base # has_many :pets # end # - # person.pets # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] - # person.pets.clear # => [] - # person.pets.size # => 0 - # - # Pet.find(1) # => #<Pet id: 1, name: "Snoop", group: "dogs", person_id: nil> + # person.pets # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] # - # If they are associated with +dependent: :destroy+ option, it deletes - # them directly from the database. + # person.pets # uses the pets cache + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] # - # class Person < ActiveRecord::Base - # has_many :pets, dependent: :destroy - # end + # person.pets.reload # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] # - # person.pets # => [#<Pet id: 2, name: "Gorby", group: "cats", person_id: 2>] - # person.pets.clear # => [] - # person.pets.size # => 0 - # - # Pet.find(2) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=2 - def clear - delete_all - self - end - + # person.pets(true)  # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload proxy_association.reload self diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 2131edbc20..f0d1120c68 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -36,7 +36,7 @@ module ActiveRecord when :destroy target.destroy when :nullify - target.update_attribute(reflection.foreign_key, nil) + target.update_column(reflection.foreign_key, nil) end end end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index fafed94ff2..54705e4950 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -12,7 +12,7 @@ module ActiveRecord # and all of its books via a single query: # # SELECT * FROM authors - # LEFT OUTER JOIN books ON authors.id = books.id + # LEFT OUTER JOIN books ON authors.id = books.author_id # WHERE authors.name = 'Ken Akamatsu' # # However, this could result in many rows that contain redundant data. After diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index bf9fe70b31..ab6d253ef8 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -1,11 +1,25 @@ require 'active_support/concern' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :whitelist_attributes, instance_accessor: false + mattr_accessor :mass_assignment_sanitizer, instance_accessor: false + end + module AttributeAssignment extend ActiveSupport::Concern include ActiveModel::MassAssignmentSecurity + included do + initialize_mass_assignment_sanitizer + end + module ClassMethods + def inherited(child) # :nodoc: + child.send :initialize_mass_assignment_sanitizer if self == Base + super + end + private # The primary key and inheritance column can never be set by mass-assignment for security reasons. @@ -14,6 +28,11 @@ module ActiveRecord default << 'id' unless primary_key.eql? 'id' default end + + def initialize_mass_assignment_sanitizer + attr_accessible(nil) if Model.whitelist_attributes + self.mass_assignment_sanitizer = Model.mass_assignment_sanitizer if Model.mass_assignment_sanitizer + end end # Allows you to set all the attributes at once by passing in a hash with keys @@ -64,11 +83,12 @@ module ActiveRecord # user.name # => "Josh" # user.is_admin? # => true def assign_attributes(new_attributes, options = {}) - return unless new_attributes + return if new_attributes.blank? attributes = new_attributes.stringify_keys multi_parameter_attributes = [] nested_parameter_attributes = [] + previous_options = @mass_assignment_options @mass_assignment_options = options unless options[:without_protection] @@ -94,8 +114,9 @@ module ActiveRecord send("#{k}=", v) end - @mass_assignment_options = nil assign_multiparameter_attributes(multi_parameter_attributes) + ensure + @mass_assignment_options = previous_options end protected @@ -111,7 +132,7 @@ module ActiveRecord 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. + # by calling new on the column type or aggregation type 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, @@ -146,7 +167,7 @@ module ActiveRecord 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 + klass = column_for_attribute(name).klass if values_hash_from_param.values.all?{|v|v.nil?} nil elsif klass == Time diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 39ea885246..f36df4a444 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -16,19 +16,6 @@ module ActiveRecord 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 @@ -149,7 +136,9 @@ module ActiveRecord # 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)] }] + attribute_names.each_with_object({}) { |name, attrs| + attrs[name] = read_attribute(name) + } end # Returns an <tt>#inspect</tt>-like string for the value of the @@ -190,6 +179,19 @@ module ActiveRecord self.class.columns_hash[name.to_s] 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)). + # (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 + protected def clone_attributes(reader_method = :read_attribute, attributes = {}) diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 11c63591e3..a24b4b7839 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,12 +1,18 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :partial_updates, instance_accessor: false + self.partial_updates = true + end + module AttributeMethods module Dirty extend ActiveSupport::Concern + include ActiveModel::Dirty - include AttributeMethods::Write included do if self < ::ActiveRecord::Timestamp @@ -14,7 +20,6 @@ module ActiveRecord end config_attribute :partial_updates - self.partial_updates = true end # Attempts to +save+ the record and clears changed attributes if successful. @@ -72,11 +77,8 @@ module ActiveRecord def _field_changed?(attr, old, value) if column = column_for_attribute(attr) - if column.number? && column.null && (old.nil? || old == 0) && value.blank? - # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. - # Hence we don't record it as a change if the value changes from nil to ''. - # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll - # be typecast back to 0 (''.to_i => 0) + if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || + changes_from_zero_to_string?(old, value)) value = nil else value = column.type_cast(value) @@ -85,6 +87,19 @@ module ActiveRecord old != value end + + def changes_from_nil_to_empty_string?(column, old, value) + # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. + # Hence we don't record it as a change if the value changes from nil to ''. + # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll + # be typecast back to 0 (''.to_i => 0) + column.null && (old.nil? || old == 0) && value.blank? + end + + def changes_from_zero_to_string?(old, value) + # For columns with old 0 and value non-empty string + old == 0 && value.present? && value != '0' + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index dcc3d79de9..a7af086e43 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,13 +1,17 @@ module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :attribute_types_cached_by_default, instance_accessor: false + end + module AttributeMethods module Read extend ActiveSupport::Concern ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] + ActiveRecord::Model.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT included do - config_attribute :attribute_types_cached_by_default, :global => true - self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT + config_attribute :attribute_types_cached_by_default end module ClassMethods diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 165785c8fb..4af4d28b74 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -6,7 +6,7 @@ module ActiveRecord 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 + class_attribute :serialized_attributes self.serialized_attributes = {} end @@ -72,12 +72,13 @@ module ActiveRecord self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) end - def initialize_attributes(attributes) #:nodoc: - super + def initialize_attributes(attributes, options = {}) #:nodoc: + serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized + super(attributes, options) serialized_attributes.each do |key, coder| if attributes.key?(key) - attributes[key] = Attribute.new(coder, attributes[key], :serialized) + attributes[key] = Attribute.new(coder, attributes[key], serialized) 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 ac31b636db..e300c9721f 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -2,6 +2,14 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/object/inclusion' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :time_zone_aware_attributes, instance_accessor: false + self.time_zone_aware_attributes = false + + mattr_accessor :skip_time_zone_conversion_for_attributes, instance_accessor: false + self.skip_time_zone_conversion_for_attributes = [] + end + module AttributeMethods module TimeZoneConversion class Type # :nodoc: @@ -22,11 +30,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :time_zone_aware_attributes, :global => true - self.time_zone_aware_attributes = false - + config_attribute :time_zone_aware_attributes, global: true config_attribute :skip_time_zone_conversion_for_attributes - self.skip_time_zone_conversion_for_attributes = [] end module ClassMethods @@ -57,8 +62,9 @@ module ActiveRecord time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time end time = time.in_time_zone rescue nil if time + changed = read_attribute(:#{attr_name}) != time write_attribute(:#{attr_name}, original_time) - #{attr_name}_will_change! + #{attr_name}_will_change! if changed @attributes_cache["#{attr_name}"] = time end EOV diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index a050fabf35..cc11567506 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -231,30 +231,6 @@ module ActiveRecord # 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 = [ diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 66a0c83c41..f17e7158de 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -1,12 +1,10 @@ +require 'yaml' + module ActiveRecord # :stopdoc: module Coders class YAMLColumn - RESCUE_ERRORS = [ ArgumentError ] - - if defined?(Psych) && defined?(Psych::SyntaxError) - RESCUE_ERRORS << Psych::SyntaxError - end + RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] attr_accessor :object_class 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 450ef69744..347d794fa3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -54,13 +54,146 @@ module ActiveRecord # # == Options # - # There are two connection-pooling-related options that you can add to + # There are several connection-pooling-related options that you can add to # your database connection configuration: # # * +pool+: number indicating size of connection pool (default 5) - # * +wait_timeout+: number of seconds to block and wait for a connection + # * +checkout_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). + # * +reaping_frequency+: frequency in seconds to periodically run the + # Reaper, which attempts to find and close dead connections, which can + # occur if a programmer forgets to close a connection at the end of a + # thread or a thread dies unexpectedly. (Default nil, which means don't + # run the Reaper). + # * +dead_connection_timeout+: number of seconds from last checkout + # after which the Reaper will consider a connection reapable. (default + # 5 seconds). class ConnectionPool + # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool + # with which it shares a Monitor. But could be a generic Queue. + # + # The Queue in stdlib's 'thread' could replace this class except + # stdlib's doesn't support waiting with a timeout. + class Queue + def initialize(lock = Monitor.new) + @lock = lock + @cond = @lock.new_cond + @num_waiting = 0 + @queue = [] + end + + # Test if any threads are currently waiting on the queue. + def any_waiting? + synchronize do + @num_waiting > 0 + end + end + + # Return the number of threads currently waiting on this + # queue. + def num_waiting + synchronize do + @num_waiting + end + end + + # Add +element+ to the queue. Never blocks. + def add(element) + synchronize do + @queue.push element + @cond.signal + end + end + + # If +element+ is in the queue, remove and return it, or nil. + def delete(element) + synchronize do + @queue.delete(element) + end + end + + # Remove all elements from the queue. + def clear + synchronize do + @queue.clear + end + end + + # Remove the head of the queue. + # + # If +timeout+ is not given, remove and return the head the + # queue if the number of available elements is strictly + # greater than the number of threads currently waiting (that + # is, don't jump ahead in line). Otherwise, return nil. + # + # If +timeout+ is given, block if it there is no element + # available, waiting up to +timeout+ seconds for an element to + # become available. + # + # Raises: + # - ConnectionTimeoutError if +timeout+ is given and no element + # becomes available after +timeout+ seconds, + def poll(timeout = nil) + synchronize do + if timeout + no_wait_poll || wait_poll(timeout) + else + no_wait_poll + end + end + end + + private + + def synchronize(&block) + @lock.synchronize(&block) + end + + # Test if the queue currently contains any elements. + def any? + !@queue.empty? + end + + # A thread can remove an element from the queue without + # waiting if an only if the number of currently available + # connections is strictly greater than the number of waiting + # threads. + def can_remove_no_wait? + @queue.size > @num_waiting + end + + # Removes and returns the head of the queue if possible, or nil. + def remove + @queue.shift + end + + # Remove and return the head the queue if the number of + # available elements is strictly greater than the number of + # threads currently waiting. Otherwise, return nil. + def no_wait_poll + remove if can_remove_no_wait? + end + + # Waits on the queue up to +timeout+ seconds, then removes and + # returns the head of the queue. + def wait_poll(timeout) + @num_waiting += 1 + + t0 = Time.now + elapsed = 0 + loop do + @cond.wait(timeout - elapsed) + + return remove if any? + + elapsed = Time.now - t0 + raise ConnectionTimeoutError if elapsed >= timeout + end + ensure + @num_waiting -= 1 + end + end + # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. # A reaper instantiated with a nil frequency will never reap the # connection pool. @@ -88,7 +221,7 @@ module ActiveRecord include MonitorMixin - attr_accessor :automatic_reconnect, :timeout + attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout attr_reader :spec, :connections, :size, :reaper # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification @@ -105,7 +238,8 @@ module ActiveRecord # The cache of reserved connections mapped to threads @reserved_connections = {} - @timeout = spec.config[:wait_timeout] || 5 + @checkout_timeout = spec.config[:checkout_timeout] || 5 + @dead_connection_timeout = spec.config[:dead_connection_timeout] @reaper = Reaper.new self, spec.config[:reaping_frequency] @reaper.run @@ -115,21 +249,14 @@ module ActiveRecord @connections = [] @automatic_reconnect = true - # connections available to be checked out - @available = [] - - # number of threads waiting to check out a connection - @num_waiting = 0 - - # signal threads waiting - @cond = new_cond + @available = Queue.new self end # Hack for tests to be able to add connections. Do not call outside of tests - def insert_connection_for_test!(c) + def insert_connection_for_test!(c) #:nodoc: synchronize do @connections << c - @available << c + @available.add c end end @@ -188,7 +315,7 @@ module ActiveRecord conn.disconnect! end @connections = [] - @available = [] + @available.clear end end @@ -203,8 +330,9 @@ module ActiveRecord @connections.delete_if do |conn| conn.requires_reloading? end - @available.delete_if do |conn| - conn.requires_reloading? + @available.clear + @connections.each do |conn| + @available.add conn end end end @@ -230,16 +358,7 @@ module ActiveRecord # - PoolFullError: no connection can be obtained from the pool. def checkout synchronize do - conn = nil - - if @num_waiting == 0 - conn = acquire_connection - end - - unless conn - conn = wait_until(@timeout) { acquire_connection } - end - + conn = acquire_connection conn.lease checkout_and_verify(conn) end @@ -258,8 +377,7 @@ module ActiveRecord release conn - @available.unshift conn - @cond.signal + @available.add conn end end @@ -274,7 +392,7 @@ module ActiveRecord # from the reserved hash will be a little easier. release conn - @cond.signal # can make a new connection now + @available.add checkout_new_connection if @available.any_waiting? end end @@ -283,61 +401,37 @@ module ActiveRecord # or a thread dies unexpectedly. def reap synchronize do - stale = Time.now - @timeout + stale = Time.now - @dead_connection_timeout connections.dup.each do |conn| remove conn if conn.in_use? && stale > conn.last_use && !conn.active? end - @cond.broadcast # may violate fairness end end private - # Take an available connection or, if possible, create a new - # one, or nil. + # Acquire a connection by one of 1) immediately removing one + # from the queue of available connections, 2) creating a new + # connection if the pool is not at capacity, 3) waiting on the + # queue for a connection to become available. # - # Monitor must be held while calling this method. - # - # Returns: a newly acquired connection. + # Raises: + # - PoolFullError if a connection could not be acquired (FIXME: + # why not ConnectionTimeoutError? def acquire_connection - if @available.any? - @available.pop - elsif connections.size < size + if conn = @available.poll + conn + elsif @connections.size < @size checkout_new_connection - end - end - - # Wait on +@cond+ until the block returns non-nil. Note that - # unlike MonitorMixin::ConditionVariable#wait_until, this method - # does not test the block before the first wait period. - # - # Monitor must be held when calling this method. - # - # +timeout+: Integer timeout in seconds - # - # Returns: the result of the block - # - # Raises: - # - PoolFullError: timeout elapsed before +&block+ returned a connection - def wait_until(timeout, &block) - @num_waiting += 1 - begin + else t0 = Time.now - loop do - elapsed = Time.now - t0 - if elapsed >= timeout - msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % - [timeout, elapsed] - raise PoolFullError, msg - end - - @cond.wait(timeout - elapsed) - - conn = yield - return conn if conn + begin + @available.poll(@checkout_timeout) + rescue ConnectionTimeoutError + msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % + [@checkout_timeout, Time.now - t0] + raise PoolFullError, msg end - ensure - @num_waiting -= 1 end end @@ -354,11 +448,11 @@ module ActiveRecord end def new_connection - ActiveRecord::Base.send(spec.adapter_method, spec.config) + ActiveRecord::Model.send(spec.adapter_method, spec.config) end def current_connection_id #:nodoc: - ActiveRecord::Base.connection_id ||= Thread.current.object_id + ActiveRecord::Model.connection_id ||= Thread.current.object_id end def checkout_new_connection @@ -469,10 +563,12 @@ module ActiveRecord end def retrieve_connection_pool(klass) - pool = get_pool_for_class klass.name - return pool if pool - return nil if ActiveRecord::Model == klass - retrieve_connection_pool klass.active_record_super + if !(klass < Model::Tag) + get_pool_for_class('ActiveRecord::Model') # default connection + else + pool = get_pool_for_class(klass.name) + pool || retrieve_connection_pool(klass.superclass) + end end private 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 7b2961a04a..b0b51f540c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -4,6 +4,7 @@ module ActiveRecord # Converts an arel AST to SQL def to_sql(arel, binds = []) if arel.respond_to?(:ast) + binds = binds.dup visitor.accept(arel.ast) do quote(*binds.shift.reverse) end @@ -369,7 +370,7 @@ module ActiveRecord records.uniq.each do |record| begin record.rolledback!(rollback) - rescue Exception => e + rescue => e record.logger.error(e) if record.respond_to?(:logger) && record.logger end end @@ -384,7 +385,7 @@ module ActiveRecord records.uniq.each do |record| begin record.committed! - rescue Exception => e + rescue => e record.logger.error(e) if record.respond_to?(:logger) && record.logger end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 62b0f51bb2..f5794a4e54 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,4 @@ require 'active_support/deprecation/reporting' -require 'active_record/schema_migration' require 'active_record/migration/join_table' module ActiveRecord @@ -549,7 +548,7 @@ module ActiveRecord if options.is_a?(Hash) && order = options[:order] case order when Hash - column_names.each {|name| option_strings[name] += " #{order[name].to_s.upcase}" if order.has_key?(name)} + column_names.each {|name| option_strings[name] += " #{order[name].upcase}" if order.has_key?(name)} when String column_names.each {|name| option_strings[name] += " #{order.upcase}"} end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index c6faae77cc..28a9821913 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -286,7 +286,7 @@ module ActiveRecord :name => name, :connection_id => object_id, :binds => binds) { yield } - rescue Exception => e + rescue => e message = "#{e.class.name}: #{e.message}: #{sql}" @logger.error message if @logger exception = translate_exception(e, message) 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 9794c5663e..921278d145 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -264,19 +264,19 @@ module ActiveRecord def begin_db_transaction execute "BEGIN" - rescue Exception + rescue # Transactions aren't supported end def commit_db_transaction #:nodoc: execute "COMMIT" - rescue Exception + rescue # Transactions aren't supported end def rollback_db_transaction #:nodoc: execute "ROLLBACK" - rescue Exception + rescue # Transactions aren't supported end @@ -313,10 +313,10 @@ module ActiveRecord sql = "SHOW TABLES" end - select_all(sql).map { |table| + select_all(sql, 'SCHEMA').map { |table| table.delete('Table_type') sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" - exec_without_stmt(sql).first['Create Table'] + ";\n\n" + exec_without_stmt(sql, 'SCHEMA').first['Create Table'] + ";\n\n" }.join end @@ -508,7 +508,7 @@ module ActiveRecord # SHOW VARIABLES LIKE 'name' def show_variable(name) - variables = select_all("SHOW VARIABLES LIKE '#{name}'") + variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') variables.first['Value'] unless variables.empty? end @@ -630,7 +630,7 @@ module ActiveRecord raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" end - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" add_column_options!(rename_column_sql, options) rename_column_sql diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 350ccce03d..8fc172f6e8 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -268,7 +268,7 @@ module ActiveRecord # increase timeout so mysql server doesn't disconnect us wait_timeout = @config[:wait_timeout] - wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) + wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) variable_assignments << "@@wait_timeout = #{wait_timeout}" execute("SET #{variable_assignments.join(', ')}", :skip_logging) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index df3d5e4657..6657491c06 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -223,6 +223,7 @@ module ActiveRecord alias_type 'bit', 'text' alias_type 'varbit', 'text' alias_type 'macaddr', 'text' + alias_type 'uuid', 'text' # FIXME: I don't think this is correct. We should probably be returning a parsed date, # but the tests pass with a string returned. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 15c3d7be36..43e243581c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -18,7 +18,7 @@ module ActiveRecord # Forward any unused config params to PGconn.connect. [:statement_limit, :encoding, :min_messages, :schema_search_path, - :schema_order, :adapter, :pool, :wait_timeout, :template, + :schema_order, :adapter, :pool, :checkout_timeout, :template, :reaping_frequency, :insert_returning].each do |key| conn_params.delete key end @@ -89,7 +89,6 @@ module ActiveRecord else string end - end def cidr_to_string(object) @@ -256,7 +255,7 @@ module ActiveRecord :integer # UUID type when 'uuid' - :string + :uuid # Small and big integer types when /^(?:small|big)int$/ :integer @@ -319,6 +318,10 @@ module ActiveRecord def macaddr(name, options = {}) column(name, 'macaddr', options) end + + def uuid(name, options = {}) + column(name, 'uuid', options) + end end ADAPTER_NAME = 'PostgreSQL' @@ -341,7 +344,8 @@ module ActiveRecord :hstore => { :name => "hstore" }, :inet => { :name => "inet" }, :cidr => { :name => "cidr" }, - :macaddr => { :name => "macaddr" } + :macaddr => { :name => "macaddr" }, + :uuid => { :name => "uuid" } } # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -457,7 +461,8 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - @connection.status == PGconn::CONNECTION_OK + @connection.query 'SELECT 1' + true rescue PGError false end @@ -523,7 +528,7 @@ module ActiveRecord # 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 + @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i end # QUOTING ================================================== @@ -964,28 +969,28 @@ module ActiveRecord binds = [[nil, table]] binds << [nil, schema] if schema - exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0 + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 SELECT COUNT(*) FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind in ('v','r') - AND c.relname = $1 - AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'} + AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' + AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} SQL end # Returns true if schema exists. def schema_exists?(name) - exec_query(<<-SQL, 'SCHEMA', [[nil, name]]).rows.first[0].to_i > 0 + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 SELECT COUNT(*) FROM pg_namespace - WHERE nspname = $1 + WHERE nspname = '#{name}' SQL end # Returns an array of indexes for the given table. def indexes(table_name, name = nil) - result = query(<<-SQL, name) + result = query(<<-SQL, 'SCHEMA') SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid @@ -997,7 +1002,6 @@ module ActiveRecord ORDER BY i.relname SQL - result.map do |row| index_name = row[0] unique = row[1] == 't' @@ -1036,7 +1040,7 @@ module ActiveRecord # Returns the current database name. def current_database - query('select current_database()')[0][0] + query('select current_database()', 'SCHEMA')[0][0] end # Returns the current schema name. @@ -1046,7 +1050,7 @@ module ActiveRecord # Returns the current database encoding format. def encoding - query(<<-end_sql)[0][0] + query(<<-end_sql, 'SCHEMA')[0][0] SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' end_sql @@ -1054,7 +1058,7 @@ module ActiveRecord # Returns an array of schema names. def schema_names - query(<<-SQL).flatten + query(<<-SQL, 'SCHEMA').flatten SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' @@ -1110,8 +1114,8 @@ module ActiveRecord end def serial_sequence(table, column) - result = exec_query(<<-eosql, 'SCHEMA', [[nil, table], [nil, column]]) - SELECT pg_get_serial_sequence($1, $2) + result = exec_query(<<-eosql, 'SCHEMA') + SELECT pg_get_serial_sequence('#{table}', '#{column}') eosql result.rows.first.first end @@ -1188,13 +1192,13 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table) - row = exec_query(<<-end_sql, 'SCHEMA', [[nil, table]]).rows.first + row = exec_query(<<-end_sql, 'SCHEMA').rows.first SELECT DISTINCT(attr.attname) FROM pg_attribute attr INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] WHERE cons.contype = 'p' - AND dep.refobjid = $1::regclass + AND dep.refobjid = '#{table}'::regclass end_sql row && row.first @@ -1274,7 +1278,7 @@ module ActiveRecord end when 'integer' return 'integer' unless limit - + case limit when 1, 2; 'smallint' when 3, 4; 'integer' @@ -1446,7 +1450,7 @@ module ActiveRecord if @config[:encoding] @connection.set_client_encoding(@config[:encoding]) end - self.client_min_messages = @config[:min_messages] if @config[:min_messages] + self.client_min_messages = @config[:min_messages] || 'warning' self.schema_search_path = @config[:schema_search_path] || @config[:schema_order] # Use standard-conforming strings if available so we don't have to do the E'...' dance. @@ -1471,7 +1475,7 @@ module ActiveRecord end def last_insert_id_result(sequence_name) #:nodoc: - exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) + exec_query("SELECT currval('#{sequence_name}')", 'SQL') end # Executes a SELECT query and returns the results, performing any data type diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index d4ffa82b17..a0c7e559ce 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -208,7 +208,6 @@ module ActiveRecord true end - # QUOTING ================================================== def quote(value, column = nil) @@ -220,7 +219,6 @@ module ActiveRecord end end - def quote_string(s) #:nodoc: @connection.class.quote(s) end @@ -359,7 +357,7 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def tables(name = 'SCHEMA', table_name = nil) #:nodoc: + def tables(name = nil, table_name = nil) #:nodoc: sql = <<-SQL SELECT name FROM sqlite_master @@ -367,13 +365,13 @@ module ActiveRecord SQL sql << " AND name = #{quote_table_name(table_name)}" if table_name - exec_query(sql, name).map do |row| + exec_query(sql, 'SCHEMA').map do |row| row['name'] end end - def table_exists?(name) - name && tables('SCHEMA', name).any? + def table_exists?(table_name) + table_name && tables(nil, table_name).any? end # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. @@ -394,12 +392,12 @@ module ActiveRecord # Returns an array of indexes for the given table. def indexes(table_name, name = nil) #:nodoc: - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row| IndexDefinition.new( table_name, row['name'], row['unique'] != 0, - exec_query("PRAGMA index_info('#{row['name']}')").map { |col| + exec_query("PRAGMA index_info('#{row['name']}')", "Columns for index #{row['name']} on #{table_name}").map { |col| col['name'] }) end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 7b218a5570..bda41df80f 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -90,6 +90,10 @@ module ActiveRecord connection_handler.remove_connection(klass) end + def clear_cache! # :nodoc: + connection.schema_cache.clear! + end + delegate :clear_active_connections!, :clear_reloadable_connections!, :clear_all_connections!, :to => :connection_handler end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index f2833fbf3c..90f156456e 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,79 +1,88 @@ require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/object/deep_dup' +require 'active_support/core_ext/module/delegation' require 'thread' module ActiveRecord - module Core - extend ActiveSupport::Concern + ActiveSupport.on_load(:active_record_config) do + ## + # :singleton-method: + # + # Accepts a logger conforming to the interface of Log4r 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+. + mattr_accessor :logger, instance_accessor: false - 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' + # } + # } + mattr_accessor :configurations, instance_accessor: false + self.configurations = {} - ## - # :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. + mattr_accessor :default_timezone, instance_accessor: false + self.default_timezone = :utc - ## - # :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. + mattr_accessor :schema_format, instance_accessor: false + self.schema_format = :ruby - ## - # :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 + mattr_accessor :timestamped_migrations, instance_accessor: false + self.timestamped_migrations = true - ## - # :singleton-method: - # Specify whether or not to use timestamps for migration versions - config_attribute :timestamped_migrations, :global => true - self.timestamped_migrations = true + mattr_accessor :connection_handler, instance_accessor: false + self.connection_handler = ConnectionAdapters::ConnectionHandler.new + + mattr_accessor :dependent_restrict_raises, instance_accessor: false + self.dependent_restrict_raises = true + end + module Core + extend ActiveSupport::Concern + + included do ## # :singleton-method: # The connection handler config_attribute :connection_handler - self.connection_handler = ConnectionAdapters::ConnectionHandler.new ## # :singleton-method: @@ -82,8 +91,11 @@ module ActiveRecord # ActiveRecord::DeleteRestrictionError exception will be raised # along with a DEPRECATION WARNING. If set to false, an error would # be added to the model instead. - config_attribute :dependent_restrict_raises, :global => true - self.dependent_restrict_raises = true + config_attribute :dependent_restrict_raises + + %w(logger configurations default_timezone schema_format timestamped_migrations).each do |name| + config_attribute name, global: true + end end module ClassMethods @@ -240,7 +252,7 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - self.class.initialize_attributes(cloned_attributes) + self.class.initialize_attributes(cloned_attributes, :serialized => false) cloned_attributes.delete(self.class.primary_key) @@ -254,7 +266,6 @@ module ActiveRecord @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) end - @aggregation_cache = {} @association_cache = {} @attributes_cache = {} @@ -379,15 +390,15 @@ module ActiveRecord @attributes[pk] = nil unless @attributes.key?(pk) - @aggregation_cache = {} - @association_cache = {} - @attributes_cache = {} - @previously_changed = {} - @changed_attributes = {} - @readonly = false - @destroyed = false - @marked_for_destruction = false - @new_record = true + @association_cache = {} + @attributes_cache = {} + @previously_changed = {} + @changed_attributes = {} + @readonly = false + @destroyed = false + @marked_for_destruction = false + @new_record = true + @mass_assignment_options = nil end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index b163ef3c12..b27a19f89a 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -1,111 +1,115 @@ module ActiveRecord # = Active Record Counter Cache module CounterCache - # Resets one or more counter caches to their correct value using an SQL - # count query. This is useful when adding new counter caches, or if the - # counter has been corrupted or modified directly by SQL. - # - # ==== Parameters - # - # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more counter names to reset - # - # ==== Examples - # - # # For Post with id #1 records reset the comments_count - # Post.reset_counters(1, :comments) - def reset_counters(id, *counters) - object = find(id) - counters.each do |association| - has_many_association = reflect_on_association(association.to_sym) + extend ActiveSupport::Concern - 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.foreign_key.to_s == foreign_key } - counter_name = reflection.counter_cache_column + module ClassMethods + # Resets one or more counter caches to their correct value using an SQL + # count query. This is useful when adding new counter caches, or if the + # counter has been corrupted or modified directly by SQL. + # + # ==== Parameters + # + # * +id+ - The id of the object you wish to reset a counter on. + # * +counters+ - One or more counter names to reset + # + # ==== Examples + # + # # For Post with id #1 records reset the comments_count + # Post.reset_counters(1, :comments) + def reset_counters(id, *counters) + object = find(id) + counters.each do |association| + has_many_association = reflect_on_association(association.to_sym) - stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(association).count - }) - connection.update stmt - end - return true - 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.foreign_key.to_s == foreign_key } + counter_name = reflection.counter_cache_column - # A generic "counter updater" implementation, intended primarily to be - # used by increment_counter and decrement_counter, but which may also - # be useful on its own. It simply does a direct SQL update for the record - # with the given ID, altering the given hash of counters by the amount - # given by the corresponding value: - # - # ==== Parameters - # - # * +id+ - The id of the object you wish to update a counter on or an Array of ids. - # * +counters+ - An Array of Hashes containing the names of the fields - # to update as keys and the amount to update the field by as values. - # - # ==== Examples - # - # # For the Post with id of 5, decrement the comment_count by 1, and - # # increment the action_count by 1 - # Post.update_counters 5, :comment_count => -1, :action_count => 1 - # # Executes the following SQL: - # # UPDATE posts - # # SET comment_count = COALESCE(comment_count, 0) - 1, - # # action_count = COALESCE(action_count, 0) + 1 - # # WHERE id = 5 - # - # # For the Posts with id of 10 and 15, increment the comment_count by 1 - # Post.update_counters [10, 15], :comment_count => 1 - # # Executes the following SQL: - # # UPDATE posts - # # 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| - operator = value < 0 ? '-' : '+' - quoted_column = connection.quote_column_name(counter_name) - "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ + arel_table[counter_name] => object.send(association).count + }) + connection.update stmt + end + return true end - where(primary_key => id).update_all updates.join(', ') - end + # A generic "counter updater" implementation, intended primarily to be + # used by increment_counter and decrement_counter, but which may also + # be useful on its own. It simply does a direct SQL update for the record + # with the given ID, altering the given hash of counters by the amount + # given by the corresponding value: + # + # ==== Parameters + # + # * +id+ - The id of the object you wish to update a counter on or an Array of ids. + # * +counters+ - An Array of Hashes containing the names of the fields + # to update as keys and the amount to update the field by as values. + # + # ==== Examples + # + # # For the Post with id of 5, decrement the comment_count by 1, and + # # increment the action_count by 1 + # Post.update_counters 5, :comment_count => -1, :action_count => 1 + # # Executes the following SQL: + # # UPDATE posts + # # SET comment_count = COALESCE(comment_count, 0) - 1, + # # action_count = COALESCE(action_count, 0) + 1 + # # WHERE id = 5 + # + # # For the Posts with id of 10 and 15, increment the comment_count by 1 + # Post.update_counters [10, 15], :comment_count => 1 + # # Executes the following SQL: + # # UPDATE posts + # # 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| + operator = value < 0 ? '-' : '+' + quoted_column = connection.quote_column_name(counter_name) + "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + end - # Increment a number field by one, usually representing a count. - # - # This is used for caching aggregate values, so that they don't need to be computed every time. - # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is - # shown it would have to run an SQL query to find how many posts and comments there are. - # - # ==== Parameters - # - # * +counter_name+ - The name of the field that should be incremented. - # * +id+ - The id of the object that should be incremented. - # - # ==== Examples - # - # # Increment the post_count column for the record with an id of 5 - # DiscussionBoard.increment_counter(:post_count, 5) - def increment_counter(counter_name, id) - update_counters(id, counter_name => 1) - end + where(primary_key => id).update_all updates.join(', ') + end + + # Increment a number field by one, usually representing a count. + # + # This is used for caching aggregate values, so that they don't need to be computed every time. + # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is + # shown it would have to run an SQL query to find how many posts and comments there are. + # + # ==== Parameters + # + # * +counter_name+ - The name of the field that should be incremented. + # * +id+ - The id of the object that should be incremented. + # + # ==== Examples + # + # # Increment the post_count column for the record with an id of 5 + # DiscussionBoard.increment_counter(:post_count, 5) + def increment_counter(counter_name, id) + update_counters(id, counter_name => 1) + end - # Decrement a number field by one, usually representing a count. - # - # This works the same as increment_counter but reduces the column value by 1 instead of increasing it. - # - # ==== Parameters - # - # * +counter_name+ - The name of the field that should be decremented. - # * +id+ - The id of the object that should be decremented. - # - # ==== Examples - # - # # Decrement the post_count column for the record with an id of 5 - # DiscussionBoard.decrement_counter(:post_count, 5) - def decrement_counter(counter_name, id) - update_counters(id, counter_name => -1) + # Decrement a number field by one, usually representing a count. + # + # This works the same as increment_counter but reduces the column value by 1 instead of increasing it. + # + # ==== Parameters + # + # * +counter_name+ - The name of the field that should be decremented. + # * +id+ - The id of the object that should be decremented. + # + # ==== Examples + # + # # Decrement the post_count column for the record with an id of 5 + # DiscussionBoard.decrement_counter(:post_count, 5) + def decrement_counter(counter_name, id) + update_counters(id, counter_name => -1) + end end end end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e278e62ce7..23aaa319d8 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -56,7 +56,7 @@ module ActiveRecord end def valid? - attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } + attribute_names.all? { |name| model.columns_hash[name] } end def define diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index fc80f3081e..9b88bb8178 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -53,6 +53,10 @@ module ActiveRecord class RecordNotSaved < ActiveRecordError end + # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false. + class RecordNotDestroyed < ActiveRecordError + end + # Raised when SQL statement cannot be executed by the database (for example, it's often the case for # MySQL when Ruby driver used is too old). class StatementInvalid < ActiveRecordError diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 313fdb3487..7ade385c70 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,12 +1,13 @@ +require 'active_support/lazy_load_hooks' require 'active_support/core_ext/class/attribute' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false + end + 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 + delegate :auto_explain_threshold_in_seconds, :auto_explain_threshold_in_seconds=, to: 'ActiveRecord::Model' # 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 @@ -52,7 +53,7 @@ module ActiveRecord # 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| + str = queries && queries.map do |sql, bind| [].tap do |msg| msg << "EXPLAIN for: #{sql}" unless bind.empty? @@ -62,6 +63,12 @@ module ActiveRecord msg << connection.explain(sql, bind) end.join("\n") end.join("\n") + + # Overriding inspect to be more human readable, specially in the console. + def str.inspect + self + end + str end # Silences automatic EXPLAIN logging for the duration of the block. diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 1f8c4fc203..d5ba343b4c 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -2,9 +2,12 @@ require 'active_support/notifications' module ActiveRecord class ExplainSubscriber # :nodoc: - def call(*args) + def start(name, id, payload) + # unused + end + + def finish(name, id, payload) if queries = Thread.current[:available_queries_for_explain] - payload = args.last queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload) end end diff --git a/activerecord/lib/active_record/fixtures/file.rb b/activerecord/lib/active_record/fixtures/file.rb index 6547791144..a9cabf5a7b 100644 --- a/activerecord/lib/active_record/fixtures/file.rb +++ b/activerecord/lib/active_record/fixtures/file.rb @@ -24,37 +24,33 @@ module ActiveRecord rows.each(&block) end - RESCUE_ERRORS = [ ArgumentError ] # :nodoc: + RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] # :nodoc: private - if defined?(Psych) && defined?(Psych::SyntaxError) - RESCUE_ERRORS << Psych::SyntaxError - end - - def rows - return @rows if @rows + def rows + return @rows if @rows + + begin + data = YAML.load(render(IO.read(@file))) + rescue *RESCUE_ERRORS => error + raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace + end + @rows = data ? validate(data).to_a : [] + end - begin - data = YAML.load(render(IO.read(@file))) - rescue *RESCUE_ERRORS => error - raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace + def render(content) + ERB.new(content).result end - @rows = data ? validate(data).to_a : [] - end - def render(content) - ERB.new(content).result - end + # Validate our unmarshalled data. + def validate(data) + unless Hash === data || YAML::Omap === data + raise Fixture::FormatError, 'fixture is not a hash' + end - # Validate our unmarshalled data. - def validate(data) - unless Hash === data || YAML::Omap === data - raise Fixture::FormatError, 'fixture is not a hash' + raise Fixture::FormatError unless data.all? { |name, row| Hash === row } + data end - - raise Fixture::FormatError unless data.all? { |name, row| Hash === row } - data - end end end end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 46d253b0a7..770083ac13 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,13 +1,17 @@ require 'active_support/concern' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + # Determine whether to store the full constant name including namespace when using STI + mattr_accessor :store_full_sti_class, instance_accessor: false + self.store_full_sti_class = true + end + 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 @@ -95,7 +99,7 @@ module ActiveRecord # 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 + unless klass < Model::Tag raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index a3412582fa..4ce42feb74 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -1,4 +1,9 @@ module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :lock_optimistically, instance_accessor: false + self.lock_optimistically = true + end + module Locking # == What is Optimistic Locking # @@ -51,8 +56,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :lock_optimistically, :global => true - self.lock_optimistically = true + config_attribute :lock_optimistically end def locking_enabled? #:nodoc: @@ -168,7 +172,7 @@ module ActiveRecord # 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: + def initialize_attributes(attributes, options = {}) #:nodoc: if attributes.key?(locking_column) && lock_optimistically attributes[locking_column] ||= 0 end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 2a9139749d..d58176bc62 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,7 +1,6 @@ require "active_support/core_ext/module/delegation" require "active_support/core_ext/class/attribute_accessors" require 'active_support/deprecation' -require 'active_record/schema_migration' require 'set' module ActiveRecord @@ -33,6 +32,12 @@ module ActiveRecord end end + class PendingMigrationError < ActiveRecordError#:nodoc: + def initialize + super("Migrations are pending run 'rake db:migrate RAILS_ENV=#{ENV['RAILS_ENV']}' to resolve the issue") + end + end + # = Active Record Migrations # # Migrations can manage the evolution of a schema used by several physical @@ -233,7 +238,7 @@ module ActiveRecord # add_column :people, :salary, :integer # Person.reset_column_information # Person.all.each do |p| - # p.update_attribute :salary, SalaryCalculator.compute(p) + # p.update_column :salary, SalaryCalculator.compute(p) # end # end # end @@ -253,7 +258,7 @@ module ActiveRecord # ... # say_with_time "Updating salaries..." do # Person.all.each do |p| - # p.update_attribute :salary, SalaryCalculator.compute(p) + # p.update_column :salary, SalaryCalculator.compute(p) # end # end # ... @@ -327,10 +332,28 @@ module ActiveRecord class Migration autoload :CommandRecorder, 'active_record/migration/command_recorder' + + # This class is used to verify that all migrations have been run before + # loading a web page if config.active_record.migration_error is set to :page_load + class CheckPending + def initialize(app) + @app = app + end + + def call(env) + ActiveRecord::Migration.check_pending! + @app.call(env) + end + end + class << self attr_accessor :delegate # :nodoc: end + def self.check_pending! + raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration? + end + def self.method_missing(name, *args, &block) # :nodoc: (delegate || superclass.delegate).send(name, *args, &block) end @@ -606,6 +629,14 @@ module ActiveRecord end end + def needs_migration? + current_version < last_version + end + + def last_version + migrations(migrations_paths).last.try(:version)||0 + end + def proper_table_name(name) # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb index 105d1e0e2b..7b3d926d91 100644 --- a/activerecord/lib/active_record/model.rb +++ b/activerecord/lib/active_record/model.rb @@ -1,6 +1,34 @@ require 'active_support/deprecation' +require 'active_support/concern' +require 'active_support/core_ext/module/delegation' +require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord + module Configuration # :nodoc: + # This just abstracts out how we define configuration options in AR. Essentially we + # have mattr_accessors on the ActiveRecord:Model constant that define global defaults. + # Classes that then use AR get class_attributes defined, which means that when they + # are assigned the default will be overridden for that class and subclasses. (Except + # when options[:global] == true, in which case there is one global value always.) + def config_attribute(name, options = {}) + if options[:global] + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def self.#{name}; ActiveRecord::Model.#{name}; end + def #{name}; ActiveRecord::Model.#{name}; end + def self.#{name}=(val); ActiveRecord::Model.#{name} = val; end + CODE + else + options[:instance_writer] ||= false + class_attribute name, options + + singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + 1 + remove_method :#{name} + def #{name}; ActiveRecord::Model.#{name}; end + CODE + end + end + end + # <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: # @@ -9,41 +37,35 @@ module ActiveRecord # 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 + extend ActiveSupport::Concern + extend ConnectionHandling + extend ActiveModel::Observing::ClassMethods - def self.included(base) - return if base.singleton_class < ClassMethods + # This allows us to detect an ActiveRecord::Model while it's in the process of being included. + module Tag; end + def self.append_features(base) base.class_eval do - extend ClassMethods - Callbacks::Register.setup(self) - initialize_generated_modules unless self == Base + include Tag + extend Configuration end + + super end - extend ActiveModel::Configuration - extend ActiveModel::Callbacks - extend ActiveModel::MassAssignmentSecurity::ClassMethods - extend ActiveModel::AttributeMethods::ClassMethods - extend Callbacks::Register - extend Explain - extend ConnectionHandling + included do + extend ActiveModel::Naming + extend ActiveSupport::Benchmarkable + extend ActiveSupport::DescendantsTracker + + extend QueryCache::ClassMethods + extend Querying + extend Translation + extend DynamicMatchers + extend Explain + extend ConnectionHandling - def self.extend(*modules) - ClassMethods.send(:include, *modules) + initialize_generated_modules unless self == Base end include Persistence @@ -56,13 +78,21 @@ module ActiveRecord include AttributeAssignment include ActiveModel::Conversion include Validations - include Locking::Optimistic, Locking::Pessimistic + include CounterCache + include Locking::Optimistic + include Locking::Pessimistic include AttributeMethods - include Callbacks, ActiveModel::Observing, Timestamp + include Callbacks + include ActiveModel::Observing + include Timestamp include Associations include ActiveModel::SecurePassword - include AutosaveAssociation, NestedAttributes - include Aggregations, Transactions, Reflection, Serialization, Store + include AutosaveAssociation + include NestedAttributes + include Transactions + include Reflection + include Serialization + include Store include Core class << self @@ -103,6 +133,15 @@ module ActiveRecord end end + # This hook is where config accessors on Model get defined. + # + # We don't want to just open the Model module and add stuff to it in other files, because + # that would cause Model to load, which causes all sorts of loading order issues. + # + # We need this hook rather than just using the :active_record one, because users of the + # :active_record hook may need to use config options. + ActiveSupport.run_load_hooks(:active_record_config, Model) + # 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 index 7f38dda11e..e6b76ddc4c 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,7 +1,19 @@ require 'active_support/concern' -require 'active_support/core_ext/class/attribute_accessors' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :primary_key_prefix_type, instance_accessor: false + + mattr_accessor :table_name_prefix, instance_accessor: false + self.table_name_prefix = "" + + mattr_accessor :table_name_suffix, instance_accessor: false + self.table_name_suffix = "" + + mattr_accessor :pluralize_table_names, instance_accessor: false + self.pluralize_table_names = true + end + module ModelSchema extend ActiveSupport::Concern @@ -13,7 +25,7 @@ module ActiveRecord # 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 + config_attribute :primary_key_prefix_type, global: true ## # :singleton-method: @@ -26,14 +38,12 @@ module ActiveRecord # 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: @@ -41,7 +51,6 @@ module ActiveRecord # 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 @@ -308,10 +317,6 @@ module ActiveRecord @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. diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 95a2ddcc11..841681e542 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -5,6 +5,11 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/class/attribute' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :nested_attributes_options, instance_accessor: false + self.nested_attributes_options = {} + end + module NestedAttributes #:nodoc: class TooManyRecords < ActiveRecordError end @@ -13,7 +18,6 @@ module ActiveRecord included do config_attribute :nested_attributes_options - self.nested_attributes_options = {} end # = Active Record Nested Attributes diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index c2d3eeb8ce..aca8291d75 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -2,24 +2,24 @@ module ActiveRecord # = Active Record Null Relation - class NullRelation < Relation + module NullRelation def exec_queries @records = [] end - def pluck(column_name) + def pluck(_column_name) [] end - def delete_all(conditions = nil) + def delete_all(_conditions = nil) 0 end - def update_all(updates, conditions = nil, options = {}) + def update_all(_updates, _conditions = nil, _options = {}) 0 end - def delete(id_or_array) + def delete(_id_or_array) 0 end @@ -51,13 +51,12 @@ module ActiveRecord 0 end - def calculate(operation, column_name, options = {}) + def calculate(_operation, _column_name, _options = {}) nil end - def exists?(id = false) + def exists?(_id = false) false end - end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a1bc39a32d..a23597be28 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -122,6 +122,11 @@ module ActiveRecord # Deletes the record in the database and freezes this instance to reflect # that no changes should be made (since they can't be persisted). + # + # There's a series of callbacks associated with <tt>destroy</tt>. If + # the <tt>before_destroy</tt> callback return +false+ the action is cancelled + # and <tt>destroy</tt> returns +false+. See + # ActiveRecord::Callbacks for further details. def destroy raise ReadOnlyRecord if readonly? destroy_associations @@ -130,6 +135,17 @@ module ActiveRecord freeze end + # Deletes the record in the database and freezes this instance to reflect + # that no changes should be made (since they can't be persisted). + # + # There's a series of callbacks associated with <tt>destroy!</tt>. If + # the <tt>before_destroy</tt> callback return +false+ the action is cancelled + # and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See + # ActiveRecord::Callbacks for further details. + def destroy! + destroy || raise(ActiveRecord::RecordNotDestroyed) + end + # Returns an instance of the specified +klass+ with the attributes of the # current record. This is mostly useful in relation to single-table # inheritance structures where you want a subclass to appear as the @@ -151,21 +167,6 @@ module ActiveRecord became end - # Updates a single attribute and saves the record. - # This is especially useful for boolean flags on existing records. Also note that - # - # * Validation is skipped. - # * Callbacks are invoked. - # * updated_at/updated_on column is updated if that column is available. - # * Updates all the attributes that are dirty in this object. - # - def update_attribute(name, value) - name = name.to_s - verify_readonly_attribute(name) - send("#{name}=", value) - save(:validate => false) - end - # Updates a single attribute of an object, without calling save. # # * Validation is skipped. @@ -224,7 +225,7 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def increment!(attribute, by = 1) - increment(attribute, by).update_attribute(attribute, self[attribute]) + increment(attribute, by).update_column(attribute, self[attribute]) end # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1). @@ -241,7 +242,7 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def decrement!(attribute, by = 1) - decrement(attribute, by).update_attribute(attribute, self[attribute]) + decrement(attribute, by).update_column(attribute, self[attribute]) end # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So @@ -258,7 +259,7 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def toggle!(attribute) - toggle(attribute).update_attribute(attribute, self[attribute]) + toggle(attribute).update_column(attribute, self[attribute]) end # Reloads the attributes of this object from the database. @@ -266,7 +267,6 @@ module ActiveRecord # may do e.g. record.reload(:lock => true) to reload the same record with # an exclusive row lock. def reload(options = nil) - clear_aggregation_cache clear_association_cache fresh_object = diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index eb2769f1ef..9432a70c41 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -30,6 +30,7 @@ module ActiveRecord ) rake_tasks do + require "active_record/base" load "active_record/railties/databases.rake" end @@ -38,10 +39,15 @@ module ActiveRecord # first time. Also, make it output to STDERR. console do |app| require "active_record/railties/console_sandbox" if app.sandbox? + require "active_record/base" console = ActiveSupport::Logger.new(STDERR) Rails.logger.extend ActiveSupport::Logger.broadcast console end + runner do |app| + require "active_record/base" + end + initializer "active_record.initialize_timezone" do ActiveSupport.on_load(:active_record) do self.time_zone_aware_attributes = true @@ -53,11 +59,15 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end + initializer "active_record.migration_error" do |app| + if config.active_record.delete(:migration_error) == :page_load + config.app_middleware.insert_after "::ActionDispatch::Callbacks", + "ActiveRecord::Migration::CheckPending" + end + end + initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - if app.config.active_record.delete(:whitelist_attributes) - attr_accessible(nil) - end app.config.active_record.each do |k,v| send "#{k}=", v end @@ -84,18 +94,12 @@ module ActiveRecord end initializer "active_record.set_reloader_hooks" do |app| - hook = lambda do - ActiveRecord::Base.clear_reloadable_connections! - ActiveRecord::Base.clear_cache! - end + hook = app.config.reload_classes_only_on_change ? :to_prepare : :to_cleanup - 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) + ActiveSupport.on_load(:active_record) do + ActionDispatch::Reloader.send(hook) do + ActiveRecord::Model.clear_reloadable_connections! + ActiveRecord::Model.clear_cache! end end end @@ -106,20 +110,21 @@ module ActiveRecord config.after_initialize do |app| ActiveSupport.on_load(:active_record) do - ActiveRecord::Base.instantiate_observers + ActiveRecord::Model.instantiate_observers ActionDispatch::Reloader.to_prepare do - ActiveRecord::Base.instantiate_observers + ActiveRecord::Model.instantiate_observers end end ActiveSupport.on_load(:active_record) do if app.config.use_schema_cache_dump filename = File.join(app.config.paths["db"].first, "schema_cache.dump") + if File.file?(filename) cache = Marshal.load File.binread filename if cache.version == ActiveRecord::Migrator.current_version - ActiveRecord::Base.connection.schema_cache = cache + ActiveRecord::Model.connection.schema_cache = cache else warn "schema_cache.dump is expired. Current version is #{ActiveRecord::Migrator.current_version}, but cache version is #{cache.version}." end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index f26e18b1e0..8199b5c2e0 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -14,138 +14,27 @@ db_namespace = namespace :db do end namespace :create do - # desc 'Create all the local databases defined in config/database.yml' task :all => :load_config do - ActiveRecord::Base.configurations.each_value do |config| - # Skip entries that don't have a database key, such as the first entry here: - # - # defaults: &defaults - # adapter: mysql - # username: root - # password: - # host: localhost - # - # development: - # database: blog_development - # *defaults - next unless config['database'] - # Only connect to local databases - local_database?(config) { create_database(config) } - end + ActiveRecord::Tasks::DatabaseTasks.create_all end end 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) - @charset = ENV['CHARSET'] || 'utf8' - @collation = ENV['COLLATION'] || 'utf8_unicode_ci' - {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)} - end - - def create_database(config) - begin - if config['adapter'] =~ /sqlite/ - if File.exist?(config['database']) - $stderr.puts "#{config['database']} already exists" - else - begin - # Create the SQLite database - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection - rescue Exception => e - $stderr.puts e, *(e.backtrace) - $stderr.puts "Couldn't create database for #{config.inspect}" - end - end - return # Skip the else clause of begin/rescue - else - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection - end - rescue - case config['adapter'] - when /mysql/ - if config['adapter'] =~ /jdbc/ - #FIXME After Jdbcmysql gives this class - require 'active_record/railties/jdbcmysql_error' - error_class = ArJdbcMySQL::Error - else - error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error - end - access_denied_error = 1045 - begin - ActiveRecord::Base.establish_connection(config.merge('database' => nil)) - ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config)) - ActiveRecord::Base.establish_connection(config) - rescue error_class => sqlerr - if sqlerr.errno == access_denied_error - print "#{sqlerr.error}. \nPlease provide the root password for your mysql installation\n>" - root_password = $stdin.gets.strip - grant_statement = "GRANT ALL PRIVILEGES ON #{config['database']}.* " \ - "TO '#{config['username']}'@'localhost' " \ - "IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;" - ActiveRecord::Base.establish_connection(config.merge( - 'database' => nil, 'username' => 'root', 'password' => root_password)) - ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config)) - ActiveRecord::Base.connection.execute grant_statement - ActiveRecord::Base.establish_connection(config) - else - $stderr.puts sqlerr.error - $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['charset'] || @charset}, collation: #{config['collation'] || @collation}" - $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['charset'] - end - end - when /postgresql/ - @encoding = config['encoding'] || ENV['CHARSET'] || 'utf8' - begin - ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) - ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => @encoding)) - ActiveRecord::Base.establish_connection(config) - rescue Exception => e - $stderr.puts e, *(e.backtrace) - $stderr.puts "Couldn't create database for #{config.inspect}" - end - end - else - $stderr.puts "#{config['database']} already exists" - end + ActiveRecord::Tasks::DatabaseTasks.create_current end namespace :drop do - # desc 'Drops all the local databases defined in config/database.yml' task :all => :load_config do - ActiveRecord::Base.configurations.each_value do |config| - # Skip entries that don't have a database key - next unless config['database'] - begin - # Only connect to local databases - local_database?(config) { drop_database(config) } - rescue Exception => e - $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}" - end - end + ActiveRecord::Tasks::DatabaseTasks.drop_all end end desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)' task :drop => :load_config do - configs_for_environment.each { |config| drop_database_and_rescue(config) } - end - - def local_database?(config, &block) - if config['host'].in?(['127.0.0.1', 'localhost']) || config['host'].blank? - yield - else - $stderr.puts "This task only modifies local databases. #{config['database']} is on a remote host." - end + ActiveRecord::Tasks::DatabaseTasks.drop_current end - desc "Migrate the database (options: VERSION=x, VERBOSE=false)." task :migrate => [:environment, :load_config] do ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true @@ -247,31 +136,18 @@ db_namespace = namespace :db do end # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' - task :reset => :environment do + task :reset => [:environment, :load_config] do db_namespace["drop"].invoke db_namespace["setup"].invoke end # desc "Retrieves the charset for the current environment's database" - task :charset => :environment do - config = ActiveRecord::Base.configurations[Rails.env || 'development'] - case config['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.charset - when /postgresql/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.encoding - when /sqlite/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.encoding - else - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end + task :charset => [:environment, :load_config] do + puts ActiveRecord::Tasks::DatabaseTasks.charset_current end # desc "Retrieves the collation for the current environment's database" - task :collation => :environment do + task :collation => [:environment, :load_config] do config = ActiveRecord::Base.configurations[Rails.env || 'development'] case config['adapter'] when /mysql/ @@ -283,12 +159,12 @@ db_namespace = namespace :db do end desc 'Retrieves the current schema version number' - task :version => :environment do + task :version => [:environment, :load_config] do puts "Current version: #{ActiveRecord::Migrator.current_version}" end # desc "Raises an error if there are pending migrations" - task :abort_if_pending_migrations => :environment do + task :abort_if_pending_migrations => [:environment, :load_config] do pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations if pending_migrations.any? @@ -311,20 +187,20 @@ db_namespace = namespace :db do namespace :fixtures do desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task :load => :environment do + task :load => [:environment, :load_config] do require 'active_record/fixtures' ActiveRecord::Base.establish_connection(Rails.env) base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.{yml,csv}"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| + (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| ActiveRecord::Fixtures.create_fixtures(fixtures_dir, fixture_file) end end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task :identify => :environment do + task :identify => [:environment, :load_config] do require 'active_record/fixtures' label, id = ENV['LABEL'], ENV['ID'] @@ -360,7 +236,7 @@ db_namespace = namespace :db do end desc 'Load a schema.rb file into the database' - task :load => :environment do + task :load => [:environment, :load_config] do file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" if File.exists?(file) load(file) @@ -375,7 +251,7 @@ db_namespace = namespace :db do namespace :cache do desc 'Create a db/schema_cache.dump file.' - task :dump => :environment do + task :dump => [:environment, :load_config] do con = ActiveRecord::Base.connection filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump") @@ -385,7 +261,7 @@ db_namespace = namespace :db do end desc 'Clear a db/schema_cache.dump file.' - task :clear => :environment do + task :clear => [:environment, :load_config] do filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump") FileUtils.rm(filename) if File.exists?(filename) end @@ -395,24 +271,15 @@ db_namespace = namespace :db do namespace :structure do desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' - task :dump => :environment do + task :dump => [:environment, :load_config] 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' + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.structure_dump(abcs[Rails.env], filename) + when 'oci', 'oracle' ActiveRecord::Base.establish_connection(abcs[Rails.env]) 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 #{filename} #{search_path} #{abcs[Rails.env]['database']}` - raise 'Error dumping database' if $?.exitstatus == 1 - when /sqlite/ - dbfile = abcs[Rails.env]['database'] - `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 #{filename} -A -U` when "firebird" @@ -436,18 +303,8 @@ db_namespace = namespace :db do 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(filename).split("\n\n").each do |table| - ActiveRecord::Base.connection.execute(table) - end - when /postgresql/ - set_psql_env(abcs[env]) - `psql -f "#{filename}" #{abcs[env]['database']}` - when /sqlite/ - dbfile = abcs[env]['database'] - `sqlite3 #{dbfile} < "#{filename}"` + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.structure_load(abcs[Rails.env], filename) when 'sqlserver' `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}` when 'oci', 'oracle' @@ -505,19 +362,11 @@ db_namespace = namespace :db do task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ] # desc "Empty the test database" - task :purge => :environment do + task :purge => [:environment, :load_config] do abcs = ActiveRecord::Base.configurations case abcs['test']['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(:test) - ActiveRecord::Base.connection.recreate_database(abcs['test']['database'], mysql_creation_options(abcs['test'])) - when /postgresql/ - ActiveRecord::Base.clear_active_connections! - drop_database(abcs['test']) - create_database(abcs['test']) - when /sqlite/ - dbfile = abcs['test']['database'] - File.delete(dbfile) if File.exist?(dbfile) + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.purge abcs['test'] when 'sqlserver' test = abcs.deep_dup['test'] test_database = test['database'] @@ -547,7 +396,7 @@ db_namespace = namespace :db do namespace :sessions do # desc "Creates a sessions migration for use with ActiveRecord::SessionStore" - task :create => :environment do + task :create => [:environment, :load_config] do raise 'Task unavailable to this database (no migration support)' unless ActiveRecord::Base.connection.supports_migrations? Rails.application.load_generators require 'rails/generators/rails/session_migration/session_migration_generator' @@ -555,7 +404,7 @@ db_namespace = namespace :db do end # desc "Clear the sessions table" - task :clear => :environment do + task :clear => [:environment, :load_config] do ActiveRecord::Base.connection.execute "DELETE FROM #{session_table_name}" end end @@ -591,37 +440,6 @@ end task 'test:prepare' => 'db:test:prepare' -def drop_database(config) - case config['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection.drop_database config['database'] - when /sqlite/ - require 'pathname' - path = Pathname.new(config['database']) - file = path.absolute? ? path.to_s : File.join(Rails.root, path) - - FileUtils.rm(file) - when /postgresql/ - ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) - ActiveRecord::Base.connection.drop_database config['database'] - end -end - -def drop_database_and_rescue(config) - begin - drop_database(config) - rescue Exception => e - $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}" - end -end - -def configs_for_environment - environments = [Rails.env] - environments << 'test' if Rails.env.development? - ActiveRecord::Base.configurations.values_at(*environments).compact.reject { |config| config['database'].blank? } -end - def session_table_name ActiveRecord::SessionStore::Session.table_name end @@ -634,10 +452,3 @@ end def firebird_db_string(config) FireRuby::Database.db_string_for(config.symbolize_keys) end - -def set_psql_env(config) - ENV['PGHOST'] = config['host'] if config['host'] - ENV['PGPORT'] = config['port'].to_s if config['port'] - ENV['PGPASSWORD'] = config['password'].to_s if config['password'] - ENV['PGUSER'] = config['username'].to_s if config['username'] -end diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 836b15e2ce..960b78dc38 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -6,7 +6,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :_attr_readonly + class_attribute :_attr_readonly, instance_writer: false self._attr_readonly = [] end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index c380b5c029..e9a8e90538 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -7,8 +7,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - extend ActiveModel::Configuration - config_attribute :reflections + class_attribute :reflections self.reflections = {} end @@ -18,36 +17,17 @@ module ActiveRecord # and creates input fields for all of the attributes depending on their type # and displays the associations to other objects. # - # MacroReflection class has info for AggregateReflection and AssociationReflection - # classes. + # MacroReflection class has info for the AssociationReflection + # class. module ClassMethods def create_reflection(macro, name, options, active_record) - case macro - when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many - klass = options[:through] ? ThroughReflection : AssociationReflection - reflection = klass.new(macro, name, options, active_record) - when :composed_of - reflection = AggregateReflection.new(macro, name, options, active_record) - end + klass = options[:through] ? ThroughReflection : AssociationReflection + reflection = klass.new(macro, name, options, active_record) self.reflections = self.reflections.merge(name => reflection) reflection end - # Returns an array of AggregateReflection objects for all the aggregations in the class. - def reflect_on_all_aggregations - reflections.values.grep(AggregateReflection) - end - - # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). - # - # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection - # - def reflect_on_aggregation(aggregation) - reflection = reflections[aggregation] - reflection if reflection.is_a?(AggregateReflection) - end - # Returns an array of AssociationReflection objects for all the # associations in the class. If you only want to reflect on a certain # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, @@ -79,24 +59,20 @@ module ActiveRecord end end - # Abstract base class for AggregateReflection and AssociationReflection. Objects of - # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. + # Abstract base class for AssociationReflection. Objects of AssociationReflection are returned by the Reflection::ClassMethods. class MacroReflection # Returns the name of the macro. # - # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt> # <tt>has_many :clients</tt> returns <tt>:clients</tt> attr_reader :name # Returns the macro type. # - # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:composed_of</tt> # <tt>has_many :clients</tt> returns <tt>:has_many</tt> attr_reader :macro # Returns the hash of options used for the macro. # - # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt> # <tt>has_many :clients</tt> returns +{}+ attr_reader :options @@ -115,7 +91,6 @@ module ActiveRecord # Returns the class for the macro. # - # <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class # <tt>has_many :clients</tt> returns the Client class def klass @klass ||= class_name.constantize @@ -123,7 +98,6 @@ module ActiveRecord # Returns the class name for the macro. # - # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt> # <tt>has_many :clients</tt> returns <tt>'Client'</tt> def class_name @class_name ||= (options[:class_name] || derive_class_name).to_s @@ -149,16 +123,6 @@ module ActiveRecord end end - - # Holds all the meta-data about an aggregation as it was specified in the - # Active Record class. - class AggregateReflection < MacroReflection #:nodoc: - def mapping - mapping = options[:mapping] || [name, name] - mapping.first.is_a?(Array) ? mapping : [mapping] - end - end - # Holds all the meta-data about an association as it was specified in the # Active Record class. class AssociationReflection < MacroReflection #:nodoc: diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 05ced3299b..fe3aa00a74 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -300,7 +300,7 @@ module ActiveRecord # Person.update(people.keys, people.values) def update(id, attributes) if id.is_a?(Array) - id.each.with_index.map {|one_id, idx| update(one_id, attributes[idx])} + id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } else object = find(id) object.update_attributes(attributes) diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 15f838a5ab..fb4388d4b2 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -2,20 +2,26 @@ require 'active_support/core_ext/object/blank' module ActiveRecord module Batches - # Yields each record that was found by the find +options+. The find is - # performed by find_in_batches with a batch size of 1000 (or as + # Looping through a collection of records from the database + # (using the +all+ method, for example) is very inefficient + # since it will try to instantiate all the objects at once. + # + # In that case, batch processing methods allow you to work + # with the records in batches, thereby greatly reducing memory consumption. + # + # The <tt>find_each</tt> method uses <tt>find_in_batches</tt> with a batch size of 1000 (or as # specified by the <tt>:batch_size</tt> option). # - # Example: + # Person.all.find_each do |person| + # person.do_awesome_stuff + # end # # Person.where("age > 21").find_each do |person| # person.party_all_night! # end # - # Note: This method is only intended to use for batch processing of - # large amounts of records that wouldn't fit in memory all at once. If - # you just need to loop over less than 1000 records, it's probably - # better just to use the regular find methods. + # You can also pass the <tt>:start</tt> option to specify + # an offset to control the starting point. def find_each(options = {}) find_in_batches(options) do |records| records.each { |record| yield record } @@ -39,12 +45,15 @@ module ActiveRecord # primary keys. You can't set the limit either, that's used to control # the batch sizes. # - # Example: - # # Person.where("age > 21").find_in_batches do |group| # sleep(50) # Make sure it doesn't get too crowded in there! # group.each { |person| person.party_all_night! } # end + # + # # Let's process the next 2000 records + # Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group| + # group.each { |person| person.party_all_night! } + # end def find_in_batches(options = {}) options.assert_valid_keys(:start, :batch_size) diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 31d99f0192..22c3e6a324 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -16,9 +16,16 @@ module ActiveRecord # # Person.count(:age, distinct: true) # # => counts the number of different age values + # + # Person.where("age > 26").count { |person| person.gender == 'female' } + # # => queries people where "age > 26" then count the loaded results filtering by gender def count(column_name = nil, options = {}) - column_name, options = nil, column_name if column_name.is_a?(Hash) - calculate(:count, column_name, options) + if block_given? + self.to_a.count { |item| yield item } + else + column_name, options = nil, column_name if column_name.is_a?(Hash) + calculate(:count, column_name, options) + end end # Calculates the average value on a given column. Returns +nil+ if there's @@ -52,9 +59,13 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.sum('age') # => 4562 + # # => returns the total sum of all people's age + # + # Person.where('age > 100').sum { |person| person.age - 100 } + # # queries people where "age > 100" then perform a sum calculation with the block returns def sum(*args) if block_given? - self.to_a.sum(*args) {|*block_args| yield(*block_args)} + self.to_a.sum(*args) { |item| yield item } else calculate(:sum, *args) end @@ -96,7 +107,8 @@ module ActiveRecord relation = with_default_scope if relation.equal?(self) - if eager_loading? || (includes_values.present? && references_eager_loaded_tables?) + + if has_include?(column_name) construct_relation_for_association_calculations.calculate(operation, column_name, options) else perform_calculation(operation, column_name, options) @@ -118,7 +130,7 @@ module ActiveRecord # Person.all.map(&:name) # # Pluck returns an <tt>Array</tt> of attribute values type-casted to match - # the plucked column name, if it can be deduced. Plucking a SQL fragment + # the plucked column name, if it can be deduced. Plucking an SQL fragment # returns String values by default. # # Examples: @@ -144,21 +156,25 @@ module ActiveRecord column_name = "#{table_name}.#{column_name}" end - result = klass.connection.select_all(select(column_name).arel, nil, bind_values) + if has_include?(column_name) + construct_relation_for_association_calculations.pluck(column_name) + else + result = klass.connection.select_all(select(column_name).arel, nil, bind_values) - key = result.columns.first - column = klass.column_types.fetch(key) { - result.column_types.fetch(key) { - Class.new { def type_cast(v); v; end }.new + key = result.columns.first + column = klass.column_types.fetch(key) { + result.column_types.fetch(key) { + Class.new { def type_cast(v); v; end }.new + } } - } - result.map do |attributes| - raise ArgumentError, "Pluck expects to select just one attribute: #{attributes.inspect}" unless attributes.one? + result.map do |attributes| + raise ArgumentError, "Pluck expects to select just one attribute: #{attributes.inspect}" unless attributes.one? - value = klass.initialize_attributes(attributes).values.first + value = klass.initialize_attributes(attributes).values.first - column.type_cast(value) + column.type_cast(value) + end end end @@ -174,6 +190,10 @@ module ActiveRecord private + def has_include?(column_name) + eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) + end + def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index f5fdf437bf..64dda4f35a 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -32,12 +32,12 @@ module ActiveRecord 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) + if @klass.respond_to?(method) ::ActiveRecord::Delegation.delegate_to_scoped_klass(method) scoping { @klass.send(method, *args, &block) } + elsif Array.method_defined?(method) + ::ActiveRecord::Delegation.delegate method, :to => :to_a + to_a.send(method, *args, &block) elsif arel.respond_to?(method) ::ActiveRecord::Delegation.delegate method, :to => :arel arel.send(method, *args, &block) @@ -46,4 +46,4 @@ module ActiveRecord end end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 4fedd33d64..c91758265b 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -7,8 +7,6 @@ module ActiveRecord # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key # is an integer, find by id coerces its arguments using +to_i+. # - # ==== Examples - # # Person.find(1) # returns the object for ID = 1 # Person.find("1") # returns the object for ID = 1 # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) @@ -49,7 +47,6 @@ module ActiveRecord # # Post.find_by name: 'Spartacus', rating: 4 # Post.find_by "published_at < ?", 2.weeks.ago - # def find_by(*args) where(*args).take end @@ -64,8 +61,6 @@ module ActiveRecord # order. The order will depend on the database implementation. # If an order is supplied it will be respected. # - # Examples: - # # Person.take # returns an object fetched by SELECT * FROM people # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5 # Person.where(["name LIKE '%?'", name]).take @@ -82,12 +77,11 @@ module ActiveRecord # Find the first record (or first N records if a parameter is supplied). # If no order is defined it will order by primary key. # - # Examples: - # # Person.first # returns the first object fetched by SELECT * FROM people # Person.where(["user_name = ?", user_name]).first # Person.where(["user_name = :u", { :u => user_name }]).first # Person.order("created_on DESC").offset(5).first + # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3 def first(limit = nil) if limit if order_values.empty? && primary_key @@ -109,11 +103,18 @@ module ActiveRecord # Find the last record (or last N records if a parameter is supplied). # If no order is defined it will order by primary key. # - # Examples: - # # Person.last # returns the last object fetched by SELECT * FROM people # Person.where(["user_name = ?", user_name]).last # Person.order("created_on DESC").offset(5).last + # Person.last(3) # returns the last three objects fetched by SELECT * FROM people. + # + # Take note that in that last case, the results are sorted in ascending order: + # + # [#<Person id:2>, #<Person id:3>, #<Person id:4>] + # + # and not: + # + # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) if limit if order_values.empty? && primary_key @@ -132,7 +133,8 @@ module ActiveRecord last or raise RecordNotFound end - # Examples: + # Runs the query on the database and returns records with the used query + # methods. # # Person.all # returns an array of objects for all the rows fetched by SELECT * FROM people # Person.where(["category IN (?)", categories]).limit(50).all @@ -163,11 +165,10 @@ module ActiveRecord # 'Jamie'</tt>), since it would be sanitized and then queried against # the primary key column, like <tt>id = 'name = \'Jamie\''</tt>. # - # ==== Examples # Person.exists?(5) # Person.exists?('5') - # Person.exists?(:name => "David") # Person.exists?(['name LIKE ?', "%#{query}%"]) + # Person.exists?(:name => "David") # Person.exists? def exists?(id = false) id = id.id if ActiveRecord::Model === id @@ -175,7 +176,7 @@ module ActiveRecord join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) - relation = relation.except(:select, :order).select("1").limit(1) + relation = relation.except(:select, :order).select("1 AS one").limit(1) case id when Array, Hash @@ -185,6 +186,8 @@ module ActiveRecord end connection.select_value(relation, "#{name} Exists", relation.bind_values) + rescue ThrowResult + false end protected diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 6a0cdd5917..cb8f903474 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -6,7 +6,7 @@ module ActiveRecord if value.is_a?(Hash) table = Arel::Table.new(column, engine) - build_from_hash(engine, value, table) + value.map { |k,v| build(table[k.to_sym], v) } else column = column.to_s diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 19fe8155d9..529ddb5e31 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -194,10 +194,102 @@ module ActiveRecord self end + # Returns a new relation, which is the result of filtering the current relation + # according to the conditions in the arguments. + # + # #where accepts conditions in one of several formats. In the examples below, the resulting + # SQL is given as an illustration; the actual query generated may be different depending + # on the database adapter. + # + # === string + # + # A single string, without additional arguments, is passed to the query + # constructor as a SQL fragment, and used in the where clause of the query. + # + # Client.where("orders_count = '2'") + # # SELECT * from clients where orders_count = '2'; + # + # Note that building your own string from user input may expose your application + # to injection attacks if not done properly. As an alternative, it is recommended + # to use one of the following methods. + # + # === array + # + # If an array is passed, then the first element of the array is treated as a template, and + # the remaining elements are inserted into the template to generate the condition. + # Active Record takes care of building the query to avoid injection attacks, and will + # convert from the ruby type to the database type where needed. Elements are inserted + # into the string in the order in which they appear. + # + # User.where(["name = ? and email = ?", "Joe", "joe@example.com"]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # Alternatively, you can use named placeholders in the template, and pass a hash as the + # second element of the array. The names in the template are replaced with the corresponding + # values from the hash. + # + # User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # This can make for more readable code in complex queries. + # + # Lastly, you can use sprintf-style % escapes in the template. This works slightly differently + # than the previous methods; you are responsible for ensuring that the values in the template + # are properly quoted. The values are passed to the connector for quoting, but the caller + # is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting, + # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>. + # + # User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # If #where is called with multiple arguments, these are treated as if they were passed as + # the elements of a single array. + # + # User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" }) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # When using strings to specify conditions, you can use any operator available from + # the database. While this provides the most flexibility, you can also unintentionally introduce + # dependencies on the underlying database. If your code is intended for general consumption, + # test with multiple database backends. + # + # === hash + # + # #where will also accept a hash condition, in which the keys are fields and the values + # are values to be searched for. + # + # Fields can be symbols or strings. Values can be single values, arrays, or ranges. + # + # User.where({ name: "Joe", email: "joe@example.com" }) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com' + # + # User.where({ name: ["Alice", "Bob"]}) + # # SELECT * FROM users WHERE name IN ('Alice', 'Bob') + # + # User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight }) + # # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000') + # + # === Joins + # + # If the relation is the result of a join, you may create a condition which uses any of the + # tables in the join. For string and array conditions, use the table name in the condition. + # + # User.joins(:posts).where("posts.created_at < ?", Time.now) + # + # For hash conditions, you can either use the table name in the key, or use a sub-hash. + # + # User.joins(:posts).where({ "posts.published" => true }) + # User.joins(:posts).where({ :posts => { :published => true } }) + # + # === empty condition + # + # If the condition returns true for blank?, then where is a no-op and returns the current relation. def where(opts, *rest) opts.blank? ? self : spawn.where!(opts, *rest) end + # #where! is identical to #where, except that instead of returning a new relation, it adds + # the condition to the existing relation. def where!(opts, *rest) references!(PredicateBuilder.references(opts)) if Hash === opts @@ -279,7 +371,7 @@ module ActiveRecord # end # def none - NullRelation.new(@klass, @table) + scoped.extending(NullRelation) end def readonly(value = true) @@ -470,8 +562,7 @@ module ActiveRecord when String, Array [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash - attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) - PredicateBuilder.build_from_hash(table.engine, attributes, table) + PredicateBuilder.build_from_hash(table.engine, opts, table) else [opts] end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 5530be3219..46f6c283e3 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -43,36 +43,6 @@ module ActiveRecord 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| - if aggregation = reflect_on_aggregation(attr.to_sym) - mapping = aggregation.mapping - 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" @@ -88,8 +58,6 @@ module ActiveRecord # { :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 diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 7cbe2db408..a25a8d79bd 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -70,8 +70,8 @@ HEADER @connection.tables.sort.each do |tbl| next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| case ignored - when String; tbl == ignored - when Regexp; tbl =~ ignored + when String; remove_prefix_and_suffix(tbl) == ignored + when Regexp; remove_prefix_and_suffix(tbl) =~ ignored else raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' end @@ -92,7 +92,7 @@ HEADER pk = @connection.primary_key(table) end - tbl.print " create_table #{table.inspect}" + tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" if columns.detect { |c| c.name == pk } if pk != 'id' tbl.print %Q(, :primary_key => "#{pk}") @@ -175,7 +175,7 @@ HEADER when BigDecimal value.to_s when Date, DateTime, Time - "'" + value.to_s(:db) + "'" + "'#{value.to_s(:db)}'" else value.inspect end @@ -185,7 +185,7 @@ HEADER if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| statement_parts = [ - ('add_index ' + index.table.inspect), + ('add_index ' + remove_prefix_and_suffix(index.table).inspect), index.columns.inspect, (':name => ' + index.name.inspect), ] @@ -206,5 +206,9 @@ HEADER stream.puts end end + + def remove_prefix_and_suffix(table) + table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2") + end end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index db833fc7f1..af51c803a7 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -8,7 +8,7 @@ module ActiveRecord included do # Stores the default scope for the class - config_attribute :default_scopes + class_attribute :default_scopes, instance_writer: false self.default_scopes = [] end diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 41e3b92499..e8dd312a47 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -1,9 +1,21 @@ module ActiveRecord #:nodoc: + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :include_root_in_json, instance_accessor: false + self.include_root_in_json = true + end + # = Active Record Serialization module Serialization extend ActiveSupport::Concern include ActiveModel::Serializers::JSON + included do + singleton_class.class_eval do + remove_method :include_root_in_json + delegate :include_root_in_json, to: 'ActiveRecord::Model' + end + end + def serializable_hash(options = nil) options = options.try(:clone) || {} diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 2e60521638..b833af64fe 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -18,8 +18,8 @@ module ActiveRecord #:nodoc: # <id type="integer">1</id> # <approved type="boolean">false</approved> # <replies-count type="integer">0</replies-count> - # <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="datetime">2003-07-16T09:28:00+1200</written-on> + # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> + # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> # <content>Have a nice day</content> # <author-email-address>david@loudthinking.com</author-email-address> # <parent-id></parent-id> diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index ce2ea85ef9..d13491502e 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -1,6 +1,10 @@ +require 'active_support/concern' +require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/class/attribute' + module ActiveRecord # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. - # It's like a simple key/value store backed into your record when you don't care about being able to + # It's like a simple key/value store baked into your record when you don't care about being able to # query that store outside the context of a single record. # # You can then declare accessors to this store that are then accessible just like any other attribute @@ -13,9 +17,6 @@ module ActiveRecord # You can set custom coder to encode/decode your serialized attributes to/from different formats. # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # - # String keys should be used for direct access to virtual attributes because of most of the coders do not - # distinguish symbols and strings as keys. - # # Examples: # # class User < ActiveRecord::Base @@ -23,34 +24,94 @@ module ActiveRecord # end # # u = User.new(color: 'black', homepage: '37signals.com') - # u.color # Accessor stored attribute - # u.settings['country'] = 'Denmark' # Any attribute, even if not specified with an accessor + # u.color # Accessor stored attribute + # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor + # + # # There is no difference between strings and symbols for accessing custom attributes + # u.settings[:country] # => 'Denmark' + # u.settings['country'] # => 'Denmark' # # # Add additional accessors to an existing store through store_accessor # class SuperUser < User # store_accessor :settings, :privileges, :servants # end + # + # The stored attribute names can be retrieved using +stored_attributes+. + # + # User.stored_attributes[:settings] # [:color, :homepage] module Store extend ActiveSupport::Concern + included do + class_attribute :stored_attributes + self.stored_attributes = {} + end + module ClassMethods def store(store_attribute, options = {}) - serialize store_attribute, options.fetch(:coder, Hash) + serialize store_attribute, IndifferentCoder.new(options[:coder]) store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors end def store_accessor(store_attribute, *keys) - keys.flatten.each do |key| + keys = keys.flatten + keys.each do |key| define_method("#{key}=") do |value| - send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) - send(store_attribute)[key.to_s] = value - send("#{store_attribute}_will_change!") + initialize_store_attribute(store_attribute) + send(store_attribute)[key] = value + send :"#{store_attribute}_will_change!" end define_method(key) do - send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) - send(store_attribute)[key.to_s] + initialize_store_attribute(store_attribute) + send(store_attribute)[key] + end + end + + self.stored_attributes[store_attribute] = keys + end + end + + private + def initialize_store_attribute(store_attribute) + case attribute = send(store_attribute) + when ActiveSupport::HashWithIndifferentAccess + # Already initialized. Do nothing. + when Hash + # Initialized as a Hash. Convert to indifferent access. + send :"#{store_attribute}=", attribute.with_indifferent_access + else + # Uninitialized. Set to an indifferent hash. + send :"#{store_attribute}=", ActiveSupport::HashWithIndifferentAccess.new + end + end + + class IndifferentCoder + def initialize(coder_or_class_name) + @coder = + if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump) + coder_or_class_name + else + ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object) end + end + + def dump(obj) + @coder.dump self.class.as_indifferent_hash(obj) + end + + def load(yaml) + self.class.as_indifferent_hash @coder.load(yaml) + end + + def self.as_indifferent_hash(obj) + case obj + when ActiveSupport::HashWithIndifferentAccess + obj + when Hash + obj.with_indifferent_access + else + HashWithIndifferentAccess.new end end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb new file mode 100644 index 0000000000..999b2ebc85 --- /dev/null +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -0,0 +1,109 @@ +module ActiveRecord + module Tasks # :nodoc: + module DatabaseTasks # :nodoc: + extend self + + TASKS_PATTERNS = { + /mysql/ => ActiveRecord::Tasks::MySQLDatabaseTasks, + /postgresql/ => ActiveRecord::Tasks::PostgreSQLDatabaseTasks, + /sqlite/ => ActiveRecord::Tasks::SQLiteDatabaseTasks + } + LOCAL_HOSTS = ['127.0.0.1', 'localhost'] + + def create(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).create + rescue Exception => error + $stderr.puts error, *(error.backtrace) + $stderr.puts "Couldn't create database for #{configuration.inspect}" + end + + def create_all + each_local_configuration { |configuration| create configuration } + end + + def create_current(environment = Rails.env) + each_current_configuration(environment) { |configuration| + create configuration + } + ActiveRecord::Base.establish_connection environment + end + + def drop(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).drop + rescue Exception => error + $stderr.puts error, *(error.backtrace) + $stderr.puts "Couldn't drop #{configuration['database']}" + end + + def drop_all + each_local_configuration { |configuration| drop configuration } + end + + def drop_current(environment = Rails.env) + each_current_configuration(environment) { |configuration| + drop configuration + } + end + + def charset_current(environment = Rails.env) + charset ActiveRecord::Base.configurations[environment] + end + + def charset(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).charset + end + + def purge(configuration) + class_for_adapter(configuration['adapter']).new(configuration).purge + end + + def structure_dump(*arguments) + configuration = arguments.first + filename = arguments.delete_at 1 + class_for_adapter(configuration['adapter']).new(*arguments).structure_dump(filename) + end + + def structure_load(*arguments) + configuration = arguments.first + filename = arguments.delete_at 1 + class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) + end + + private + + def class_for_adapter(adapter) + key = TASKS_PATTERNS.keys.detect { |pattern| adapter[pattern] } + TASKS_PATTERNS[key] + end + + def each_current_configuration(environment) + environments = [environment] + environments << 'test' if environment.development? + + configurations = ActiveRecord::Base.configurations.values_at(*environments) + configurations.compact.each do |configuration| + yield configuration unless configuration['database'].blank? + end + end + + def each_local_configuration + ActiveRecord::Base.configurations.each_value do |configuration| + next unless configuration['database'] + + if local_database?(configuration) + yield configuration + else + $stderr.puts "This task only modifies local databases. #{configuration['database']} is on a remote host." + end + end + end + + def local_database?(configuration) + configuration['host'].in?(LOCAL_HOSTS) || configuration['host'].blank? + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb new file mode 100644 index 0000000000..b39cd2282f --- /dev/null +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -0,0 +1,110 @@ +module ActiveRecord + module Tasks # :nodoc: + class MySQLDatabaseTasks # :nodoc: + + DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8' + DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci' + ACCESS_DENIED_ERROR = 1045 + + delegate :connection, :establish_connection, to: ActiveRecord::Base + + def initialize(configuration) + @configuration = configuration + end + + def create + establish_connection configuration_without_database + connection.create_database configuration['database'], creation_options + establish_connection configuration + rescue error_class => error + raise error unless error.errno == ACCESS_DENIED_ERROR + + $stdout.print error.error + establish_connection root_configuration_without_database + connection.create_database configuration['database'], creation_options + connection.execute grant_statement.gsub(/\s+/, ' ').strip + establish_connection configuration + rescue error_class => error + $stderr.puts error.error + $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" + $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['charset'] + end + + def drop + establish_connection configuration + connection.drop_database configuration['database'] + end + + def purge + establish_connection :test + connection.recreate_database configuration['database'], creation_options + end + + def charset + connection.charset + end + + def structure_dump(filename) + establish_connection configuration + File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } + end + + def structure_load(filename) + establish_connection(configuration) + connection.execute('SET foreign_key_checks = 0') + IO.read(filename).split("\n\n").each do |table| + connection.execute(table) + end + end + + private + + def configuration + @configuration + end + + def configuration_without_database + configuration.merge('database' => nil) + end + + def creation_options + { + charset: (configuration['charset'] || DEFAULT_CHARSET), + collation: (configuration['collation'] || DEFAULT_COLLATION) + } + end + + def error_class + case configuration['adapter'] + when /jdbc/ + require 'active_record/railties/jdbcmysql_error' + ArJdbcMySQL::Error + when /mysql2/ + Mysql2::Error + else + Mysql::Error + end + end + + def grant_statement + <<-SQL +GRANT ALL PRIVILEGES ON #{configuration['database']}.* + TO '#{configuration['username']}'@'localhost' +IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; + SQL + end + + def root_configuration_without_database + configuration_without_database.merge( + 'username' => 'root', + 'password' => root_password + ) + end + + def root_password + $stdout.print "Please provide the root password for your mysql installation\n>" + $stdin.gets.strip + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb new file mode 100644 index 0000000000..a210392e53 --- /dev/null +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -0,0 +1,81 @@ +require 'shellwords' + +module ActiveRecord + module Tasks # :nodoc: + class PostgreSQLDatabaseTasks # :nodoc: + + DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8' + + delegate :connection, :establish_connection, :clear_active_connections!, + to: ActiveRecord::Base + + def initialize(configuration) + @configuration = configuration + end + + def create(master_established = false) + establish_master_connection unless master_established + connection.create_database configuration['database'], + configuration.merge('encoding' => encoding) + establish_connection configuration + end + + def drop + establish_master_connection + connection.drop_database configuration['database'] + end + + def charset + connection.encoding + end + + def purge + clear_active_connections! + drop + create true + end + + def structure_dump(filename) + set_psql_env + search_path = configuration['schema_search_path'] + unless search_path.blank? + search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") + end + + command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" + raise 'Error dumping database' unless Kernel.system(command) + + File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } + end + + def structure_load(filename) + set_psql_env + Kernel.system("psql -f #{filename} #{configuration['database']}") + end + + private + + def configuration + @configuration + end + + def encoding + configuration['encoding'] || DEFAULT_ENCODING + end + + def establish_master_connection + establish_connection configuration.merge( + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + end + + def set_psql_env + ENV['PGHOST'] = configuration['host'] if configuration['host'] + ENV['PGPORT'] = configuration['port'].to_s if configuration['port'] + ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] + ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb new file mode 100644 index 0000000000..da01058a82 --- /dev/null +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -0,0 +1,55 @@ +module ActiveRecord + module Tasks # :nodoc: + class SQLiteDatabaseTasks # :nodoc: + + delegate :connection, :establish_connection, to: ActiveRecord::Base + + def initialize(configuration, root = Rails.root) + @configuration, @root = configuration, root + end + + def create + if File.exist?(configuration['database']) + $stderr.puts "#{configuration['database']} already exists" + return + end + + establish_connection configuration + connection + end + + def drop + require 'pathname' + path = Pathname.new configuration['database'] + file = path.absolute? ? path.to_s : File.join(root, path) + + FileUtils.rm(file) if File.exist?(file) + end + alias :purge :drop + + def charset + connection.encoding + end + + def structure_dump(filename) + dbfile = configuration['database'] + `sqlite3 #{dbfile} .schema > #{filename}` + end + + def structure_load(filename) + dbfile = configuration['database'] + `sqlite3 #{dbfile} < "#{filename}"` + end + + private + + def configuration + @configuration + end + + def root + @root + end + end + end +end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index fcaa4b74a6..c7a6c37d50 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -8,7 +8,7 @@ module ActiveRecord # Defines some test assertions to test against SQL queries. class TestCase < ActiveSupport::TestCase #:nodoc: def teardown - SQLCounter.log.clear + SQLCounter.clear_log end def assert_date_from_db(expected, actual, message = nil) @@ -22,47 +22,57 @@ module ActiveRecord end def assert_sql(*patterns_to_match) - SQLCounter.log = [] + SQLCounter.clear_log yield - SQLCounter.log + SQLCounter.log_all ensure failed_patterns = [] patterns_to_match.each do |pattern| - failed_patterns << pattern unless SQLCounter.log.any?{ |sql| pattern === sql } + failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql } end assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" end - def assert_queries(num = 1) - SQLCounter.log = [] + def assert_queries(num = 1, options = {}) + ignore_none = options.fetch(:ignore_none) { num == :any } + SQLCounter.clear_log yield ensure - assert_equal num, SQLCounter.log.size, "#{SQLCounter.log.size} instead of #{num} queries were executed.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" + the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log + if num == :any + assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed." + else + mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}" + assert_equal num, the_log.size, mesg + end end def assert_no_queries(&block) - prev_ignored_sql = SQLCounter.ignored_sql - SQLCounter.ignored_sql = [] - assert_queries(0, &block) - ensure - SQLCounter.ignored_sql = prev_ignored_sql + assert_queries(0, :ignore_none => true, &block) end end class SQLCounter class << self - attr_accessor :ignored_sql, :log + attr_accessor :ignored_sql, :log, :log_all + def clear_log; self.log = []; self.log_all = []; end end - self.log = [] + self.clear_log self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/] # FIXME: this needs to be refactored so specific database can add their own - # ignored SQL. This ignored SQL is for Oracle. - ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] - + # ignored SQL, or better yet, use a different notification for the queries + # instead examining the SQL content. + oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] + mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/] + postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im] + + [oracle_ignored, mysql_ignored, postgresql_ignored].each do |db_ignored_sql| + ignored_sql.concat db_ignored_sql + end attr_reader :ignore @@ -75,8 +85,10 @@ module ActiveRecord # FIXME: this seems bad. we should probably have a better way to indicate # the query was cached - return if 'CACHE' == values[:name] || ignore =~ sql - self.class.log << sql + return if 'CACHE' == values[:name] + + self.class.log_all << sql + self.class.log << sql unless ignore =~ sql end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index c717fdea47..e5b7a6bfba 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,6 +1,11 @@ require 'active_support/core_ext/class/attribute' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :record_timestamps, instance_accessor: false + self.record_timestamps = true + end + # = Active Record Timestamp # # Active Record automatically timestamps create and update operations if the @@ -33,8 +38,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :record_timestamps, :instance_writer => true - self.record_timestamps = true + config_attribute :record_timestamps, instance_writer: true end def initialize_dup(other) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 30e1035300..9cb9b4627b 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -329,7 +329,8 @@ module ActiveRecord @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 restore_state = remove_instance_variable(:@_start_transaction_state) - @attributes = @attributes.dup if @attributes.frozen? + was_frozen = @attributes.frozen? + @attributes = @attributes.dup if was_frozen @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] if restore_state.has_key?(:id) @@ -338,6 +339,7 @@ module ActiveRecord @attributes.delete(self.class.primary_key) @attributes_cache.delete(self.class.primary_key) end + @attributes.freeze if was_frozen end end end diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 69dfd2503e..1199be68eb 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -1,6 +1,6 @@ module ActiveRecord - class Base - def self.fake_connection(config) + module ConnectionHandling + def fake_connection(config) ConnectionAdapters::FakeAdapter.new nil, logger end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 684c7f5929..276c499276 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -4,6 +4,13 @@ class MysqlConnectionTest < ActiveRecord::TestCase def setup super @connection = ActiveRecord::Model.connection + @connection.extend(LogIntercepter) + @connection.intercepted = true + end + + def teardown + @connection.intercepted = false + @connection.logged = [] end def test_no_automatic_reconnection_after_timeout @@ -45,6 +52,26 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_logs_name_structure_dump + @connection.structure_dump + assert_equal "SCHEMA", @connection.logged[0][1] + assert_equal "SCHEMA", @connection.logged[2][1] + end + + def test_logs_name_show_variable + @connection.show_variable 'foo' + assert_equal "SCHEMA", @connection.logged[0][1] + end + + def test_logs_name_rename_column_sql + @connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))" + @connection.logged = [] + @connection.send(:rename_column_sql, 'bar_baz', 'foo', 'foo2') + assert_equal "SCHEMA", @connection.logged[0][1] + ensure + @connection.execute "DROP TABLE `bar_baz`" + end + private def run_without_connection diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 4baec749ff..f6e168caf2 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -8,12 +8,23 @@ module ActiveRecord def setup super @connection = ActiveRecord::Base.connection + @connection.extend(LogIntercepter) + @connection.intercepted = true + end + + def teardown + @connection.intercepted = false + @connection.logged = [] end def test_encoding assert_not_nil @connection.encoding end + def test_default_client_min_messages + assert_equal "warning", @connection.client_min_messages + end + # Ensure, we can set connection params using the example of Generic # Query Optimizer (geqo). It is 'on' per default. def test_connection_options @@ -25,5 +36,42 @@ module ActiveRecord expect = NonExistentTable.connection.query('show geqo').first.first assert_equal 'off', expect end + + def test_tables_logs_name + @connection.tables('hello') + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_indexes_logs_name + @connection.indexes('items', 'hello') + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_table_exists_logs_name + @connection.table_exists?('items') + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_table_alias_length_logs_name + @connection.instance_variable_set("@table_alias_length", nil) + @connection.table_alias_length + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_current_database_logs_name + @connection.current_database + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_encoding_logs_name + @connection.encoding + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_schema_names_logs_name + @connection.schema_names + assert_equal 'SCHEMA', @connection.logged[0][1] + end + end end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 34660577da..a4d9286d52 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -27,6 +27,9 @@ end class PostgresqlTimestampWithZone < ActiveRecord::Base end +class PostgresqlUUID < ActiveRecord::Base +end + class PostgresqlDataTypeTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -61,6 +64,9 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase @first_oid = PostgresqlOid.find(1) @connection.execute("INSERT INTO postgresql_timestamp_with_zones (time) VALUES ('2010-01-01 10:00:00-1')") + + @connection.execute("INSERT INTO postgresql_uuids (guid, compact_guid) VALUES('d96c3da0-96c1-012f-1316-64ce8f32c6d8', 'f06c715096c1012f131764ce8f32c6d8')") + @first_uuid = PostgresqlUUID.find(1) end def test_data_type_of_array_types @@ -100,6 +106,10 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type end + def test_data_type_of_uuid_types + assert_equal :uuid, @first_uuid.column_for_attribute(:guid).type + end + def test_array_values assert_equal '{35000,21000,18000,17000}', @first_array.commission_by_quarter assert_equal '{foo,bar,baz}', @first_array.nicknames @@ -143,6 +153,11 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address end + def test_uuid_values + assert_equal 'd96c3da0-96c1-012f-1316-64ce8f32c6d8', @first_uuid.guid + assert_equal 'f06c7150-96c1-012f-1317-64ce8f32c6d8', @first_uuid.compact_guid + end + def test_bit_string_values assert_equal '00010101', @first_bit_string.bit_string assert_equal '00010101', @first_bit_string.bit_string_varying diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 8a7f44d0a3..5e947799cc 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -20,6 +20,14 @@ module ActiveRecord number integer ) eosql + + @conn.extend(LogIntercepter) + @conn.intercepted = true + end + + def teardown + @conn.intercepted = false + @conn.logged = [] end def test_column_types @@ -232,13 +240,23 @@ module ActiveRecord end def test_tables_logs_name - name = "hello" - assert_logged [[name, []]] do - @conn.tables(name) + assert_logged [['SCHEMA', []]] do + @conn.tables('hello') assert_not_nil @conn.logged.first.shift end end + def test_indexes_logs_name + assert_logged [["PRAGMA index_list(\"items\")", 'SCHEMA', []]] do + @conn.indexes('items', 'hello') + end + end + + def test_table_exists_logs_name + assert @conn.table_exists?('items') + assert_equal 'SCHEMA', @conn.logged[0][1] + end + def test_columns columns = @conn.columns('items').sort_by { |x| x.name } assert_equal 2, columns.length @@ -274,7 +292,6 @@ module ActiveRecord end def test_indexes_logs - intercept_logs_on @conn assert_difference('@conn.logged.length') do @conn.indexes('items') end @@ -326,21 +343,10 @@ module ActiveRecord private def assert_logged logs - intercept_logs_on @conn yield assert_equal logs, @conn.logged end - def intercept_logs_on ctx - @conn.extend(Module.new { - attr_accessor :logged - def log sql, name, binds = [] - @logged << [sql, name, binds] - yield - end - }) - @conn.logged = [] - end end end end diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb deleted file mode 100644 index 5bd8f76ba2..0000000000 --- a/activerecord/test/cases/aggregations_test.rb +++ /dev/null @@ -1,158 +0,0 @@ -require "cases/helper" -require 'models/customer' -require 'active_support/core_ext/exception' - -class AggregationsTest < ActiveRecord::TestCase - fixtures :customers - - def test_find_single_value_object - assert_equal 50, customers(:david).balance.amount - assert_kind_of Money, customers(:david).balance - assert_equal 300, customers(:david).balance.exchange_to("DKK").amount - end - - def test_find_multiple_value_object - assert_equal customers(:david).address_street, customers(:david).address.street - assert( - customers(:david).address.close_to?(Address.new("Different Street", customers(:david).address_city, customers(:david).address_country)) - ) - end - - def test_change_single_value_object - customers(:david).balance = Money.new(100) - customers(:david).save - assert_equal 100, customers(:david).reload.balance.amount - end - - def test_immutable_value_objects - customers(:david).balance = Money.new(100) - assert_raise(ActiveSupport::FrozenObjectError) { customers(:david).balance.instance_eval { @amount = 20 } } - end - - def test_inferred_mapping - assert_equal "35.544623640962634", customers(:david).gps_location.latitude - assert_equal "-105.9309951055148", customers(:david).gps_location.longitude - - customers(:david).gps_location = GpsLocation.new("39x-110") - - assert_equal "39", customers(:david).gps_location.latitude - assert_equal "-110", customers(:david).gps_location.longitude - - customers(:david).save - - customers(:david).reload - - assert_equal "39", customers(:david).gps_location.latitude - assert_equal "-110", customers(:david).gps_location.longitude - end - - def test_reloaded_instance_refreshes_aggregations - assert_equal "35.544623640962634", customers(:david).gps_location.latitude - assert_equal "-105.9309951055148", customers(:david).gps_location.longitude - - Customer.update_all("gps_location = '24x113'") - customers(:david).reload - assert_equal '24x113', customers(:david)['gps_location'] - - assert_equal GpsLocation.new('24x113'), customers(:david).gps_location - end - - def test_gps_equality - assert_equal GpsLocation.new('39x110'), GpsLocation.new('39x110') - end - - def test_gps_inequality - assert_not_equal GpsLocation.new('39x110'), GpsLocation.new('39x111') - end - - def test_allow_nil_gps_is_nil - assert_nil customers(:zaphod).gps_location - end - - def test_allow_nil_gps_set_to_nil - customers(:david).gps_location = nil - customers(:david).save - customers(:david).reload - assert_nil customers(:david).gps_location - end - - def test_allow_nil_set_address_attributes_to_nil - customers(:zaphod).address = nil - assert_nil customers(:zaphod).attributes[:address_street] - assert_nil customers(:zaphod).attributes[:address_city] - assert_nil customers(:zaphod).attributes[:address_country] - end - - def test_allow_nil_address_set_to_nil - customers(:zaphod).address = nil - customers(:zaphod).save - customers(:zaphod).reload - assert_nil customers(:zaphod).address - end - - def test_nil_raises_error_when_allow_nil_is_false - assert_raise(NoMethodError) { customers(:david).balance = nil } - end - - def test_allow_nil_address_loaded_when_only_some_attributes_are_nil - customers(:zaphod).address_street = nil - customers(:zaphod).save - customers(:zaphod).reload - assert_kind_of Address, customers(:zaphod).address - assert_nil customers(:zaphod).address.street - end - - def test_nil_assignment_results_in_nil - customers(:david).gps_location = GpsLocation.new('39x111') - assert_not_nil customers(:david).gps_location - customers(:david).gps_location = nil - assert_nil customers(:david).gps_location - end - - def test_nil_return_from_converter_is_respected_when_allow_nil_is_true - customers(:david).non_blank_gps_location = "" - customers(:david).save - customers(:david).reload - assert_nil customers(:david).non_blank_gps_location - end - - def test_nil_return_from_converter_results_in_failure_when_allow_nil_is_false - assert_raises(NoMethodError) do - customers(:barney).gps_location = "" - end - end - - def test_do_not_run_the_converter_when_nil_was_set - customers(:david).non_blank_gps_location = nil - assert_nil Customer.gps_conversion_was_run - end - - def test_custom_constructor - assert_equal 'Barney GUMBLE', customers(:barney).fullname.to_s - assert_kind_of Fullname, customers(:barney).fullname - end - - def test_custom_converter - customers(:barney).fullname = 'Barnoit Gumbleau' - assert_equal 'Barnoit GUMBLEAU', customers(:barney).fullname.to_s - assert_kind_of Fullname, customers(:barney).fullname - end -end - -class OverridingAggregationsTest < ActiveRecord::TestCase - class Name; end - class DifferentName; end - - class Person < ActiveRecord::Base - composed_of :composed_of, :mapping => %w(person_first_name first_name) - end - - class DifferentPerson < Person - composed_of :composed_of, :class_name => 'DifferentName', :mapping => %w(different_person_first_name first_name) - end - - def test_composed_of_aggregation_redefinition_reflections_should_differ_and_not_inherited - assert_not_equal Person.reflect_on_aggregation(:composed_of), - DifferentPerson.reflect_on_aggregation(:composed_of) - end -end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 08467900f9..31b11ee334 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -29,11 +29,6 @@ class EagerAssociationTest < ActiveRecord::TestCase :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books, :developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors - def setup - # preheat table existence caches - Comment.find_by_id(1) - end - def test_eager_with_has_one_through_join_model_with_conditions_on_the_through member = Member.scoped(:includes => :favourite_club).find(members(:some_other_guy).id) assert_nil member.favourite_club @@ -983,7 +978,6 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.scoped(:includes => :author, :joins => {:taggings => {:tag => :taggings}}, :where => "taggings_tags.super_tag_id=2", :order => 'posts.id').all end assert_equal posts(:welcome, :thinking), posts - end def test_eager_loading_with_conditions_on_string_joined_table_preloads @@ -998,7 +992,6 @@ class EagerAssociationTest < ActiveRecord::TestCase end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} - end def test_eager_loading_with_select_on_joined_table_preloads @@ -1010,8 +1003,6 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_loading_with_conditions_on_join_model_preloads - Author.columns - authors = assert_queries(2) do Author.scoped(:includes => :author_address, :joins => :comments, :where => "posts.title like 'Welcome%'").all end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index ed1caa2ef5..9d693bae0c 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 @@ -817,11 +817,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase # clear cache possibly created by other tests david.projects.reset_column_information - assert_queries(1) { david.projects.columns; david.projects.columns } + assert_queries(:any) { david.projects.columns } + assert_no_queries { david.projects.columns } ## and again to verify that reset_column_information clears the cache correctly david.projects.reset_column_information - assert_queries(1) { david.projects.columns; david.projects.columns } + + assert_queries(:any) { david.projects.columns } + assert_no_queries { 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 8b384c2513..b8481175b2 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -936,10 +936,24 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, summit.client_of end - def test_deleting_type_mismatch + def test_deleting_by_fixnum_id david = Developer.find(1) - david.projects.reload - assert_raise(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) } + + assert_difference 'david.projects.count', -1 do + assert_equal 1, david.projects.delete(1).size + end + + assert_equal 1, david.projects.size + end + + def test_deleting_by_string_id + david = Developer.find(1) + + assert_difference 'david.projects.count', -1 do + assert_equal 1, david.projects.delete('1').size + end + + assert_equal 1, david.projects.size end def test_deleting_self_type_mismatch @@ -1344,7 +1358,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal author.essays, Essay.where(writer_id: "David") - end def test_has_many_custom_primary_key @@ -1424,7 +1437,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase :group => "#{Namespaced::Firm.table_name}.id" ).find firm.id assert_equal 1, stats.num_clients.to_i - ensure ActiveRecord::Base.store_full_sti_class = old end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index ecc676f300..783b83631c 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -578,7 +578,27 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_deleting_junk_from_has_many_through_should_raise_type_mismatch - assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete("Uhh what now?") } + assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete(Object.new) } + end + + def test_deleting_by_fixnum_id_from_has_many_through + post = posts(:thinking) + + assert_difference 'post.tags.count', -1 do + assert_equal 1, post.tags.delete(1).size + end + + assert_equal 0, post.tags.size + end + + def test_deleting_by_string_id_from_has_many_through + post = posts(:thinking) + + assert_difference 'post.tags.count', -1 do + assert_equal 1, post.tags.delete('1').size + end + + assert_equal 0, post.tags.size end def test_has_many_through_sum_uses_calculations diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 98c38535a6..f4c40b8b97 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -15,6 +15,7 @@ module ActiveRecord def self.active_record_super; Base; end def self.base_class; self; end + extend ActiveRecord::Configuration include ActiveRecord::AttributeMethods def self.define_attribute_methods diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 619fb881fa..2fee553cef 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -888,22 +888,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on end - def test_multiparameter_assignment_of_aggregation - customer = Customer.new - address = Address.new("The Street", "The City", "The Country") - attributes = { "address(1)" => address.street, "address(2)" => address.city, "address(3)" => address.country } - customer.attributes = attributes - assert_equal address, customer.address - end - - def test_multiparameter_assignment_of_aggregation_out_of_order - customer = Customer.new - address = Address.new("The Street", "The City", "The Country") - attributes = { "address(3)" => address.country, "address(2)" => address.city, "address(1)" => address.street } - customer.attributes = attributes - assert_equal address, customer.address - end - def test_multiparameter_assignment_of_aggregation_with_missing_values ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do customer = Customer.new @@ -914,14 +898,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal("address", ex.errors[0].attribute) end - def test_multiparameter_assignment_of_aggregation_with_blank_values - customer = Customer.new - address = Address.new("The Street", "The City", "The Country") - attributes = { "address(1)" => "", "address(2)" => address.city, "address(3)" => address.country } - customer.attributes = attributes - assert_equal Address.new(nil, "The City", "The Country"), customer.address - end - def test_multiparameter_assignment_of_aggregation_with_large_index ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do customer = Customer.new @@ -1021,26 +997,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal("c", duped_topic.title) end - def test_dup_with_aggregate_of_same_name_as_attribute - dev = DeveloperWithAggregate.find(1) - assert_kind_of DeveloperSalary, dev.salary - - dup = nil - assert_nothing_raised { dup = dev.dup } - assert_kind_of DeveloperSalary, dup.salary - assert_equal dev.salary.amount, dup.salary.amount - assert !dup.persisted? - - # test if the attributes have been dupd - original_amount = dup.salary.amount - dev.salary.amount = 1 - assert_equal original_amount, dup.salary.amount - - assert dup.save - assert dup.persisted? - assert_not_equal dup.id, dev.id - end - def test_dup_does_not_copy_associations author = authors(:david) assert_not_equal [], author.posts @@ -1309,6 +1265,15 @@ class BasicsTest < ActiveRecord::TestCase assert_equal({ :foo => :bar }, t.content_before_type_cast) end + def test_serialized_attribute_calling_dup_method + klass = Class.new(ActiveRecord::Base) + klass.table_name = "topics" + klass.serialize :content, JSON + + t = klass.new(:content => { :foo => :bar }).dup + assert_equal({ :foo => :bar }, t.content_before_type_cast) + end + def test_serialized_attribute_declared_in_subclass hash = { 'important1' => 'value1', 'important2' => 'value2' } important_topic = ImportantTopic.create("important" => hash) @@ -1514,6 +1479,8 @@ class BasicsTest < ActiveRecord::TestCase after_seq = Joke.sequence_name assert_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil? + ensure + Joke.reset_sequence_name end def test_dont_clear_inheritnce_column_when_setting_explicitly @@ -1926,7 +1893,7 @@ class BasicsTest < ActiveRecord::TestCase def test_cache_key_format_for_existing_record_with_nil_updated_at dev = Developer.first - dev.update_attribute(:updated_at, nil) + dev.update_column(:updated_at, nil) assert_match(/\/#{dev.id}$/, dev.cache_key) end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 041f8ffb7c..f748b897ee 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -376,6 +376,22 @@ class CalculationsTest < ActiveRecord::TestCase Company.where(:type => "Firm").from('companies').count(:type) end + def test_count_with_block_acts_as_array + accounts = Account.where('id > 0') + assert_equal Account.count, accounts.count { true } + assert_equal 0, accounts.count { false } + assert_equal Account.where('credit_limit > 50').size, accounts.count { |account| account.credit_limit > 50 } + assert_equal Account.count, Account.count { true } + assert_equal 0, Account.count { false } + end + + def test_sum_with_block_acts_as_array + accounts = Account.where('id > 0') + assert_equal Account.sum(:credit_limit), accounts.sum { |account| account.credit_limit } + assert_equal Account.sum(:credit_limit) + Account.count, accounts.sum{ |account| account.credit_limit + 1 } + assert_equal 0, accounts.sum { |account| 0 } + end + def test_sum_with_from_option assert_equal Account.sum(:credit_limit), Account.from('accounts').sum(:credit_limit) assert_equal Account.where("credit_limit > 50").sum(:credit_limit), @@ -404,6 +420,40 @@ class CalculationsTest < ActiveRecord::TestCase Account.where("credit_limit > 50").from('accounts').maximum(:credit_limit) end + def test_maximum_with_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + + # TODO: Investigate why PG isn't being typecast + if current_adapter?(:PostgreSQLAdapter) + assert_equal "7", Company.includes(:contracts).maximum(:developer_id) + else + assert_equal 7, Company.includes(:contracts).maximum(:developer_id) + end + end + + def test_minimum_with_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + + # TODO: Investigate why PG isn't being typecast + if current_adapter?(:PostgreSQLAdapter) + assert_equal "7", Company.includes(:contracts).minimum(:developer_id) + else + assert_equal 7, Company.includes(:contracts).minimum(:developer_id) + end + end + + def test_sum_with_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + + # TODO: Investigate why PG isn't being typecast + if current_adapter?(:MysqlAdapter) || current_adapter?(:PostgreSQLAdapter) + assert_equal "7", Company.includes(:contracts).sum(:developer_id) + else + assert_equal 7, Company.includes(:contracts).sum(:developer_id) + end + end + + def test_from_option_with_specified_index if Edge.connection.adapter_name == 'MySQL' or Edge.connection.adapter_name == 'Mysql2' assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all) @@ -461,6 +511,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [c.id], Company.joins(:contracts).pluck(:id) end + def test_pluck_if_table_included + c = Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + assert_equal [c.id], Company.includes(:contracts).where("contracts.id" => c.contracts.first).pluck(:id) + end + def test_pluck_not_auto_table_name_prefix_if_column_joined Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) assert_equal [7], Company.joins(:contracts).pluck(:developer_id) @@ -484,4 +539,11 @@ class CalculationsTest < ActiveRecord::TestCase def test_plucks_with_ids assert_equal Company.all.map(&:id).sort, Company.ids.sort end + + def test_pluck_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + ids = Company.includes(:contracts).pluck(:developer_id) + assert_equal Company.count, ids.length + assert_equal [7], ids.compact + end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 7690769226..deeef3a3fd 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -426,11 +426,13 @@ class CallbacksTest < ActiveRecord::TestCase def test_before_destroy_returning_false david = ImmutableDeveloper.find(1) assert !david.destroy + assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackCancellationDeveloper.find(1) someone.cancel_before_destroy = true assert !someone.destroy + assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } assert !someone.after_destroy_called end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index a44b49466f..bd2fbaa7db 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -136,12 +136,6 @@ module ActiveRecord smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint") assert_equal :integer, smallint_column.type end - - def test_uuid_column_should_map_to_string - oid = PostgreSQLAdapter::OID::Identity.new - uuid_column = PostgreSQLColumn.new('unique_id', nil, oid, "uuid") - assert_equal :string, uuid_column.type - 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 dc99ac665c..17cb447105 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -7,12 +7,11 @@ module ActiveRecord @handler = ConnectionHandler.new @handler.establish_connection 'america', Base.connection_pool.spec @klass = Class.new do + include Model::Tag def self.name; 'america'; end - class << self - alias active_record_super superclass - end end @subklass = Class.new(@klass) do + include Model::Tag def self.name; 'north america'; end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 6871e628aa..8287b35aaf 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -124,7 +124,7 @@ module ActiveRecord @pool.checkout @pool.checkout @pool.checkout - @pool.timeout = 0 + @pool.dead_connection_timeout = 0 connections = @pool.connections.dup @@ -137,7 +137,7 @@ module ActiveRecord @pool.checkout @pool.checkout @pool.checkout - @pool.timeout = 0 + @pool.dead_connection_timeout = 0 connections = @pool.connections.dup connections.each do |conn| @@ -213,13 +213,8 @@ module ActiveRecord # Thus this test prepares waiting threads and then trickles in # available connections slowly, ensuring the wakeup order is # correct in this case. - # - # Try a few times since it might work out just by chance. def test_checkout_fairness - 4.times { setup; do_checkout_fairness } - end - - def do_checkout_fairness + @pool.instance_variable_set(:@size, 10) expected = (1..@pool.size).to_a.freeze # check out all connections so our threads start out waiting conns = expected.map { @pool.checkout } @@ -230,7 +225,7 @@ module ActiveRecord threads = expected.map do |i| t = Thread.new { begin - conn = @pool.checkout # never checked back in + @pool.checkout # never checked back in mutex.synchronize { order << i } rescue => e mutex.synchronize { errors << e } @@ -256,13 +251,7 @@ module ActiveRecord # group2. Enough connections are checked in to wakeup all # group1 threads, and the fact that only group1 and no group2 # threads acquired a connection is enforced. - # - # Try a few times since it might work out just by chance. def test_checkout_fairness_by_group - 4.times { setup; do_checkout_fairness_by_group } - end - - def do_checkout_fairness_by_group @pool.instance_variable_set(:@size, 10) # take all the connections conns = (1..10).map { @pool.checkout } @@ -273,7 +262,7 @@ module ActiveRecord make_thread = proc do |i| t = Thread.new { begin - conn = @pool.checkout # never checked back in + @pool.checkout # never checked back in mutex.synchronize { successes << i } rescue => e mutex.synchronize { errors << e } diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index e6cb1b9521..673a2b2b88 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -9,6 +9,7 @@ module ActiveRecord end def test_url_host_no_db + skip "only if mysql is available" unless defined?(MysqlAdapter) spec = resolve 'mysql://foo?encoding=utf8' assert_equal({ :adapter => "mysql", @@ -18,6 +19,7 @@ module ActiveRecord end def test_url_host_db + skip "only if mysql is available" unless defined?(MysqlAdapter) spec = resolve 'mysql://foo/bar?encoding=utf8' assert_equal({ :adapter => "mysql", @@ -27,6 +29,7 @@ module ActiveRecord end def test_url_port + skip "only if mysql is available" unless defined?(MysqlAdapter) spec = resolve 'mysql://foo:123?encoding=utf8' assert_equal({ :adapter => "mysql", diff --git a/activerecord/test/cases/database_tasks_test.rb b/activerecord/test/cases/database_tasks_test.rb new file mode 100644 index 0000000000..5f36b2c841 --- /dev/null +++ b/activerecord/test/cases/database_tasks_test.rb @@ -0,0 +1,275 @@ +require 'cases/helper' + +module ActiveRecord + module DatabaseTasksSetupper + def setup + @mysql_tasks, @postgresql_tasks, @sqlite_tasks = stub, stub, stub + ActiveRecord::Tasks::MySQLDatabaseTasks.stubs(:new).returns @mysql_tasks + ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stubs(:new).returns @postgresql_tasks + ActiveRecord::Tasks::SQLiteDatabaseTasks.stubs(:new).returns @sqlite_tasks + end + end + + ADAPTERS_TASKS = { + :mysql => :mysql_tasks, + :mysql2 => :mysql_tasks, + :postgresql => :postgresql_tasks, + :sqlite3 => :sqlite_tasks + } + + class DatabaseTasksCreateTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_create") do + eval("@#{v}").expects(:create) + ActiveRecord::Tasks::DatabaseTasks.create 'adapter' => k + end + end + end + + class DatabaseTasksCreateAllTest < ActiveRecord::TestCase + def setup + @configurations = {'development' => {'database' => 'my-db'}} + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_ignores_configurations_without_databases + @configurations['development'].merge!('database' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:create).never + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_ignores_remote_databases + @configurations['development'].merge!('host' => 'my.server.tld') + $stderr.stubs(:puts).returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:create).never + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_warning_for_remote_databases + @configurations['development'].merge!('host' => 'my.server.tld') + + $stderr.expects(:puts).with('This task only modifies local databases. my-db is on a remote host.') + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_creates_configurations_with_local_ip + @configurations['development'].merge!('host' => '127.0.0.1') + + ActiveRecord::Tasks::DatabaseTasks.expects(:create) + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_creates_configurations_with_local_host + @configurations['development'].merge!('host' => 'localhost') + + ActiveRecord::Tasks::DatabaseTasks.expects(:create) + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_creates_configurations_with_blank_hosts + @configurations['development'].merge!('host' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:create) + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end + + class DatabaseTasksCreateCurrentTest < ActiveRecord::TestCase + def setup + @configurations = { + 'development' => {'database' => 'dev-db'}, + 'test' => {'database' => 'test-db'}, + 'production' => {'database' => 'prod-db'} + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_creates_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'prod-db') + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new('production') + ) + end + + def test_creates_test_database_when_environment_is_database + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'dev-db') + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'test-db') + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new('development') + ) + end + + def test_establishes_connection_for_the_given_environment + ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true + + ActiveRecord::Base.expects(:establish_connection).with('development') + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new('development') + ) + end + end + + class DatabaseTasksDropTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_drop") do + eval("@#{v}").expects(:drop) + ActiveRecord::Tasks::DatabaseTasks.drop 'adapter' => k + end + end + end + + class DatabaseTasksDropAllTest < ActiveRecord::TestCase + def setup + @configurations = {:development => {'database' => 'my-db'}} + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_ignores_configurations_without_databases + @configurations[:development].merge!('database' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_ignores_remote_databases + @configurations[:development].merge!('host' => 'my.server.tld') + $stderr.stubs(:puts).returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_warning_for_remote_databases + @configurations[:development].merge!('host' => 'my.server.tld') + + $stderr.expects(:puts).with('This task only modifies local databases. my-db is on a remote host.') + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_creates_configurations_with_local_ip + @configurations[:development].merge!('host' => '127.0.0.1') + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop) + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_creates_configurations_with_local_host + @configurations[:development].merge!('host' => 'localhost') + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop) + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_creates_configurations_with_blank_hosts + @configurations[:development].merge!('host' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop) + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end + + class DatabaseTasksDropCurrentTest < ActiveRecord::TestCase + def setup + @configurations = { + 'development' => {'database' => 'dev-db'}, + 'test' => {'database' => 'test-db'}, + 'production' => {'database' => 'prod-db'} + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_creates_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'prod-db') + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new('production') + ) + end + + def test_creates_test_database_when_environment_is_database + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'dev-db') + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'test-db') + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new('development') + ) + end + end + + + class DatabaseTasksPurgeTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_purge") do + eval("@#{v}").expects(:purge) + ActiveRecord::Tasks::DatabaseTasks.purge 'adapter' => k + end + end + end + + class DatabaseTasksCharsetTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_charset") do + eval("@#{v}").expects(:charset) + ActiveRecord::Tasks::DatabaseTasks.charset 'adapter' => k + end + end + end + + class DatabaseTasksStructureDumpTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_structure_dump") do + eval("@#{v}").expects(:structure_dump).with("awesome-file.sql") + ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => k}, "awesome-file.sql") + end + end + end + + class DatabaseTasksStructureLoadTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_structure_load") do + eval("@#{v}").expects(:structure_load).with("awesome-file.sql") + ActiveRecord::Tasks::DatabaseTasks.structure_load({'adapter' => k}, "awesome-file.sql") + end + end + end +end diff --git a/activerecord/test/cases/deprecated_dynamic_methods_test.rb b/activerecord/test/cases/deprecated_dynamic_methods_test.rb index 77a609f49a..7c108b983e 100644 --- a/activerecord/test/cases/deprecated_dynamic_methods_test.rb +++ b/activerecord/test/cases/deprecated_dynamic_methods_test.rb @@ -45,32 +45,6 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert_equal [], Topic.find_all_by_title("The First Topic!!") end - def test_find_all_by_one_attribute_that_is_an_aggregate - balance = customers(:david).balance - assert_kind_of Money, balance - found_customers = Customer.find_all_by_balance(balance) - assert_equal 1, found_customers.size - assert_equal customers(:david), found_customers.first - end - - def test_find_all_by_two_attributes_that_are_both_aggregates - balance = customers(:david).balance - address = customers(:david).address - assert_kind_of Money, balance - assert_kind_of Address, address - found_customers = Customer.find_all_by_balance_and_address(balance, address) - assert_equal 1, found_customers.size - assert_equal customers(:david), found_customers.first - end - - def test_find_all_by_two_attributes_with_one_being_an_aggregate - balance = customers(:david).balance - assert_kind_of Money, balance - found_customers = Customer.find_all_by_balance_and_name(balance, customers(:david).name) - assert_equal 1, found_customers.size - assert_equal customers(:david), found_customers.first - end - def test_find_all_by_one_attribute_with_options topics = Topic.find_all_by_content("Have a nice day", :order => "id DESC") assert_equal topics(:first), topics.last @@ -137,14 +111,6 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert_equal 17, sig38.firm_id end - def test_find_or_create_from_two_attributes_with_one_being_an_aggregate - number_of_customers = Customer.count - created_customer = Customer.find_or_create_by_balance_and_name(Money.new(123), "Elizabeth") - assert_equal number_of_customers + 1, Customer.count - assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123), "Elizabeth") - assert created_customer.persisted? - end - def test_find_or_create_from_one_attribute_and_hash number_of_companies = Company.count sig38 = Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) @@ -167,38 +133,12 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert_equal 23, sig38.client_of end - def test_find_or_create_from_one_aggregate_attribute - number_of_customers = Customer.count - created_customer = Customer.find_or_create_by_balance(Money.new(123)) - assert_equal number_of_customers + 1, Customer.count - assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123)) - assert created_customer.persisted? - end - - def test_find_or_create_from_one_aggregate_attribute_and_hash - number_of_customers = Customer.count - balance = Money.new(123) - name = "Elizabeth" - created_customer = Customer.find_or_create_by_balance({:balance => balance, :name => name}) - assert_equal number_of_customers + 1, Customer.count - assert_equal created_customer, Customer.find_or_create_by_balance({:balance => balance, :name => name}) - assert created_customer.persisted? - assert_equal balance, created_customer.balance - assert_equal name, created_customer.name - end - def test_find_or_initialize_from_one_attribute sig38 = Company.find_or_initialize_by_name("38signals") assert_equal "38signals", sig38.name assert !sig38.persisted? end - def test_find_or_initialize_from_one_aggregate_attribute - new_customer = Customer.find_or_initialize_by_balance(Money.new(123)) - assert_equal 123, new_customer.balance.amount - assert !new_customer.persisted? - end - def test_find_or_initialize_from_one_attribute_should_not_set_attribute_even_when_protected c = Company.find_or_initialize_by_name({:name => "Fortune 1000", :rating => 1000}) assert_equal "Fortune 1000", c.name @@ -285,13 +225,6 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert_raise(ArgumentError) { Topic.find_or_initialize_by_title_and_author_name("Another topic") } end - def test_find_or_initialize_from_one_aggregate_attribute_and_one_not - new_customer = Customer.find_or_initialize_by_balance_and_name(Money.new(123), "Elizabeth") - assert_equal 123, new_customer.balance.amount - assert_equal "Elizabeth", new_customer.name - assert !new_customer.persisted? - end - def test_find_or_initialize_from_one_attribute_and_hash sig38 = Company.find_or_initialize_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) assert_equal "38signals", sig38.name @@ -300,15 +233,6 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert !sig38.persisted? end - def test_find_or_initialize_from_one_aggregate_attribute_and_hash - balance = Money.new(123) - name = "Elizabeth" - new_customer = Customer.find_or_initialize_by_balance({:balance => balance, :name => name}) - assert_equal balance, new_customer.balance - assert_equal name, new_customer.name - assert !new_customer.persisted? - end - def test_find_last_by_one_attribute assert_equal Topic.last, Topic.find_last_by_title(Topic.last.title) assert_nil Topic.find_last_by_title("A title with no matches") diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 2650040a80..97ffc068af 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -79,6 +79,17 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_setting_time_attributes_with_time_zone_field_to_itself_should_not_be_marked_as_a_change + in_time_zone 'Paris' do + target = Class.new(ActiveRecord::Base) + target.table_name = 'pirates' + + pirate = target.create + pirate.created_on = pirate.created_on + assert !pirate.created_on_changed? + end + end + def test_time_attributes_changes_without_time_zone_by_skip in_time_zone 'Paris' do target = Class.new(ActiveRecord::Base) @@ -485,16 +496,6 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes['updated_on'][1] assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') - - pirate = Pirate.find_by_catchphrase("Ahoy!") - pirate.update_attribute(:catchphrase, "Ninjas suck!") - - assert_equal 2, pirate.previous_changes.size - assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes['catchphrase'] - assert_not_nil pirate.previous_changes['updated_on'][0] - assert_not_nil pirate.previous_changes['updated_on'][1] - assert !pirate.previous_changes.key?('parrot_id') - assert !pirate.previous_changes.key?('created_on') end if ActiveRecord::Base.connection.supports_migrations? diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index e118add44c..91e1df91cd 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -6,14 +6,14 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_if_available_queries_for_explain_is_nil with_queries(nil) do - SUBSCRIBER.call + SUBSCRIBER.finish(nil, nil, {}) assert_nil Thread.current[:available_queries_for_explain] end end def test_collects_nothing_if_the_payload_has_an_exception with_queries([]) do |queries| - SUBSCRIBER.call(:exception => Exception.new) + SUBSCRIBER.finish(nil, nil, :exception => Exception.new) assert queries.empty? end end @@ -21,7 +21,7 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_for_ignored_payloads with_queries([]) do |queries| ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip| - SUBSCRIBER.call(:name => ip) + SUBSCRIBER.finish(nil, nil, :name => ip) end assert queries.empty? end @@ -31,7 +31,7 @@ if ActiveRecord::Base.connection.supports_explain? sql = 'select 1 from users' binds = [1, 2] with_queries([]) do |queries| - SUBSCRIBER.call(:name => 'SQL', :sql => sql, :binds => binds) + SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => sql, :binds => binds) assert_equal 1, queries.size assert_equal sql, queries[0][0] assert_equal binds, queries[0][1] @@ -45,4 +45,4 @@ if ActiveRecord::Base.connection.supports_explain? Thread.current[:available_queries_for_explain] = nil end end -end
\ No newline at end of file +end diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 810c1500cc..eedbaac3bd 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -80,7 +80,6 @@ class FinderRespondToTest < ActiveRecord::TestCase private def ensure_topic_method_is_not_cached(method_id) - class << Topic; self; end.send(:remove_method, method_id) if Topic.public_methods.any? { |m| m.to_s == method_id.to_s } + class << Topic; self; end.send(:remove_method, method_id) if Topic.public_methods.include? method_id end - end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index f7ecab28ce..df21585961 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -45,6 +45,12 @@ class FinderTest < ActiveRecord::TestCase assert_raise(NoMethodError) { Topic.exists?([1,2]) } end + def test_exists_does_not_select_columns_without_alias + assert_sql(/SELECT\W+1 AS one FROM ["`]topics["`]/i) do + Topic.exists? + end + end + def test_exists_returns_true_with_one_record_and_no_args assert Topic.exists? end @@ -63,24 +69,14 @@ class FinderTest < ActiveRecord::TestCase assert Topic.order(:id).uniq.exists? end - def test_does_not_exist_with_empty_table_and_no_args_given - Topic.delete_all - assert !Topic.exists? + def test_exists_with_includes_limit_and_empty_result + assert !Topic.includes(:replies).limit(0).exists? + assert !Topic.includes(:replies).limit(1).where('0 = 1').exists? end - def test_exists_with_aggregate_having_three_mappings - existing_address = customers(:david).address - assert Customer.exists?(:address => existing_address) - end - - def test_exists_with_aggregate_having_three_mappings_with_one_difference - existing_address = customers(:david).address - assert !Customer.exists?(:address => - Address.new(existing_address.street, existing_address.city, existing_address.country + "1")) - assert !Customer.exists?(:address => - Address.new(existing_address.street, existing_address.city + "1", existing_address.country)) - assert !Customer.exists?(:address => - Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) + def test_exists_with_empty_table_and_no_args_given + Topic.delete_all + assert !Topic.exists? end def test_exists_does_not_instantiate_records @@ -301,14 +297,6 @@ class FinderTest < ActiveRecord::TestCase assert_equal companies(:rails_core), firms.first end - def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate - david = customers(:david) - assert Customer.scoped(:where => { 'customers.name' => david.name, :address => david.address }).find(david.id) - assert_raise(ActiveRecord::RecordNotFound) { - Customer.scoped(:where => { 'customers.name' => david.name + "1", :address => david.address }).find(david.id) - } - end - def test_find_on_association_proxy_conditions assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10, 12], Comment.where(post_id: authors(:david).posts).map(&:id).sort end @@ -383,48 +371,6 @@ class FinderTest < ActiveRecord::TestCase assert_nil topic.last_read end - def test_hash_condition_find_with_aggregate_having_one_mapping - balance = customers(:david).balance - assert_kind_of Money, balance - found_customer = Customer.scoped(:where => {:balance => balance}).first - assert_equal customers(:david), found_customer - end - - def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate - gps_location = customers(:david).gps_location - assert_kind_of GpsLocation, gps_location - found_customer = Customer.scoped(:where => {:gps_location => gps_location}).first - assert_equal customers(:david), found_customer - end - - def test_hash_condition_find_with_aggregate_having_one_mapping_and_key_value_being_attribute_value - balance = customers(:david).balance - assert_kind_of Money, balance - found_customer = Customer.scoped(:where => {:balance => balance.amount}).first - assert_equal customers(:david), found_customer - end - - def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_attribute_value - gps_location = customers(:david).gps_location - assert_kind_of GpsLocation, gps_location - found_customer = Customer.scoped(:where => {:gps_location => gps_location.gps_location}).first - assert_equal customers(:david), found_customer - end - - def test_hash_condition_find_with_aggregate_having_three_mappings - address = customers(:david).address - assert_kind_of Address, address - found_customer = Customer.scoped(:where => {:address => address}).first - assert_equal customers(:david), found_customer - end - - def test_hash_condition_find_with_one_condition_being_aggregate_and_another_not - address = customers(:david).address - assert_kind_of Address, address - found_customer = Customer.scoped(:where => {:address => address, :name => customers(:david).name}).first - assert_equal customers(:david), found_customer - end - def test_condition_utc_time_interpolation_with_default_timezone_local with_env_tz 'America/New_York' do with_active_record_default_timezone :local do @@ -593,43 +539,9 @@ class FinderTest < ActiveRecord::TestCase assert_equal accounts(:rails_core_account), Account.where('firm_id = ?', 6).find_by_credit_limit(50) end - def test_find_by_one_attribute_that_is_an_aggregate - address = customers(:david).address - assert_kind_of Address, address - found_customer = Customer.find_by_address(address) - assert_equal customers(:david), found_customer - end - - def test_find_by_one_attribute_that_is_an_aggregate_with_one_attribute_difference - address = customers(:david).address - assert_kind_of Address, address - missing_address = Address.new(address.street, address.city, address.country + "1") - assert_nil Customer.find_by_address(missing_address) - missing_address = Address.new(address.street, address.city + "1", address.country) - assert_nil Customer.find_by_address(missing_address) - missing_address = Address.new(address.street + "1", address.city, address.country) - assert_nil Customer.find_by_address(missing_address) - end - - def test_find_by_two_attributes_that_are_both_aggregates - balance = customers(:david).balance - address = customers(:david).address - assert_kind_of Money, balance - assert_kind_of Address, address - found_customer = Customer.find_by_balance_and_address(balance, address) - assert_equal customers(:david), found_customer - end - - def test_find_by_two_attributes_with_one_being_an_aggregate - balance = customers(:david).balance - assert_kind_of Money, balance - found_customer = Customer.find_by_balance_and_name(balance, customers(:david).name) - assert_equal customers(:david), found_customer - end - def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching # ensure this test can run independently of order - class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' } + class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.include?(:find_by_credit_limit) a = Account.where('firm_id = ?', 6).find_by_credit_limit(50) assert_equal a, Account.where('firm_id = ?', 6).find_by_credit_limit(50) # find_by_credit_limit has been cached end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 37fa13f771..afff020561 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -121,3 +121,19 @@ class << Time @now = nil end end + +module LogIntercepter + attr_accessor :logged, :intercepted + def self.extended(base) + base.logged = [] + end + def log(sql, name, binds = [], &block) + if @intercepted + @logged << [sql, name, binds] + yield + else + super(sql, name,binds, &block) + end + end +end + diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb index 2f98d3c646..73a01906b9 100644 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -95,7 +95,11 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase end def test_mass_assigning_does_not_choke_on_nil - Firm.new.assign_attributes(nil) + assert_nil Firm.new.assign_attributes(nil) + end + + def test_mass_assigning_does_not_choke_on_empty_hash + assert_nil Firm.new.assign_attributes({}) end def test_assign_attributes_uses_default_role_when_no_role_is_provided @@ -247,6 +251,65 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase assert !Task.new.respond_to?("#{method}=") end end + + test "ActiveRecord::Model.whitelist_attributes works for models which include Model" do + begin + prev, ActiveRecord::Model.whitelist_attributes = ActiveRecord::Model.whitelist_attributes, true + + klass = Class.new { include ActiveRecord::Model } + assert_equal ActiveModel::MassAssignmentSecurity::WhiteList, klass.active_authorizers[:default].class + assert_equal [], klass.active_authorizers[:default].to_a + ensure + ActiveRecord::Model.whitelist_attributes = prev + end + end + + test "ActiveRecord::Model.whitelist_attributes works for models which inherit Base" do + begin + prev, ActiveRecord::Model.whitelist_attributes = ActiveRecord::Model.whitelist_attributes, true + + klass = Class.new(ActiveRecord::Base) + assert_equal ActiveModel::MassAssignmentSecurity::WhiteList, klass.active_authorizers[:default].class + assert_equal [], klass.active_authorizers[:default].to_a + + klass.attr_accessible 'foo' + assert_equal ['foo'], Class.new(klass).active_authorizers[:default].to_a + ensure + ActiveRecord::Model.whitelist_attributes = prev + end + end + + test "ActiveRecord::Model.mass_assignment_sanitizer works for models which include Model" do + begin + sanitizer = Object.new + prev, ActiveRecord::Model.mass_assignment_sanitizer = ActiveRecord::Model.mass_assignment_sanitizer, sanitizer + + klass = Class.new { include ActiveRecord::Model } + assert_equal sanitizer, klass._mass_assignment_sanitizer + + ActiveRecord::Model.mass_assignment_sanitizer = nil + klass = Class.new { include ActiveRecord::Model } + assert_not_nil klass._mass_assignment_sanitizer + ensure + ActiveRecord::Model.mass_assignment_sanitizer = prev + end + end + + test "ActiveRecord::Model.mass_assignment_sanitizer works for models which inherit Base" do + begin + sanitizer = Object.new + prev, ActiveRecord::Model.mass_assignment_sanitizer = ActiveRecord::Model.mass_assignment_sanitizer, sanitizer + + klass = Class.new(ActiveRecord::Base) + assert_equal sanitizer, klass._mass_assignment_sanitizer + + sanitizer2 = Object.new + klass.mass_assignment_sanitizer = sanitizer2 + assert_equal sanitizer2, Class.new(klass)._mass_assignment_sanitizer + ensure + ActiveRecord::Model.mass_assignment_sanitizer = prev + end + end end @@ -878,4 +941,26 @@ class MassAssignmentSecurityNestedAttributesTest < ActiveRecord::TestCase assert_all_attributes(person.best_friends.first) end + def test_mass_assignment_options_are_reset_after_exception + person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin) + person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin) + + attributes = { :best_friend_attributes => { :comments => 'rides a sweet bike' } } + assert_raises(RuntimeError) { person.assign_attributes(attributes, :as => :admin) } + assert_equal 'm', person.best_friend.gender + + person.best_friend_attributes = { :gender => 'f' } + assert_equal 'm', person.best_friend.gender + end + + def test_mass_assignment_options_are_nested_correctly + person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin) + person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin) + + attributes = { :best_friend_first_name => 'Josh', :best_friend_attributes => { :gender => 'f' } } + person.assign_attributes(attributes, :as => :admin) + assert_equal 'Josh', person.best_friend.first_name + assert_equal 'f', person.best_friend.gender + end + end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 18f8d82bfe..3014bbe273 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -174,7 +174,7 @@ module ActiveRecord 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_match(/\A-05:00\Z/, bob.moment_of_truth.zone) assert_equal DateTime::ITALY, bob.moment_of_truth.start end end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index 8ab1c59724..264a99f9ce 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -51,6 +51,8 @@ module ActiveRecord end def test_creates_polymorphic_index + return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter + connection.create_table table_name do |t| t.references :foo, :polymorphic => true, :index => true end @@ -86,6 +88,7 @@ module ActiveRecord end def test_creates_polymorphic_index_for_existing_table + return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter connection.create_table table_name connection.change_table table_name do |t| t.references :foo, :polymorphic => true, :index => true diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 5d1bad0d54..3c0d2b18d9 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -56,6 +56,21 @@ class MigrationTest < ActiveRecord::TestCase Person.reset_column_information end + def test_migrator_versions + migrations_path = MIGRATIONS_ROOT + "/valid" + ActiveRecord::Migrator.migrations_paths = migrations_path + + ActiveRecord::Migrator.up(migrations_path) + assert_equal 3, ActiveRecord::Migrator.current_version + assert_equal 3, ActiveRecord::Migrator.last_version + assert_equal false, ActiveRecord::Migrator.needs_migration? + + ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid") + assert_equal 0, ActiveRecord::Migrator.current_version + assert_equal 3, ActiveRecord::Migrator.last_version + assert_equal true, ActiveRecord::Migrator.needs_migration? + end + def test_create_table_with_force_true_does_not_drop_nonexisting_table if Person.connection.table_exists?(:testings2) Person.connection.drop_table :testings2 @@ -523,7 +538,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? # 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 + assert_queries(3, :ignore_none => true) do with_bulk_change_table do |t| t.change :name, :string, :default => 'NONAME' t.change :birthdate, :datetime diff --git a/activerecord/test/cases/mysql_rake_test.rb b/activerecord/test/cases/mysql_rake_test.rb new file mode 100644 index 0000000000..42a11b0171 --- /dev/null +++ b/activerecord/test/cases/mysql_rake_test.rb @@ -0,0 +1,246 @@ +require 'cases/helper' +require 'mysql' + +module ActiveRecord + class MysqlDBCreateTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_without_database + ActiveRecord::Base.expects(:establish_connection). + with('adapter' => 'mysql', 'database' => nil) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_default_options + @connection.expects(:create_database). + with('my-app-db', {:charset => 'utf8', :collation => 'utf8_unicode_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_given_options + @connection.expects(:create_database). + with('my-app-db', {:charset => 'latin', :collation => 'latin_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge( + 'charset' => 'latin', 'collation' => 'latin_ci' + ) + end + + def test_establishes_connection_to_database + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end + + class MysqlDBCreateAsRootTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true, :execute => true) + @error = Mysql::Error.new "Invalid permissions" + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'wossname' + } + + $stdin.stubs(:gets).returns("secret\n") + $stdout.stubs(:print).returns(nil) + @error.stubs(:errno).returns(1045) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).raises(@error).then. + returns(true) + end + + def test_root_password_is_requested + $stdin.expects(:gets).returns("secret\n") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_connection_established_as_root + ActiveRecord::Base.expects(:establish_connection).with({ + 'adapter' => 'mysql', + 'database' => nil, + 'username' => 'root', + 'password' => 'secret' + }) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_database_created_by_root + @connection.expects(:create_database). + with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_grant_privileges_for_normal_user + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON my-app-db.* TO 'pat'@'localhost' IDENTIFIED BY 'wossname' WITH GRANT OPTION;") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_connection_established_as_normal_user + ActiveRecord::Base.expects(:establish_connection).returns do + ActiveRecord::Base.expects(:establish_connection).with({ + 'adapter' => 'mysql', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'secret' + }) + + raise @error + end + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_sends_output_to_stderr_when_other_errors + @error.stubs(:errno).returns(42) + + $stderr.expects(:puts).at_least_once.returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end + + class MySQLDBDropTest < ActiveRecord::TestCase + def setup + @connection = stub(:drop_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_mysql_database + ActiveRecord::Base.expects(:establish_connection).with @configuration + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + + def test_drops_database + @connection.expects(:drop_database).with('my-app-db') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + end + + class MySQLPurgeTest < ActiveRecord::TestCase + def setup + @connection = stub(:recreate_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'test-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_test_database + ActiveRecord::Base.expects(:establish_connection).with(:test) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_recreates_database_with_the_default_options + @connection.expects(:recreate_database). + with('test-db', {:charset => 'utf8', :collation => 'utf8_unicode_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_recreates_database_with_the_given_options + @connection.expects(:recreate_database). + with('test-db', {:charset => 'latin', :collation => 'latin_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge( + 'charset' => 'latin', 'collation' => 'latin_ci' + ) + end + end + + class MysqlDBCharsetTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_charset + @connection.expects(:charset) + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + end + + class MySQLStructureDumpTest < ActiveRecord::TestCase + def setup + @connection = stub(:structure_dump => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'test-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_structure_dump + filename = "awesome-file.sql" + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + @connection.expects(:structure_dump) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + assert File.exists?(filename) + ensure + FileUtils.rm(filename) + end + end + + class MySQLStructureLoadTest < ActiveRecord::TestCase + def setup + @connection = stub + @configuration = { + 'adapter' => 'mysql', + 'database' => 'test-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_structure_load + filename = "awesome-file.sql" + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + @connection.expects(:execute).twice + + open(filename, 'w') { |f| f.puts("SELECT CURDATE();") } + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + ensure + FileUtils.rm(filename) + end + end + +end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index bf825c002a..c886160af3 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -19,7 +19,6 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_found_items_are_cached - Topic.columns all_posts = Topic.base assert_queries(1) do @@ -46,6 +45,15 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count) end + def test_method_missing_priority_when_delegating + klazz = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + scope :since, Proc.new { where('written_on >= ?', Time.now - 1.day) } + scope :to, Proc.new { where('written_on <= ?', Time.now) } + end + assert_equal klazz.to.since.all, klazz.since.to.all + end + def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy assert Topic.approved.respond_to?(:limit) assert Topic.approved.respond_to?(:count) diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 0559bbbe9a..3daa033ed0 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -781,6 +781,16 @@ module NestedAttributesOnACollectionAssociationTests assert_nothing_raised(NoMethodError) { @pirate.save! } end + def test_numeric_colum_changes_from_zero_to_no_empty_string + Man.accepts_nested_attributes_for(:interests) + Interest.validates_numericality_of(:zine_id) + man = Man.create(:name => 'John') + interest = man.interests.create(:topic=>'bar',:zine_id => 0) + assert interest.save + + assert !man.update_attributes({:interests_attributes => { :id => interest.id, :zine_id => 'foo' }}) + end + private def association_setter diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 0933a4ff3d..b7b77b24af 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -305,6 +305,13 @@ class PersistencesTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } end + def test_destroy! + topic = Topic.find(1) + assert_equal topic, topic.destroy!, 'topic.destroy! did not return self' + assert topic.frozen?, 'topic not frozen after destroy!' + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } + end + def test_record_not_found_exception assert_raise(ActiveRecord::RecordNotFound) { Topic.find(99999) } end @@ -364,71 +371,10 @@ class PersistencesTest < ActiveRecord::TestCase assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" } end - def test_update_attribute - assert !Topic.find(1).approved? - Topic.find(1).update_attribute("approved", true) - assert Topic.find(1).approved? - - Topic.find(1).update_attribute(:approved, false) - assert !Topic.find(1).approved? - end - def test_update_attribute_does_not_choke_on_nil assert Topic.find(1).update_attributes(nil) end - def test_update_attribute_for_readonly_attribute - minivan = Minivan.find('m1') - assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') } - end - - # This test is correct, but it is hard to fix it since - # update_attribute trigger simply call save! that triggers - # all callbacks. - # def test_update_attribute_with_one_changed_and_one_updated - # t = Topic.order('id').limit(1).first - # title, author_name = t.title, t.author_name - # t.author_name = 'John' - # t.update_attribute(:title, 'super_title') - # assert_equal 'John', t.author_name - # assert_equal 'super_title', t.title - # assert t.changed?, "topic should have changed" - # assert t.author_name_changed?, "author_name should have changed" - # assert !t.title_changed?, "title should not have changed" - # assert_nil t.title_change, 'title change should be nil' - # assert_equal ['author_name'], t.changed - # - # t.reload - # assert_equal 'David', t.author_name - # assert_equal 'super_title', t.title - # end - - def test_update_attribute_with_one_updated - t = Topic.first - t.update_attribute(:title, 'super_title') - assert_equal 'super_title', t.title - assert !t.changed?, "topic should not have changed" - assert !t.title_changed?, "title should not have changed" - assert_nil t.title_change, 'title change should be nil' - - t.reload - assert_equal 'super_title', t.title - end - - def test_update_attribute_for_updated_at_on - developer = Developer.find(1) - prev_month = Time.now.prev_month - - developer.update_attribute(:updated_at, prev_month) - assert_equal prev_month, developer.updated_at - - developer.update_attribute(:salary, 80001) - assert_not_equal prev_month, developer.updated_at - - developer.reload - assert_not_equal prev_month, developer.updated_at - end - def test_update_column topic = Topic.find(1) topic.update_column("approved", true) @@ -460,7 +406,7 @@ class PersistencesTest < ActiveRecord::TestCase def test_update_column_should_not_leave_the_object_dirty topic = Topic.find(1) - topic.update_attribute("content", "Have a nice day") + topic.update_column("content", "Have a nice day") topic.reload topic.update_column(:content, "You too") diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index fba3006ebe..0a6354f5cc 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -17,7 +17,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase end def checkout_connections - ActiveRecord::Model.establish_connection(@connection.merge({:pool => 2, :wait_timeout => 0.3})) + ActiveRecord::Model.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.3})) @connections = [] @timed_out = 0 @@ -34,7 +34,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase # Will deadlock due to lack of Monitor timeouts in 1.9 def checkout_checkin_connections(pool_size, threads) - ActiveRecord::Model.establish_connection(@connection.merge({:pool => pool_size, :wait_timeout => 0.5})) + ActiveRecord::Model.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5})) @connection_count = 0 @timed_out = 0 threads.times do diff --git a/activerecord/test/cases/postgresql_rake_test.rb b/activerecord/test/cases/postgresql_rake_test.rb new file mode 100644 index 0000000000..e8769bd4df --- /dev/null +++ b/activerecord/test/cases/postgresql_rake_test.rb @@ -0,0 +1,200 @@ +require 'cases/helper' + +module ActiveRecord + class PostgreSQLDBCreateTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_postgresql_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'postgresql', + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_default_encoding + @connection.expects(:create_database). + with('my-app-db', @configuration.merge('encoding' => 'utf8')) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_given_encoding + @connection.expects(:create_database). + with('my-app-db', @configuration.merge('encoding' => 'latin')) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration. + merge('encoding' => 'latin') + end + + def test_establishes_connection_to_new_database + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_db_create_with_error_prints_message + ActiveRecord::Base.stubs(:establish_connection).raises(Exception) + + $stderr.stubs(:puts).returns(true) + $stderr.expects(:puts). + with("Couldn't create database for #{@configuration.inspect}") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end + + class PostgreSQLDBDropTest < ActiveRecord::TestCase + def setup + @connection = stub(:drop_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_postgresql_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'postgresql', + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + + def test_drops_database + @connection.expects(:drop_database).with('my-app-db') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + end + + class PostgreSQLPurgeTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true, :drop_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:clear_active_connections!).returns(true) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_clears_active_connections + ActiveRecord::Base.expects(:clear_active_connections!) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_establishes_connection_to_postgresql_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'postgresql', + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_drops_database + @connection.expects(:drop_database).with('my-app-db') + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_creates_database + @connection.expects(:create_database). + with('my-app-db', @configuration.merge('encoding' => 'utf8')) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_establishes_connection + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end + + class PostgreSQLDBCharsetTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_charset + @connection.expects(:encoding) + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + end + + class PostgreSQLStructureDumpTest < ActiveRecord::TestCase + def setup + @connection = stub(:structure_dump => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + Kernel.stubs(:system) + end + + def test_structure_dump + filename = "awesome-file.sql" + Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{filename} my-app-db").returns(true) + @connection.expects(:schema_search_path).returns("foo") + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + assert File.exists?(filename) + ensure + FileUtils.rm(filename) + end + end + + class PostgreSQLStructureLoadTest < ActiveRecord::TestCase + def setup + @connection = stub + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + Kernel.stubs(:system) + end + + def test_structure_dump + filename = "awesome-file.sql" + Kernel.expects(:system).with("psql -f #{filename} my-app-db") + + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + end + +end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index a712e5f689..08f655d7fa 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -151,16 +151,11 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_cache_does_not_wrap_string_results_in_arrays - if current_adapter?(:SQLite3Adapter) - require 'sqlite3/version' - sqlite3_version = RUBY_PLATFORM =~ /java/ ? Jdbc::SQLite3::VERSION : SQLite3::VERSION - end - Task.cache do # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter) && sqlite3_version > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) + elsif current_adapter?(:SQLite3Adapter) || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index 576ab60090..e53a27d5dd 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -64,7 +64,7 @@ module ActiveRecord spec.config[:reaping_frequency] = 0.0001 pool = ConnectionPool.new spec - pool.timeout = 0 + pool.dead_connection_timeout = 0 conn = pool.checkout count = pool.connections.length diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 7dd5698dcf..bac3376c9f 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -83,30 +83,6 @@ class ReflectionTest < ActiveRecord::TestCase end end - def test_aggregation_reflection - reflection_for_address = AggregateReflection.new( - :composed_of, :address, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer - ) - - reflection_for_balance = AggregateReflection.new( - :composed_of, :balance, { :class_name => "Money", :mapping => %w(balance amount) }, Customer - ) - - reflection_for_gps_location = AggregateReflection.new( - :composed_of, :gps_location, { }, Customer - ) - - assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location) - assert Customer.reflect_on_all_aggregations.include?(reflection_for_balance) - assert Customer.reflect_on_all_aggregations.include?(reflection_for_address) - - assert_equal reflection_for_address, Customer.reflect_on_aggregation(:address) - - assert_equal Address, Customer.reflect_on_aggregation(:address).klass - - assert_equal Money, Customer.reflect_on_aggregation(:balance).klass - end - def test_reflect_on_all_autosave_associations expected = Pirate.reflect_on_all_associations.select { |r| r.options[:autosave] } received = Pirate.reflect_on_all_autosave_associations diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb new file mode 100644 index 0000000000..90c690e266 --- /dev/null +++ b/activerecord/test/cases/relation/where_test.rb @@ -0,0 +1,19 @@ +require "cases/helper" +require 'models/post' + +module ActiveRecord + class WhereTest < ActiveRecord::TestCase + fixtures :posts + + def test_where_error + assert_raises(ActiveRecord::StatementInvalid) do + Post.where(:id => { 'posts.author_id' => 10 }).first + end + end + + def test_where_with_table_name + post = Post.first + assert_equal post, Post.where(:posts => { 'id' => post.id }).first + end + end +end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 2dc8f0053b..6c5bee7382 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -226,7 +226,6 @@ class RelationTest < ActiveRecord::TestCase assert_no_queries do assert_equal [], Developer.none assert_equal [], Developer.scoped.none - assert Developer.none.is_a?(ActiveRecord::NullRelation) end end @@ -236,6 +235,12 @@ class RelationTest < ActiveRecord::TestCase end end + def test_none_chainable_to_existing_scope_extension_method + assert_no_queries do + assert_equal 1, Topic.anonymous_extension.none.one + end + end + def test_none_chained_to_methods_firing_queries_straight_to_db assert_no_queries do assert_equal [], Developer.none.pluck(:id) # => uses select_all @@ -690,6 +695,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal 1, comments.count end + def test_relation_merging_with_association + assert_queries(2) do # one for loading post, and another one merged query + post = Post.where(:body => 'Such a lovely day').first + comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments) + assert_equal 1, comments.count + end + end + def test_count posts = Post.scoped diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index ab80dd1d6d..01dd25a9df 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -257,6 +257,13 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dump_includes_uuid_shorthand_definition + output = standard_dump + if %r{create_table "poistgresql_uuids"} =~ output + assert_match %r{t.uuid "guid"}, output + end + end + def test_schema_dump_includes_hstores_shorthand_definition output = standard_dump if %r{create_table "postgresql_hstores"} =~ output @@ -294,4 +301,36 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump assert_match %r{create_table "subscribers", :id => false}, output end + + class CreateDogMigration < ActiveRecord::Migration + def up + create_table("dogs") do |t| + t.column :name, :string + end + add_index "dogs", [:name] + end + def down + drop_table("dogs") + end + end + + def test_schema_dump_with_table_name_prefix_and_suffix + original, $stdout = $stdout, StringIO.new + ActiveRecord::Base.table_name_prefix = 'foo_' + ActiveRecord::Base.table_name_suffix = '_bar' + + migration = CreateDogMigration.new + migration.migrate(:up) + + output = standard_dump + assert_no_match %r{create_table "foo_.+_bar"}, output + assert_no_match %r{create_index "foo_.+_bar"}, output + assert_no_match %r{create_table "schema_migrations"}, output + ensure + migration.migrate(:down) + + ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' + $stdout = original + end + end diff --git a/activerecord/test/cases/sqlite_rake_test.rb b/activerecord/test/cases/sqlite_rake_test.rb new file mode 100644 index 0000000000..b5557fc953 --- /dev/null +++ b/activerecord/test/cases/sqlite_rake_test.rb @@ -0,0 +1,170 @@ +require 'cases/helper' +require 'pathname' + +module ActiveRecord + class SqliteDBCreateTest < ActiveRecord::TestCase + def setup + @database = 'db_create.sqlite3' + @connection = stub :connection + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + + File.stubs(:exist?).returns(false) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_checks_database_exists + File.expects(:exist?).with(@database).returns(false) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_when_file_exists + File.stubs(:exist?).returns(true) + + $stderr.expects(:puts).with("#{@database} already exists") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_with_file_does_nothing + File.stubs(:exist?).returns(true) + $stderr.stubs(:puts).returns(nil) + + ActiveRecord::Base.expects(:establish_connection).never + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_establishes_a_connection + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_with_error_prints_message + ActiveRecord::Base.stubs(:establish_connection).raises(Exception) + + $stderr.stubs(:puts).returns(true) + $stderr.expects(:puts). + with("Couldn't create database for #{@configuration.inspect}") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + end + + class SqliteDBDropTest < ActiveRecord::TestCase + def setup + @database = "db_create.sqlite3" + @path = stub(:to_s => '/absolute/path', :absolute? => true) + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + + Pathname.stubs(:new).returns(@path) + File.stubs(:join).returns('/former/relative/path') + FileUtils.stubs(:rm).returns(true) + end + + def test_creates_path_from_database + Pathname.expects(:new).with(@database).returns(@path) + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + + def test_removes_file_with_absolute_path + File.stubs(:exist?).returns(true) + @path.stubs(:absolute?).returns(true) + + FileUtils.expects(:rm).with('/absolute/path') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + + def test_generates_absolute_path_with_given_root + @path.stubs(:absolute?).returns(false) + + File.expects(:join).with('/rails/root', @path). + returns('/former/relative/path') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + + def test_removes_file_with_relative_path + File.stubs(:exist?).returns(true) + @path.stubs(:absolute?).returns(false) + + FileUtils.expects(:rm).with('/former/relative/path') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + end + + class SqliteDBCharsetTest < ActiveRecord::TestCase + def setup + @database = 'db_create.sqlite3' + @connection = stub :connection + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + + File.stubs(:exist?).returns(false) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_charset + @connection.expects(:encoding) + ActiveRecord::Tasks::DatabaseTasks.charset @configuration, '/rails/root' + end + end + + class SqliteStructureDumpTest < ActiveRecord::TestCase + def setup + @database = "db_create.sqlite3" + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + end + + def test_structure_dump + dbfile = @database + filename = "awesome-file.sql" + + ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, '/rails/root' + assert File.exists?(dbfile) + assert File.exists?(filename) + ensure + FileUtils.rm(filename) + FileUtils.rm(dbfile) + end + end + + class SqliteStructureLoadTest < ActiveRecord::TestCase + def setup + @database = "db_create.sqlite3" + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + end + + def test_structure_load + dbfile = @database + filename = "awesome-file.sql" + + open(filename, 'w') { |f| f.puts("select datetime('now', 'localtime');") } + ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, '/rails/root' + assert File.exists?(dbfile) + ensure + FileUtils.rm(filename) + FileUtils.rm(dbfile) + end + end +end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index e1d0f1f799..e401dd4b12 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -3,15 +3,17 @@ require 'models/admin' require 'models/admin/user' class StoreTest < ActiveRecord::TestCase + fixtures :'admin/users' + setup do - @john = Admin::User.create(:name => 'John Doe', :color => 'black', :remember_login => true, :height => 'tall', :is_a_good_guy => true) + @john = Admin::User.create!(:name => 'John Doe', :color => 'black', :remember_login => true, :height => 'tall', :is_a_good_guy => true) end test "reading store attributes through accessors" do assert_equal 'black', @john.color assert_nil @john.homepage end - + test "writing store attributes through accessors" do @john.color = 'red' @john.homepage = '37signals.com' @@ -41,6 +43,41 @@ class StoreTest < ActiveRecord::TestCase assert_equal false, @john.remember_login end + test "preserve store attributes data in HashWithIndifferentAccess format without any conversion" do + @john.json_data = HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy') + @john.height = 'low' + assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess) + assert_equal 'low', @john.json_data[:height] + assert_equal 'low', @john.json_data['height'] + assert_equal 'heavy', @john.json_data[:weight] + assert_equal 'heavy', @john.json_data['weight'] + end + + test "convert store attributes from Hash to HashWithIndifferentAccess saving the data and access attributes indifferently" do + user = Admin::User.find_by_name('Jamis') + assert_equal 'symbol', user.settings[:symbol] + assert_equal 'symbol', user.settings['symbol'] + assert_equal 'string', user.settings[:string] + assert_equal 'string', user.settings['string'] + assert_equal true, user.settings.instance_of?(ActiveSupport::HashWithIndifferentAccess) + + user.height = 'low' + assert_equal 'symbol', user.settings[:symbol] + assert_equal 'symbol', user.settings['symbol'] + assert_equal 'string', user.settings[:string] + assert_equal 'string', user.settings['string'] + assert_equal true, user.settings.instance_of?(ActiveSupport::HashWithIndifferentAccess) + end + + test "convert store attributes from any format other than Hash or HashWithIndifferent access losing the data" do + @john.json_data = "somedata" + @john.height = 'low' + assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess) + assert_equal 'low', @john.json_data[:height] + assert_equal 'low', @john.json_data['height'] + assert_equal false, @john.json_data.delete_if { |k, v| k == 'height' }.any? + end + test "reading store attributes through accessors encoded with JSON" do assert_equal 'tall', @john.height assert_nil @john.weight @@ -74,4 +111,8 @@ class StoreTest < ActiveRecord::TestCase @john.is_a_good_guy = false assert_equal false, @john.is_a_good_guy end + + test "stored attributes are returned" do + assert_equal [:color, :homepage], Admin::User.stored_attributes[:settings] + end end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 447aa29ffe..7df6993b30 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -11,7 +11,7 @@ class TimestampTest < ActiveRecord::TestCase def setup @developer = Developer.first - @developer.update_attribute(:updated_at, Time.now.prev_month) + @developer.update_column(:updated_at, Time.now.prev_month) @previously_updated_at = @developer.updated_at end @@ -133,7 +133,7 @@ class TimestampTest < ActiveRecord::TestCase pet = Pet.first owner = pet.owner - owner.update_attribute(:happy_at, 3.days.ago) + owner.update_column(:happy_at, 3.days.ago) previously_owner_updated_at = owner.updated_at pet.name = "I'm a parrot" diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 203dd054f1..a9ccd00fac 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -362,6 +362,16 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_rollback_when_saving_a_frozen_record + topic = Topic.new(:title => 'test') + topic.freeze + e = assert_raise(RuntimeError) { topic.save } + assert_equal "can't modify frozen Hash", e.message + assert !topic.persisted?, 'not persisted' + assert_nil topic.id + assert topic.frozen?, 'not frozen' + end + def test_restore_active_record_state_for_all_records_in_a_transaction topic_1 = Topic.new(:title => 'test_1') topic_2 = Topic.new(:title => 'test_2') diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 88751a72f9..12373333b0 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -92,7 +92,7 @@ class DefaultXmlSerializationTest < ActiveRecord::TestCase end def test_should_serialize_datetime - assert_match %r{<created-at type=\"datetime\">2006-08-01T00:00:00Z</created-at>}, @xml + assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @xml end def test_should_serialize_boolean @@ -109,7 +109,7 @@ class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"datetime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml ensure Time.zone = timezone end @@ -118,7 +118,7 @@ class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"datetime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml ensure Time.zone = timezone end @@ -152,7 +152,7 @@ class NilXmlSerializationTest < ActiveRecord::TestCase assert %r{<created-at (.*)></created-at>}.match(@xml) attributes = $1 assert_match %r{nil="true"}, attributes - assert_match %r{type="datetime"}, attributes + assert_match %r{type="dateTime"}, attributes end def test_should_serialize_boolean @@ -188,7 +188,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text - assert_equal "datetime" , xml.elements["//written-on"].attributes['type'] + assert_equal "dateTime" , xml.elements["//written-on"].attributes['type'] assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text @@ -198,7 +198,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase if current_adapter?(:SybaseAdapter) assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text - assert_equal "datetime" , xml.elements["//last-read"].attributes['type'] + assert_equal "dateTime" , xml.elements["//last-read"].attributes['type'] else # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) assert_equal "2004-04-15", xml.elements["//last-read"].text @@ -211,7 +211,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert_equal "boolean" , xml.elements["//approved"].attributes['type'] assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text - assert_equal "datetime" , xml.elements["//bonus-time"].attributes['type'] + assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type'] end end diff --git a/activerecord/test/fixtures/admin/users.yml b/activerecord/test/fixtures/admin/users.yml index 6f11f2509e..e2884beda5 100644 --- a/activerecord/test/fixtures/admin/users.yml +++ b/activerecord/test/fixtures/admin/users.yml @@ -5,3 +5,6 @@ david: jamis: name: Jamis account: signals37 + settings: + :symbol: symbol + string: string diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 807f25b687..594c484f20 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -1,13 +1,5 @@ class Customer < ActiveRecord::Base - cattr_accessor :gps_conversion_was_run - - composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true - composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money } - composed_of :gps_location, :allow_nil => true - composed_of :non_blank_gps_location, :class_name => "GpsLocation", :allow_nil => true, :mapping => %w(gps_location gps_location), - :converter => lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps)} - composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse end class Address diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 83482f4d07..9fefa9ad3a 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -65,11 +65,6 @@ class AuditLog < ActiveRecord::Base end DeveloperSalary = Struct.new(:amount) -class DeveloperWithAggregate < ActiveRecord::Base - self.table_name = 'developers' - composed_of :salary, :class_name => 'DeveloperSalary', :mapping => [%w(salary amount)] -end - class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base self.table_name = 'developers' has_and_belongs_to_many :projects, :join_table => 'developers_projects', :foreign_key => 'developer_id' diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index d5c0b351aa..33cd6020a1 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -88,6 +88,25 @@ class TightDescendant < TightPerson; end class RichPerson < ActiveRecord::Base self.table_name = 'people' - + has_and_belongs_to_many :treasures, :join_table => 'peoples_treasures' end + +class NestedPerson < ActiveRecord::Base + self.table_name = 'people' + + attr_accessible :first_name, :best_friend_first_name, :best_friend_attributes + attr_accessible :first_name, :gender, :comments, :as => :admin + attr_accessible :best_friend_attributes, :best_friend_first_name, :as => :admin + + has_one :best_friend, :class_name => 'NestedPerson', :foreign_key => :best_friend_id + accepts_nested_attributes_for :best_friend, :update_only => true + + def comments=(new_comments) + raise RuntimeError + end + + def best_friend_first_name=(new_name) + assign_attributes({ :best_friend_attributes => { :first_name => new_name } }) + end +end
\ No newline at end of file diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index e51db50ae3..5f01f1fc50 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,6 +1,6 @@ ActiveRecord::Schema.define do - %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings + %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -59,6 +59,14 @@ _SQL _SQL execute <<_SQL + CREATE TABLE postgresql_uuids ( + id SERIAL PRIMARY KEY, + guid uuid, + compact_guid uuid + ); +_SQL + + execute <<_SQL CREATE TABLE postgresql_tsvectors ( id SERIAL PRIMARY KEY, text_vector tsvector |