diff options
Diffstat (limited to 'activerecord')
43 files changed, 898 insertions, 591 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index ee2811c2be..2c8ec3d4d1 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,33 @@ ## Rails 4.0.0 (unreleased) ## +* Added the `ActiveRecord::Model` module which can be included in a + class as an alternative to inheriting from `ActiveRecord::Base`: + + class Post + include ActiveRecord::Model + end + + Please note: + + * Up until now it has been safe to assume that all AR models are + descendants of `ActiveRecord::Base`. This is no longer a safe + assumption, but it may transpire that there are areas of the + code which still make this assumption. So there may be + 'teething difficulties' with this feature. (But please do try it + and report bugs.) + + * Plugins & libraries etc that add methods to `ActiveRecord::Base` + will not be compatible with `ActiveRecord::Model`. Those libraries + should add to `ActiveRecord::Model` instead (which is included in + `Base`), or better still, avoid monkey-patching AR and instead + provide a module that users can include where they need it. + + * To minimise the risk of conflicts with other code, it is + advisable to include `ActiveRecord::Model` early in your class + definition. + + *Jon Leighton* + * PostgreSQL hstore records can be created. * PostgreSQL hstore types are automatically deserialized from the database. @@ -36,7 +64,7 @@ * Implemented ActiveRecord::Relation#pluck method Method returns Array of column value from table under ActiveRecord model - + Client.pluck(:id) *Bogdan Gusiev* @@ -53,7 +81,7 @@ Post.find(1) Post.connection.close }.join - + Only people who spawn threads in their application code need to worry about this change. @@ -152,7 +180,7 @@ during :reject_if => :all_blank (fixes #2937) *Aaron Christy* - + ## Rails 3.1.3 (unreleased) ## * Perf fix: If we're deleting all records in an association, don't add a IN(..) clause diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 77971973f6..4d143146d5 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -57,6 +57,8 @@ module ActiveRecord autoload :Base autoload :Callbacks + autoload :Configuration + autoload :Core autoload :CounterCache autoload :DynamicMatchers autoload :DynamicFinderMatch @@ -67,6 +69,7 @@ module ActiveRecord autoload :Integration autoload :Migration autoload :Migrator, 'active_record/migration' + autoload :Model autoload :ModelSchema autoload :NestedAttributes autoload :Observer diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 237343c252..44b0956e4e 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -52,7 +52,7 @@ module ActiveRecord end def undefine_attribute_methods - super + super if attribute_methods_generated? @attribute_methods_generated = false end @@ -61,10 +61,12 @@ module ActiveRecord raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" end - if superclass == Base + if active_record_super == Base super else - method_defined_within?(method_name, superclass, superclass.generated_attribute_methods) || super + # If B < A and A defines its own attribute method, then we don't want to overwrite that. + defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods) + defined && !ActiveRecord::Base.method_defined?(method_name) || super end end @@ -74,9 +76,6 @@ module ActiveRecord method_defined_within?(name, Base) end - # Note that we could do this via klass.instance_methods(false), but this would require us - # to maintain a cached Set (for speed) and invalidate it at the correct time, which would - # be a pain. This implementation is also O(1) while avoiding maintaining a cached Set. def method_defined_within?(name, klass, sup = klass.superclass) if klass.method_defined?(name) || klass.private_method_defined?(name) if sup.method_defined?(name) || sup.private_method_defined?(name) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 1548114580..7d2d1db4b5 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -5,10 +5,7 @@ module ActiveRecord ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] - included do - cattr_accessor :attribute_types_cached_by_default, :instance_writer => false - self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - end + Configuration.define :attribute_types_cached_by_default, ATTRIBUTE_TYPES_CACHED_BY_DEFAULT module ClassMethods # +cache_attributes+ allows you to declare which converted attribute values should diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 17cf34cdf6..5e5392441b 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -6,10 +6,9 @@ module ActiveRecord module TimeZoneConversion extend ActiveSupport::Concern - included do - cattr_accessor :time_zone_aware_attributes, :instance_writer => false - self.time_zone_aware_attributes = false + Configuration.define :time_zone_aware_attributes, false + included do class_attribute :skip_time_zone_conversion_for_attributes, :instance_writer => false self.skip_time_zone_conversion_for_attributes = [] end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 6a6f463ddd..6085df7d9f 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -327,374 +327,10 @@ module ActiveRecord #:nodoc: # So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all # instances in the current object space. class Base - ## - # :singleton-method: - # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, - # which is then passed on to any new database connections made and which can be retrieved on both - # a class and instance level by calling +logger+. - cattr_accessor :logger, :instance_writer => false - - ## - # :singleton-method: - # Contains the database configuration - as is typically stored in config/database.yml - - # as a Hash. - # - # For example, the following database.yml... - # - # development: - # adapter: sqlite3 - # database: db/development.sqlite3 - # - # production: - # adapter: sqlite3 - # database: db/production.sqlite3 - # - # ...would result in ActiveRecord::Base.configurations to look like this: - # - # { - # 'development' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/development.sqlite3' - # }, - # 'production' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/production.sqlite3' - # } - # } - cattr_accessor :configurations, :instance_writer => false - @@configurations = {} - - ## - # :singleton-method: - # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling - # dates and times from the database. This is set to :local by default. - cattr_accessor :default_timezone, :instance_writer => false - @@default_timezone = :local - - ## - # :singleton-method: - # Specifies the format to use when dumping the database schema with Rails' - # Rakefile. If :sql, the schema is dumped as (potentially database- - # specific) SQL statements. If :ruby, the schema is dumped as an - # ActiveRecord::Schema file which can be loaded into any database that - # supports migrations. Use :ruby if you want to have different database - # adapters for, e.g., your development and test environments. - cattr_accessor :schema_format , :instance_writer => false - @@schema_format = :ruby - - ## - # :singleton-method: - # Specify whether or not to use timestamps for migration versions - cattr_accessor :timestamped_migrations , :instance_writer => false - @@timestamped_migrations = true - - class << self # Class methods - def inherited(child_class) #:nodoc: - # force attribute methods to be higher in inheritance hierarchy than other generated methods - child_class.generated_attribute_methods - child_class.generated_feature_methods - super - end - - def generated_feature_methods - @generated_feature_methods ||= begin - mod = const_set(:GeneratedFeatureMethods, Module.new) - include mod - mod - end - end - - # Returns a string like 'Post(id:integer, title:string, body:text)' - def inspect - if self == Base - super - elsif abstract_class? - "#{super}(abstract)" - elsif table_exists? - attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' - "#{super}(#{attr_list})" - else - "#{super}(Table doesn't exist)" - end - end - - # Overwrite the default class equality method to provide support for association proxies. - def ===(object) - object.is_a?(self) - end - - def arel_table - @arel_table ||= Arel::Table.new(table_name, arel_engine) - end - - def arel_engine - @arel_engine ||= begin - if self == ActiveRecord::Base - ActiveRecord::Base - else - connection_handler.connection_pools[name] ? self : superclass.arel_engine - end - end - end - - private - - def relation #:nodoc: - @relation ||= Relation.new(self, arel_table) - - if finder_needs_type_condition? - @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) - else - @relation - end - end - end - - public - # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with - # attributes but not yet saved (pass a hash with key names matching the associated table column names). - # In both instances, valid attribute keys are determined by the column names of the associated table -- - # hence you can't have attributes that aren't part of the table columns. - # - # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options - # in the +options+ parameter. - # - # ==== Examples - # # Instantiates a single new object - # User.new(:first_name => 'Jamie') - # - # # Instantiates a single new object using the :admin mass-assignment security role - # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) - # - # # Instantiates a single new object bypassing mass-assignment security - # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) - def initialize(attributes = nil, options = {}) - @attributes = self.class.initialize_attributes(self.class.column_defaults.dup) - @association_cache = {} - @aggregation_cache = {} - @attributes_cache = {} - @new_record = true - @readonly = false - @destroyed = false - @marked_for_destruction = false - @previously_changed = {} - @changed_attributes = {} - @relation = nil - - ensure_proper_type - - populate_with_current_scope_attributes - - assign_attributes(attributes, options) if attributes - - yield self if block_given? - run_callbacks :initialize - end - - # Initialize an empty model object from +coder+. +coder+ must contain - # the attributes necessary for initializing an empty model object. For - # example: - # - # class Post < ActiveRecord::Base - # end - # - # post = Post.allocate - # post.init_with('attributes' => { 'title' => 'hello world' }) - # post.title # => 'hello world' - def init_with(coder) - @attributes = self.class.initialize_attributes(coder['attributes']) - @relation = nil - - @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} - @association_cache = {} - @aggregation_cache = {} - @readonly = @destroyed = @marked_for_destruction = false - @new_record = false - run_callbacks :find - run_callbacks :initialize - - self - end - - # Duped objects have no id assigned and are treated as new records. Note - # that this is a "shallow" copy as it copies the object's attributes - # only, not its associations. The extent of a "deep" copy is application - # specific and is therefore left to the application to implement according - # to its need. - # The dup method does not preserve the timestamps (created|updated)_(at|on). - def initialize_dup(other) - cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - cloned_attributes.delete(self.class.primary_key) - - @attributes = cloned_attributes - - _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) - - @changed_attributes = {} - self.class.column_defaults.each do |attr, orig_value| - @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) - end - - @aggregation_cache = {} - @association_cache = {} - @attributes_cache = {} - @new_record = true - - ensure_proper_type - populate_with_current_scope_attributes - super - end - - # Populate +coder+ with attributes about this record that should be - # serialized. The structure of +coder+ defined in this method is - # guaranteed to match the structure of +coder+ passed to the +init_with+ - # method. - # - # Example: - # - # class Post < ActiveRecord::Base - # end - # coder = {} - # Post.new.encode_with(coder) - # coder # => { 'id' => nil, ... } - def encode_with(coder) - coder['attributes'] = attributes - end - - # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ - # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+. - # - # Note that new records are different from any other record by definition, unless the - # other record is the receiver itself. Besides, if you fetch existing records with - # +select+ and leave the ID out, you're on your own, this predicate will return false. - # - # Note also that destroying a record preserves its ID in the model instance, so deleted - # models are still comparable. - def ==(comparison_object) - super || - comparison_object.instance_of?(self.class) && - id.present? && - comparison_object.id == id - end - alias :eql? :== - - # Delegates to id in order to allow two records of the same type and id to work with something like: - # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] - def hash - id.hash - end - - # Freeze the attributes hash such that associations are still accessible, even on destroyed records. - def freeze - @attributes.freeze; self - end - - # Returns +true+ if the attributes hash has been frozen. - def frozen? - @attributes.frozen? - end - - # Allows sort on objects - def <=>(other_object) - if other_object.is_a?(self.class) - self.to_key <=> other_object.to_key - else - nil - end - end - - # Returns +true+ if the record is read only. Records loaded through joins with piggy-back - # attributes will be marked as read only since they cannot be saved. - def readonly? - @readonly - end - - # Marks this record as read only. - def readonly! - @readonly = true - end - - # Returns the contents of the record as a nicely formatted string. - def inspect - inspection = if @attributes - self.class.column_names.collect { |name| - if has_attribute?(name) - "#{name}: #{attribute_for_inspect(name)}" - end - }.compact.join(", ") - else - "not initialized" - end - "#<#{self.class} #{inspection}>" - end - - # Hackery to accomodate Syck. Remove for 4.0. - def to_yaml(opts = {}) #:nodoc: - if YAML.const_defined?(:ENGINE) && !YAML::ENGINE.syck? - super - else - coder = {} - encode_with(coder) - YAML.quick_emit(self, opts) do |out| - out.map(taguri, to_yaml_style) do |map| - coder.each { |k, v| map.add(k, v) } - end - end - end - end - - # Hackery to accomodate Syck. Remove for 4.0. - def yaml_initialize(tag, coder) #:nodoc: - init_with(coder) - end - - private - - # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements - # of the array, and then rescues from the possible NoMethodError. If those elements are - # ActiveRecord::Base's, then this triggers the various method_missing's that we have, - # which significantly impacts upon performance. - # - # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here. - # - # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/ - def to_ary # :nodoc: - nil - end - - include ActiveRecord::Persistence - extend ActiveModel::Naming - extend QueryCache::ClassMethods - extend ActiveSupport::Benchmarkable - extend ActiveSupport::DescendantsTracker - - extend Querying - include ReadonlyAttributes - include ModelSchema - extend Translation - include Inheritance - include Scoping - extend DynamicMatchers - include Sanitization - include Integration - include AttributeAssignment - include ActiveModel::Conversion - include Validations - extend CounterCache - include Locking::Optimistic, Locking::Pessimistic - include AttributeMethods - include Callbacks, ActiveModel::Observing, Timestamp - include Associations - include IdentityMap - include ActiveModel::SecurePassword - extend Explain - - # AutosaveAssociation needs to be included before Transactions, because we want - # #save_with_autosave_associations to be wrapped inside a transaction. - include AutosaveAssociation, NestedAttributes - include Aggregations, Transactions, Reflection, Serialization, Store + include ActiveRecord::Model + self.connection_handler = ConnectionAdapters::ConnectionHandler.new end end require 'active_record/connection_adapters/abstract/connection_specification' -ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) +ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy) diff --git a/activerecord/lib/active_record/configuration.rb b/activerecord/lib/active_record/configuration.rb new file mode 100644 index 0000000000..d58ed82258 --- /dev/null +++ b/activerecord/lib/active_record/configuration.rb @@ -0,0 +1,36 @@ +require 'active_support/concern' + +module ActiveRecord + # This module allows configuration options to be specified in a way such that + # ActiveRecord::Base and ActiveRecord::Model will have access to the same value, + # and will automatically get the appropriate readers and writers defined. + # + # In the future, we should probably move away from defining global config + # directly on ActiveRecord::Base / ActiveRecord::Model. + module Configuration #:nodoc: + extend ActiveSupport::Concern + + module ClassMethods + end + + def self.define(name, default = nil) + singleton_class.send(:attr_accessor, name) + + [self, ClassMethods].each do |klass| + klass.class_eval <<-CODE, __FILE__, __LINE__ + def #{name} + ActiveRecord::Configuration.#{name} + end + CODE + end + + ClassMethods.class_eval <<-CODE, __FILE__, __LINE__ + def #{name}=(val) + ActiveRecord::Configuration.#{name} = val + end + CODE + + send("#{name}=", default) unless default.nil? + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 401398c56b..5749d45a18 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -371,7 +371,7 @@ connection. For example: ActiveRecord::Base.connection.close pool = @class_to_pool[klass.name] return pool if pool return nil if ActiveRecord::Base == klass - retrieve_connection_pool klass.superclass + retrieve_connection_pool klass.active_record_super end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 7145dc0692..63e4020113 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -1,5 +1,7 @@ +require 'active_support/core_ext/module/delegation' + module ActiveRecord - class Base + module Core class ConnectionSpecification #:nodoc: attr_reader :config, :adapter_method def initialize (config, adapter_method) @@ -75,12 +77,6 @@ module ActiveRecord end end - ## - # :singleton-method: - # The connection handler - class_attribute :connection_handler, :instance_writer => false - self.connection_handler = ConnectionAdapters::ConnectionHandler.new - # Returns the connection currently associated with the class. This can # also be used to "borrow" the connection to do database work that isn't # easily done without going straight to SQL. @@ -88,53 +84,53 @@ module ActiveRecord self.class.connection end - # Establishes the connection to the database. Accepts a hash as input where - # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case) - # example for regular databases (MySQL, Postgresql, etc): - # - # ActiveRecord::Base.establish_connection( - # :adapter => "mysql", - # :host => "localhost", - # :username => "myuser", - # :password => "mypass", - # :database => "somedatabase" - # ) - # - # Example for SQLite database: - # - # ActiveRecord::Base.establish_connection( - # :adapter => "sqlite", - # :database => "path/to/dbfile" - # ) - # - # Also accepts keys as strings (for parsing from YAML for example): - # - # ActiveRecord::Base.establish_connection( - # "adapter" => "sqlite", - # "database" => "path/to/dbfile" - # ) - # - # Or a URL: - # - # ActiveRecord::Base.establish_connection( - # "postgres://myuser:mypass@localhost/somedatabase" - # ) - # - # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError - # may be returned on an error. - def self.establish_connection(spec = ENV["DATABASE_URL"]) - resolver = ConnectionSpecification::Resolver.new spec, configurations - spec = resolver.spec - - unless respond_to?(spec.adapter_method) - raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" - end + module ClassMethods + # Establishes the connection to the database. Accepts a hash as input where + # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case) + # example for regular databases (MySQL, Postgresql, etc): + # + # ActiveRecord::Base.establish_connection( + # :adapter => "mysql", + # :host => "localhost", + # :username => "myuser", + # :password => "mypass", + # :database => "somedatabase" + # ) + # + # Example for SQLite database: + # + # ActiveRecord::Base.establish_connection( + # :adapter => "sqlite", + # :database => "path/to/dbfile" + # ) + # + # Also accepts keys as strings (for parsing from YAML for example): + # + # ActiveRecord::Base.establish_connection( + # "adapter" => "sqlite", + # "database" => "path/to/dbfile" + # ) + # + # Or a URL: + # + # ActiveRecord::Base.establish_connection( + # "postgres://myuser:mypass@localhost/somedatabase" + # ) + # + # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError + # may be returned on an error. + def establish_connection(spec = ENV["DATABASE_URL"]) + resolver = ConnectionSpecification::Resolver.new spec, configurations + spec = resolver.spec + + unless respond_to?(spec.adapter_method) + raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" + end - remove_connection - connection_handler.establish_connection name, spec - end + remove_connection + connection_handler.establish_connection name, spec + end - class << self # Returns the connection currently associated with the class. This can # also be used to "borrow" the connection to do database work unrelated # to any of the specific Active Records. diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 626571a948..e51796871a 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -4,9 +4,9 @@ gem 'mysql2', '~> 0.3.10' require 'mysql2' module ActiveRecord - class Base + module Core::ClassMethods # Establishes a connection to the database that's used by all Active Record objects. - def self.mysql2_connection(config) + def mysql2_connection(config) config[:username] = 'root' if config[:username].nil? if Mysql2::Client.const_defined? :FOUND_ROWS diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index f092edecda..901d8422f2 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -18,9 +18,9 @@ class Mysql end module ActiveRecord - class Base + module Core::ClassMethods # Establishes a connection to the database that's used by all Active Record objects. - def self.mysql_connection(config) # :nodoc: + def mysql_connection(config) # :nodoc: config = config.symbolize_keys host = config[:host] port = config[:port] @@ -224,52 +224,48 @@ module ActiveRecord @statements.clear end - if "<3".respond_to?(:encode) - # Taken from here: - # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb - # Author: TOMITA Masahiro <tommy@tmtm.org> - ENCODINGS = { - "armscii8" => nil, - "ascii" => Encoding::US_ASCII, - "big5" => Encoding::Big5, - "binary" => Encoding::ASCII_8BIT, - "cp1250" => Encoding::Windows_1250, - "cp1251" => Encoding::Windows_1251, - "cp1256" => Encoding::Windows_1256, - "cp1257" => Encoding::Windows_1257, - "cp850" => Encoding::CP850, - "cp852" => Encoding::CP852, - "cp866" => Encoding::IBM866, - "cp932" => Encoding::Windows_31J, - "dec8" => nil, - "eucjpms" => Encoding::EucJP_ms, - "euckr" => Encoding::EUC_KR, - "gb2312" => Encoding::EUC_CN, - "gbk" => Encoding::GBK, - "geostd8" => nil, - "greek" => Encoding::ISO_8859_7, - "hebrew" => Encoding::ISO_8859_8, - "hp8" => nil, - "keybcs2" => nil, - "koi8r" => Encoding::KOI8_R, - "koi8u" => Encoding::KOI8_U, - "latin1" => Encoding::ISO_8859_1, - "latin2" => Encoding::ISO_8859_2, - "latin5" => Encoding::ISO_8859_9, - "latin7" => Encoding::ISO_8859_13, - "macce" => Encoding::MacCentEuro, - "macroman" => Encoding::MacRoman, - "sjis" => Encoding::SHIFT_JIS, - "swe7" => nil, - "tis620" => Encoding::TIS_620, - "ucs2" => Encoding::UTF_16BE, - "ujis" => Encoding::EucJP_ms, - "utf8" => Encoding::UTF_8, - "utf8mb4" => Encoding::UTF_8, - } - else - ENCODINGS = Hash.new { |h,k| h[k] = k } - end + # Taken from here: + # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb + # Author: TOMITA Masahiro <tommy@tmtm.org> + ENCODINGS = { + "armscii8" => nil, + "ascii" => Encoding::US_ASCII, + "big5" => Encoding::Big5, + "binary" => Encoding::ASCII_8BIT, + "cp1250" => Encoding::Windows_1250, + "cp1251" => Encoding::Windows_1251, + "cp1256" => Encoding::Windows_1256, + "cp1257" => Encoding::Windows_1257, + "cp850" => Encoding::CP850, + "cp852" => Encoding::CP852, + "cp866" => Encoding::IBM866, + "cp932" => Encoding::Windows_31J, + "dec8" => nil, + "eucjpms" => Encoding::EucJP_ms, + "euckr" => Encoding::EUC_KR, + "gb2312" => Encoding::EUC_CN, + "gbk" => Encoding::GBK, + "geostd8" => nil, + "greek" => Encoding::ISO_8859_7, + "hebrew" => Encoding::ISO_8859_8, + "hp8" => nil, + "keybcs2" => nil, + "koi8r" => Encoding::KOI8_R, + "koi8u" => Encoding::KOI8_U, + "latin1" => Encoding::ISO_8859_1, + "latin2" => Encoding::ISO_8859_2, + "latin5" => Encoding::ISO_8859_9, + "latin7" => Encoding::ISO_8859_13, + "macce" => Encoding::MacCentEuro, + "macroman" => Encoding::MacRoman, + "sjis" => Encoding::SHIFT_JIS, + "swe7" => nil, + "tis620" => Encoding::TIS_620, + "ucs2" => Encoding::UTF_16BE, + "ujis" => Encoding::EucJP_ms, + "utf8" => Encoding::UTF_8, + "utf8mb4" => Encoding::UTF_8, + } # Get the client encoding for this database def client_encoding diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index d7adcdc5d4..74a9be99bd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -7,9 +7,9 @@ gem 'pg', '~> 0.11' require 'pg' module ActiveRecord - class Base + module Core::ClassMethods # Establishes a connection to the database that's used by all Active Record objects - def self.postgresql_connection(config) # :nodoc: + def postgresql_connection(config) # :nodoc: config = config.symbolize_keys host = config[:host] port = config[:port] || 5432 @@ -876,7 +876,7 @@ module ActiveRecord # add info on sort order for columns (only desc order is explicitly specified, asc is the default) desc_order_columns = inddef.scan(/(\w+) DESC/).flatten orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} - + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders) end.compact end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 11bb457d03..ac3fb72b6e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -4,9 +4,9 @@ gem 'sqlite3', '~> 1.3.5' require 'sqlite3' module ActiveRecord - class Base + module Core::ClassMethods # sqlite3 adapter reuses sqlite_connection. - def self.sqlite3_connection(config) # :nodoc: + def sqlite3_connection(config) # :nodoc: # Require database. unless config[:database] raise ArgumentError, "No database file specified. Missing argument: database" diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 55818b3fbf..69750a911d 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -16,7 +16,7 @@ module ActiveRecord end def binary_to_string(value) - if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT + if value.encoding != Encoding::ASCII_8BIT value = value.force_encoding(Encoding::ASCII_8BIT) end @@ -201,25 +201,17 @@ module ActiveRecord end end - if "<3".encoding_aware? - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value + def type_cast(value, column) # :nodoc: + return value.to_f if BigDecimal === value + return super unless String === value + return super unless column && value - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - @logger.error "Binary data inserted for `string` type on column `#{column.name}`" - value.encode! 'utf-8' - end - value - end - else - def type_cast(value, column) # :nodoc: - return super unless BigDecimal === value - - value.to_f + value = super + if column.type == :string && value.encoding == Encoding::ASCII_8BIT + @logger.error "Binary data inserted for `string` type on column `#{column.name}`" + value.encode! 'utf-8' end + value end # DATABASE STATEMENTS ====================================== diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb new file mode 100644 index 0000000000..4f118e46a9 --- /dev/null +++ b/activerecord/lib/active_record/core.rb @@ -0,0 +1,350 @@ +require 'active_support/concern' + +module ActiveRecord + module Core + extend ActiveSupport::Concern + + ## + # :singleton-method: + # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, + # which is then passed on to any new database connections made and which can be retrieved on both + # a class and instance level by calling +logger+. + Configuration.define :logger + + ## + # :singleton-method: + # Contains the database configuration - as is typically stored in config/database.yml - + # as a Hash. + # + # For example, the following database.yml... + # + # development: + # adapter: sqlite3 + # database: db/development.sqlite3 + # + # production: + # adapter: sqlite3 + # database: db/production.sqlite3 + # + # ...would result in ActiveRecord::Base.configurations to look like this: + # + # { + # 'development' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/development.sqlite3' + # }, + # 'production' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/production.sqlite3' + # } + # } + Configuration.define :configurations, {} + + ## + # :singleton-method: + # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling + # dates and times from the database. This is set to :local by default. + Configuration.define :default_timezone, :local + + ## + # :singleton-method: + # Specifies the format to use when dumping the database schema with Rails' + # Rakefile. If :sql, the schema is dumped as (potentially database- + # specific) SQL statements. If :ruby, the schema is dumped as an + # ActiveRecord::Schema file which can be loaded into any database that + # supports migrations. Use :ruby if you want to have different database + # adapters for, e.g., your development and test environments. + Configuration.define :schema_format, :ruby + + ## + # :singleton-method: + # Specify whether or not to use timestamps for migration versions + Configuration.define :timestamped_migrations, true + + included do + ## + # :singleton-method: + # The connection handler + class_attribute :connection_handler, :instance_writer => false + + initialize_generated_modules unless self == Base + end + + module ClassMethods + def inherited(child_class) #:nodoc: + child_class.initialize_generated_modules + super + end + + def initialize_generated_modules + # force attribute methods to be higher in inheritance hierarchy than other generated methods + generated_attribute_methods + generated_feature_methods + end + + def generated_feature_methods + @generated_feature_methods ||= begin + mod = const_set(:GeneratedFeatureMethods, Module.new) + include mod + mod + end + end + + # Returns a string like 'Post(id:integer, title:string, body:text)' + def inspect + if self == Base + super + elsif abstract_class? + "#{super}(abstract)" + elsif table_exists? + attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' + "#{super}(#{attr_list})" + else + "#{super}(Table doesn't exist)" + end + end + + # Overwrite the default class equality method to provide support for association proxies. + def ===(object) + object.is_a?(self) + end + + def arel_table + @arel_table ||= Arel::Table.new(table_name, arel_engine) + end + + def arel_engine + @arel_engine ||= begin + if self == ActiveRecord::Base + ActiveRecord::Base + else + connection_handler.connection_pools[name] ? self : active_record_super.arel_engine + end + end + end + + private + + def relation #:nodoc: + @relation ||= Relation.new(self, arel_table) + + if finder_needs_type_condition? + @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + else + @relation + end + end + end + + # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with + # attributes but not yet saved (pass a hash with key names matching the associated table column names). + # In both instances, valid attribute keys are determined by the column names of the associated table -- + # hence you can't have attributes that aren't part of the table columns. + # + # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options + # in the +options+ parameter. + # + # ==== Examples + # # Instantiates a single new object + # User.new(:first_name => 'Jamie') + # + # # Instantiates a single new object using the :admin mass-assignment security role + # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) + # + # # Instantiates a single new object bypassing mass-assignment security + # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) + def initialize(attributes = nil, options = {}) + @attributes = self.class.initialize_attributes(self.class.column_defaults.dup) + @association_cache = {} + @aggregation_cache = {} + @attributes_cache = {} + @new_record = true + @readonly = false + @destroyed = false + @marked_for_destruction = false + @previously_changed = {} + @changed_attributes = {} + @relation = nil + + ensure_proper_type + + populate_with_current_scope_attributes + + assign_attributes(attributes, options) if attributes + + yield self if block_given? + run_callbacks :initialize + end + + # Initialize an empty model object from +coder+. +coder+ must contain + # the attributes necessary for initializing an empty model object. For + # example: + # + # class Post < ActiveRecord::Base + # end + # + # post = Post.allocate + # post.init_with('attributes' => { 'title' => 'hello world' }) + # post.title # => 'hello world' + def init_with(coder) + @attributes = self.class.initialize_attributes(coder['attributes']) + @relation = nil + + @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} + @association_cache = {} + @aggregation_cache = {} + @readonly = @destroyed = @marked_for_destruction = false + @new_record = false + run_callbacks :find + run_callbacks :initialize + + self + end + + # Duped objects have no id assigned and are treated as new records. Note + # that this is a "shallow" copy as it copies the object's attributes + # only, not its associations. The extent of a "deep" copy is application + # specific and is therefore left to the application to implement according + # to its need. + # The dup method does not preserve the timestamps (created|updated)_(at|on). + def initialize_dup(other) + cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) + cloned_attributes.delete(self.class.primary_key) + + @attributes = cloned_attributes + + _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) + + @changed_attributes = {} + self.class.column_defaults.each do |attr, orig_value| + @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) + end + + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + @new_record = true + + ensure_proper_type + populate_with_current_scope_attributes + super + end + + # Populate +coder+ with attributes about this record that should be + # serialized. The structure of +coder+ defined in this method is + # guaranteed to match the structure of +coder+ passed to the +init_with+ + # method. + # + # Example: + # + # class Post < ActiveRecord::Base + # end + # coder = {} + # Post.new.encode_with(coder) + # coder # => { 'id' => nil, ... } + def encode_with(coder) + coder['attributes'] = attributes + end + + # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ + # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+. + # + # Note that new records are different from any other record by definition, unless the + # other record is the receiver itself. Besides, if you fetch existing records with + # +select+ and leave the ID out, you're on your own, this predicate will return false. + # + # Note also that destroying a record preserves its ID in the model instance, so deleted + # models are still comparable. + def ==(comparison_object) + super || + comparison_object.instance_of?(self.class) && + id.present? && + comparison_object.id == id + end + alias :eql? :== + + # Delegates to id in order to allow two records of the same type and id to work with something like: + # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] + def hash + id.hash + end + + # Freeze the attributes hash such that associations are still accessible, even on destroyed records. + def freeze + @attributes.freeze; self + end + + # Returns +true+ if the attributes hash has been frozen. + def frozen? + @attributes.frozen? + end + + # Allows sort on objects + def <=>(other_object) + if other_object.is_a?(self.class) + self.to_key <=> other_object.to_key + else + nil + end + end + + # Returns +true+ if the record is read only. Records loaded through joins with piggy-back + # attributes will be marked as read only since they cannot be saved. + def readonly? + @readonly + end + + # Marks this record as read only. + def readonly! + @readonly = true + end + + # Returns the contents of the record as a nicely formatted string. + def inspect + inspection = if @attributes + self.class.column_names.collect { |name| + if has_attribute?(name) + "#{name}: #{attribute_for_inspect(name)}" + end + }.compact.join(", ") + else + "not initialized" + end + "#<#{self.class} #{inspection}>" + end + + # Hackery to accomodate Syck. Remove for 4.0. + def to_yaml(opts = {}) #:nodoc: + if YAML.const_defined?(:ENGINE) && !YAML::ENGINE.syck? + super + else + coder = {} + encode_with(coder) + YAML.quick_emit(self, opts) do |out| + out.map(taguri, to_yaml_style) do |map| + coder.each { |k, v| map.add(k, v) } + end + end + end + end + + # Hackery to accomodate Syck. Remove for 4.0. + def yaml_initialize(tag, coder) #:nodoc: + init_with(coder) + end + + private + + # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements + # of the array, and then rescues from the possible NoMethodError. If those elements are + # ActiveRecord::Base's, then this triggers the various method_missing's that we have, + # which significantly impacts upon performance. + # + # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here. + # + # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/ + def to_ary # :nodoc: + nil + end + end +end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index b64390250d..b5a67afd88 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -2,14 +2,9 @@ require 'active_support/core_ext/class/attribute' module ActiveRecord module Explain - def self.extended(base) - base.class_eval do - # If a query takes longer than these many seconds we log its query plan - # automatically. nil disables this feature. - class_attribute :auto_explain_threshold_in_seconds, :instance_writer => false - self.auto_explain_threshold_in_seconds = nil - end - end + # If a query takes longer than these many seconds we log its query plan + # automatically. nil disables this feature. + Configuration.define :auto_explain_threshold_in_seconds # If auto explain is enabled, this method triggers EXPLAIN logging for the # queries triggered by the block if it takes more than the threshold as a diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index da72d1718e..65c7f3afbb 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -559,7 +559,7 @@ module ActiveRecord rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash - if model_class && model_class < ActiveRecord::Base + if model_class && model_class < ActiveRecord::Model # fill in timestamp columns if they aren't specified and the model is set to record_timestamps if model_class.record_timestamps timestamp_column_names.each do |name| diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index de9461982a..ec57151d40 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -13,10 +13,12 @@ module ActiveRecord module ClassMethods # True if this isn't a concrete subclass needing a STI type condition. def descends_from_active_record? - if superclass.abstract_class? - superclass.descends_from_active_record? + sup = active_record_super + + if sup.abstract_class? + sup.descends_from_active_record? else - superclass == Base || !columns_hash.include?(inheritance_column) + sup == Base || !columns_hash.include?(inheritance_column) end end @@ -79,17 +81,34 @@ module ActiveRecord instance end + # If this class includes ActiveRecord::Model then it won't have a + # superclass. So this provides a way to get to the 'root' (ActiveRecord::Base), + # through inheritance hierarchy, ending in Base, whether or not that is + # actually an ancestor of the class. + # + # Mainly for internal use. + def active_record_super #:nodoc: + if self == Base || superclass && superclass < Model::Tag + superclass + else + Base + end + end + protected # Returns the class descending directly from ActiveRecord::Base or an # abstract class, if any, in the inheritance hierarchy. def class_of_active_record_descendant(klass) - if klass == Base || klass.superclass == Base || klass.superclass.abstract_class? - klass - elsif klass.superclass.nil? + unless klass < Model::Tag raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" + end + + sup = klass.active_record_super + if klass == Base || sup == Base || sup.abstract_class? + klass else - class_of_active_record_descendant(klass.superclass) + class_of_active_record_descendant(sup) end end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 27267c9d38..b80d01db81 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -48,10 +48,7 @@ module ActiveRecord module Optimistic extend ActiveSupport::Concern - included do - cattr_accessor :lock_optimistically, :instance_writer => false - self.lock_optimistically = true - end + Configuration.define :lock_optimistically, true def locking_enabled? #:nodoc: self.class.locking_enabled? diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb new file mode 100644 index 0000000000..f87be257db --- /dev/null +++ b/activerecord/lib/active_record/model.rb @@ -0,0 +1,89 @@ +require 'active_support/deprecation' + +module ActiveRecord + # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record persistence. + # This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. Example: + # + # class Post + # include ActiveRecord::Model + # end + # + module Model + # So we can recognise an AR class even while self.included is being + # executed. (At that time, klass < Model == false.) + module Tag #:nodoc: + end + + def self.included(base) + return if base < Tag + + base.class_eval do + include Tag + + include Configuration + + include ActiveRecord::Persistence + extend ActiveModel::Naming + extend QueryCache::ClassMethods + extend ActiveSupport::Benchmarkable + extend ActiveSupport::DescendantsTracker + + extend Querying + include ReadonlyAttributes + include ModelSchema + extend Translation + include Inheritance + include Scoping + extend DynamicMatchers + include Sanitization + include Integration + include AttributeAssignment + include ActiveModel::Conversion + include Validations + extend CounterCache + include Locking::Optimistic, Locking::Pessimistic + include AttributeMethods + include Callbacks, ActiveModel::Observing, Timestamp + include Associations + include IdentityMap + include ActiveModel::SecurePassword + extend Explain + + # AutosaveAssociation needs to be included before Transactions, because we want + # #save_with_autosave_associations to be wrapped inside a transaction. + include AutosaveAssociation, NestedAttributes + include Aggregations, Transactions, Reflection, Serialization, Store + + include Core + + self.connection_handler = Base.connection_handler + end + end + + module DeprecationProxy #:nodoc: + class << self + instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ } + + def method_missing(name, *args, &block) + if Model.respond_to?(name) + Model.send(name, *args, &block) + else + ActiveSupport::Deprecation.warn( + "The object passed to the active_record load hook was previously ActiveRecord::Base " \ + "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ + "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ + "a module rather than a class. (Model is included in Base, so anything added to Model " \ + "will be available on Base as well.)" + ) + Base.send(name, *args, &block) + end + end + + alias send method_missing + end + end + end + + # Load Base at this point, because the active_record load hook is run in that file. + Base +end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 1de820b3a6..adf85c6436 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,20 +1,20 @@ require 'active_support/concern' +require 'active_support/core_ext/class/attribute_accessors' module ActiveRecord module ModelSchema extend ActiveSupport::Concern - included do - ## - # :singleton-method: - # Accessor for the prefix type that will be prepended to every primary key column name. - # The options are :table_name and :table_name_with_underscore. If the first is specified, - # the Product class will look for "productid" instead of "id" as the primary column. If the - # latter is specified, the Product class will look for "product_id" instead of "id". Remember - # that this is a global setting for all Active Records. - cattr_accessor :primary_key_prefix_type, :instance_writer => false - self.primary_key_prefix_type = nil + ## + # :singleton-method: + # Accessor for the prefix type that will be prepended to every primary key column name. + # The options are :table_name and :table_name_with_underscore. If the first is specified, + # the Product class will look for "productid" instead of "id" as the primary column. If the + # latter is specified, the Product class will look for "product_id" instead of "id". Remember + # that this is a global setting for all Active Records. + Configuration.define :primary_key_prefix_type + included do ## # :singleton-method: # Accessor for the name of the prefix string to prepend to every table name. So if set @@ -128,10 +128,10 @@ module ActiveRecord # Computes the table name, (re)sets it internally, and returns it. def reset_table_name #:nodoc: - if superclass.abstract_class? - self.table_name = superclass.table_name || compute_table_name + if active_record_super.abstract_class? + self.table_name = active_record_super.table_name || compute_table_name elsif abstract_class? - self.table_name = superclass == Base ? nil : superclass.table_name + self.table_name = active_record_super == Base ? nil : active_record_super.table_name else self.table_name = compute_table_name end @@ -146,7 +146,7 @@ module ActiveRecord if self == Base 'type' else - (@inheritance_column ||= nil) || superclass.inheritance_column + (@inheritance_column ||= nil) || active_record_super.inheritance_column end end @@ -291,7 +291,7 @@ module ActiveRecord base = base_class if self == base # Nested classes are prefixed with singular parent table name. - if parent < ActiveRecord::Base && !parent.abstract_class? + if parent < ActiveRecord::Model && !parent.abstract_class? contained = parent.table_name contained = contained.singularize if parent.pluralize_table_names contained += '_' diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3c8e0f2052..311bf4dc0f 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -187,7 +187,7 @@ module ActiveRecord def exists?(id = false) return false if id.nil? - id = id.id if ActiveRecord::Base === id + id = id.id if ActiveRecord::Model === id join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index a789f48725..eee198e760 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -22,7 +22,7 @@ module ActiveRecord value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? attribute.in(value.arel.ast) when Array, ActiveRecord::Associations::CollectionProxy - values = value.to_a.map {|x| x.is_a?(ActiveRecord::Base) ? x.id : x} + values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x} ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)} array_predicates = ranges.map {|range| attribute.in(range)} @@ -41,7 +41,7 @@ module ActiveRecord array_predicates.inject {|composite, predicate| composite.or(predicate)} when Range, Arel::Relation attribute.in(value) - when ActiveRecord::Base + when ActiveRecord::Model attribute.eq(value.id) when Class # FIXME: I think we need to deprecate this behavior diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 21aff475a8..5398a14fc6 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -13,13 +13,6 @@ module ActiveRecord ActiveRecord::IdentityMap.clear end - # Backport skip to Ruby 1.8. test/unit doesn't support it, so just - # make it a noop. - unless instance_methods.map(&:to_s).include?("skip") - def skip(message) - end - end - def assert_date_from_db(expected, actual, message = nil) # SybaseAdapter doesn't have a separate column type just for dates, # so the time is in the string and incorrectly formatted diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index f6159deeeb..1509e34473 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -3,7 +3,7 @@ require 'rails/generators/active_record' module ActiveRecord module Generators class MigrationGenerator < Base - argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]" def create_migration_file set_local_assigns! diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index ce8d7eed42..d084a00ed7 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -2,14 +2,20 @@ class <%= migration_class_name %> < ActiveRecord::Migration <%- if migration_action == 'add' -%> def change <% attributes.each do |attribute| -%> - add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %> + add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- if attribute.has_index? -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end %> <%- end -%> end <%- else -%> def up <% attributes.each do |attribute| -%> <%- if migration_action -%> - <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end %> + <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> + <% if attribute.has_index? && migration_action == 'add' %> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <% end -%> <%- end -%> <%- end -%> end @@ -17,7 +23,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration def down <% attributes.reverse.each do |attribute| -%> <%- if migration_action -%> - <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><% end %> + <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> <%- end -%> <%- end -%> end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index f7caa43ac8..99a022461e 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -3,7 +3,7 @@ require 'rails/generators/active_record' module ActiveRecord module Generators class ModelGenerator < Base - argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]" check_class_collision @@ -26,6 +26,10 @@ module ActiveRecord template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke end + def attributes_with_index + attributes.select { |a| a.has_index? || (a.reference? && options[:indexes]) } + end + hook_for :test_framework protected diff --git a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb index 851930344a..3a3cf86d73 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb @@ -2,16 +2,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration def change create_table :<%= table_name %> do |t| <% attributes.each do |attribute| -%> - t.<%= attribute.type %> :<%= attribute.name %> + t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> <% end -%> <% if options[:timestamps] %> t.timestamps <% end -%> end -<% if options[:indexes] -%> -<% attributes.select {|attr| attr.reference? }.each do |attribute| -%> - add_index :<%= table_name %>, :<%= attribute.name %>_id -<% end -%> +<% attributes_with_index.each do |attribute| -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> <% end -%> end end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 146b77a95c..7fe2c02c04 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -17,11 +17,7 @@ module ActiveRecord end def test_client_encoding - if "<3".respond_to?(:encoding) - assert_equal Encoding::UTF_8, @conn.client_encoding - else - assert_equal 'utf8', @conn.client_encoding - end + assert_equal Encoding::UTF_8, @conn.client_encoding end def test_exec_insert_number @@ -41,13 +37,11 @@ module ActiveRecord value = result.rows.last.last - if "<3".respond_to?(:encoding) - # FIXME: this should probably be inside the mysql AR adapter? - value.force_encoding(@conn.client_encoding) + # FIXME: this should probably be inside the mysql AR adapter? + value.force_encoding(@conn.client_encoding) - # The strings in this file are utf-8, so transcode to utf-8 - value.encode!(Encoding::UTF_8) - end + # The strings in this file are utf-8, so transcode to utf-8 + value.encode!(Encoding::UTF_8) assert_equal str, value end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 97b56d38d7..17bde6cb62 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -23,8 +23,6 @@ module ActiveRecord end def test_column_types - return skip('only test encoding on 1.9') unless "<3".encoding_aware? - owner = Owner.create!(:name => "hello".encode('ascii-8bit')) owner.reload select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ', ' @@ -144,8 +142,6 @@ module ActiveRecord end def test_quote_binary_column_escapes_it - return unless "<3".respond_to?(:encode) - DualEncoding.connection.execute(<<-eosql) CREATE TABLE dual_encodings ( id integer PRIMARY KEY AUTOINCREMENT, @@ -159,9 +155,7 @@ module ActiveRecord assert_equal str, binary.data ensure - if "<3".respond_to?(:encode) - DualEncoding.connection.drop_table('dual_encodings') - end + DualEncoding.connection.drop_table('dual_encodings') end def test_execute diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 7665f1c12e..375c207d20 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -15,8 +15,10 @@ module ActiveRecord def setup @klass = Class.new do def self.superclass; Base; end + def self.active_record_super; Base; end def self.base_class; self; end + include ActiveRecord::Configuration include ActiveRecord::AttributeMethods def self.column_names diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 6ff0c1355c..c465e9b556 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -76,6 +76,7 @@ class BasicsTest < ActiveRecord::TestCase assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_feature_methods), "generated_attribute_methods must be higher in inheritance hierarchy than generated_feature_methods") assert_not_equal Computer.generated_feature_methods, Post.generated_feature_methods + assert(modules.index(Computer.generated_attribute_methods) < modules.index(ActiveRecord::Base.ancestors[1])) end def test_column_names_are_escaped diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index 06c14cb108..f97aade311 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -12,7 +12,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter) def test_mixed_encoding str = "\x80" - str.force_encoding('ASCII-8BIT') if str.respond_to?(:force_encoding) + str.force_encoding('ASCII-8BIT') binary = Binary.new :name => 'いただきます!', :data => str binary.save! @@ -23,7 +23,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter) # Mysql adapter doesn't properly encode things, so we have to do it if current_adapter?(:MysqlAdapter) - name.force_encoding('UTF-8') if name.respond_to?(:force_encoding) + name.force_encoding('UTF-8') end assert_equal 'いただきます!', name end @@ -33,7 +33,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter) FIXTURES.each do |filename| data = File.read(ASSETS_ROOT + "/#{filename}") - data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding) + data.force_encoding('ASCII-8BIT') data.freeze bin = Binary.new(:data => data) diff --git a/activerecord/test/cases/configuration_test.rb b/activerecord/test/cases/configuration_test.rb new file mode 100644 index 0000000000..872f1fc33b --- /dev/null +++ b/activerecord/test/cases/configuration_test.rb @@ -0,0 +1,26 @@ +require 'cases/helper' + +class ConfigurationTest < ActiveRecord::TestCase + def test_configuration + @klass = Class.new do + include ActiveRecord::Configuration + end + + ActiveRecord::Configuration.define :omg + + ActiveRecord::Configuration.omg = "omg" + + assert_equal "omg", @klass.new.omg + assert !@klass.new.respond_to?(:omg=) + assert_equal "omg", @klass.omg + + @klass.omg = "wtf" + + assert_equal "wtf", @klass.omg + assert_equal "wtf", @klass.new.omg + ensure + ActiveRecord::Configuration.send(:undef_method, :omg) + ActiveRecord::Configuration::ClassMethods.send(:undef_method, :omg) + ActiveRecord::Configuration::ClassMethods.send(:undef_method, :omg=) + end +end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 04d543fea9..dc99ac665c 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -8,6 +8,9 @@ module ActiveRecord @handler.establish_connection 'america', Base.connection_pool.spec @klass = Class.new do def self.name; 'america'; end + class << self + alias active_record_super superclass + end end @subklass = Class.new(@klass) do def self.name; 'north america'; end diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index d4b0f236ee..5f9a742285 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -1,7 +1,7 @@ require "cases/helper" module ActiveRecord - class Base + module Core class ConnectionSpecification class ResolverTest < ActiveRecord::TestCase def resolve(spec) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 99dd74c561..7295d3c6f1 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -213,7 +213,7 @@ class FixturesTest < ActiveRecord::TestCase def test_binary_in_fixtures data = File.open(ASSETS_ROOT + "/flowers.jpg", 'rb') { |f| f.read } - data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding) + data.force_encoding('ASCII-8BIT') data.freeze assert_equal data, @flowers.data end diff --git a/activerecord/test/cases/inclusion_test.rb b/activerecord/test/cases/inclusion_test.rb new file mode 100644 index 0000000000..f2c442c2e1 --- /dev/null +++ b/activerecord/test/cases/inclusion_test.rb @@ -0,0 +1,110 @@ +require 'cases/helper' +require 'models/teapot' + +class BasicInclusionModelTest < ActiveRecord::TestCase + def test_basic_model + Teapot.create!(:name => "Ronnie Kemper") + assert_equal "Ronnie Kemper", Teapot.find(1).name + end + + def test_initialization + t = Teapot.new(:name => "Bob") + assert_equal "Bob", t.name + end + + def test_inherited_model + teapot = CoolTeapot.create!(:name => "Bob") + teapot.reload + + assert_equal "Bob", teapot.name + assert_equal "mmm", teapot.aaahhh + end + + def test_generated_feature_methods + assert Teapot < Teapot::GeneratedFeatureMethods + end + + def test_exists + t = Teapot.create!(:name => "Ronnie Kemper") + assert Teapot.exists?(t) + end + + def test_predicate_builder + t = Teapot.create!(:name => "Bob") + assert_equal "Bob", Teapot.where(:id => [t]).first.name + assert_equal "Bob", Teapot.where(:id => t).first.name + end + + def test_nested_model + assert_equal "ceiling_teapots", Ceiling::Teapot.table_name + end +end + +class InclusionUnitTest < ActiveRecord::TestCase + def setup + @klass = Class.new { include ActiveRecord::Model } + end + + def test_non_abstract_class + assert !@klass.abstract_class? + end + + def test_abstract_class + @klass.abstract_class = true + assert @klass.abstract_class? + end + + def test_establish_connection + assert @klass.respond_to?(:establish_connection) + end + + def test_adapter_connection + assert @klass.respond_to?("#{ActiveRecord::Base.connection_config[:adapter]}_connection") + end + + def test_connection_handler + assert_equal ActiveRecord::Base.connection_handler, @klass.connection_handler + end + + def test_mirrored_configuration + ActiveRecord::Base.time_zone_aware_attributes = true + assert @klass.time_zone_aware_attributes + ActiveRecord::Base.time_zone_aware_attributes = false + assert !@klass.time_zone_aware_attributes + ensure + ActiveRecord::Base.time_zone_aware_attributes = false + end + + # Doesn't really test anything, but this is here to ensure warnings don't occur + def test_included_twice + @klass.send :include, ActiveRecord::Model + end + + def test_deprecation_proxy + assert_equal ActiveRecord::Model.name, ActiveRecord::Model::DeprecationProxy.name + assert_equal ActiveRecord::Base.superclass, assert_deprecated { ActiveRecord::Model::DeprecationProxy.superclass } + + sup, sup2 = nil, nil + ActiveSupport.on_load(:__test_active_record_model_deprecation) do + sup = superclass + sup2 = send(:superclass) + end + assert_deprecated do + ActiveSupport.run_load_hooks(:__test_active_record_model_deprecation, ActiveRecord::Model::DeprecationProxy) + end + assert_equal ActiveRecord::Base.superclass, sup + assert_equal ActiveRecord::Base.superclass, sup2 + end +end + +class InclusionFixturesTest < ActiveRecord::TestCase + fixtures :teapots + + def test_fixtured_record + assert_equal "Bob", teapots(:bob).name + end + + def test_timestamped_fixture + assert_not_nil teapots(:bob).created_at + end +end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 5c3a78688e..dd6d7e52d5 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -13,10 +13,8 @@ class SchemaDumperTest < ActiveRecord::TestCase @stream.string end - if "string".encoding_aware? - def test_magic_comment - assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump - end + def test_magic_comment + assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump end def test_schema_dump diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 5a38f2c6ee..2b4ec81199 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -5,10 +5,16 @@ class YamlSerializationTest < ActiveRecord::TestCase fixtures :topics def test_to_yaml_with_time_with_zone_should_not_raise_exception + tz = Time.zone Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"] ActiveRecord::Base.time_zone_aware_attributes = true + topic = Topic.new(:written_on => DateTime.now) assert_nothing_raised { topic.to_yaml } + + ensure + Time.zone = tz + ActiveRecord::Base.time_zone_aware_attributes = false end def test_roundtrip diff --git a/activerecord/test/fixtures/teapots.yml b/activerecord/test/fixtures/teapots.yml new file mode 100644 index 0000000000..ff515beb45 --- /dev/null +++ b/activerecord/test/fixtures/teapots.yml @@ -0,0 +1,3 @@ +bob: + id: 1 + name: Bob diff --git a/activerecord/test/models/teapot.rb b/activerecord/test/models/teapot.rb new file mode 100644 index 0000000000..ff18b6a96d --- /dev/null +++ b/activerecord/test/models/teapot.rb @@ -0,0 +1,32 @@ +class Teapot + # I'm a little teapot, + # Short and stout, + # Here is my handle + # Here is my spout + # When I get all steamed up, + # Hear me shout, + # Tip me over and pour me out! + # + # HELL YEAH TEAPOT SONG + + include ActiveRecord::Model +end + +class OMFGIMATEAPOT + def aaahhh + "mmm" + end +end + +class CoolTeapot < OMFGIMATEAPOT + include ActiveRecord::Model + self.table_name = "teapots" +end + +class Ceiling + include ActiveRecord::Model + + class Teapot + include ActiveRecord::Model + end +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 5933e1f46e..8706732230 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -596,6 +596,11 @@ ActiveRecord::Schema.define do t.datetime :ending end + create_table :teapots, :force => true do |t| + t.string :name + t.timestamps + end + create_table :topics, :force => true do |t| t.string :title t.string :author_name |