diff options
Diffstat (limited to 'activerecord')
58 files changed, 713 insertions, 355 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 31886c8212..11fd0e7047 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,24 @@ ## Rails 4.0.0 (unreleased) ## +* It's not possible anymore to destroy a model marked as read only. + + *Johannes Barre* + + +* Added ability to ActiveRecord::Relation#from to accept other ActiveRecord::Relation objects + + Record.from(subquery) + Record.from(subquery, :a) + + *Radoslav Stankov* + +* Added custom coders support for ActiveRecord::Store. Now you can set + your custom coder like this: + + store :settings, accessors: [ :color, :homepage ], coder: JSON + + *Andrey Voronkov* + * `mysql` and `mysql2` connections will set `SQL_MODE=STRICT_ALL_TABLES` by default to avoid silent data loss. This can be disabled by specifying `strict: false` in your `database.yml`. @@ -626,7 +645,7 @@ has_one :account end - user.build_account{ |a| a.credit_limit => 100.0 } + user.build_account{ |a| a.credit_limit = 100.0 } The block is called after the instance has been initialized. *Andrew White* @@ -3999,7 +4018,7 @@ * Fixed that adding a record to a has_and_belongs_to collection would always save it -- now it only saves if its a new record #1203 *Alisdair McDiarmid* -* Fixed saving of in-memory association structures to happen as a after_create/after_update callback instead of after_save -- that way you can add new associations in after_create/after_update callbacks without getting them saved twice +* Fixed saving of in-memory association structures to happen as an after_create/after_update callback instead of after_save -- that way you can add new associations in after_create/after_update callbacks without getting them saved twice * Allow any Enumerable, not just Array, to work as bind variables #1344 *Jeremy Kemper* @@ -5512,7 +5531,7 @@ * Fixed that adding a record to a has_and_belongs_to collection would always save it -- now it only saves if its a new record #1203 *Alisdair McDiarmid* -* Fixed saving of in-memory association structures to happen as a after_create/after_update callback instead of after_save -- that way you can add new associations in after_create/after_update callbacks without getting them saved twice +* Fixed saving of in-memory association structures to happen as an after_create/after_update callback instead of after_save -- that way you can add new associations in after_create/after_update callbacks without getting them saved twice * Allow any Enumerable, not just Array, to work as bind variables #1344 *Jeremy Kemper* @@ -6434,7 +6453,7 @@ post.categories.push_with_attributes(category, :added_on => Date.today) post.categories.first.added_on # => Date.today - NOTE: The categories table doesn't have a added_on column, it's the categories_post join table that does! + NOTE: The categories table doesn't have an added_on column, it's the categories_post join table that does! * Fixed that :exclusively_dependent and :dependent can't be activated at the same time on has_many associations *Jeremy Kemper* diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index ed26b4899f..210820062b 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -22,7 +22,6 @@ #++ require 'active_support' -require 'active_support/i18n' require 'active_model' require 'arel' require 'active_record_deprecated_finders' @@ -145,4 +144,6 @@ ActiveSupport.on_load(:active_record) do Arel::Table.engine = self end -I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml' +ActiveSupport.on_load(:i18n) do + I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml' +end diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index a4db627535..c7a329d74d 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -71,7 +71,7 @@ module ActiveRecord # 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 any other attribute, though: + # 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 diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index c30e8e08b8..68f8bbeb1c 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -543,7 +543,7 @@ module ActiveRecord # end # # @group = Group.first - # @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group + # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group # @group.avatars # selects all avatars by going through the User join model. # # An important caveat with going through +has_one+ or +has_many+ associations on the diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 14aa557b6c..3af5ff3eab 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -16,12 +16,6 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: - attr_reader :proxy - - def initialize(owner, reflection) - super - @proxy = CollectionProxy.new(self) - end # Implements the reader method, e.g. foo.items for Foo.has_many :items def reader(force_reload = false) @@ -31,7 +25,7 @@ module ActiveRecord reload end - proxy + CollectionProxy.new(self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -248,8 +242,12 @@ module ActiveRecord # This method is abstract in the sense that it relies on # +count_records+, which is a method descendants have to provide. def size - if !find_target? || (loaded? && !options[:uniq]) - target.size + if !find_target? || loaded? + if options[:uniq] + target.uniq.size + else + target.size + end elsif !loaded? && options[:group] load_target.size elsif !loaded? && !options[:uniq] && target.is_a?(Array) @@ -474,6 +472,8 @@ module ActiveRecord raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ "new records could not be saved." end + + new_target end def concat_records(records) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 261a829281..cf4cc98f38 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -33,14 +33,7 @@ module ActiveRecord # # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. - class CollectionProxy # :nodoc: - alias :proxy_extend :extend - - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } - - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, - :lock, :readonly, :having, :pluck, :to => :scoped - + class CollectionProxy < Relation # :nodoc: delegate :target, :load_target, :loaded?, :to => :@association delegate :select, :find, :first, :last, @@ -52,7 +45,8 @@ module ActiveRecord def initialize(association) @association = association - Array(association.options[:extend]).each { |ext| proxy_extend(ext) } + super association.klass, association.klass.arel_table + merge! association.scoped end alias_method :new, :build @@ -61,54 +55,28 @@ module ActiveRecord @association end - def scoped(options = nil) - association = @association - scope = association.scoped - - scope.extending! do - define_method(:proxy_association) { association } - end - scope.merge!(options) if options - scope + # We don't want this object to be put on the scoping stack, because + # that could create an infinite loop where we call an @association + # method, which gets the current scope, which is this object, which + # delegates to @association, and so on. + def scoping + @association.scoped.scoping { yield } end - def respond_to?(name, include_private = false) - super || - (load_target && target.respond_to?(name, include_private)) || - proxy_association.klass.respond_to?(name, include_private) + def spawn + scoped end - def method_missing(method, *args, &block) - match = DynamicMatchers::Method.match(self, method) - if match && match.is_a?(DynamicMatchers::Instantiator) - scoped.send(method, *args) do |r| - proxy_association.send :set_owner_attributes, r - proxy_association.send :add_to_target, r - yield(r) if block_given? - end - - elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method)) - if load_target - if target.respond_to?(method) - target.send(method, *args, &block) - else - begin - super - rescue NoMethodError => e - raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}") - end - end - end + def scoped(options = nil) + association = @association - else - scoped.readonly(nil).public_send(method, *args, &block) + super.extending! do + define_method(:proxy_association) { association } end end - # Forwards <tt>===</tt> explicitly to the \target because the instance method - # removal above doesn't catch it. Loads the \target if needed. - def ===(other) - other === load_target + def ==(other) + load_target == other end def to_ary @@ -130,19 +98,6 @@ module ActiveRecord proxy_association.reload self end - - # Define array public methods because we know it should be invoked over - # the target, so we can have a performance improvement using those methods - # in association collections - Array.public_instance_methods.each do |m| - unless method_defined?(m) - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{m}(*args, &block) - target.public_send(:#{m}, *args, &block) if load_target - end - RUBY - end - end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 3005bef092..d545e7799d 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -350,7 +350,7 @@ module ActiveRecord end records_to_destroy.each do |record| - association.proxy.destroy(record) + association.destroy(record) end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 44ac37c498..6f9f0399db 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -34,6 +34,7 @@ module ActiveRecord when Numeric then value.to_s when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" + when Class then "'#{value.to_s}'" else "'#{quote_string(YAML.dump(value))}'" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index f0b6ae2b7d..df78ba6c5a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -23,7 +23,7 @@ module ActiveRecord end def sql_type - base.type_to_sql(type.to_sym, limit, precision, scale) rescue type + base.type_to_sql(type.to_sym, limit, precision, scale) end def to_sql @@ -262,7 +262,7 @@ module ActiveRecord # Adds index options to the indexes hash, keyed by column name # This is primarily used to track indexes that need to be created after the table - # === Examples + # # index(:account_id, :name => 'index_projects_on_account_id') def index(column_name, options = {}) indexes[column_name] = options @@ -348,7 +348,7 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. - # ===== Example + # # ====== Creating a simple column # t.column(:name, :string) def column(column_name, type, options = {}) @@ -363,7 +363,6 @@ module ActiveRecord # Adds a new index to the table. +column_name+ can be a single Symbol, or # an Array of Symbols. See SchemaStatements#add_index # - # ===== Examples # ====== Creating a simple index # t.index(:name) # ====== Creating a unique index @@ -380,7 +379,7 @@ module ActiveRecord end # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps - # ===== Example + # # t.timestamps def timestamps @base.add_timestamps(@table_name) @@ -388,7 +387,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # t.change(:name, :string, :limit => 80) # t.change(:description, :text) def change(column_name, type, options = {}) @@ -396,7 +395,7 @@ module ActiveRecord end # Sets a new default value for a column. See SchemaStatements#change_column_default - # ===== Examples + # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) def change_default(column_name, default) @@ -404,7 +403,7 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # t.remove(:qualification) # t.remove(:qualification, :experience) def remove(*column_names) @@ -413,7 +412,6 @@ module ActiveRecord # Removes the given index from the table. # - # ===== Examples # ====== Remove the index_table_name_on_column in the table_name table # t.remove_index :column # ====== Remove the index named index_table_name_on_branch_id in the table_name table @@ -427,14 +425,14 @@ module ActiveRecord end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. - # ===== Example + # # t.remove_timestamps def remove_timestamps @base.remove_timestamps(@table_name) end # Renames a column. - # ===== Example + # # t.rename(:description, :name) def rename(column_name, new_column_name) @base.rename_column(@table_name, column_name, new_column_name) @@ -442,7 +440,7 @@ module ActiveRecord # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. - # ===== Examples + # # t.references(:goat) # t.references(:goat, :polymorphic => true) # t.belongs_to(:goat) @@ -460,7 +458,7 @@ module ActiveRecord # Removes a reference. Optionally removes a +type+ column. # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. - # ===== Examples + # # t.remove_references(:goat) # t.remove_references(:goat, :polymorphic => true) # t.remove_belongs_to(:goat) @@ -475,7 +473,7 @@ module ActiveRecord alias :remove_belongs_to :remove_references # Adds a column or columns of a specified type - # ===== Examples + # # t.string(:goat) # t.string(:goat, :sheep) %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| 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 e7a4f061fd..62b0f51bb2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -21,7 +21,6 @@ module ActiveRecord # Checks to see if the table +table_name+ exists on the database. # - # === Example # table_exists?(:developers) def table_exists?(table_name) tables.include?(table_name.to_s) @@ -32,7 +31,6 @@ module ActiveRecord # Checks to see if an index exists on a table for a given index definition. # - # === Examples # # Check an index exists # index_exists?(:suppliers, :company_id) # @@ -126,7 +124,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -193,7 +190,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -215,7 +211,6 @@ module ActiveRecord # A block for changing columns in +table+. # - # === Example # # change_table() yields a Table instance # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -229,7 +224,6 @@ module ActiveRecord # # Defaults to false. # - # ===== Examples # ====== Add a column # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -288,7 +282,7 @@ module ActiveRecord end # Renames a table. - # ===== Example + # # rename_table('octopuses', 'octopi') def rename_table(table_name, new_name) raise NotImplementedError, "rename_table is not implemented" @@ -308,7 +302,7 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # remove_column(:suppliers, :qualification) # remove_columns(:suppliers, :qualification, :experience) def remove_column(table_name, *column_names) @@ -318,7 +312,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # change_column(:suppliers, :name, :string, :limit => 80) # change_column(:accounts, :description, :text) def change_column(table_name, column_name, type, options = {}) @@ -326,7 +320,7 @@ module ActiveRecord end # Sets a new default value for a column. - # ===== Examples + # # change_column_default(:suppliers, :qualification, 'new') # change_column_default(:accounts, :authorized, 1) # change_column_default(:users, :email, nil) @@ -335,7 +329,7 @@ module ActiveRecord end # Renames a column. - # ===== Example + # # rename_column(:suppliers, :description, :name) def rename_column(table_name, column_name, new_column_name) raise NotImplementedError, "rename_column is not implemented" @@ -347,8 +341,6 @@ module ActiveRecord # The index will be named after the table and the column name(s), unless # you pass <tt>:name</tt> as an option. # - # ===== Examples - # # ====== Creating a simple index # add_index(:suppliers, :name) # generates @@ -537,7 +529,7 @@ module ActiveRecord end # Adds timestamps (created_at and updated_at) columns to the named table. - # ===== Examples + # # add_timestamps(:suppliers) def add_timestamps(table_name) add_column table_name, :created_at, :datetime @@ -545,7 +537,7 @@ module ActiveRecord end # Removes the timestamp columns (created_at and updated_at) from the table definition. - # ===== Examples + # # remove_timestamps(:suppliers) def remove_timestamps(table_name) remove_column table_name, :updated_at 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 a848838a4e..9794c5663e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -251,7 +251,7 @@ module ActiveRecord end # MysqlAdapter has to free a result after using it, so we use this method to write - # stuff in a abstract way without concerning ourselves about whether it needs to be + # stuff in an abstract way without concerning ourselves about whether it needs to be # explicitly freed or not. def execute_and_free(sql, name = nil) #:nodoc: yield execute(sql, name) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 1933ce2b46..01bd3ae26c 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -124,6 +124,7 @@ module ActiveRecord when :binary then "#{klass}.binary_to_string(#{var_name})" when :boolean then "#{klass}.value_to_boolean(#{var_name})" when :hstore then "#{klass}.string_to_hstore(#{var_name})" + when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})" else var_name end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index c82afc232c..df3d5e4657 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -137,6 +137,14 @@ module ActiveRecord end end + class Cidr < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_cidr value + end + end + class TypeMap def initialize @mapping = {} @@ -212,11 +220,9 @@ module ActiveRecord # FIXME: why are we keeping these types as strings? alias_type 'tsvector', 'text' alias_type 'interval', 'text' - alias_type 'cidr', 'text' - alias_type 'inet', 'text' - alias_type 'macaddr', 'text' alias_type 'bit', 'text' alias_type 'varbit', 'text' + alias_type 'macaddr', '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. @@ -237,6 +243,9 @@ module ActiveRecord register_type 'polygon', OID::Identity.new register_type 'circle', OID::Identity.new register_type 'hstore', OID::Hstore.new + + register_type 'cidr', OID::Cidr.new + alias_type 'inet', 'cidr' end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 4d5459939b..6714970103 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -8,6 +8,8 @@ require 'arel/visitors/bind_visitor' gem 'pg', '~> 0.11' require 'pg' +require 'ipaddr' + module ActiveRecord module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects @@ -79,6 +81,25 @@ module ActiveRecord end end + def string_to_cidr(string) + if string.nil? + nil + elsif String === string + IPAddr.new(string) + else + string + end + + end + + def cidr_to_string(object) + if IPAddr === object + "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + else + object + end + end + private HstorePair = begin quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ @@ -197,6 +218,13 @@ module ActiveRecord :decimal when 'hstore' :hstore + # Network address types + when 'inet' + :inet + when 'cidr' + :cidr + when 'macaddr' + :macaddr # Character types when /^(?:character varying|bpchar)(?:\(\d+\))?$/ :string @@ -211,9 +239,6 @@ module ActiveRecord # Geometric types when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ :string - # Network address types - when /^(?:cidr|inet|macaddr)$/ - :string # Bit strings when /^bit(?: varying)?(?:\(\d+\))?$/ :string @@ -282,6 +307,18 @@ module ActiveRecord def hstore(name, options = {}) column(name, 'hstore', options) end + + def inet(name, options = {}) + column(name, 'inet', options) + end + + def cidr(name, options = {}) + column(name, 'cidr', options) + end + + def macaddr(name, options = {}) + column(name, 'macaddr', options) + end end ADAPTER_NAME = 'PostgreSQL' @@ -301,7 +338,10 @@ module ActiveRecord :boolean => { :name => "boolean" }, :xml => { :name => "xml" }, :tsvector => { :name => "tsvector" }, - :hstore => { :name => "hstore" } + :hstore => { :name => "hstore" }, + :inet => { :name => "inet" }, + :cidr => { :name => "cidr" }, + :macaddr => { :name => "macaddr" } } # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -510,6 +550,11 @@ module ActiveRecord when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) else super end + when IPAddr + case column.sql_type + when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) + else super + end when Float if value.infinite? && column.type == :datetime "'#{value.to_s.downcase}'" @@ -549,6 +594,9 @@ module ActiveRecord when Hash return super unless 'hstore' == column.sql_type PostgreSQLColumn.hstore_to_string(value) + when IPAddr + return super unless ['inet','cidr'].includes? column.sql_type + PostgreSQLColumn.cidr_to_string(value) else super end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 44e407a561..d4ffa82b17 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -21,10 +21,6 @@ module ActiveRecord config[:database] = File.expand_path(config[:database], Rails.root) end - unless 'sqlite3' == config[:adapter] - raise ArgumentError, 'adapter name should be "sqlite3"' - end - db = SQLite3::Database.new( config[:database], :results_as_hash => true @@ -195,7 +191,7 @@ module ActiveRecord :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, + :time => { :name => "datetime" }, :date => { :name => "date" }, :binary => { :name => "blob" }, :boolean => { :name => "boolean" } @@ -522,7 +518,11 @@ module ActiveRecord end def copy_table(from, to, options = {}) #:nodoc: - options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s)) + from_primary_key = primary_key(from) + options[:primary_key] = from_primary_key if from_primary_key != 'id' + unless options[:primary_key] + options[:id] = columns(from).detect{|c| c.name == 'id'}.present? && from_primary_key == 'id' + end create_table(to, options) do |definition| @definition = definition columns(from).each do |column| @@ -536,7 +536,7 @@ module ActiveRecord :precision => column.precision, :scale => column.scale, :null => column.null) end - @definition.primary_key(primary_key(from)) if primary_key(from) + @definition.primary_key(from_primary_key) if from_primary_key yield @definition if block_given? end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index a869ed8c04..b2ed606e5f 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -77,7 +77,7 @@ module ActiveRecord ## # :singleton-method: - # Specifies wether or not has_many or has_one association option + # Specifies whether or not has_many or has_one association option # :dependent => :restrict raises an exception. If set to true, the # ActiveRecord::DeleteRestrictionError exception will be raised # along with a DEPRECATION WARNING. If set to false, an error would @@ -127,10 +127,16 @@ module ActiveRecord object.is_a?(self) end + # Returns an instance of <tt>Arel::Table</tt> loaded with the curent table name. + # + # class Post < ActiveRecord::Base + # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0)) + # end def arel_table @arel_table ||= Arel::Table.new(table_name, arel_engine) end + # Returns the Arel engine. def arel_engine @arel_engine ||= connection_handler.retrieve_connection_pool(self) ? self : active_record_super.arel_engine end @@ -205,13 +211,34 @@ module ActiveRecord self end + ## + # :method: clone + # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied. + # That means that modifying attributes of the clone will modify the original, since they will both point to the + # same attributes hash. If you need a copy of your attributes hash, please use the #dup method. + # + # user = User.first + # new_user = user.clone + # user.name # => "Bob" + # new_user.name = "Joe" + # user.name # => "Joe" + # + # user.object_id == new_user.object_id # => false + # user.name.object_id == new_user.name.object_id # => true + # + # user.name.object_id == user.dup.name.object_id # => false + + ## + # :method: dup # Duped objects have no id assigned and are treated as new records. Note # that this is a "shallow" copy as it copies the object's attributes # only, not its associations. The extent of a "deep" copy is application # specific and is therefore left to the application to implement according # to its need. # The dup method does not preserve the timestamps (created|updated)_(at|on). - def initialize_dup(other) + + ## + def initialize_dup(other) # :nodoc: cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) self.class.initialize_attributes(cloned_attributes) diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index a01e2f74ff..7e6512501c 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -83,7 +83,7 @@ module ActiveRecord # end # # test "find_alt_method_2" do - # assert_equal "Ruby on Rails", @rubyonrails.news + # assert_equal "Ruby on Rails", @rubyonrails.name # end # # In order to use these methods to access fixtured data within your testcases, you must specify one of the @@ -372,19 +372,23 @@ module ActiveRecord # # Any fixture labeled "DEFAULTS" is safely ignored. class Fixtures + #-- + # NOTE: an instance of Fixtures can be called fixture_set, it is normally stored in a single YAML file and possibly in a folder with the same name. + #++ MAX_ID = 2 ** 30 - 1 @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.default_fixture_model_name(fixture_name) # :nodoc: + def self.default_fixture_model_name(fixture_set_name) # :nodoc: ActiveRecord::Base.pluralize_table_names ? - fixture_name.singularize.camelize : - fixture_name.camelize + fixture_set_name.singularize.camelize : + fixture_set_name.camelize end - def self.default_fixture_table_name(fixture_name) # :nodoc: - "#{ActiveRecord::Base.table_name_prefix}"\ - "#{fixture_name.tr('/', '_')}#{ActiveRecord::Base.table_name_suffix}".to_sym + def self.default_fixture_table_name(fixture_set_name) # :nodoc: + "#{ ActiveRecord::Base.table_name_prefix }"\ + "#{ fixture_set_name.tr('/', '_') }"\ + "#{ ActiveRecord::Base.table_name_suffix }".to_sym end def self.reset_cache @@ -411,11 +415,11 @@ module ActiveRecord cache_for_connection(connection).update(fixtures_map) end - def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true) + def self.instantiate_fixtures(object, fixture_set, load_instances = true) if load_instances - fixtures.each do |name, fixture| + fixture_set.each do |fixture_name, fixture| begin - object.instance_variable_set "@#{name}", fixture.find + object.instance_variable_set "@#{fixture_name}", fixture.find rescue FixtureClassNotFound nil end @@ -424,61 +428,59 @@ module ActiveRecord end def self.instantiate_all_loaded_fixtures(object, load_instances = true) - all_loaded_fixtures.each do |table_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances) + all_loaded_fixtures.each_value do |fixture_set| + instantiate_fixtures(object, fixture_set, load_instances) end end cattr_accessor :all_loaded_fixtures self.all_loaded_fixtures = {} - def self.create_fixtures(fixtures_directory, table_names, class_names = {}) - table_names = Array(table_names).map(&:to_s) + def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}) + fixture_set_names = Array(fixture_set_names).map(&:to_s) class_names = class_names.stringify_keys # FIXME: Apparently JK uses this. connection = block_given? ? yield : ActiveRecord::Base.connection - files_to_read = table_names.reject { |table_name| - fixture_is_cached?(connection, table_name) + files_to_read = fixture_set_names.reject { |fs_name| + fixture_is_cached?(connection, fs_name) } unless files_to_read.empty? connection.disable_referential_integrity do fixtures_map = {} - fixture_files = files_to_read.map do |path| - fixture_name = path - - fixtures_map[fixture_name] = new( # ActiveRecord::Fixtures.new + fixture_sets = files_to_read.map do |fs_name| + fixtures_map[fs_name] = new( # ActiveRecord::Fixtures.new connection, - fixture_name, - class_names[fixture_name.to_s] || default_fixture_model_name(fixture_name), - ::File.join(fixtures_directory, path)) + fs_name, + class_names[fs_name] || default_fixture_model_name(fs_name), + ::File.join(fixtures_directory, fs_name)) end all_loaded_fixtures.update(fixtures_map) connection.transaction(:requires_new => true) do - fixture_files.each do |ff| - conn = ff.model_class.respond_to?(:connection) ? ff.model_class.connection : connection - table_rows = ff.table_rows + fixture_sets.each do |fs| + conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection + table_rows = fs.table_rows table_rows.keys.each do |table| conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' end - table_rows.each do |table_name,rows| + table_rows.each do |fixture_set_name, rows| rows.each do |row| - conn.insert_fixture(row, table_name) + conn.insert_fixture(row, fixture_set_name) end end end # Cap primary key sequences to max(pk). if connection.respond_to?(:reset_pk_sequence!) - fixture_files.each do |ff| - connection.reset_pk_sequence!(ff.table_name) + fixture_sets.each do |fs| + connection.reset_pk_sequence!(fs.table_name) end end end @@ -486,7 +488,7 @@ module ActiveRecord cache_fixtures(connection, fixtures_map) end end - cached_fixtures(connection, table_names) + cached_fixtures(connection, fixture_set_names) end # Returns a consistent, platform-independent identifier for +label+. @@ -497,26 +499,23 @@ module ActiveRecord attr_reader :table_name, :name, :fixtures, :model_class - def initialize(connection, fixture_name, class_name, fixture_path) - @connection = connection - @fixture_path = fixture_path - @name = fixture_name - @class_name = class_name - - @fixtures = {} + def initialize(connection, name, class_name, path) + @fixtures = {} # Ordered hash + @name = name + @path = path - # Should be an AR::Base type class - if class_name.is_a?(Class) + if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name else @model_class = class_name.constantize rescue nil end - @connection = model_class.connection if model_class && model_class.respond_to?(:connection) + @connection = ( model_class.respond_to?(:connection) ? + model_class.connection : connection ) @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : - self.class.default_fixture_table_name(fixture_name) ) + self.class.default_fixture_table_name(name) ) read_fixture_files end @@ -555,8 +554,8 @@ module ActiveRecord if model_class && model_class < ActiveRecord::Model # fill in timestamp columns if they aren't specified and the model is set to record_timestamps if model_class.record_timestamps - timestamp_column_names.each do |name| - row[name] = now unless row.key?(name) + timestamp_column_names.each do |c_name| + row[c_name] = now unless row.key?(c_name) end end @@ -634,26 +633,23 @@ module ActiveRecord end def read_fixture_files - yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f| + yaml_files = Dir["#{@path}/**/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path] yaml_files.each do |file| Fixtures::File.open(file) do |fh| - fh.each do |name, row| - fixtures[name] = ActiveRecord::Fixture.new(row, model_class) + fh.each do |fixture_name, row| + fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end end end end def yaml_file_path - "#{@fixture_path}.yml" + "#{@path}.yml" end - def yaml_fixtures_key(path) - ::File.basename(@fixture_path).split(".").first - end end class Fixture #:nodoc: @@ -708,7 +704,7 @@ module ActiveRecord class_attribute :fixture_table_names class_attribute :fixture_class_names class_attribute :use_transactional_fixtures - class_attribute :use_instantiated_fixtures # true, false, or :no_instances + class_attribute :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures self.fixture_table_names = [] @@ -716,9 +712,8 @@ module ActiveRecord self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false - self.fixture_class_names = Hash.new do |h, fixture_name| - fixture_name = fixture_name.to_s - h[fixture_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_name) + self.fixture_class_names = Hash.new do |h, fixture_set_name| + h[fixture_set_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_set_name) end end @@ -742,18 +737,18 @@ module ActiveRecord self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) end - def fixtures(*fixture_names) - if fixture_names.first == :all - fixture_names = Dir["#{fixture_path}/**/*.yml"].map { |f| + def fixtures(*fixture_set_names) + if fixture_set_names.first == :all + fixture_set_names = Dir["#{fixture_path}/**/*.yml"].map { |f| File.basename f, '.yml' } else - fixture_names = fixture_names.flatten.map { |n| n.to_s } + fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } end - self.fixture_table_names |= fixture_names - require_fixture_classes(fixture_names) - setup_fixture_accessors(fixture_names) + self.fixture_table_names |= fixture_set_names + require_fixture_classes(fixture_set_names) + setup_fixture_accessors(fixture_set_names) end def try_to_load_dependency(file_name) @@ -768,33 +763,39 @@ module ActiveRecord end end - def require_fixture_classes(fixture_names = nil) - (fixture_names || fixture_table_names).each do |fixture_name| - file_name = fixture_name.to_s + def require_fixture_classes(fixture_set_names = nil) + if fixture_set_names + fixture_set_names = fixture_set_names.map { |n| n.to_s } + else + fixture_set_names = fixture_table_names + end + + fixture_set_names.each do |file_name| file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names try_to_load_dependency(file_name) end end - def setup_fixture_accessors(fixture_names = nil) - fixture_names = Array(fixture_names || fixture_table_names) + def setup_fixture_accessors(fixture_set_names = nil) + fixture_set_names = Array(fixture_set_names || fixture_table_names) methods = Module.new do - fixture_names.each do |fixture_name| - fixture_name = fixture_name.to_s - accessor_name = fixture_name.tr('/', '_').to_sym + fixture_set_names.each do |fs_name| + fs_name = fs_name.to_s + accessor_name = fs_name.tr('/', '_').to_sym - define_method(accessor_name) do |*fixtures| - force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload + define_method(accessor_name) do |*fixture_names| + force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload - @fixture_cache[fixture_name] ||= {} + @fixture_cache[fs_name] ||= {} - instances = fixtures.map do |fixture| - @fixture_cache[fixture_name].delete(fixture) if force_reload + instances = fixture_names.map do |f_name| + f_name = f_name.to_s + @fixture_cache[fs_name].delete(f_name) if force_reload - if @loaded_fixtures[fixture_name][fixture.to_s] - @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find + if @loaded_fixtures[fs_name][f_name] + @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find else - raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'" + raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'" end end @@ -901,8 +902,8 @@ module ActiveRecord ActiveRecord::Fixtures.instantiate_all_loaded_fixtures(self, load_instances?) else raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? - @loaded_fixtures.each do |fixture_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?) + @loaded_fixtures.each_value do |fixture_set| + ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_set, load_instances?) end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 32a1dae6bc..95a2ddcc11 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -19,10 +19,10 @@ module ActiveRecord # = Active Record Nested Attributes # # Nested attributes allow you to save attributes on associated records - # through the parent. By default nested attribute updating is turned off, - # you can enable it using the accepts_nested_attributes_for class method. - # When you enable nested attributes an attribute writer is defined on - # the model. + # through the parent. By default nested attribute updating is turned off + # and you can enable it using the accepts_nested_attributes_for class + # method. When you enable nested attributes an attribute writer is + # defined on the model. # # The attribute writer is named after the association, which means that # in the following example, two new methods are added to your model: diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 4a987c2343..a1bc39a32d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -123,6 +123,7 @@ 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). def destroy + raise ReadOnlyRecord if readonly? destroy_associations destroy_row if persisted? @destroyed = true diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 3ce9995031..31d99f0192 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -108,34 +108,57 @@ module ActiveRecord 0 end - # This method is designed to perform select by a single column as direct SQL query - # Returns <tt>Array</tt> with values of the specified column name - # The values has same data type as column. + # Use <tt>pluck</tt> as a shortcut to select a single attribute without + # loading a bunch of records just to grab one attribute you want. + # + # Person.pluck(:name) + # + # instead of + # + # 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 + # returns String values by default. # # Examples: # - # Person.pluck(:id) # SELECT people.id FROM people - # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people - # Person.where(:age => 21).limit(5).pluck(:id) # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5 + # Person.pluck(:id) + # # SELECT people.id FROM people + # # => [1, 2, 3] + # + # Person.uniq.pluck(:role) + # # SELECT DISTINCT role FROM people + # # => ['admin', 'member', 'guest'] + # + # Person.where(:age => 21).limit(5).pluck(:id) + # # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5 + # # => [2, 3] + # + # Person.pluck('DATEDIFF(updated_at, created_at)') + # # SELECT DATEDIFF(updated_at, created_at) FROM people + # # => ['0', '27761', '173'] # def pluck(column_name) - key = column_name.to_s.split('.', 2).last - if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s) column_name = "#{table_name}.#{column_name}" end result = klass.connection.select_all(select(column_name).arel, nil, bind_values) - types = result.column_types.merge klass.column_types - column = types[key] + + 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| - value = klass.initialize_attributes(attributes)[key] - if column - column.type_cast value - else - value - end + raise ArgumentError, "Pluck expects to select just one attribute: #{attributes.inspect}" unless attributes.one? + + value = klass.initialize_attributes(attributes).values.first + + column.type_cast(value) end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index cc716bbfd1..4fedd33d64 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -170,9 +170,8 @@ module ActiveRecord # Person.exists?(['name LIKE ?', "%#{query}%"]) # Person.exists? def exists?(id = false) - return false if id.nil? - id = id.id if ActiveRecord::Model === id + return false if id.nil? join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 3f880ce5e9..36f98c6480 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -43,7 +43,7 @@ module ActiveRecord def normal_values Relation::SINGLE_VALUE_METHODS + Relation::MULTI_VALUE_METHODS - - [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering] + [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] end def merge @@ -76,6 +76,7 @@ module ActiveRecord end def merge_single_values + relation.from_value = values[:from] unless relation.from_value relation.lock_value = values[:lock] unless relation.lock_value relation.reverse_order_value = values[:reverse_order] diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index b40bf2b3cf..6a0cdd5917 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -34,9 +34,6 @@ module ActiveRecord private def self.build(attribute, value) case value - when ActiveRecord::Relation - value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? - attribute.in(value.arel.ast) when Array, ActiveRecord::Associations::CollectionProxy values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x} ranges, values = values.partition {|v| v.is_a?(Range)} @@ -59,6 +56,9 @@ module ActiveRecord array_predicates = ranges.map { |range| attribute.in(range) } array_predicates << values_predicate array_predicates.inject { |composite, predicate| composite.or(predicate) } + when ActiveRecord::Relation + value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? + attribute.in(value.arel.ast) when Range attribute.in(value) when ActiveRecord::Model diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 855477eaed..19fe8155d9 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -40,7 +40,7 @@ module ActiveRecord alias extensions extending_values def includes(*args) - args.empty? ? self : clone.includes!(*args) + args.empty? ? self : spawn.includes!(*args) end def includes!(*args) @@ -51,7 +51,7 @@ module ActiveRecord end def eager_load(*args) - args.blank? ? self : clone.eager_load!(*args) + args.blank? ? self : spawn.eager_load!(*args) end def eager_load!(*args) @@ -60,7 +60,7 @@ module ActiveRecord end def preload(*args) - args.blank? ? self : clone.preload!(*args) + args.blank? ? self : spawn.preload!(*args) end def preload!(*args) @@ -79,7 +79,7 @@ module ActiveRecord # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN def references(*args) - args.blank? ? self : clone.references!(*args) + args.blank? ? self : spawn.references!(*args) end def references!(*args) @@ -120,7 +120,7 @@ module ActiveRecord if block_given? to_a.select { |*block_args| value.call(*block_args) } else - clone.select!(value) + spawn.select!(value) end end @@ -130,7 +130,7 @@ module ActiveRecord end def group(*args) - args.blank? ? self : clone.group!(*args) + args.blank? ? self : spawn.group!(*args) end def group!(*args) @@ -139,7 +139,7 @@ module ActiveRecord end def order(*args) - args.blank? ? self : clone.order!(*args) + args.blank? ? self : spawn.order!(*args) end def order!(*args) @@ -165,7 +165,7 @@ module ActiveRecord # generates a query with 'ORDER BY id ASC, name ASC'. # def reorder(*args) - args.blank? ? self : clone.reorder!(*args) + args.blank? ? self : spawn.reorder!(*args) end def reorder!(*args) @@ -175,7 +175,7 @@ module ActiveRecord end def joins(*args) - args.compact.blank? ? self : clone.joins!(*args) + args.compact.blank? ? self : spawn.joins!(*args) end def joins!(*args) @@ -186,7 +186,7 @@ module ActiveRecord end def bind(value) - clone.bind!(value) + spawn.bind!(value) end def bind!(value) @@ -195,7 +195,7 @@ module ActiveRecord end def where(opts, *rest) - opts.blank? ? self : clone.where!(opts, *rest) + opts.blank? ? self : spawn.where!(opts, *rest) end def where!(opts, *rest) @@ -206,7 +206,7 @@ module ActiveRecord end def having(opts, *rest) - opts.blank? ? self : clone.having!(opts, *rest) + opts.blank? ? self : spawn.having!(opts, *rest) end def having!(opts, *rest) @@ -217,7 +217,7 @@ module ActiveRecord end def limit(value) - clone.limit!(value) + spawn.limit!(value) end def limit!(value) @@ -226,7 +226,7 @@ module ActiveRecord end def offset(value) - clone.offset!(value) + spawn.offset!(value) end def offset!(value) @@ -235,7 +235,7 @@ module ActiveRecord end def lock(locks = true) - clone.lock!(locks) + spawn.lock!(locks) end def lock!(locks = true) @@ -283,7 +283,7 @@ module ActiveRecord end def readonly(value = true) - clone.readonly!(value) + spawn.readonly!(value) end def readonly!(value = true) @@ -292,7 +292,7 @@ module ActiveRecord end def create_with(value) - clone.create_with!(value) + spawn.create_with!(value) end def create_with!(value) @@ -300,12 +300,25 @@ module ActiveRecord self end - def from(value) - clone.from!(value) + # Specifies table from which the records will be fetched. For example: + # + # Topic.select('title').from('posts') + # #=> SELECT title FROM posts + # + # Can accept other relation objects. For example: + # + # Topic.select('title').from(Topics.approved) + # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery + # + # Topics.select('a.title').from(Topics.approved, :a) + # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a + # + def from(value, subquery_name = nil) + spawn.from!(value, subquery_name) end - def from!(value) - self.from_value = value + def from!(value, subquery_name = nil) + self.from_value = [value, subquery_name] self end @@ -320,7 +333,7 @@ module ActiveRecord # User.select(:name).uniq.uniq(false) # # => You can also remove the uniqueness def uniq(value = true) - clone.uniq!(value) + spawn.uniq!(value) end def uniq!(value = true) @@ -366,7 +379,7 @@ module ActiveRecord # end def extending(*modules, &block) if modules.any? || block - clone.extending!(*modules, &block) + spawn.extending!(*modules, &block) else self end @@ -382,7 +395,7 @@ module ActiveRecord end def reverse_order - clone.reverse_order! + spawn.reverse_order! end def reverse_order! @@ -415,7 +428,7 @@ module ActiveRecord build_select(arel, select_values.uniq) arel.distinct(uniq_value) - arel.from(from_value) if from_value + arel.from(build_from) if from_value arel.lock(lock_value) if lock_value arel @@ -464,6 +477,17 @@ module ActiveRecord end end + def build_from + opts, name = from_value + case opts + when Relation + name ||= 'subquery' + opts.arel.as(name.to_s) + else + opts + end + end + def build_joins(manager, joins) buckets = joins.group_by do |join| case join diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index f6d178db7a..80d087a9ea 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -5,7 +5,12 @@ require 'active_record/relation/merger' module ActiveRecord module SpawnMethods - + + # This is overridden by Associations::CollectionProxy + def spawn #:nodoc: + clone + end + # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>. # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. # @@ -23,7 +28,7 @@ module ActiveRecord if other.is_a?(Array) to_a & other elsif other - clone.merge!(other) + spawn.merge!(other) else self end @@ -42,7 +47,7 @@ module ActiveRecord # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order # def except(*skips) - result = self.class.new(@klass, table, values.except(*skips)) + result = Relation.new(klass, table, values.except(*skips)) result.default_scoped = default_scoped result.extend(*extending_values) if extending_values.any? result @@ -56,7 +61,7 @@ module ActiveRecord # Post.order('id asc').only(:where, :order) # uses the specified order # def only(*onlies) - result = self.class.new(@klass, table, values.slice(*onlies)) + result = Relation.new(klass, table, values.slice(*onlies)) result.default_scoped = default_scoped result.extend(*extending_values) if extending_values.any? result diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index d815ab05ac..599e68379a 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -34,6 +34,15 @@ module ActiveRecord ActiveRecord::Migrator.migrations_paths end + def define(info, &block) + instance_eval(&block) + + unless info[:version].blank? + initialize_schema_migrations_table + assume_migrated_upto_version(info[:version], migrations_paths) + end + end + # Eval the given block. All methods available to the current connection # adapter are available within the block, so you can easily use the # database definition DSL to build up your schema (+create_table+, @@ -46,13 +55,7 @@ module ActiveRecord # ... # end def self.define(info={}, &block) - schema = new - schema.instance_eval(&block) - - unless info[:version].blank? - initialize_schema_migrations_table - assume_migrated_upto_version(info[:version], schema.migrations_paths) - end + new.define(info, &block) end end end diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index ed47a26749..5a256b040b 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -201,10 +201,10 @@ module ActiveRecord class << self alias :data_column_name :data_column - + # Use the ActiveRecord::Base.connection by default. attr_writer :connection - + # Use the ActiveRecord::Base.connection_pool by default. attr_writer :connection_pool @@ -218,12 +218,12 @@ module ActiveRecord # Look up a session by id and unmarshal its data if found. def find_by_session_id(session_id) - if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id)}") + if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id.to_s)}") new(:session_id => session_id, :marshaled_data => record['data']) end end end - + delegate :connection, :connection=, :connection_pool, :connection_pool=, :to => self attr_reader :session_id, :new_record @@ -241,6 +241,11 @@ module ActiveRecord @new_record = @marshaled_data.nil? end + # Returns true if the record is persisted, i.e. it's not a new record + def persisted? + !@new_record + end + # Lazy-unmarshal session state. def data unless @data @@ -287,7 +292,7 @@ module ActiveRecord connect = connection connect.delete <<-end_sql, 'Destroy session' DELETE FROM #{table_name} - WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} + WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id.to_s)} end_sql end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 1c7b839e5e..ce2ea85ef9 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -10,15 +10,21 @@ module ActiveRecord # Make sure that you declare the database column used for the serialized store as a text, so there's # plenty of room. # + # 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 - # store :settings, accessors: [ :color, :homepage ] + # store :settings, accessors: [ :color, :homepage ], coder: JSON # 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 # # # Add additional accessors to an existing store through store_accessor # class SuperUser < User @@ -29,7 +35,7 @@ module ActiveRecord module ClassMethods def store(store_attribute, options = {}) - serialize store_attribute, Hash + serialize store_attribute, options.fetch(:coder, Hash) store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors end @@ -37,13 +43,13 @@ module ActiveRecord keys.flatten.each do |key| define_method("#{key}=") do |value| send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) - send(store_attribute)[key] = value + send(store_attribute)[key.to_s] = 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] + send(store_attribute)[key.to_s] end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 743dfc5a38..30e1035300 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -290,7 +290,15 @@ module ActiveRecord status = nil self.class.transaction do add_to_transaction - status = yield + begin + status = yield + rescue ActiveRecord::Rollback + if defined?(@_start_transaction_state) + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + end + status = nil + end + raise ActiveRecord::Rollback unless status end status @@ -302,12 +310,8 @@ module ActiveRecord def remember_transaction_record_state #:nodoc: @_start_transaction_state ||= {} @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) - unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @destroyed - end + @_start_transaction_state[:new_record] = @new_record + @_start_transaction_state[:destroyed] = @destroyed @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index ce08e4c6a7..34660577da 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -86,9 +86,9 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_data_type_of_network_address_types - assert_equal :string, @first_network_address.column_for_attribute(:cidr_address).type - assert_equal :string, @first_network_address.column_for_attribute(:inet_address).type - assert_equal :string, @first_network_address.column_for_attribute(:mac_address).type + assert_equal :cidr, @first_network_address.column_for_attribute(:cidr_address).type + assert_equal :inet, @first_network_address.column_for_attribute(:inet_address).type + assert_equal :macaddr, @first_network_address.column_for_attribute(:mac_address).type end def test_data_type_of_bit_string_types @@ -134,9 +134,12 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '-1 years -2 days', @first_time.time_interval end - def test_network_address_values - assert_equal '192.168.0.0/24', @first_network_address.cidr_address - assert_equal '172.16.1.254', @first_network_address.inet_address + def test_network_address_values_ipaddr + cidr_address = IPAddr.new '192.168.0.0/24' + inet_address = IPAddr.new '172.16.1.254' + + assert_equal cidr_address, @first_network_address.cidr_address + assert_equal inet_address, @first_network_address.inet_address assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address end @@ -200,8 +203,8 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_update_network_address - new_cidr_address = '10.1.2.3/32' - new_inet_address = '10.0.0.0/8' + new_inet_address = '10.1.2.3/32' + new_cidr_address = '10.0.0.0/8' new_mac_address = 'bc:de:f0:12:34:56' assert @first_network_address.cidr_address = new_cidr_address assert @first_network_address.inet_address = new_inet_address diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 575b4806c1..7eef4ace81 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -57,6 +57,14 @@ class CopyTableTest < ActiveRecord::TestCase end end + def test_copy_table_with_unconventional_primary_key + test_copy_table('owners', 'owners_unconventional') do |from, to, options| + original_pk = @connection.primary_key('owners') + copied_pk = @connection.primary_key('owners_unconventional') + assert_equal original_pk, copied_pk + end + end + protected def copy_table(from, to, options = {}) @connection.copy_table(from, to, {:temporary => true}.merge(options)) diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 17bde6cb62..8a7f44d0a3 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -35,6 +35,11 @@ module ActiveRecord assert(!result.rows.first.include?("blob"), "should not store blobs") end + def test_time_column + owner = Owner.create!(:eats_at => Time.utc(1995,1,1,6,0)) + assert_match(/1995-01-01/, owner.reload.eats_at.to_s) + end + def test_exec_insert column = @conn.columns('items').find { |col| col.name == 'number' } vals = [[column, 10]] @@ -58,18 +63,6 @@ module ActiveRecord end end - def test_connection_no_adapter - assert_raises(ArgumentError) do - Base.sqlite3_connection :database => ':memory:' - end - end - - def test_connection_wrong_adapter - assert_raises(ArgumentError) do - Base.sqlite3_connection :database => ':memory:',:adapter => 'vuvuzela' - end - end - def test_bad_timeout assert_raises(TypeError) do Base.sqlite3_connection :database => ':memory:', diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 588adc38e3..b2eac0349b 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -37,6 +37,13 @@ if ActiveRecord::Base.connection.supports_migrations? end end end + + def test_schema_subclass + Class.new(ActiveRecord::Schema).define(:version => 9) do + create_table :fruits + end + assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + end end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 2e44005847..08467900f9 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -189,7 +189,7 @@ class EagerAssociationTest < ActiveRecord::TestCase author = assert_queries(3) { Author.scoped(:includes => {:posts_with_comments => :comments}).find(author_id) } # find the author, then find the posts, then find the comments author.posts_with_comments.each do |post_with_comments| assert_equal post_with_comments.comments.length, post_with_comments.comments.count - assert_nil post_with_comments.comments.uniq! + assert_nil post_with_comments.comments.to_a.uniq! end 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 22fd80df28..a6daf54c01 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 @@ -123,11 +123,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert active_record.developers.include?(david) end - def test_triple_equality - assert !(Array === Developer.find(1).projects) - assert Developer.find(1).projects === Array - end - def test_adding_single jamis = Developer.find(2) jamis.projects.reload # causing the collection to load @@ -338,6 +333,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, project.developers.size end + def test_uniq_when_association_already_loaded + project = projects(:active_record) + project.developers << [ developers(:jamis), developers(:david), developers(:jamis), developers(:david) ] + assert_equal 3, Project.includes(:developers).find(project.id).developers.size + end + def test_deleting david = Developer.find(1) active_record = Project.find(1) @@ -497,7 +498,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_include_uses_array_include_after_loaded project = projects(:active_record) - project.developers.class # force load target + project.developers.load_target developer = project.developers.first diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index f74fe42dc2..2e24f8ebe1 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -326,12 +326,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase authors(:david).readonly_comments.each { |c| assert c.readonly? } end - def test_triple_equality - # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - assert !(Array === Firm.scoped(:order => "id").first.clients) - assert Firm.scoped(:order => "id").first.clients === Array - end - def test_finding_default_orders assert_equal "Summit", Firm.scoped(:order => "id").first.clients.first.name end @@ -892,12 +886,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase client_id = firm.clients_of_firm.first.id assert_equal 1, firm.clients_of_firm.size - cleared = firm.clients_of_firm.clear + firm.clients_of_firm.clear assert_equal 0, firm.clients_of_firm.size assert_equal 0, firm.clients_of_firm(true).size assert_equal [], Client.destroyed_client_ids[firm.id] - assert_equal firm.clients_of_firm.object_id, cleared.object_id # Should not be destroyed since the association is not dependent. assert_nothing_raised do @@ -1359,7 +1352,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_include_uses_array_include_after_loaded firm = companies(:first_firm) - firm.clients.class # force load target + firm.clients.load_target client = firm.clients.first @@ -1409,7 +1402,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_first_or_last_on_loaded_association_should_not_fetch_with_query firm = companies(:first_firm) - firm.clients.class # force load target + firm.clients.load_target assert firm.clients.loaded? assert_no_queries do @@ -1692,6 +1685,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [bulb2], car.reload.bulbs end + def test_replace_returns_new_target + car = Car.create(:name => 'honda') + bulb1 = car.bulbs.create + bulb2 = car.bulbs.create + bulb3 = Bulb.create + + assert_equal [bulb1, bulb2], car.bulbs + result = car.bulbs.replace([bulb1, bulb3]) + assert_equal [bulb1, bulb3], car.bulbs + assert_equal [bulb1, bulb3], result + end + def test_building_has_many_association_with_restrict_dependency option_before = ActiveRecord::Base.dependent_restrict_raises ActiveRecord::Base.dependent_restrict_raises = true @@ -1708,4 +1713,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) assert_equal [accounts(:signals37)], firm.accounts.open end + + test "first_or_initialize adds the record to the association" do + firm = Firm.create! name: 'omg' + client = firm.clients_of_firm.first_or_initialize + assert_equal [client], firm.clients_of_firm + end + + test "first_or_create adds the record to the association" do + firm = Firm.create! name: 'omg' + firm.clients_of_firm.load_target + client = firm.clients_of_firm.first_or_create name: 'lol' + assert_equal [client], firm.clients_of_firm + assert_equal [client], firm.reload.clients_of_firm + end end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 3606ce691c..20b0eeb5ea 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -676,7 +676,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_include_uses_array_include_after_loaded david = authors(:david) - david.categories.class # force load target + david.categories.load_target category = david.categories.first diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index c93c91803e..8ef3bfef15 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -872,7 +872,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") } before = @pirate.parrots.map { |c| c.mark_for_destruction ; c } - class << @pirate.parrots + class << @pirate.association(:parrots) def destroy(*args) super raise 'Oh noes!' @@ -1277,7 +1277,7 @@ module AutosaveAssociationOnACollectionAssociationTests def test_should_not_load_the_associated_models_if_they_were_not_loaded_yet assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! } - @pirate.send(@association_name).class # hack to load the target + @pirate.send(@association_name).load_target assert_queries(3) do @pirate.catchphrase = 'Yarr' diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index c1b0cb8886..66a16d8b5f 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -522,7 +522,7 @@ class BasicsTest < ActiveRecord::TestCase end # Oracle, and Sybase do not have a TIME datatype. - unless current_adapter?(:OracleAdapter, :SybaseAdapter) + unless current_adapter?(:OracleAdapter, :SybaseAdapter, :SQLite3Adapter) def test_utc_as_time_zone Topic.default_timezone = :utc attributes = { "bonus_time" => "5:42:00AM" } @@ -764,6 +764,9 @@ class BasicsTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing + ActiveRecord::Base.time_zone_aware_attributes = false + ActiveRecord::Base.default_timezone = :local + Time.zone = nil attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", "written_on(5i)" => "12", "written_on(6i)" => "02" @@ -870,7 +873,7 @@ class BasicsTest < ActiveRecord::TestCase end # Oracle, and Sybase do not have a TIME datatype. - unless current_adapter?(:OracleAdapter, :SybaseAdapter) + unless current_adapter?(:OracleAdapter, :SybaseAdapter, :SQLite3Adapter) def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion ActiveRecord::Base.time_zone_aware_attributes = true ActiveRecord::Base.default_timezone = :utc @@ -891,6 +894,9 @@ class BasicsTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time_with_empty_seconds + ActiveRecord::Base.time_zone_aware_attributes = false + ActiveRecord::Base.default_timezone = :local + Time.zone = nil attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" @@ -946,7 +952,7 @@ class BasicsTest < ActiveRecord::TestCase def test_attributes_on_dummy_time # Oracle, and Sybase do not have a TIME datatype. - return true if current_adapter?(:OracleAdapter, :SybaseAdapter) + return true if current_adapter?(:OracleAdapter, :SybaseAdapter, :SQLite3Adapter) attributes = { "bonus_time" => "5:42:00AM" @@ -1514,11 +1520,7 @@ class BasicsTest < ActiveRecord::TestCase after_seq = Joke.sequence_name assert_not_equal before_columns, after_columns - unless before_seq.nil? && after_seq.nil? - assert_not_equal before_seq, after_seq - assert_equal "cold_jokes_id_seq", before_seq - assert_equal "funny_jokes_id_seq", after_seq - end + assert_not_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil? end def test_dont_clear_sequence_name_when_setting_explicitly diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 3652255c38..03aa9fdb27 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -23,6 +23,8 @@ module ActiveRecord @listener = LogListener.new @pk = Topic.columns.find { |c| c.primary } ActiveSupport::Notifications.subscribe('sql.active_record', @listener) + + skip_if_prepared_statement_caching_is_not_supported end def teardown @@ -30,9 +32,6 @@ module ActiveRecord end def test_binds_are_logged - # FIXME: use skip with minitest - return unless @connection.supports_statement_cache? - sub = @connection.substitute_at(@pk, 0) binds = [[@pk, 1]] sql = "select * from topics where id = #{sub}" @@ -44,9 +43,6 @@ module ActiveRecord end def test_find_one_uses_binds - # FIXME: use skip with minitest - return unless @connection.supports_statement_cache? - Topic.find(1) binds = [[@pk, 1]] message = @listener.calls.find { |args| args[4][:binds] == binds } @@ -54,9 +50,6 @@ module ActiveRecord end def test_logs_bind_vars - # FIXME: use skip with minitest - return unless @connection.supports_statement_cache? - pk = Topic.columns.find { |x| x.primary } payload = { @@ -86,5 +79,11 @@ module ActiveRecord logger.sql event assert_match([[pk.name, 10]].inspect, logger.debugs.first) end + + private + + def skip_if_prepared_statement_caching_is_not_supported + skip('prepared statement caching is not supported') unless @connection.supports_statement_cache? + end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index e096585f62..041f8ffb7c 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -466,6 +466,21 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [7], Company.joins(:contracts).pluck(:developer_id) end + def test_pluck_with_selection_clause + assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT credit_limit').sort + assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT accounts.credit_limit').sort + assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT(credit_limit)').sort + + # MySQL returns "SUM(DISTINCT(credit_limit))" as the column name unless + # an alias is provided. Without the alias, the column cannot be found + # and properly typecast. + assert_equal [50 + 53 + 55 + 60], Account.pluck('SUM(DISTINCT(credit_limit)) as credit_limit') + end + + def test_pluck_expects_a_single_selection + assert_raise(ArgumentError) { Account.pluck 'id, credit_limit' } + end + def test_plucks_with_ids assert_equal Company.all.map(&:id).sort, Company.ids.sort end diff --git a/activerecord/test/cases/connection_adapters/quoting_test.rb b/activerecord/test/cases/connection_adapters/quoting_test.rb new file mode 100644 index 0000000000..59dcb96ebc --- /dev/null +++ b/activerecord/test/cases/connection_adapters/quoting_test.rb @@ -0,0 +1,13 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + module Quoting + class QuotingTest < ActiveRecord::TestCase + def test_quoting_classes + assert_equal "'Object'", AbstractAdapter.new(nil).quote(Object) + end + end + end + end +end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 630acdbc46..c960773284 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -32,6 +32,7 @@ class FinderTest < ActiveRecord::TestCase assert Topic.exists?(:author_name => "Mary", :approved => true) assert Topic.exists?(["parent_id = ?", 1]) assert !Topic.exists?(45) + assert !Topic.exists?(Topic.new) begin assert !Topic.exists?("foo") diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 345ae0b582..20279f814b 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -80,8 +80,8 @@ class ActiveSupport::TestCase self.use_instantiated_fixtures = false self.use_transactional_fixtures = true - def create_fixtures(*table_names, &block) - ActiveRecord::Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, table_names, fixture_class_names, &block) + def create_fixtures(*fixture_set_names, &block) + ActiveRecord::Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block) end end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index f0b1f74bd3..ab61a4dcef 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -83,7 +83,6 @@ module ActiveRecord t.column :one_int, :integer, :limit => 1 t.column :four_int, :integer, :limit => 4 t.column :eight_int, :integer, :limit => 8 - t.column :eleven_int, :integer, :limit => 11 end columns = connection.columns(:testings) @@ -94,20 +93,17 @@ module ActiveRecord one = columns.detect { |c| c.name == "one_int" } four = columns.detect { |c| c.name == "four_int" } eight = columns.detect { |c| c.name == "eight_int" } - eleven = columns.detect { |c| c.name == "eleven_int" } if current_adapter?(:PostgreSQLAdapter) assert_equal 'integer', default.sql_type assert_equal 'smallint', one.sql_type assert_equal 'integer', four.sql_type assert_equal 'bigint', eight.sql_type - assert_equal 'integer', eleven.sql_type elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) assert_match 'int(11)', default.sql_type assert_match 'tinyint', one.sql_type assert_match 'int', four.sql_type assert_match 'bigint', eight.sql_type - assert_match 'int(11)', eleven.sql_type elsif current_adapter?(:OracleAdapter) assert_equal 'NUMBER(38)', default.sql_type assert_equal 'NUMBER(1)', one.sql_type diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 409a558f5c..18f8d82bfe 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -183,6 +183,16 @@ module ActiveRecord assert_instance_of TrueClass, bob.male? assert_kind_of BigDecimal, bob.wealth end + + def test_out_of_range_limit_should_raise + skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + + assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 } + + unless current_adapter?(:PostgreSQLAdapter) + assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff } + end + end end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index f788690b0d..cad93936c9 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -375,6 +375,27 @@ class MigrationTest < ActiveRecord::TestCase end end + def test_out_of_range_limit_should_raise + skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + + Person.connection.drop_table :test_limits rescue nil + assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do + Person.connection.create_table :test_integer_limits, :force => true do |t| + t.column :bigone, :integer, :limit => 10 + end + end + + unless current_adapter?(:PostgreSQLAdapter) + assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do + Person.connection.create_table :test_text_limits, :force => true do |t| + t.column :bigtext, :text, :limit => 0xfffffffff + end + end + end + + Person.connection.drop_table :test_limits rescue nil + end + protected def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz @@ -883,8 +904,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase def test_skipping_migrations @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" @existing_migrations = Dir[@migrations_path + "/*.rb"] - - sources = {} + + sources = {} sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_name_collision" diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 39b66b3399..df0399f548 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -23,6 +23,7 @@ class ReadOnlyTest < ActiveRecord::TestCase end assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save } assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! } + assert_raise(ActiveRecord::ReadOnlyRecord) { dev.destroy } end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 342f7a86fa..3462fd99bd 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -84,7 +84,7 @@ class RelationScopingTest < ActiveRecord::TestCase def test_scope_select_concatenates Developer.select("id, name").scoping do - developer = Developer.select('id, salary').where("name = 'David'").first + developer = Developer.select('salary').where("name = 'David'").first assert_equal 80000, developer.salary assert developer.has_attribute?(:id) assert developer.has_attribute?(:name) diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 8a7a2441d4..89f818a689 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -203,13 +203,18 @@ module ActiveRecord assert_equal [], relation.extending_values end - (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal :foo, relation.public_send("#{method}_value") end end + test '#from!' do + assert relation.from!('foo').equal?(relation) + assert_equal ['foo', nil], relation.from_value + end + test '#lock!' do assert relation.lock!('foo').equal?(relation) assert_equal 'foo', relation.lock_value diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 8cef4423c5..dee31f4772 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -133,6 +133,13 @@ class RelationTest < ActiveRecord::TestCase assert topics.loaded? end + def test_finiding_with_subquery + relation = Topic.where(:approved => true) + assert_equal relation.to_a, Topic.select('*').from(relation).to_a + assert_equal relation.to_a, Topic.select('subquery.*').from(relation).to_a + assert_equal relation.to_a, Topic.select('a.*').from(relation, :a).to_a + end + def test_finding_with_conditions assert_equal ["David"], Author.where(:name => 'David').map(&:name) assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name) @@ -644,6 +651,7 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.exists?(authors(:mary).id) assert ! davids.exists?("42") assert ! davids.exists?(42) + assert ! davids.exists?(davids.new) fake = Author.where(:name => 'fake author') assert ! fake.exists? diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 15ceaa1fcc..ab80dd1d6d 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -236,6 +236,27 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dump_includes_inet_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_network_address"} =~ output + assert_match %r{t.inet "inet_address"}, output + end + end + + def test_schema_dump_includes_cidr_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_network_address"} =~ output + assert_match %r{t.cidr "cidr_address"}, output + end + end + + def test_schema_dump_includes_macaddr_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_network_address"} =~ output + assert_match %r{t.macaddr "macaddr_address"}, output + end + end + def test_schema_dump_includes_hstores_shorthand_definition output = standard_dump if %r{create_table "postgresql_hstores"} =~ output diff --git a/activerecord/test/cases/session_store/sql_bypass.rb b/activerecord/test/cases/session_store/sql_bypass_test.rb index 7402b2afd6..6749d4ce98 100644 --- a/activerecord/test/cases/session_store/sql_bypass.rb +++ b/activerecord/test/cases/session_store/sql_bypass_test.rb @@ -18,6 +18,11 @@ module ActiveRecord assert !Session.table_exists? end + def test_new_record? + s = SqlBypass.new :data => 'foo', :session_id => 10 + assert s.new_record?, 'this is a new record!' + end + def test_persisted? s = SqlBypass.new :data => 'foo', :session_id => 10 assert !s.persisted?, 'this is a new record!' diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 40520d6da2..e1d0f1f799 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -4,7 +4,7 @@ require 'models/admin/user' class StoreTest < ActiveRecord::TestCase setup do - @john = Admin::User.create(:name => 'John Doe', :color => 'black', :remember_login => 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 @@ -40,4 +40,38 @@ class StoreTest < ActiveRecord::TestCase @john.remember_login = false assert_equal false, @john.remember_login end + + test "reading store attributes through accessors encoded with JSON" do + assert_equal 'tall', @john.height + assert_nil @john.weight + end + + test "writing store attributes through accessors encoded with JSON" do + @john.height = 'short' + @john.weight = 'heavy' + + assert_equal 'short', @john.height + assert_equal 'heavy', @john.weight + end + + test "accessing attributes not exposed by accessors encoded with JSON" do + @john.json_data['somestuff'] = 'somecoolstuff' + @john.save + + assert_equal 'somecoolstuff', @john.reload.json_data['somestuff'] + end + + test "updating the store will mark it as changed encoded with JSON" do + @john.height = 'short' + assert @john.json_data_changed? + end + + test "object initialization with not nullable column encoded with JSON" do + assert_equal true, @john.is_a_good_guy + end + + test "writing with not nullable column encoded with JSON" do + @john.is_a_good_guy = false + assert_equal false, @john.is_a_good_guy + end end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index f8b3e01a49..961ba8d9ba 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -287,6 +287,74 @@ class TransactionObserverCallbacksTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end + assert topic.id.nil? + assert !topic.persisted? assert_equal %w{ after_rollback }, topic.history end + + class TopicWithManualRollbackObserverAttached < ActiveRecord::Base + self.table_name = :topics + def history + @history ||= [] + end + end + + class TopicWithManualRollbackObserverAttachedObserver < ActiveRecord::Observer + def after_save(record) + record.history.push "after_save" + raise ActiveRecord::Rollback + end + end + + def test_after_save_called_with_manual_rollback + assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer' + + topic = TopicWithManualRollbackObserverAttached.new + + assert !topic.save + assert_equal nil, topic.id + assert !topic.persisted? + assert_equal %w{ after_save }, topic.history + end + def test_after_save_called_with_manual_rollback_bang + assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer' + + topic = TopicWithManualRollbackObserverAttached.new + + topic.save! + assert_equal nil, topic.id + assert !topic.persisted? + assert_equal %w{ after_save }, topic.history + end +end + +class SaveFromAfterCommitBlockTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class TopicWithSaveInCallback < ActiveRecord::Base + self.table_name = :topics + after_commit :cache_topic, :on => :create + after_commit :call_update, :on => :update + attr_accessor :cached, :record_updated + + def call_update + self.record_updated = true + end + + def cache_topic + unless cached + self.cached = true + self.save + else + self.cached = false + end + end + end + + def test_after_commit_in_save + topic = TopicWithSaveInCallback.new() + topic.save + assert_equal true, topic.cached + assert_equal true, topic.record_updated + end end diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index d0e628bd50..ad30039304 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -2,4 +2,6 @@ class Admin::User < ActiveRecord::Base belongs_to :account store :settings, :accessors => [ :color, :homepage ] store :preferences, :accessors => [ :remember_login ] + store :json_data, :accessors => [ :height, :weight ], :coder => JSON + store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => JSON end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index cef08cd99c..3fc58c25b1 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -41,6 +41,8 @@ ActiveRecord::Schema.define do # MySQL does not allow default values for blobs. Fake it out with a # big varchar below. t.string :preferences, :null => false, :default => '', :limit => 1024 + t.string :json_data, :null => true, :limit => 1024 + t.string :json_data_empty, :null => false, :default => "", :limit => 1024 t.references :account end @@ -429,6 +431,7 @@ ActiveRecord::Schema.define do t.string :name t.column :updated_at, :datetime t.column :happy_at, :datetime + t.column :eats_at, :time t.string :essay_id end diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index ea05b35fe0..e9ddeb32cf 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -1,5 +1,5 @@ ActiveRecord::Schema.define do - # For sqlite 3.1.0+, make a table with a autoincrement column + # For sqlite 3.1.0+, make a table with an autoincrement column if supports_autoincrement? create_table :table_with_autoincrement, :force => true do |t| t.column :name, :string |