aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/aggregations.rb27
-rw-r--r--activerecord/lib/active_record/association_relation.rb22
-rw-r--r--activerecord/lib/active_record/associations.rb392
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb83
-rw-r--r--activerecord/lib/active_record/associations/association.rb65
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb213
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb77
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb9
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb149
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb120
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb89
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb135
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb10
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb18
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb24
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb304
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb166
-rw-r--r--activerecord/lib/active_record/associations/foreign_association.rb11
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb65
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb99
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb104
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb19
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb383
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb173
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb16
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb67
-rw-r--r--activerecord/lib/active_record/associations/join_helper.rb45
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb169
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb99
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb60
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb84
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb32
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb50
-rw-r--r--activerecord/lib/active_record/attribute.rb149
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb47
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb66
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb263
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb5
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb164
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb27
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb140
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb157
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb79
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb80
-rw-r--r--activerecord/lib/active_record/attribute_set.rb77
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb86
-rw-r--r--activerecord/lib/active_record/attributes.rb140
-rw-r--r--activerecord/lib/active_record/autosave_association.rb422
-rw-r--r--activerecord/lib/active_record/base.rb91
-rw-r--r--activerecord/lib/active_record/callbacks.rb27
-rw-r--r--activerecord/lib/active_record/coders/json.rb13
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb28
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb173
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb135
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb132
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb130
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb457
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb764
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb230
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb316
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb563
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb298
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb272
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb68
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb265
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb74
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb156
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb86
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb310
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb99
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb52
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb46
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb59
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb79
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb97
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb28
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb167
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb150
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb259
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb77
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb758
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb61
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb322
-rw-r--r--activerecord/lib/active_record/connection_handling.rb54
-rw-r--r--activerecord/lib/active_record/core.rb360
-rw-r--r--activerecord/lib/active_record/counter_cache.rb73
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb38
-rw-r--r--activerecord/lib/active_record/enum.rb198
-rw-r--r--activerecord/lib/active_record/errors.rb98
-rw-r--r--activerecord/lib/active_record/explain.rb69
-rw-r--r--activerecord/lib/active_record/explain_registry.rb30
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb7
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb6
-rw-r--r--activerecord/lib/active_record/fixtures.rb331
-rw-r--r--activerecord/lib/active_record/gem_version.rb15
-rw-r--r--activerecord/lib/active_record/inheritance.rb105
-rw-r--r--activerecord/lib/active_record/integration.rb73
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb67
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb10
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb33
-rw-r--r--activerecord/lib/active_record/migration.rb309
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb51
-rw-r--r--activerecord/lib/active_record/migration/join_table.rb2
-rw-r--r--activerecord/lib/active_record/model_schema.rb119
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb246
-rw-r--r--activerecord/lib/active_record/no_touching.rb52
-rw-r--r--activerecord/lib/active_record/null_relation.rb36
-rw-r--r--activerecord/lib/active_record/persistence.rb229
-rw-r--r--activerecord/lib/active_record/query_cache.rb6
-rw-r--r--activerecord/lib/active_record/querying.rb51
-rw-r--r--activerecord/lib/active_record/railtie.rb84
-rw-r--r--activerecord/lib/active_record/railties/console_sandbox.rb5
-rw-r--r--activerecord/lib/active_record/railties/controller_runtime.rb3
-rw-r--r--activerecord/lib/active_record/railties/databases.rake255
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb9
-rw-r--r--activerecord/lib/active_record/reflection.rb586
-rw-r--r--activerecord/lib/active_record/relation.rb331
-rw-r--r--activerecord/lib/active_record/relation/batches.rb107
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb171
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb106
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb390
-rw-r--r--activerecord/lib/active_record/relation/merger.rb133
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb153
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb45
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb58
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/base_handler.rb17
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb17
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/class_handler.rb27
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/range_handler.rb17
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb13
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb559
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb6
-rw-r--r--activerecord/lib/active_record/result.rb78
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb22
-rw-r--r--activerecord/lib/active_record/sanitization.rb49
-rw-r--r--activerecord/lib/active_record/schema.rb3
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb128
-rw-r--r--activerecord/lib/active_record/schema_migration.rb52
-rw-r--r--activerecord/lib/active_record/scoping.rb61
-rw-r--r--activerecord/lib/active_record/scoping/default.rb43
-rw-r--r--activerecord/lib/active_record/scoping/named.rb47
-rw-r--r--activerecord/lib/active_record/serialization.rb4
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb10
-rw-r--r--activerecord/lib/active_record/statement_cache.rb111
-rw-r--r--activerecord/lib/active_record/store.rb126
-rw-r--r--activerecord/lib/active_record/table_metadata.rb53
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb197
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb53
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb11
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb14
-rw-r--r--activerecord/lib/active_record/test_case.rb95
-rw-r--r--activerecord/lib/active_record/timestamp.rb32
-rw-r--r--activerecord/lib/active_record/transactions.rb124
-rw-r--r--activerecord/lib/active_record/type.rb23
-rw-r--r--activerecord/lib/active_record/type/big_integer.rb13
-rw-r--r--activerecord/lib/active_record/type/binary.rb50
-rw-r--r--activerecord/lib/active_record/type/boolean.rb30
-rw-r--r--activerecord/lib/active_record/type/date.rb46
-rw-r--r--activerecord/lib/active_record/type/date_time.rb43
-rw-r--r--activerecord/lib/active_record/type/decimal.rb48
-rw-r--r--activerecord/lib/active_record/type/decimal_without_scale.rb11
-rw-r--r--activerecord/lib/active_record/type/decorator.rb14
-rw-r--r--activerecord/lib/active_record/type/float.rb19
-rw-r--r--activerecord/lib/active_record/type/hash_lookup_type_map.rb17
-rw-r--r--activerecord/lib/active_record/type/integer.rb55
-rw-r--r--activerecord/lib/active_record/type/mutable.rb16
-rw-r--r--activerecord/lib/active_record/type/numeric.rb36
-rw-r--r--activerecord/lib/active_record/type/serialized.rb56
-rw-r--r--activerecord/lib/active_record/type/string.rb40
-rw-r--r--activerecord/lib/active_record/type/text.rb11
-rw-r--r--activerecord/lib/active_record/type/time.rb26
-rw-r--r--activerecord/lib/active_record/type/time_value.rb38
-rw-r--r--activerecord/lib/active_record/type/type_map.rb64
-rw-r--r--activerecord/lib/active_record/type/unsigned_integer.rb15
-rw-r--r--activerecord/lib/active_record/type/value.rb105
-rw-r--r--activerecord/lib/active_record/type_caster.rb7
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb34
-rw-r--r--activerecord/lib/active_record/type_caster/map.rb19
-rw-r--r--activerecord/lib/active_record/validations.rb48
-rw-r--r--activerecord/lib/active_record/validations/associated.rb14
-rw-r--r--activerecord/lib/active_record/validations/length.rb21
-rw-r--r--activerecord/lib/active_record/validations/presence.rb14
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb75
-rw-r--r--activerecord/lib/active_record/version.rb12
213 files changed, 13647 insertions, 7110 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 8101f7a45e..1040e6e3bb 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -113,7 +113,7 @@ module ActiveRecord
# other than the writer method.
#
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value
- # object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError.
+ # object. Attempting to change it afterwards will result in a RuntimeError.
#
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
# keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
@@ -129,10 +129,10 @@ module ActiveRecord
# is an instance of the value class. Specifying a custom converter allows the new value to be automatically
# converted to an instance of value class if necessary.
#
- # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
- # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
- # for the value class is called +create+ and it expects a CIDR address string as a parameter. New
- # values can be assigned to the value object using either another NetAddr::CIDR object, a string
+ # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be
+ # aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html).
+ # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter.
+ # New values can be assigned to the value object using either another NetAddr::CIDR object, a string
# or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
# these requirements:
#
@@ -165,7 +165,7 @@ module ActiveRecord
# by specifying an instance of the value object in the conditions hash. The following example
# finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
#
- # Customer.where(balance: Money.new(20, "USD")).all
+ # Customer.where(balance: Money.new(20, "USD"))
#
module ClassMethods
# Adds reader and writer methods for manipulating a value object:
@@ -223,14 +223,15 @@ module ActiveRecord
reader_method(name, class_name, mapping, allow_nil, constructor)
writer_method(name, class_name, mapping, allow_nil, converter)
- create_reflection(:composed_of, part_id, nil, options, self)
+ reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
+ Reflection.add_aggregate_reflection self, part_id, reflection
end
private
def reader_method(name, class_name, mapping, allow_nil, constructor)
define_method(name) do
- if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
- attrs = mapping.collect {|pair| read_attribute(pair.first)}
+ if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !_read_attribute(key).nil? })
+ attrs = mapping.collect {|key, _| _read_attribute(key)}
object = constructor.respond_to?(:call) ?
constructor.call(*attrs) :
class_name.constantize.send(constructor, *attrs)
@@ -243,15 +244,19 @@ module ActiveRecord
def writer_method(name, class_name, mapping, allow_nil, converter)
define_method("#{name}=") do |part|
klass = class_name.constantize
+ if part.is_a?(Hash)
+ part = klass.new(*part.values)
+ end
+
unless part.is_a?(klass) || converter.nil? || part.nil?
part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
end
if part.nil? && allow_nil
- mapping.each { |pair| self[pair.first] = nil }
+ mapping.each { |key, _| self[key] = nil }
@aggregation_cache[name] = nil
else
- mapping.each { |pair| self[pair.first] = part.send(pair.last) }
+ mapping.each { |key, value| self[key] = part.send(value) }
@aggregation_cache[name] = part.freeze
end
end
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
new file mode 100644
index 0000000000..f2b44913db
--- /dev/null
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -0,0 +1,22 @@
+module ActiveRecord
+ class AssociationRelation < Relation
+ def initialize(klass, table, predicate_builder, association)
+ super(klass, table, predicate_builder)
+ @association = association
+ end
+
+ def proxy_association
+ @association
+ end
+
+ def ==(other)
+ other == to_a
+ end
+
+ private
+
+ def exec_queries
+ super.each { |r| @association.set_inverse_instance r }
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index d8b6d7a86b..14af55f327 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1,11 +1,15 @@
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/string/conversions'
require 'active_support/core_ext/module/remove_method'
-require 'active_support/dependencies/autoload'
-require 'active_support/concern'
require 'active_record/errors'
module ActiveRecord
+ class AssociationNotFoundError < ConfigurationError #:nodoc:
+ def initialize(record, association_name)
+ super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
+ end
+ end
+
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
def initialize(reflection, associated_class = nil)
super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
@@ -42,12 +46,18 @@ module ActiveRecord
end
end
+ class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
+ def initialize(owner_class_name, reflection)
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ end
+ end
+
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
def initialize(reflection)
through_reflection = reflection.through_reflection
source_reflection_names = reflection.source_reflection_names
- source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
- super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
+ source_associations = reflection.through_reflection.klass._reflections.keys
+ super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
end
end
@@ -75,21 +85,15 @@ module ActiveRecord
end
end
- class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
- end
- end
-
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
def initialize(reflection)
- super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
+ super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
end
end
class ReadOnlyAssociation < ActiveRecordError #:nodoc:
def initialize(reflection)
- super("Can not add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
+ super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
end
end
@@ -112,11 +116,11 @@ module ActiveRecord
autoload :Association, 'active_record/associations/association'
autoload :SingularAssociation, 'active_record/associations/singular_association'
autoload :CollectionAssociation, 'active_record/associations/collection_association'
+ autoload :ForeignAssociation, 'active_record/associations/foreign_association'
autoload :CollectionProxy, 'active_record/associations/collection_proxy'
autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
- autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
@@ -139,7 +143,6 @@ module ActiveRecord
autoload :JoinDependency, 'active_record/associations/join_dependency'
autoload :AssociationScope, 'active_record/associations/association_scope'
autoload :AliasTracker, 'active_record/associations/alias_tracker'
- autoload :JoinHelper, 'active_record/associations/join_helper'
end
# Clears out the association cache.
@@ -155,7 +158,7 @@ module ActiveRecord
association = association_instance_get(name)
if association.nil?
- reflection = self.class.reflect_on_association(name)
+ raise AssociationNotFoundError.new(self, name) unless reflection = self.class._reflect_on_association(name)
association = reflection.association_class.new(self, reflection)
association_instance_set(name, association)
end
@@ -166,7 +169,7 @@ module ActiveRecord
private
# Returns the specified association instance if it responds to :loaded?, nil otherwise.
def association_instance_get(name)
- @association_cache[name.to_sym]
+ @association_cache[name]
end
# Set the specified association instance.
@@ -174,7 +177,7 @@ module ActiveRecord
@association_cache[name] = association
end
- # Associations are a set of macro-like class methods for tying objects together through
+ # \Associations are a set of macro-like class methods for tying objects together through
# foreign keys. They express relationships like "Project has one Project Manager"
# or "Project belongs to a Portfolio". Each macro adds a number of methods to the
# class which are specialized according to the collection or association symbol and the
@@ -193,8 +196,8 @@ module ActiveRecord
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
- # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(mileston), Project#milestones.find(milestone_id),</tt>
- # <tt>Project#milestones.all(options), Project#milestones.build, Project#milestones.create</tt>
+ # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt>
+ # <tt>Project#milestones.build, Project#milestones.create</tt>
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
# <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt>
#
@@ -206,12 +209,13 @@ module ActiveRecord
# For instance, +attributes+ and +connection+ would be bad choices for association names.
#
# == Auto-generated methods
+ # See also Instance Public methods below for more details.
#
# === Singular associations (one-to-one)
# | | belongs_to |
# generated methods | belongs_to | :polymorphic | has_one
# ----------------------------------+------------+--------------+---------
- # other | X | X | X
+ # other(force_reload=false) | X | X | X
# other=(other) | X | X | X
# build_other(attributes={}) | X | | X
# create_other(attributes={}) | X | | X
@@ -221,7 +225,7 @@ module ActiveRecord
# | | | has_many
# generated methods | habtm | has_many | :through
# ----------------------------------+-------+----------+----------
- # others | X | X | X
+ # others(force_reload=false) | X | X | X
# others=(other,other,...) | X | X | X
# other_ids | X | X | X
# other_ids=(id,id,...) | X | X | X
@@ -243,6 +247,7 @@ module ActiveRecord
# others.destroy_all | X | X | X
# others.find(*args) | X | X | X
# others.exists? | X | X | X
+ # others.distinct | X | X | X
# others.uniq | X | X | X
# others.reset | X | X | X
#
@@ -366,11 +371,11 @@ module ActiveRecord
# there is some special behavior you should be aware of, mostly involving the saving of
# associated objects.
#
- # You can set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
+ # You can set the <tt>:autosave</tt> option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
# <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it
# to +true+ will _always_ save the members, whereas setting it to +false+ will
- # _never_ save the members. More details about :autosave option is available at
- # autosave_association.rb .
+ # _never_ save the members. More details about <tt>:autosave</tt> option is available at
+ # AutosaveAssociation.
#
# === One-to-one associations
#
@@ -403,7 +408,7 @@ module ActiveRecord
#
# == Customizing the query
#
- # Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax
+ # \Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax
# to customize them. For example, to add a condition:
#
# class Blog < ActiveRecord::Base
@@ -422,6 +427,10 @@ module ActiveRecord
# has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event'
# end
#
+ # Note: Joining, eager loading and preloading of these associations is not fully possible.
+ # These operations happen before instance creation and the scope will be called with a +nil+ argument.
+ # This can lead to unexpected behavior and is deprecated.
+ #
# == Association callbacks
#
# Similar to the normal callbacks that hook into the life cycle of an Active Record object,
@@ -445,9 +454,11 @@ module ActiveRecord
#
# Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
#
- # Should any of the +before_add+ callbacks throw an exception, the object does not get
- # added to the collection. Same with the +before_remove+ callbacks; if an exception is
- # thrown the object doesn't get removed.
+ # If any of the +before_add+ callbacks throw an exception, the object will not be
+ # added to the collection.
+ #
+ # Similarly, if any of the +before_remove+ callbacks throw an exception, the object
+ # will not be removed from the collection.
#
# == Association extensions
#
@@ -459,7 +470,7 @@ module ActiveRecord
# has_many :people do
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
- # find_or_create_by_first_name_and_last_name(first_name, last_name)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
# end
@@ -474,7 +485,7 @@ module ActiveRecord
# module FindOrCreateByNameExtension
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
- # find_or_create_by_first_name_and_last_name(first_name, last_name)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
#
@@ -539,8 +550,8 @@ module ActiveRecord
# end
#
# @firm = Firm.first
- # @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm
- # @firm.invoices # selects all invoices by going through the Client join model
+ # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
+ # @firm.invoices # selects all invoices by going through the Client join model
#
# Similarly you can go through a +has_one+ association on the join model:
#
@@ -569,6 +580,8 @@ module ActiveRecord
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
# @group.avatars.delete(@group.avatars.last) # so would this
#
+ # == Setting Inverses
+ #
# If you are using a +belongs_to+ on the join model, it is a good idea to set the
# <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example
# works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association):
@@ -585,7 +598,27 @@ module ActiveRecord
# belongs_to :tag, inverse_of: :taggings
# end
#
- # == Nested Associations
+ # If you do not set the <tt>:inverse_of</tt> record, the association will
+ # do its best to match itself up with the correct inverse. Automatic
+ # inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and
+ # <tt>belongs_to</tt> associations.
+ #
+ # Extra options on the associations, as defined in the
+ # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will
+ # also prevent the association's inverse from being found automatically.
+ #
+ # The automatic guessing of the inverse association uses a heuristic based
+ # on the name of the class, so it may not work for all associations,
+ # especially the ones with non-standard names.
+ #
+ # You can turn off the automatic detection of inverse associations by setting
+ # the <tt>:inverse_of</tt> option to <tt>false</tt> like so:
+ #
+ # class Taggable < ActiveRecord::Base
+ # belongs_to :tag, inverse_of: false
+ # end
+ #
+ # == Nested \Associations
#
# You can actually specify *any* association with the <tt>:through</tt> option, including an
# association which has a <tt>:through</tt> option itself. For example:
@@ -623,12 +656,12 @@ module ActiveRecord
# belongs_to :commenter
# end
#
- # When using nested association, you will not be able to modify the association because there
+ # When using a nested association, you will not be able to modify the association because there
# is not enough information to know what modification to make. For example, if you tried to
# add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
# intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
#
- # == Polymorphic Associations
+ # == Polymorphic \Associations
#
# Polymorphic associations on models are not restricted on what types of models they
# can be associated with. Rather, they specify an interface that a +has_many+ association
@@ -655,11 +688,14 @@ module ActiveRecord
# and member posts that use the posts table for STI. In this case, there must be a +type+
# column in the posts table.
#
+ # Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+.
+ # The +class_name+ of the +attachable+ is passed as a String.
+ #
# class Asset < ActiveRecord::Base
# belongs_to :attachable, polymorphic: true
#
- # def attachable_type=(sType)
- # super(sType.to_s.classify.constantize.base_class.to_s)
+ # def attachable_type=(class_name)
+ # super(class_name.constantize.base_class.to_s)
# end
# end
#
@@ -690,9 +726,9 @@ module ActiveRecord
# == Eager loading of associations
#
# Eager loading is a way to find objects of a certain class and a number of named associations.
- # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100
+ # It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100
# posts that each need to display their author triggers 101 database queries. Through the
- # use of eager loading, the 101 queries can be reduced to 2.
+ # use of eager loading, the number of queries will be reduced from 101 to 2.
#
# class Post < ActiveRecord::Base
# belongs_to :author
@@ -722,16 +758,16 @@ module ActiveRecord
# Post.includes(:author, :comments).each do |post|
#
# This will load all comments with a single query. This reduces the total number of queries
- # to 3. More generally the number of queries will be 1 plus the number of associations
+ # to 3. In general, the number of queries will be 1 plus the number of associations
# named (except if some of the associations are polymorphic +belongs_to+ - see below).
#
# To include a deep hierarchy of associations, use a hash:
#
- # Post.includes(:author, {comments: {author: :gravatar}}).each do |post|
+ # Post.includes(:author, { comments: { author: :gravatar } }).each do |post|
#
- # That'll grab not only all the comments but all their authors and gravatar pictures.
- # You can mix and match symbols, arrays and hashes in any combination to describe the
- # associations you want to load.
+ # The above code will load all the comments and all of their associated
+ # authors and gravatars. You can mix and match any combination of symbols,
+ # arrays, and hashes to retrieve the associations you want to load.
#
# All of this power shouldn't fool you into thinking that you can pull out huge amounts
# of data with no performance penalty just because you've reduced the number of queries.
@@ -740,10 +776,10 @@ module ActiveRecord
# cut down on the number of queries in a situation as the one described above.
#
# Since only one table is loaded at a time, conditions or orders cannot reference tables
- # other than the main one. If this is the case Active Record falls back to the previously
- # used LEFT OUTER JOIN based strategy. For example
+ # other than the main one. If this is the case, Active Record falls back to the previously
+ # used LEFT OUTER JOIN based strategy. For example:
#
- # Post.includes([:author, :comments]).where(['comments.approved = ?', true]).all
+ # Post.includes([:author, :comments]).where(['comments.approved = ?', true])
#
# This will result in a single SQL query with joins along the lines of:
# <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
@@ -751,11 +787,16 @@ module ActiveRecord
# like this can have unintended consequences.
# In the above example posts with no approved comments are not returned at all, because
# the conditions apply to the SQL statement as a whole and not just to the association.
+ #
# You must disambiguate column references for this fallback to happen, for example
# <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not.
#
- # If you do want eager load only some members of an association it is usually more natural
- # to include an association which has conditions defined on it:
+ # If you want to load all posts (including posts with no approved comments) then write
+ # your own LEFT OUTER JOIN query using ON
+ #
+ # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")
+ #
+ # In this case it is usually more natural to include an association which has conditions defined on it:
#
# class Post < ActiveRecord::Base
# has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
@@ -790,7 +831,7 @@ module ActiveRecord
# For example if all the addressables are either of class Person or Company then a total
# of 3 queries will be executed. The list of addressable types to load is determined on
# the back of the addresses loaded. This is not supported if Active Record has to fallback
- # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError.
+ # to the previous implementation of eager loading and will raise <tt>ActiveRecord::EagerLoadPolymorphicError</tt>.
# The reason is that the parent model's type is a column value so its corresponding table
# name cannot be put in the +FROM+/+JOIN+ clauses of that query.
#
@@ -951,7 +992,7 @@ module ActiveRecord
#
# The <tt>:dependent</tt> option can have different values which specify how the deletion
# is done. For more information, see the documentation for this option on the different
- # specific association types. When no option is given, the behaviour is to do nothing
+ # specific association types. When no option is given, the behavior is to do nothing
# with the associated records when destroying a record.
#
# Note that <tt>:dependent</tt> is implemented using Rails' callback
@@ -967,8 +1008,8 @@ module ActiveRecord
# For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they
# cause the records in the join table to be removed.
#
- # For +has_many+, <tt>destroy</tt> will always call the <tt>destroy</tt> method of the
- # record(s) being removed so that callbacks are run. However <tt>delete</tt> will either
+ # For +has_many+, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the
+ # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either
# do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
# if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
# The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for
@@ -989,7 +1030,7 @@ module ActiveRecord
# associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+
# <tt>:through</tt>, the join records will be deleted, but the associated records won't.
#
- # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by_name('food'))</tt>
+ # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt>
# you would want the 'food' tag to be unlinked from the post, rather than for the tag itself
# to be removed from the database.
#
@@ -1020,13 +1061,16 @@ module ActiveRecord
# Specifies a one-to-many association. The following methods for retrieval and query of
# collections of associated objects will be added:
#
+ # +collection+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
+ #
# [collection(force_reload = false)]
# Returns an array of all the associated objects.
# An empty array is returned if none are found.
# [collection<<(object, ...)]
# Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
- # Note that this operation instantly fires update sql without waiting for the save or update call on the
- # parent object.
+ # Note that this operation instantly fires update SQL without waiting for the save or update call on the
+ # parent object, unless the parent object is a new record.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by setting their foreign keys to +NULL+.
# Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>,
@@ -1061,10 +1105,10 @@ module ActiveRecord
# [collection.size]
# Returns the number of associated objects.
# [collection.find(...)]
- # Finds an associated object according to the same rules as ActiveRecord::Base.find.
+ # Finds an associated object according to the same rules as <tt>ActiveRecord::Base.find</tt>.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
- # Uses the same rules as ActiveRecord::Base.exists?.
+ # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
# [collection.build(attributes = {}, ...)]
# Returns one or more new objects of the collection type that have been instantiated
# with +attributes+ and linked to this object through a foreign key, but have not yet
@@ -1074,14 +1118,14 @@ module ActiveRecord
# with +attributes+, linked to this object through a foreign key, and that has already
# been saved (if it passed the validation). *Note*: This only works if the base model
# already exists in the DB, not if it is a new (unsaved) record!
- #
- # (*Note*: +collection+ is replaced with the symbol passed as the first argument, so
- # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.)
+ # [collection.create!(attributes = {})]
+ # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
+ # if the record is invalid.
#
# === Example
#
- # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
- # * <tt>Firm#clients</tt> (similar to <tt>Clients.all conditions: ["firm_id = ?", id]</tt>)
+ # A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add:
+ # * <tt>Firm#clients</tt> (similar to <tt>Client.where(firm_id: id)</tt>)
# * <tt>Firm#clients<<</tt>
# * <tt>Firm#clients.delete</tt>
# * <tt>Firm#clients.destroy</tt>
@@ -1091,11 +1135,37 @@ module ActiveRecord
# * <tt>Firm#clients.clear</tt>
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
- # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, conditions: "firm_id = #{id}")</tt>)
+ # * <tt>Firm#clients.find</tt> (similar to <tt>Client.where(firm_id: id).find(id)</tt>)
# * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>)
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>)
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific set of records or customize the generated
+ # query when you access the associated collection.
+ #
+ # Scope examples:
+ # has_many :comments, -> { where(author_id: 1) }
+ # has_many :employees, -> { joins(:address) }
+ # has_many :posts, ->(post) { where("max_post_length > ?", post.length) }
+ #
+ # === Extensions
+ #
+ # The +extension+ argument allows you to pass a block into a has_many
+ # association. This is useful for adding new finders, creators and other
+ # factory-type methods to be used as part of the association.
+ #
+ # Extension examples:
+ # has_many :employees do
+ # def find_or_create_by_name(name)
+ # first_name, last_name = name.split(" ", 2)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
+ # end
+ # end
#
# === Options
# [:class_name]
@@ -1107,20 +1177,26 @@ module ActiveRecord
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+
# association will use "person_id" as the default <tt>:foreign_key</tt>.
+ # [:foreign_type]
+ # Specify the column used to store the associated object's type, if this is a polymorphic
+ # association. By default this is guessed to be the name of the polymorphic association
+ # specified on "as" option with a "_type" suffix. So a class that defines a
+ # <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the
+ # default <tt>:foreign_type</tt>.
# [:primary_key]
- # Specify the method that returns the primary key used for the association. By default this is +id+.
+ # Specify the name of the column to use as the primary key for the association. By default this is +id+.
# [:dependent]
# Controls what happens to the associated objects when
# their owner is destroyed. Note that these are implemented as
# callbacks, and Rails executes callbacks in order. Therefore, other
- # similar callbacks may affect the :dependent behavior, and the
- # :dependent behavior may affect other callbacks.
+ # similar callbacks may affect the <tt>:dependent</tt> behavior, and the
+ # <tt>:dependent</tt> behavior may affect other callbacks.
#
- # * <tt>:destroy</tt> causes all the associated objects to also be destroyed
- # * <tt>:delete_all</tt> causes all the asssociated objects to be deleted directly from the database (so callbacks will not execute)
+ # * <tt>:destroy</tt> causes all the associated objects to also be destroyed.
+ # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed).
# * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed.
- # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records
- # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects
+ # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records.
+ # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects.
#
# If using with the <tt>:through</tt> option, the association on the join model must be
# a +belongs_to+, and the records which get deleted are the join records, rather than
@@ -1160,8 +1236,8 @@ module ActiveRecord
# If true, always save the associated objects or destroy them if marked for destruction,
# when saving the parent object. If false, never save or destroy the associated objects.
# By default, only save associated objects that are new records. This option is implemented as a
- # before_save callback. Because callbacks are run in the order they are defined, associated objects
- # may need to be explicitly saved in any user-defined before_save callbacks.
+ # +before_save+ callback. Because callbacks are run in the order they are defined, associated objects
+ # may need to be explicitly saved in any user-defined +before_save+ callbacks.
#
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
@@ -1173,28 +1249,33 @@ module ActiveRecord
# Option examples:
# has_many :comments, -> { order "posted_on" }
# has_many :comments, -> { includes :author }
- # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person"
+ # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
# has_many :tracks, -> { order "position" }, dependent: :destroy
# has_many :comments, dependent: :nullify
# has_many :tags, as: :taggable
# has_many :reports, -> { readonly }
# has_many :subscribers, through: :subscriptions, source: :user
def has_many(name, scope = nil, options = {}, &extension)
- Builder::HasMany.build(self, name, scope, options, &extension)
+ reflection = Builder::HasMany.build(self, name, scope, options, &extension)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
# if the other class contains the foreign key. If the current class contains the foreign key,
# then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview
- # on when to use has_one and when to use belongs_to.
+ # on when to use +has_one+ and when to use +belongs_to+.
#
# The following methods for retrieval and query of a single associated object will be added:
#
+ # +association+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
+ #
# [association(force_reload = false)]
# Returns the associated object. +nil+ is returned if none is found.
# [association=(associate)]
# Assigns the associate object, extracts the primary key, sets it as the foreign key,
- # and saves the associate object.
+ # and saves the associate object. To avoid database inconsistencies, permanently deletes an existing
+ # associated object when assigning a new one, even if the new one isn't saved to database.
# [build_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key, but has not
@@ -1207,21 +1288,29 @@ module ActiveRecord
# Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
# if the record is invalid.
#
- # (+association+ is replaced with the symbol passed as the first argument, so
- # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.)
- #
# === Example
#
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
- # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.first(conditions: "account_id = #{id}")</tt>)
+ # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</tt>)
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
# * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>)
#
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific record or customize the generated query
+ # when you access the associated object.
+ #
+ # Scope examples:
+ # has_one :author, -> { where(comment_id: 1) }
+ # has_one :employer, -> { joins(:company) }
+ # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) }
+ #
# === Options
#
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# Options are:
# [:class_name]
@@ -1233,7 +1322,7 @@ module ActiveRecord
# its owner is destroyed:
#
# * <tt>:destroy</tt> causes the associated object to also be destroyed
- # * <tt>:delete</tt> causes the asssociated object to be deleted directly from the database (so callbacks will not execute)
+ # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute)
# * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed.
# * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
@@ -1241,6 +1330,12 @@ module ActiveRecord
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association
# will use "person_id" as the default <tt>:foreign_key</tt>.
+ # [:foreign_type]
+ # Specify the column used to store the associated object's type, if this is a polymorphic
+ # association. By default this is guessed to be the name of the polymorphic association
+ # specified on "as" option with a "_type" suffix. So a class that defines a
+ # <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the
+ # default <tt>:foreign_type</tt>.
# [:primary_key]
# Specify the method that returns the primary key used for the association. By default this is +id+.
# [:as]
@@ -1271,6 +1366,10 @@ module ActiveRecord
# that is the inverse of this <tt>has_one</tt> association. Does not work in combination
# with <tt>:through</tt> or <tt>:as</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
@@ -1282,8 +1381,10 @@ module ActiveRecord
# has_one :boss, readonly: :true
# has_one :club, through: :membership
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
+ # has_one :credit_card, required: true
def has_one(name, scope = nil, options = {})
- Builder::HasOne.build(self, name, scope, options)
+ reflection = Builder::HasOne.build(self, name, scope, options)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1294,6 +1395,9 @@ module ActiveRecord
# Methods will be added for retrieval and query for a single associated object, for which
# this object holds an id:
#
+ # +association+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
+ #
# [association(force_reload = false)]
# Returns the associated object. +nil+ is returned if none is found.
# [association=(associate)]
@@ -1309,9 +1413,6 @@ module ActiveRecord
# Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
# if the record is invalid.
#
- # (+association+ is replaced with the symbol passed as the first argument, so
- # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.)
- #
# === Example
#
# A Post class declares <tt>belongs_to :author</tt>, which will add:
@@ -1320,7 +1421,18 @@ module ActiveRecord
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
# * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific record or customize the generated query
+ # when you access the associated object.
+ #
+ # Scope examples:
+ # belongs_to :user, -> { where(id: 2) }
+ # belongs_to :user, -> { joins(:friends) }
+ # belongs_to :level, ->(level) { where("game_level > ?", level.current) }
#
# === Options
#
@@ -1354,7 +1466,7 @@ module ActiveRecord
# class is created and decremented when it's destroyed. This requires that a column
# named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
# is used on the associate class (such as a Post class) - that is the migration for
- # <tt>#{table_name}_count</tt> is created on the associate class (such that Post.comments_count will
+ # <tt>#{table_name}_count</tt> is created on the associate class (such that <tt>Post.comments_count</tt> will
# return the count cached, see note below). You can also specify a custom counter
# cache column by providing a column name instead of a +true+/+false+ value to this
# option (e.g., <tt>counter_cache: :my_custom_counter</tt>.)
@@ -1374,7 +1486,7 @@ module ActiveRecord
#
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
# [:touch]
- # If true, the associated object will be touched (the updated_at/on attributes set to now)
+ # If true, the associated object will be touched (the updated_at/on attributes set to current time)
# when this record is either saved or destroyed. If you specify a symbol, that attribute
# will be updated with the current time in addition to the updated_at/on attribute.
# [:inverse_of]
@@ -1382,20 +1494,26 @@ module ActiveRecord
# object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
# combination with the <tt>:polymorphic</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# belongs_to :firm, foreign_key: "client_of"
# belongs_to :person, primary_key: "name", foreign_key: "person_name"
# belongs_to :author, class_name: "Person", foreign_key: "author_id"
- # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" },
+ # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
# class_name: "Coupon", foreign_key: "coupon_id"
# belongs_to :attachable, polymorphic: true
# belongs_to :project, readonly: true
# belongs_to :post, counter_cache: true
# belongs_to :company, touch: true
# belongs_to :company, touch: :employees_last_updated_at
+ # belongs_to :company, required: true
def belongs_to(name, scope = nil, options = {})
- Builder::BelongsTo.build(self, name, scope, options)
+ reflection = Builder::BelongsTo.build(self, name, scope, options)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a many-to-many relationship with another class. This associates two classes via an
@@ -1409,6 +1527,8 @@ module ActiveRecord
# to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes",
# but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
# custom <tt>:join_table</tt> option if you need to.
+ # If your tables share a common prefix, it will only appear once at the beginning. For example,
+ # the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products".
#
# The join table should not have a primary key or a model associated with it. You must manually generate the
# join table with a migration such as this:
@@ -1428,14 +1548,17 @@ module ActiveRecord
#
# Adds the following methods for retrieval and query:
#
+ # +collection+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
+ #
# [collection(force_reload = false)]
# Returns an array of all the associated objects.
# An empty array is returned if none are found.
# [collection<<(object, ...)]
# Adds one or more objects to the collection by creating associations in the join table
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
- # Note that this operation instantly fires update sql without waiting for the save or update call on the
- # parent object.
+ # Note that this operation instantly fires update SQL without waiting for the save or update call on the
+ # parent object, unless the parent object is a new record.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by removing their associations from the join table.
# This does not destroy the objects.
@@ -1457,10 +1580,10 @@ module ActiveRecord
# [collection.find(id)]
# Finds an associated object responding to the +id+ and that
# meets the condition that it has to be associated with this object.
- # Uses the same rules as ActiveRecord::Base.find.
+ # Uses the same rules as <tt>ActiveRecord::Base.find</tt>.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
- # Uses the same rules as ActiveRecord::Base.exists?.
+ # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
# [collection.build(attributes = {})]
# Returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
@@ -1469,9 +1592,6 @@ module ActiveRecord
# with +attributes+, linked to this object through the join table, and that has already been
# saved (if it passed the validation).
#
- # (+collection+ is replaced with the symbol passed as the first argument, so
- # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.)
- #
# === Example
#
# A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
@@ -1489,7 +1609,34 @@ module ActiveRecord
# * <tt>Developer#projects.exists?(...)</tt>
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>)
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>)
- # The declaration may include an options hash to specialize the behavior of the association.
+ # The declaration may include an +options+ hash to specialize the behavior of the association.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific set of records or customize the generated
+ # query when you access the associated collection.
+ #
+ # Scope examples:
+ # has_and_belongs_to_many :projects, -> { includes :milestones, :manager }
+ # has_and_belongs_to_many :categories, ->(category) {
+ # where("default_category = ?", category.name)
+ # }
+ #
+ # === Extensions
+ #
+ # The +extension+ argument allows you to pass a block into a
+ # has_and_belongs_to_many association. This is useful for adding new
+ # finders, creators and other factory-type methods to be used as part of
+ # the association.
+ #
+ # Extension examples:
+ # has_and_belongs_to_many :contractors do
+ # def find_or_create_by_name(name)
+ # first_name, last_name = name.split(" ", 2)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
+ # end
+ # end
#
# === Options
#
@@ -1530,7 +1677,48 @@ module ActiveRecord
# has_and_belongs_to_many :categories, join_table: "prods_cats"
# has_and_belongs_to_many :categories, -> { readonly }
def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)
- Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension)
+ if scope.is_a?(Hash)
+ options = scope
+ scope = nil
+ end
+
+ habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
+
+ builder = Builder::HasAndBelongsToMany.new name, self, options
+
+ join_model = builder.through_model
+
+ # FIXME: we should move this to the internal constants. Also people
+ # should never directly access this constant so I'm not happy about
+ # setting it.
+ const_set join_model.name, join_model
+
+ middle_reflection = builder.middle_reflection join_model
+
+ Builder::HasMany.define_callbacks self, middle_reflection
+ Reflection.add_reflection self, middle_reflection.name, middle_reflection
+ middle_reflection.parent_reflection = [name.to_s, habtm_reflection]
+
+ include Module.new {
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def destroy_associations
+ association(:#{middle_reflection.name}).delete_all(:delete_all)
+ association(:#{name}).reset
+ super
+ end
+ RUBY
+ }
+
+ hm_options = {}
+ hm_options[:through] = middle_reflection.name
+ hm_options[:source] = join_model.right_reflection.name
+
+ [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name].each do |k|
+ hm_options[k] = options[k] if options.key? k
+ end
+
+ has_many name, scope, hm_options, &extension
+ self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection]
end
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 0c23029981..f9c9f8afda 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -5,32 +5,62 @@ module ActiveRecord
# Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
# ActiveRecord::Associations::ThroughAssociationScope
class AliasTracker # :nodoc:
- attr_reader :aliases, :table_joins, :connection
+ attr_reader :aliases, :connection
- # table_joins is an array of arel joins which might conflict with the aliases we assign here
- def initialize(connection = Base.connection, table_joins = [])
- @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) }
- @table_joins = table_joins
- @connection = connection
+ def self.empty(connection)
+ new connection, Hash.new(0)
end
- def aliased_table_for(table_name, aliased_name = nil)
- table_alias = aliased_name_for(table_name, aliased_name)
-
- if table_alias == table_name
- Arel::Table.new(table_name)
+ def self.create(connection, table_joins)
+ if table_joins.empty?
+ empty connection
else
- Arel::Table.new(table_name).alias(table_alias)
+ aliases = Hash.new { |h,k|
+ h[k] = initial_count_for(connection, k, table_joins)
+ }
+ new connection, aliases
end
end
- def aliased_name_for(table_name, aliased_name = nil)
- aliased_name ||= table_name
+ def self.initial_count_for(connection, name, table_joins)
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name = connection.quote_table_name(name).downcase
+ counts = table_joins.map do |join|
+ if join.is_a?(Arel::Nodes::StringJoin)
+ # Table names + table aliases
+ join.left.downcase.scan(
+ /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
+ ).size
+ elsif join.respond_to? :left
+ join.left.table_name == name ? 1 : 0
+ else
+ # this branch is reached by two tests:
+ #
+ # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37
+ # with :posts
+ #
+ # activerecord/test/cases/associations/eager_test.rb:1133
+ # with :comments
+ #
+ 0
+ end
+ end
+
+ counts.sum
+ end
+
+ # table_joins is an array of arel joins which might conflict with the aliases we assign here
+ def initialize(connection, aliases)
+ @aliases = aliases
+ @connection = connection
+ end
+
+ def aliased_table_for(table_name, aliased_name, **table_options)
if aliases[table_name].zero?
# If it's zero, we can have our table_name
aliases[table_name] = 1
- table_name
+ Arel::Table.new(table_name, table_options)
else
# Otherwise, we need to use an alias
aliased_name = connection.table_alias_for(aliased_name)
@@ -38,36 +68,17 @@ module ActiveRecord
# Update the count
aliases[aliased_name] += 1
- if aliases[aliased_name] > 1
+ table_alias = if aliases[aliased_name] > 1
"#{truncate(aliased_name)}_#{aliases[aliased_name]}"
else
aliased_name
end
+ Arel::Table.new(table_name, table_options).alias(table_alias)
end
end
private
- def initial_count_for(name)
- return 0 if Arel::Table === table_joins
-
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = connection.quote_table_name(name).downcase
-
- counts = table_joins.map do |join|
- if join.is_a?(Arel::Nodes::StringJoin)
- # Table names + table aliases
- join.left.downcase.scan(
- /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
- ).size
- else
- join.left.table_name == name ? 1 : 0
- end
- end
-
- counts.sum
- end
-
def truncate(name)
name.slice(0, connection.table_alias_length - 2)
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 3f0e4ca999..0d8e4ba870 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -13,11 +13,11 @@ module ActiveRecord
# BelongsToAssociation
# BelongsToPolymorphicAssociation
# CollectionAssociation
- # HasAndBelongsToManyAssociation
# HasManyAssociation
# HasManyThroughAssociation + ThroughAssociation
class Association #:nodoc:
attr_reader :owner, :target, :reflection
+ attr_accessor :inversed
delegate :options, :to => :reflection
@@ -30,7 +30,7 @@ module ActiveRecord
reset_scope
end
- # Returns the name of the table of the related class:
+ # Returns the name of the table of the associated class:
#
# post.comments.aliased_table_name # => "comments"
#
@@ -43,6 +43,7 @@ module ActiveRecord
@loaded = false
@target = nil
@stale_state = nil
+ @inversed = false
end
# Reloads the \target and returns +self+ on success.
@@ -60,18 +61,19 @@ module ActiveRecord
# Asserts the \target has been loaded setting the \loaded flag to +true+.
def loaded!
- @loaded = true
+ @loaded = true
@stale_state = stale_state
+ @inversed = false
end
# The target is stale if the target no longer points to the record(s) that the
# relevant foreign_key(s) refers to. If stale, the association accessor method
# on the owner will reload the target. It's up to subclasses to implement the
- # state_state method if relevant.
+ # stale_state method if relevant.
#
# Note that if the target has not been loaded, it is not considered stale.
def stale_target?
- loaded? && @stale_state != stale_state
+ !inversed && loaded? && @stale_state != stale_state
end
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
@@ -84,20 +86,15 @@ module ActiveRecord
target_scope.merge(association_scope)
end
- def scoped
- ActiveSupport::Deprecation.warn "#scoped is deprecated. use #scope instead."
- scope
- end
-
# The scope for this association.
#
# Note that the association_scope is merged into the target_scope only when the
- # scoped method is called. This is because at that point the call may be surrounded
+ # scope method is called. This is because at that point the call may be surrounded
# by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
# actually gets built.
def association_scope
if klass
- @association_scope ||= AssociationScope.new(self).scope
+ @association_scope ||= AssociationScope.scope(self, klass.connection)
end
end
@@ -107,13 +104,15 @@ module ActiveRecord
# Set the inverse association, if possible
def set_inverse_instance(record)
- if record && invertible_for?(record)
+ if invertible_for?(record)
inverse = record.association(inverse_reflection_for(record).name)
inverse.target = owner
+ inverse.inversed = true
end
+ record
end
- # This class of the target. belongs_to polymorphic overrides this to look at the
+ # Returns the class of the target. belongs_to polymorphic overrides this to look at the
# polymorphic_type field on the owner.
def klass
reflection.klass
@@ -122,7 +121,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- klass.all
+ AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all)
end
# Loads the \target if needed and returns it.
@@ -146,7 +145,7 @@ module ActiveRecord
def interpolate(sql, record = nil)
if sql.respond_to?(:to_proc)
- owner.send(:instance_exec, record, &sql)
+ owner.instance_exec(record, &sql)
else
sql
end
@@ -161,7 +160,14 @@ module ActiveRecord
def marshal_load(data)
reflection_name, ivars = data
ivars.each { |name, val| instance_variable_set(name, val) }
- @reflection = @owner.class.reflect_on_association(reflection_name)
+ @reflection = @owner.class._reflect_on_association(reflection_name)
+ end
+
+ def initialize_attributes(record) #:nodoc:
+ skip_assign = [reflection.foreign_key, reflection.type].compact
+ attributes = create_scope.except(*(record.changed - skip_assign))
+ record.assign_attributes(attributes)
+ set_inverse_instance(record)
end
private
@@ -173,7 +179,7 @@ module ActiveRecord
def creation_attributes
attributes = {}
- if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through]
+ if (reflection.has_one? || reflection.collection?) && !options[:through]
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
if reflection.options[:as]
@@ -189,13 +195,14 @@ module ActiveRecord
creation_attributes.each { |key, value| record[key] = value }
end
- # Should be true if there is a foreign key present on the owner which
+ # Returns true if there is a foreign key present on the owner which
# references the target. This is used to determine whether we can load
# the target if the owner is currently a new record (and therefore
- # without a key).
+ # without a key). If the owner is a new record then foreign_key must
+ # be present in order to load target.
#
# Currently implemented by belongs_to (vanilla and polymorphic) and
- # has_one/has_many :through associations which go through a belongs_to
+ # has_one/has_many :through associations which go through a belongs_to.
def foreign_key_present?
false
end
@@ -203,7 +210,7 @@ module ActiveRecord
# Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
# the kind of the class of the associated objects. Meant to be used as
# a sanity check when you are about to assign an associated record.
- def raise_on_type_mismatch(record)
+ def raise_on_type_mismatch!(record)
unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize)
message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
raise ActiveRecord::AssociationTypeMismatch, message
@@ -217,9 +224,15 @@ module ActiveRecord
reflection.inverse_of
end
- # Is this association invertible? Can be redefined by subclasses.
+ # Returns true if inverse association on the given record needs to be set.
+ # This method is redefined by subclasses.
def invertible_for?(record)
- inverse_reflection_for(record)
+ foreign_key_for?(record) && inverse_reflection_for(record)
+ end
+
+ # Returns true if record contains the foreign_key
+ def foreign_key_for?(record)
+ record.has_attribute?(reflection.foreign_key)
end
# This should be implemented to return the values of the relevant key(s) on the owner,
@@ -232,9 +245,7 @@ module ActiveRecord
def build_record(attributes)
reflection.build_association(attributes) do |record|
- skip_assign = [reflection.foreign_key, reflection.type].compact
- attributes = create_scope.except(*(record.changed - skip_assign))
- record.assign_attributes(attributes)
+ initialize_attributes(record)
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 1303822868..53f65920e1 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -1,113 +1,184 @@
module ActiveRecord
module Associations
class AssociationScope #:nodoc:
- include JoinHelper
+ def self.scope(association, connection)
+ INSTANCE.scope association, connection
+ end
+
+ class BindSubstitution
+ def initialize(block)
+ @block = block
+ end
+
+ def bind_value(scope, column, value, connection)
+ substitute = connection.substitute_at(column)
+ scope.bind_values += [[column, @block.call(value)]]
+ substitute
+ end
+ end
- attr_reader :association, :alias_tracker
+ def self.create(&block)
+ block = block ? block : lambda { |val| val }
+ new BindSubstitution.new(block)
+ end
+
+ def initialize(bind_substitution)
+ @bind_substitution = bind_substitution
+ end
- delegate :klass, :owner, :reflection, :interpolate, :to => :association
- delegate :chain, :scope_chain, :options, :source_options, :active_record, :to => :reflection
+ INSTANCE = create
+
+ def scope(association, connection)
+ klass = association.klass
+ reflection = association.reflection
+ scope = klass.unscoped
+ owner = association.owner
+ alias_tracker = AliasTracker.empty connection
+
+ scope.extending! Array(reflection.options[:extend])
+ add_constraints(scope, owner, klass, reflection, alias_tracker)
+ end
- def initialize(association)
- @association = association
- @alias_tracker = AliasTracker.new klass.connection
+ def join_type
+ Arel::Nodes::InnerJoin
end
- def scope
- scope = klass.unscoped
- scope.merge! eval_scope(klass, reflection.scope) if reflection.scope
- add_constraints(scope)
+ def self.get_bind_values(owner, chain)
+ binds = []
+ last_reflection = chain.last
+
+ binds << last_reflection.join_id_for(owner)
+ if last_reflection.type
+ binds << owner.class.base_class.name
+ end
+
+ chain.each_cons(2).each do |reflection, next_reflection|
+ if reflection.type
+ binds << next_reflection.klass.base_class.name
+ end
+ end
+ binds
end
private
- def column_for(table_name, column_name)
- columns = alias_tracker.connection.schema_cache.columns_hash[table_name]
+ def construct_tables(chain, klass, refl, alias_tracker)
+ chain.map do |reflection|
+ alias_tracker.aliased_table_for(
+ table_name_for(reflection, klass, refl),
+ table_alias_for(reflection, refl, reflection != refl),
+ type_caster: klass.type_caster,
+ )
+ end
+ end
+
+ def table_alias_for(reflection, refl, join = false)
+ name = "#{reflection.plural_name}_#{alias_suffix(refl)}"
+ name << "_join" if join
+ name
+ end
+
+ def join(table, constraint)
+ table.create_join(table, table.create_on(constraint), join_type)
+ end
+
+ def column_for(table_name, column_name, connection)
+ columns = connection.schema_cache.columns_hash(table_name)
columns[column_name]
end
- def bind_value(scope, column, value)
- substitute = alias_tracker.connection.substitute_at(
- column, scope.bind_values.length)
- scope.bind_values += [[column, value]]
- substitute
+ def bind_value(scope, column, value, connection)
+ @bind_substitution.bind_value scope, column, value, connection
end
- def bind(scope, table_name, column_name, value)
- column = column_for table_name, column_name
- bind_value scope, column, value
+ def bind(scope, table_name, column_name, value, connection)
+ column = column_for table_name, column_name, connection
+ bind_value scope, column, value, connection
end
- def add_constraints(scope)
- tables = construct_tables
+ def last_chain_scope(scope, table, reflection, owner, connection, assoc_klass)
+ join_keys = reflection.join_keys(assoc_klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
- chain.each_with_index do |reflection, i|
- table, foreign_table = tables.shift, tables.first
+ bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], connection
+ scope = scope.where(table[key].eq(bind_val))
- if reflection.source_macro == :has_and_belongs_to_many
- join_table = tables.shift
+ if reflection.type
+ value = owner.class.base_class.name
+ bind_val = bind scope, table.table_name, reflection.type, value, connection
+ scope = scope.where(table[reflection.type].eq(bind_val))
+ else
+ scope
+ end
+ end
- scope = scope.joins(join(
- join_table,
- table[reflection.association_primary_key].
- eq(join_table[reflection.association_foreign_key])
- ))
+ def next_chain_scope(scope, table, reflection, connection, assoc_klass, foreign_table, next_reflection)
+ join_keys = reflection.join_keys(assoc_klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
- table, foreign_table = join_table, tables.first
- end
+ constraint = table[key].eq(foreign_table[foreign_key])
- if reflection.source_macro == :belongs_to
- if reflection.options[:polymorphic]
- key = reflection.association_primary_key(klass)
- else
- key = reflection.association_primary_key
- end
+ if reflection.type
+ value = next_reflection.klass.base_class.name
+ bind_val = bind scope, table.table_name, reflection.type, value, connection
+ scope = scope.where(table[reflection.type].eq(bind_val))
+ end
- foreign_key = reflection.foreign_key
- else
- key = reflection.foreign_key
- foreign_key = reflection.active_record_primary_key
- end
+ scope = scope.joins(join(foreign_table, constraint))
+ end
- if reflection == chain.last
- bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key]
- scope = scope.where(table[key].eq(bind_val))
+ def add_constraints(scope, owner, assoc_klass, refl, tracker)
+ chain = refl.chain
+ scope_chain = refl.scope_chain
+ connection = tracker.connection
- if reflection.type
- value = owner.class.base_class.name
- bind_val = bind scope, table.table_name, reflection.type.to_s, value
- scope = scope.where(table[reflection.type].eq(bind_val))
- end
- else
- constraint = table[key].eq(foreign_table[foreign_key])
+ tables = construct_tables(chain, assoc_klass, refl, tracker)
- if reflection.type
- type = chain[i + 1].klass.base_class.name
- constraint = constraint.and(table[reflection.type].eq(type))
- end
+ owner_reflection = chain.last
+ table = tables.last
+ scope = last_chain_scope(scope, table, owner_reflection, owner, connection, assoc_klass)
+
+ chain.each_with_index do |reflection, i|
+ table, foreign_table = tables.shift, tables.first
- scope = scope.joins(join(foreign_table, constraint))
+ unless reflection == chain.last
+ next_reflection = chain[i + 1]
+ scope = next_chain_scope(scope, table, reflection, connection, assoc_klass, foreign_table, next_reflection)
end
+ is_first_chain = i == 0
+ klass = is_first_chain ? assoc_klass : reflection.klass
+
# Exclude the scope of the association itself, because that
# was already merged in the #scope method.
- (scope_chain[i] - [self.reflection.scope]).each do |scope_chain_item|
- item = eval_scope(reflection.klass, scope_chain_item)
+ scope_chain[i].each do |scope_chain_item|
+ item = eval_scope(klass, scope_chain_item, owner)
+
+ if scope_chain_item == refl.scope
+ scope.merge! item.except(:where, :includes, :bind)
+ end
+
+ if is_first_chain
+ scope.includes! item.includes_values
+ end
- scope.includes! item.includes_values
scope.where_values += item.where_values
+ scope.bind_values += item.bind_values
+ scope.order_values |= item.order_values
end
end
scope
end
- def alias_suffix
- reflection.name
+ def alias_suffix(refl)
+ refl.name
end
- def table_name_for(reflection)
- if reflection == self.reflection
+ def table_name_for(reflection, klass, refl)
+ if reflection == refl
# If this is a polymorphic belongs_to, we want to get the klass from the
# association because it depends on the polymorphic_type attribute of
# the owner
@@ -117,12 +188,8 @@ module ActiveRecord
end
end
- def eval_scope(klass, scope)
- if scope.is_a?(Relation)
- scope
- else
- klass.unscoped.instance_exec(owner, &scope)
- end
+ def eval_scope(klass, scope, owner)
+ klass.unscoped.instance_exec(owner, &scope)
end
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 75f72c1a46..c63b42e2a0 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # = Active Record Belongs To Associations
+ # = Active Record Belongs To Association
module Associations
class BelongsToAssociation < SingularAssociation #:nodoc:
@@ -8,13 +8,16 @@ module ActiveRecord
end
def replace(record)
- raise_on_type_mismatch(record) if record
-
- update_counters(record)
- replace_keys(record)
- set_inverse_instance(record)
-
- @updated = true if record
+ if record
+ raise_on_type_mismatch!(record)
+ update_counters(record)
+ replace_keys(record)
+ set_inverse_instance(record)
+ @updated = true
+ else
+ decrement_counters
+ remove_keys
+ end
self.target = record
end
@@ -28,61 +31,81 @@ module ActiveRecord
@updated
end
+ def decrement_counters # :nodoc:
+ with_cache_name { |name| decrement_counter name }
+ end
+
+ def increment_counters # :nodoc:
+ with_cache_name { |name| increment_counter name }
+ end
+
private
def find_target?
!loaded? && foreign_key_present? && klass
end
- def update_counters(record)
+ def with_cache_name
counter_cache_name = reflection.counter_cache_column
+ return unless counter_cache_name && owner.persisted?
+ yield counter_cache_name
+ end
- if counter_cache_name && owner.persisted? && different_target?(record)
- if record
- record.class.increment_counter(counter_cache_name, record.id)
- end
+ def update_counters(record)
+ with_cache_name do |name|
+ return unless different_target? record
+ record.class.increment_counter(name, record.id)
+ decrement_counter name
+ end
+ end
+
+ def decrement_counter(counter_cache_name)
+ if foreign_key_present?
+ klass.decrement_counter(counter_cache_name, target_id)
+ end
+ end
- if foreign_key_present?
- klass.decrement_counter(counter_cache_name, target_id)
- end
+ def increment_counter(counter_cache_name)
+ if foreign_key_present?
+ klass.increment_counter(counter_cache_name, target_id)
end
end
# Checks whether record is different to the current target, without loading it
def different_target?(record)
- record.nil? && owner[reflection.foreign_key] ||
- record && record.id != owner[reflection.foreign_key]
+ record.id != owner._read_attribute(reflection.foreign_key)
end
def replace_keys(record)
- if record
- owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
- else
- owner[reflection.foreign_key] = nil
- end
+ owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class))
+ end
+
+ def remove_keys
+ owner[reflection.foreign_key] = nil
end
def foreign_key_present?
- owner[reflection.foreign_key]
+ owner._read_attribute(reflection.foreign_key)
end
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
# has_one associations.
def invertible_for?(record)
inverse = inverse_reflection_for(record)
- inverse && inverse.macro == :has_one
+ inverse && inverse.has_one?
end
def target_id
if options[:primary_key]
owner.send(reflection.name).try(:id)
else
- owner[reflection.foreign_key]
+ owner._read_attribute(reflection.foreign_key)
end
end
def stale_state
- owner[reflection.foreign_key] && owner[reflection.foreign_key].to_s
+ result = owner._read_attribute(reflection.foreign_key)
+ result && result.to_s
end
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index 88ce03a3cd..b710cf6bdb 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -11,7 +11,12 @@ module ActiveRecord
def replace_keys(record)
super
- owner[reflection.foreign_type] = record && record.class.base_class.name
+ owner[reflection.foreign_type] = record.class.base_class.name
+ end
+
+ def remove_keys
+ super
+ owner[reflection.foreign_type] = nil
end
def different_target?(record)
@@ -22,7 +27,7 @@ module ActiveRecord
reflection.polymorphic_inverse_of(record.class)
end
- def raise_on_type_mismatch(record)
+ def raise_on_type_mismatch!(record)
# A polymorphic association cannot have a type mismatch, by definition
end
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 5c37f42794..88406740d8 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,70 +1,111 @@
+# This is the parent Association class which defines the variables
+# used by all associations.
+#
+# The hierarchy is defined as follows:
+# Association
+# - SingularAssociation
+# - BelongsToAssociation
+# - HasOneAssociation
+# - CollectionAssociation
+# - HasManyAssociation
+
module ActiveRecord::Associations::Builder
class Association #:nodoc:
class << self
- attr_accessor :valid_options
+ attr_accessor :extensions
end
+ self.extensions = []
- self.valid_options = [:class_name, :foreign_key, :validate]
+ VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] # :nodoc:
- attr_reader :model, :name, :scope, :options, :reflection
+ def self.build(model, name, scope, options, &block)
+ if model.dangerous_attribute_method?(name)
+ raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
+ "this will conflict with a method #{name} already defined by Active Record. " \
+ "Please choose a different association name."
+ end
- def self.build(*args, &block)
- new(*args, &block).build
+ extension = define_extensions model, name, &block
+ reflection = create_reflection model, name, scope, options, extension
+ define_accessors model, reflection
+ define_callbacks model, reflection
+ define_validations model, reflection
+ reflection
end
- def initialize(model, name, scope, options)
+ def self.create_reflection(model, name, scope, options, extension = nil)
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
- @model = model
- @name = name
-
if scope.is_a?(Hash)
- @scope = nil
- @options = scope
- else
- @scope = scope
- @options = options
+ options = scope
+ scope = nil
+ end
+
+ validate_options(options)
+
+ scope = build_scope(scope, extension)
+
+ ActiveRecord::Reflection.create(macro, name, scope, options, model)
+ end
+
+ def self.build_scope(scope, extension)
+ new_scope = scope
+
+ if scope && scope.arity == 0
+ new_scope = proc { instance_exec(&scope) }
end
- if @scope && @scope.arity == 0
- prev_scope = @scope
- @scope = proc { instance_exec(&prev_scope) }
+ if extension
+ new_scope = wrap_scope new_scope, extension
end
+
+ new_scope
end
- def mixin
- @model.generated_feature_methods
+ def self.wrap_scope(scope, extension)
+ scope
end
- include Module.new { def build; end }
+ def self.macro
+ raise NotImplementedError
+ end
- def build
- validate_options
- define_accessors
- configure_dependency if options[:dependent]
- @reflection = model.create_reflection(macro, name, scope, options, model)
- super # provides an extension point
- @reflection
+ def self.valid_options(options)
+ VALID_OPTIONS + Association.extensions.flat_map(&:valid_options)
end
- def macro
- raise NotImplementedError
+ def self.validate_options(options)
+ options.assert_valid_keys(valid_options(options))
end
- def valid_options
- Association.valid_options
+ def self.define_extensions(model, name)
end
- def validate_options
- options.assert_valid_keys(valid_options)
+ def self.define_callbacks(model, reflection)
+ if dependent = reflection.options[:dependent]
+ check_dependent_options(dependent)
+ add_destroy_callbacks(model, reflection)
+ end
+
+ Association.extensions.each do |extension|
+ extension.build model, reflection
+ end
end
- def define_accessors
- define_readers
- define_writers
+ # Defines the setter and getter methods for the association
+ # class Post < ActiveRecord::Base
+ # has_many :comments
+ # end
+ #
+ # Post.first.comments and Post.first.comments= methods are defined by this method...
+ def self.define_accessors(model, reflection)
+ mixin = model.generated_association_methods
+ name = reflection.name
+ define_readers(mixin, name)
+ define_writers(mixin, name)
end
- def define_readers
+ def self.define_readers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}(*args)
association(:#{name}).reader(*args)
@@ -72,7 +113,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}=(value)
association(:#{name}).writer(value)
@@ -80,29 +121,23 @@ module ActiveRecord::Associations::Builder
CODE
end
- def configure_dependency
- unless valid_dependent_options.include? options[:dependent]
- raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}"
- end
-
- if options[:dependent] == :restrict
- ActiveSupport::Deprecation.warn(
- "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \
- "provides the same functionality."
- )
- end
+ def self.define_validations(model, reflection)
+ # noop
+ end
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{macro}_dependent_for_#{name}
- association(:#{name}).handle_dependency
- end
- CODE
+ def self.valid_dependent_options
+ raise NotImplementedError
+ end
- model.before_destroy "#{macro}_dependent_for_#{name}"
+ def self.check_dependent_options(dependent)
+ unless valid_dependent_options.include? dependent
+ raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
+ end
end
- def valid_dependent_options
- raise NotImplementedError
+ def self.add_destroy_callbacks(model, reflection)
+ name = reflection.name
+ model.before_destroy lambda { |o| o.association(name).handle_dependency }
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 2f2600b7fb..d0ad57f9c6 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -1,66 +1,114 @@
module ActiveRecord::Associations::Builder
class BelongsTo < SingularAssociation #:nodoc:
- def macro
+ def self.macro
:belongs_to
end
- def valid_options
- super + [:foreign_type, :polymorphic, :touch]
+ def self.valid_options(options)
+ super + [:foreign_type, :polymorphic, :touch, :counter_cache]
end
- def constructable?
- !options[:polymorphic]
+ def self.valid_dependent_options
+ [:destroy, :delete]
end
- def build
- reflection = super
- add_counter_cache_callbacks(reflection) if options[:counter_cache]
- add_touch_callbacks(reflection) if options[:touch]
- reflection
+ def self.define_callbacks(model, reflection)
+ super
+ add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
+ add_touch_callbacks(model, reflection) if reflection.options[:touch]
end
- def add_counter_cache_callbacks(reflection)
- cache_column = reflection.counter_cache_column
+ def self.define_accessors(mixin, reflection)
+ super
+ add_counter_cache_methods mixin
+ end
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def belongs_to_counter_cache_after_create_for_#{name}
- record = #{name}
- record.class.increment_counter(:#{cache_column}, record.id) unless record.nil?
- end
+ def self.add_counter_cache_methods(mixin)
+ return if mixin.method_defined? :belongs_to_counter_cache_after_update
+
+ mixin.class_eval do
+ def belongs_to_counter_cache_after_update(reflection)
+ foreign_key = reflection.foreign_key
+ cache_column = reflection.counter_cache_column
- def belongs_to_counter_cache_before_destroy_for_#{name}
- unless marked_for_destruction?
- record = #{name}
- record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil?
+ if (@_after_create_counter_called ||= false)
+ @_after_create_counter_called = false
+ elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable?
+ model = reflection.klass
+ foreign_key_was = attribute_was foreign_key
+ foreign_key = attribute foreign_key
+
+ if foreign_key && model.respond_to?(:increment_counter)
+ model.increment_counter(cache_column, foreign_key)
+ end
+ if foreign_key_was && model.respond_to?(:decrement_counter)
+ model.decrement_counter(cache_column, foreign_key_was)
+ end
end
end
- CODE
+ end
+ end
+
+ def self.add_counter_cache_callbacks(model, reflection)
+ cache_column = reflection.counter_cache_column
- model.after_create "belongs_to_counter_cache_after_create_for_#{name}"
- model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}"
+ model.after_update lambda { |record|
+ record.belongs_to_counter_cache_after_update(reflection)
+ }
klass = reflection.class_name.safe_constantize
klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)
end
- def add_touch_callbacks(reflection)
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def belongs_to_touch_after_save_or_destroy_for_#{name}
- record = #{name}
+ def self.touch_record(o, foreign_key, name, touch) # :nodoc:
+ old_foreign_id = o.changed_attributes[foreign_key]
- unless record.nil?
- record.touch #{options[:touch].inspect if options[:touch] != true}
+ if old_foreign_id
+ association = o.association(name)
+ reflection = association.reflection
+ if reflection.polymorphic?
+ klass = o.public_send("#{reflection.foreign_type}_was").constantize
+ else
+ klass = association.klass
+ end
+ old_record = klass.find_by(klass.primary_key => old_foreign_id)
+
+ if old_record
+ if touch != true
+ old_record.touch touch
+ else
+ old_record.touch
end
end
- CODE
+ end
- model.after_save "belongs_to_touch_after_save_or_destroy_for_#{name}"
- model.after_touch "belongs_to_touch_after_save_or_destroy_for_#{name}"
- model.after_destroy "belongs_to_touch_after_save_or_destroy_for_#{name}"
+ record = o.send name
+ if record && record.persisted?
+ if touch != true
+ record.touch touch
+ else
+ record.touch
+ end
+ end
end
- def valid_dependent_options
- [:destroy, :delete]
+ def self.add_touch_callbacks(model, reflection)
+ foreign_key = reflection.foreign_key
+ n = reflection.name
+ touch = reflection.options[:touch]
+
+ callback = lambda { |record|
+ BelongsTo.touch_record(record, foreign_key, n, touch)
+ }
+
+ model.after_save callback, if: :changed?
+ model.after_touch callback
+ model.after_destroy callback
+ end
+
+ def self.add_destroy_callbacks(model, reflection)
+ name = reflection.name
+ model.after_destroy lambda { |o| o.association(name).handle_dependency }
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index fcdfc1e150..2ff67f904d 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -1,3 +1,5 @@
+# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
+
require 'active_record/associations'
module ActiveRecord::Associations::Builder
@@ -5,67 +7,48 @@ module ActiveRecord::Associations::Builder
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
- def valid_options
- super + [:table_name, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove]
- end
-
- attr_reader :block_extension, :extension_module
-
- def initialize(*args, &extension)
- super(*args)
- @block_extension = extension
- end
-
- def build
- show_deprecation_warnings
- wrap_block_extension
- reflection = super
- CALLBACKS.each { |callback_name| define_callback(callback_name) }
- reflection
+ def self.valid_options(options)
+ super + [:table_name, :before_add,
+ :after_add, :before_remove, :after_remove, :extend]
end
- def writable?
- true
+ def self.define_callbacks(model, reflection)
+ super
+ name = reflection.name
+ options = reflection.options
+ CALLBACKS.each { |callback_name|
+ define_callback(model, callback_name, name, options)
+ }
end
- def show_deprecation_warnings
- [:finder_sql, :counter_sql].each do |name|
- if options.include? name
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).")
- end
+ def self.define_extensions(model, name)
+ if block_given?
+ extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
+ extension = Module.new(&Proc.new)
+ model.parent.const_set(extension_module_name, extension)
end
end
- def wrap_block_extension
- if block_extension
- @extension_module = mod = Module.new(&block_extension)
- silence_warnings do
- model.parent.const_set(extension_module_name, mod)
- end
-
- prev_scope = @scope
+ def self.define_callback(model, callback_name, name, options)
+ full_callback_name = "#{callback_name}_for_#{name}"
- if prev_scope
- @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) }
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
+ model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
+ callbacks = Array(options[callback_name.to_sym]).map do |callback|
+ case callback
+ when Symbol
+ ->(method, owner, record) { owner.send(callback, record) }
+ when Proc
+ ->(method, owner, record) { callback.call(owner, record) }
else
- @scope = proc { extending(mod) }
+ ->(method, owner, record) { callback.send(method, owner, record) }
end
end
+ model.send "#{full_callback_name}=", callbacks
end
- def extension_module_name
- @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
- end
-
- def define_callback(callback_name)
- full_callback_name = "#{callback_name}_for_#{name}"
-
- # TODO : why do i need method_defined? I think its because of the inheritance chain
- model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name)
- model.send("#{full_callback_name}=", Array(options[callback_name.to_sym]))
- end
-
- def define_readers
+ # Defines the setter and getter methods for the collection_singular_ids.
+ def self.define_readers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -75,7 +58,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -84,5 +67,13 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ def self.wrap_scope(scope, mod)
+ if scope
+ proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ else
+ proc { extending(mod) }
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index bdac02b5bf..93dc4ae118 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -1,39 +1,124 @@
module ActiveRecord::Associations::Builder
- class HasAndBelongsToMany < CollectionAssociation #:nodoc:
- def macro
- :has_and_belongs_to_many
- end
+ class HasAndBelongsToMany # :nodoc:
+ class JoinTableResolver
+ KnownTable = Struct.new :join_table
+
+ class KnownClass
+ def initialize(lhs_class, rhs_class_name)
+ @lhs_class = lhs_class
+ @rhs_class_name = rhs_class_name
+ @join_table = nil
+ end
+
+ def join_table
+ @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
+ end
+
+ private
+
+ def klass
+ @lhs_class.send(:compute_type, @rhs_class_name)
+ end
+ end
- def valid_options
- super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql]
+ def self.build(lhs_class, name, options)
+ if options[:join_table]
+ KnownTable.new options[:join_table].to_s
+ else
+ class_name = options.fetch(:class_name) {
+ name.to_s.camelize.singularize
+ }
+ KnownClass.new lhs_class, class_name
+ end
+ end
end
- def build
- reflection = super
- define_destroy_hook
- reflection
+ attr_reader :lhs_model, :association_name, :options
+
+ def initialize(association_name, lhs_model, options)
+ @association_name = association_name
+ @lhs_model = lhs_model
+ @options = options
end
- def show_deprecation_warnings
- super
+ def through_model
+ habtm = JoinTableResolver.build lhs_model, association_name, options
+
+ join_model = Class.new(ActiveRecord::Base) {
+ class << self;
+ attr_accessor :class_resolver
+ attr_accessor :name
+ attr_accessor :table_name_resolver
+ attr_accessor :left_reflection
+ attr_accessor :right_reflection
+ end
+
+ def self.table_name
+ table_name_resolver.join_table
+ end
+
+ def self.compute_type(class_name)
+ class_resolver.compute_type class_name
+ end
+
+ def self.add_left_association(name, options)
+ belongs_to name, options
+ self.left_reflection = _reflect_on_association(name)
+ end
- [:delete_sql, :insert_sql].each do |name|
- if options.include? name
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).")
+ def self.add_right_association(name, options)
+ rhs_name = name.to_s.singularize.to_sym
+ belongs_to rhs_name, options
+ self.right_reflection = _reflect_on_association(rhs_name)
end
+
+ }
+
+ join_model.name = "HABTM_#{association_name.to_s.camelize}"
+ join_model.table_name_resolver = habtm
+ join_model.class_resolver = lhs_model
+
+ join_model.add_left_association :left_side, class: lhs_model
+ join_model.add_right_association association_name, belongs_to_options(options)
+ join_model
+ end
+
+ def middle_reflection(join_model)
+ middle_name = [lhs_model.name.downcase.pluralize,
+ association_name].join('_').gsub(/::/, '_').to_sym
+ middle_options = middle_options join_model
+
+ HasMany.create_reflection(lhs_model,
+ middle_name,
+ nil,
+ middle_options)
+ end
+
+ private
+
+ def middle_options(join_model)
+ middle_options = {}
+ middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}"
+ middle_options[:source] = join_model.left_reflection.name
+ if options.key? :foreign_key
+ middle_options[:foreign_key] = options[:foreign_key]
end
+ middle_options
end
- def define_destroy_hook
- name = self.name
- model.send(:include, Module.new {
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def destroy_associations
- association(:#{name}).delete_all
- super
- end
- RUBY
- })
+ def belongs_to_options(options)
+ rhs_options = {}
+
+ if options.key? :class_name
+ rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key
+ rhs_options[:class_name] = options[:class_name]
+ end
+
+ if options.key? :association_foreign_key
+ rhs_options[:foreign_key] = options[:association_foreign_key]
+ end
+
+ rhs_options
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 0d1bdd21ee..1c1b47bd56 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -1,15 +1,15 @@
module ActiveRecord::Associations::Builder
class HasMany < CollectionAssociation #:nodoc:
- def macro
+ def self.macro
:has_many
end
- def valid_options
- super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache]
+ def self.valid_options(options)
+ super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type]
end
- def valid_dependent_options
- [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.valid_dependent_options
+ [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index 0da564f402..64e9e6b334 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -1,25 +1,21 @@
module ActiveRecord::Associations::Builder
class HasOne < SingularAssociation #:nodoc:
- def macro
+ def self.macro
:has_one
end
- def valid_options
- valid = super + [:order, :as]
+ def self.valid_options(options)
+ valid = super + [:as, :foreign_type]
valid += [:through, :source, :source_type] if options[:through]
valid
end
- def constructable?
- !options[:through]
+ def self.valid_dependent_options
+ [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
end
- def configure_dependency
- super unless options[:through]
- end
-
- def valid_dependent_options
- [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.add_destroy_callbacks(model, reflection)
+ super unless reflection.options[:through]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 6a5830e57f..1369212837 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,19 +1,18 @@
+# This class is inherited by the has_one and belongs_to association classes
+
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
- def valid_options
- super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
- end
-
- def constructable?
- true
+ def self.valid_options(options)
+ super + [:dependent, :primary_key, :inverse_of, :required]
end
- def define_accessors
+ def self.define_accessors(model, reflection)
super
- define_constructors if constructable?
+ define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable?
end
- def define_constructors
+ # Defines the (build|create)_association methods for belongs_to or has_one association
+ def self.define_constructors(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def build_#{name}(*args, &block)
association(:#{name}).build(*args, &block)
@@ -28,5 +27,12 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ def self.define_validations(model, reflection)
+ super
+ if reflection.options[:required]
+ model.validates_presence_of reflection.name
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 832b963052..f2c96e9a2a 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -4,10 +4,9 @@ module ActiveRecord
#
# CollectionAssociation is an abstract class that provides common stuff to
# ease the implementation of association proxies that represent
- # collections. See the class hierarchy in AssociationProxy.
+ # collections. See the class hierarchy in Association.
#
# CollectionAssociation:
- # HasAndBelongsToManyAssociation => has_and_belongs_to_many
# HasManyAssociation => has_many
# HasManyThroughAssociation + ThroughAssociation => has_many :through
#
@@ -34,7 +33,13 @@ module ActiveRecord
reload
end
- CollectionProxy.new(klass, self)
+ if owner.new_record?
+ # Cache the proxy separately before the owner has an id
+ # or else a post-save proxy will still lack the id
+ @new_record_proxy ||= CollectionProxy.create(klass, self)
+ else
+ @proxy ||= CollectionProxy.create(klass, self)
+ end
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -44,7 +49,7 @@ module ActiveRecord
# Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
def ids_reader
- if loaded? || options[:finder_sql]
+ if loaded?
load_target.map do |record|
record.send(reflection.association_primary_key)
end
@@ -56,10 +61,10 @@ module ActiveRecord
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
- pk_column = reflection.primary_key_column
- ids = Array(ids).reject { |id| id.blank? }
- ids.map! { |i| pk_column.type_cast(i) }
- replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
+ pk_type = reflection.primary_key_type
+ ids = Array(ids).reject(&:blank?)
+ ids.map! { |i| pk_type.type_cast_from_user(i) }
+ replace(klass.find(ids).index_by(&:id).values_at(*ids))
end
def reset
@@ -67,11 +72,11 @@ module ActiveRecord
@target = []
end
- def select(select = nil)
+ def select(*fields)
if block_given?
load_target.select.each { |e| yield e }
else
- scope.select(select)
+ scope.select(*fields)
end
end
@@ -79,8 +84,17 @@ module ActiveRecord
if block_given?
load_target.find(*args) { |*block_args| yield(*block_args) }
else
- if options[:finder_sql]
- find_by_scan(*args)
+ if options[:inverse_of] && loaded?
+ args_flatten = args.flatten
+ raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
+ result = find_by_scan(*args)
+
+ result_size = Array(result).size
+ if !result || result_size != args_flatten.size
+ scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
+ else
+ result
+ end
else
scope.find(*args)
end
@@ -88,11 +102,31 @@ module ActiveRecord
end
def first(*args)
- first_or_last(:first, *args)
+ first_nth_or_last(:first, *args)
+ end
+
+ def second(*args)
+ first_nth_or_last(:second, *args)
+ end
+
+ def third(*args)
+ first_nth_or_last(:third, *args)
+ end
+
+ def fourth(*args)
+ first_nth_or_last(:fourth, *args)
+ end
+
+ def fifth(*args)
+ first_nth_or_last(:fifth, *args)
+ end
+
+ def forty_two(*args)
+ first_nth_or_last(:forty_two, *args)
end
def last(*args)
- first_or_last(:last, *args)
+ first_nth_or_last(:last, *args)
end
def build(attributes = {}, &block)
@@ -106,20 +140,19 @@ module ActiveRecord
end
def create(attributes = {}, &block)
- create_record(attributes, &block)
+ _create_record(attributes, &block)
end
def create!(attributes = {}, &block)
- create_record(attributes, true, &block)
+ _create_record(attributes, true, &block)
end
# Add +records+ to this association. Returns +self+ so method calls may
# be chained. Since << flattens its argument list and inserts each record,
# +push+ and +concat+ behave identically.
def concat(*records)
- load_target if owner.new_record?
-
if owner.new_record?
+ load_target
concat_records(records)
else
transaction { concat_records(records) }
@@ -141,11 +174,33 @@ module ActiveRecord
end
end
- # Remove all records from this association.
+ # Removes all records from the association without calling callbacks
+ # on the associated records. It honors the +:dependent+ option. However
+ # if the +:dependent+ value is +:destroy+ then in that case the +:delete_all+
+ # deletion strategy for the association is applied.
+ #
+ # You can force a particular deletion strategy by passing a parameter.
+ #
+ # Example:
+ #
+ # @author.books.delete_all(:nullify)
+ # @author.books.delete_all(:delete_all)
#
# See delete for more info.
- def delete_all
- delete(:all).tap do
+ def delete_all(dependent = nil)
+ if dependent && ![:nullify, :delete_all].include?(dependent)
+ raise ArgumentError, "Valid values are :nullify or :delete_all"
+ end
+
+ dependent = if dependent
+ dependent
+ elsif options[:dependent] == :destroy
+ :delete_all
+ else
+ options[:dependent]
+ end
+
+ delete_or_nullify_all_records(dependent).tap do
reset
loaded!
end
@@ -161,35 +216,25 @@ module ActiveRecord
end
end
- # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
- # association, it will be used for the query. Otherwise, construct options and pass them with
+ # Count all records using SQL. Construct options and pass them with
# scope to the target class's +count+.
- def count(column_name = nil, count_options = {})
- column_name, count_options = nil, column_name if column_name.is_a?(Hash)
-
- if options[:counter_sql] || options[:finder_sql]
- unless count_options.blank?
- raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
- end
-
- reflection.klass.count_by_sql(custom_counter_sql)
- else
- if association_scope.uniq_value
- # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
- column_name ||= reflection.klass.primary_key
- count_options[:distinct] = true
- end
+ def count(column_name = nil)
+ relation = scope
+ if association_scope.distinct_value
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
+ column_name ||= reflection.klass.primary_key
+ relation = relation.distinct
+ end
- value = scope.count(column_name, count_options)
+ value = relation.count(column_name)
- limit = options[:limit]
- offset = options[:offset]
+ limit = options[:limit]
+ offset = options[:offset]
- if limit || offset
- [ [value - offset.to_i, 0].max, limit.to_i ].min
- else
- value
- end
+ if limit || offset
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
+ else
+ value
end
end
@@ -201,26 +246,21 @@ module ActiveRecord
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
- dependent = options[:dependent]
+ return if records.empty?
+ _options = records.extract_options!
+ dependent = _options[:dependent] || options[:dependent]
- if records.first == :all
- if loaded? || dependent == :destroy
- delete_or_destroy(load_target, dependent)
- else
- delete_records(:all, dependent)
- end
- else
- records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
- delete_or_destroy(records, dependent)
- end
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
+ delete_or_destroy(records, dependent)
end
- # Destroy +records+ and remove them from this association calling
- # +before_remove+ and +after_remove+ callbacks.
+ # Deletes the +records+ and removes them from this association calling
+ # +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
#
- # Note that this method will _always_ remove records from the database
- # ignoring the +:dependent+ option.
+ # Note that this method removes records from the database ignoring the
+ # +:dependent+ option.
def destroy(*records)
+ return if records.empty?
records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
delete_or_destroy(records, :destroy)
end
@@ -237,15 +277,15 @@ module ActiveRecord
# +count_records+, which is a method descendants have to provide.
def size
if !find_target? || loaded?
- if association_scope.uniq_value
+ if association_scope.distinct_value
target.uniq.size
else
target.size
end
elsif !loaded? && !association_scope.group_values.empty?
load_target.size
- elsif !loaded? && !association_scope.uniq_value && target.is_a?(Array)
- unsaved_records = target.select { |r| r.new_record? }
+ elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
+ unsaved_records = target.select(&:new_record?)
unsaved_records.size + count_records
else
count_records
@@ -263,17 +303,17 @@ module ActiveRecord
# Returns true if the collection is empty.
#
- # If the collection has been loaded or the <tt>:counter_sql</tt> option
- # is provided, it is equivalent to <tt>collection.size.zero?</tt>. If the
+ # If the collection has been loaded
+ # it is equivalent to <tt>collection.size.zero?</tt>. If the
# collection has not been loaded, it is equivalent to
# <tt>collection.exists?</tt>. If the collection has not already been
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded? || options[:counter_sql]
+ if loaded?
size.zero?
else
- !scope.exists?
+ @target.blank? && !scope.exists?
end
end
@@ -297,23 +337,27 @@ module ActiveRecord
end
end
- def uniq
+ def distinct
seen = {}
load_target.find_all do |record|
seen[record.id] = true unless seen.key?(record.id)
end
end
+ alias uniq distinct
# Replace this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
def replace(other_array)
- other_array.each { |val| raise_on_type_mismatch(val) }
+ other_array.each { |val| raise_on_type_mismatch!(val) }
original_target = load_target.dup
if owner.new_record?
replace_records(other_array, original_target)
else
- transaction { replace_records(other_array, original_target) }
+ replace_common_records_in_memory(other_array, original_target)
+ if other_array != original_target
+ transaction { replace_records(other_array, original_target) }
+ end
end
end
@@ -322,8 +366,7 @@ module ActiveRecord
if record.new_record?
include_in_memory?(record)
else
- load_target if options[:finder_sql]
- loaded? ? target.include?(record) : scope.exists?(record)
+ loaded? ? target.include?(record) : scope.exists?(record.id)
end
else
false
@@ -339,17 +382,24 @@ module ActiveRecord
target
end
- def add_to_target(record)
- callback(:before_add, record)
+ def add_to_target(record, skip_callbacks = false, &block)
+ if association_scope.distinct_value
+ index = @target.index(record)
+ end
+ replace_on_target(record, index, skip_callbacks, &block)
+ end
+
+ def replace_on_target(record, index, skip_callbacks)
+ callback(:before_add, record) unless skip_callbacks
yield(record) if block_given?
- if association_scope.uniq_value && index = @target.index(record)
+ if index
@target[index] = record
else
@target << record
end
- callback(:after_add, record)
+ callback(:after_add, record) unless skip_callbacks
set_inverse_instance(record)
record
@@ -366,32 +416,29 @@ module ActiveRecord
end
private
+ def get_records
+ if reflection.scope_chain.any?(&:any?) ||
+ scope.eager_loading? ||
+ klass.current_scope ||
+ klass.default_scopes.any?
- def custom_counter_sql
- if options[:counter_sql]
- interpolate(options[:counter_sql])
- else
- # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
- interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
- count_with = $2.to_s
- count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/
- "SELECT #{$1}COUNT(#{count_with}) FROM"
- end
- end
+ return scope.to_a
end
- def custom_finder_sql
- interpolate(options[:finder_sql])
+ conn = klass.connection
+ sc = reflection.association_scope_cache(conn, owner) do
+ StatementCache.create(conn) { |params|
+ as = AssociationScope.create { params.bind }
+ target_scope.merge as.scope(self, conn)
+ }
end
- def find_target
- records =
- if options[:finder_sql]
- reflection.klass.find_by_sql(custom_finder_sql)
- else
- scope.to_a
- end
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
+ sc.execute binds, klass, klass.connection
+ end
+ def find_target
+ records = get_records
records.each { |record| set_inverse_instance(record) }
records
end
@@ -426,13 +473,13 @@ module ActiveRecord
persisted + memory
end
- def create_record(attributes, raise = false, &block)
+ def _create_record(attributes, raise = false, &block)
unless owner.persisted?
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
end
if attributes.is_a?(Array)
- attributes.collect { |attr| create_record(attr, raise, &block) }
+ attributes.collect { |attr| _create_record(attr, raise, &block) }
else
transaction do
add_to_target(build_record(attributes)) do |record|
@@ -454,8 +501,8 @@ module ActiveRecord
def delete_or_destroy(records, method)
records = records.flatten
- records.each { |record| raise_on_type_mismatch(record) }
- existing_records = records.reject { |r| r.new_record? }
+ records.each { |record| raise_on_type_mismatch!(record) }
+ existing_records = records.reject(&:new_record?)
if existing_records.empty?
remove_records(existing_records, records, method)
@@ -491,13 +538,21 @@ module ActiveRecord
target
end
- def concat_records(records)
+ def replace_common_records_in_memory(new_target, original_target)
+ common_records = new_target & original_target
+ common_records.each do |record|
+ skip_callbacks = true
+ replace_on_target(record, @target.index(record), skip_callbacks)
+ end
+ end
+
+ def concat_records(records, should_raise = false)
result = true
records.flatten.each do |record|
- raise_on_type_mismatch(record)
- add_to_target(record) do |r|
- result &&= insert_record(record) unless owner.new_record?
+ raise_on_type_mismatch!(record)
+ add_to_target(record) do |rec|
+ result &&= insert_record(rec, true, should_raise) unless owner.new_record?
end
end
@@ -506,20 +561,13 @@ module ActiveRecord
def callback(method, record)
callbacks_for(method).each do |callback|
- case callback
- when Symbol
- owner.send(callback, record)
- when Proc
- callback.call(owner, record)
- else
- callback.send(method, owner, record)
- end
+ callback.call(method, owner, record)
end
end
def callbacks_for(callback_name)
full_callback_name = "#{callback_name}_for_#{reflection.name}"
- owner.class.send(full_callback_name.to_sym) || []
+ owner.class.send(full_callback_name)
end
# Should we deal with assoc.first or assoc.last by issuing an independent query to
@@ -530,51 +578,49 @@ module ActiveRecord
# Otherwise, go to the database only if none of the following are true:
# * target already loaded
# * owner is new record
- # * custom :finder_sql exists
# * target contains new or changed record(s)
- # * the first arg is an integer (which indicates the number of records to be returned)
- def fetch_first_or_last_using_find?(args)
+ def fetch_first_nth_or_last_using_find?(args)
if args.first.is_a?(Hash)
true
else
!(loaded? ||
owner.new_record? ||
- options[:finder_sql] ||
- target.any? { |record| record.new_record? || record.changed? } ||
- args.first.kind_of?(Integer))
+ target.any? { |record| record.new_record? || record.changed? })
end
end
def include_in_memory?(record)
if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
- owner.send(reflection.through_reflection.name).any? { |source|
- target = source.send(reflection.source_reflection.name)
- target.respond_to?(:include?) ? target.include?(record) : target == record
+ assoc = owner.association(reflection.through_reflection.name)
+ assoc.reader.any? { |source|
+ target_reflection = source.send(reflection.source_reflection.name)
+ target_reflection.respond_to?(:include?) ? target_reflection.include?(record) : target_reflection == record
} || target.include?(record)
else
target.include?(record)
end
end
- # If using a custom finder_sql, #find scans the entire collection.
+ # If the :inverse_of option has been
+ # specified, then #find scans the entire collection.
def find_by_scan(*args)
expects_array = args.first.kind_of?(Array)
- ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq
+ ids = args.flatten.compact.map(&:to_s).uniq
if ids.size == 1
id = ids.first
- record = load_target.detect { |r| id == r.id }
+ record = load_target.detect { |r| id == r.id.to_s }
expects_array ? [ record ] : record
else
- load_target.select { |r| ids.include?(r.id) }
+ load_target.select { |r| ids.include?(r.id.to_s) }
end
end
# Fetches the first/last using SQL if possible, otherwise from the target array.
- def first_or_last(type, *args)
+ def first_nth_or_last(type, *args)
args.shift if args.first.is_a?(Hash) && args.first.empty?
- collection = fetch_first_or_last_using_find?(args) ? scope : load_target
+ collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target
collection.send(type, *args).tap do |record|
set_inverse_instance record if record.is_a? ActiveRecord::Base
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 7c43e37cf2..c22dc6e11e 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -29,10 +29,11 @@ module ActiveRecord
# instantiation of the actual post records.
class CollectionProxy < Relation
delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope)
+ delegate :find_nth, to: :scope
def initialize(klass, association) #:nodoc:
@association = association
- super klass, klass.arel_table
+ super klass, klass.arel_table, klass.predicate_builder
merge! association.scope(nullify: false)
end
@@ -75,23 +76,23 @@ module ActiveRecord
# # #<Pet id: nil, name: "Choo-Choo">
# # ]
#
- # person.pets.select([:id, :name])
+ # person.pets.select(:id, :name )
# # => [
# # #<Pet id: 1, name: "Fancy-Fancy">,
# # #<Pet id: 2, name: "Spook">,
# # #<Pet id: 3, name: "Choo-Choo">
# # ]
#
- # Be careful because this also means you’re initializing a model
- # object with only the fields that you’ve selected. If you attempt
- # to access a field that is not in the initialized record you’ll
+ # Be careful because this also means you're initializing a model
+ # object with only the fields that you've selected. If you attempt
+ # to access a field except +id+ that is not in the initialized record you'll
# receive:
#
# person.pets.select(:name).first.person_id
# # => ActiveModel::MissingAttributeError: missing attribute: person_id
#
# *Second:* You can pass a block so it can be used just like Array#select.
- # This build an array of objects from the database for the scope,
+ # This builds an array of objects from the database for the scope,
# converting them into an array and iterating through them using
# Array#select.
#
@@ -106,13 +107,13 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook">,
# # #<Pet id: 3, name: "Choo-Choo">
# # ]
- def select(select = nil, &block)
- @association.select(select, &block)
+ def select(*fields, &block)
+ @association.select(*fields, &block)
end
# Finds an object in the collection responding to the +id+. Uses the same
# rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt>
- # error if the object can not be found.
+ # error if the object cannot be found.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -170,6 +171,32 @@ module ActiveRecord
@association.first(*args)
end
+ # Same as +first+ except returns only the second record.
+ def second(*args)
+ @association.second(*args)
+ end
+
+ # Same as +first+ except returns only the third record.
+ def third(*args)
+ @association.third(*args)
+ end
+
+ # Same as +first+ except returns only the fourth record.
+ def fourth(*args)
+ @association.fourth(*args)
+ end
+
+ # Same as +first+ except returns only the fifth record.
+ def fifth(*args)
+ @association.fifth(*args)
+ end
+
+ # Same as +first+ except returns only the forty second record.
+ # Also known as accessing "the reddit".
+ def forty_two(*args)
+ @association.forty_two(*args)
+ end
+
# Returns the last record, or the last +n+ records, from the collection.
# If the collection is empty, the first form returns +nil+, and the second
# form returns an empty array.
@@ -227,6 +254,7 @@ module ActiveRecord
def build(attributes = {}, &block)
@association.build(attributes, &block)
end
+ alias_method :new, :build
# Returns a new object of the collection type that has been instantiated with
# attributes, linked to this object and that has already been saved (if it
@@ -280,7 +308,7 @@ module ActiveRecord
# so method calls may be chained.
#
# class Person < ActiveRecord::Base
- # pets :has_many
+ # has_many :pets
# end
#
# person.pets.size # => 0
@@ -302,7 +330,7 @@ module ActiveRecord
@association.concat(*records)
end
- # Replace this collection with +other_array+. This will perform a diff
+ # Replaces this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
#
# class Person < ActiveRecord::Base
@@ -328,14 +356,15 @@ module ActiveRecord
@association.replace(other_array)
end
- # Deletes all the records from the collection. For +has_many+ associations,
- # the deletion is done according to the strategy specified by the <tt>:dependent</tt>
- # option. Returns an array with the deleted records.
+ # Deletes all the records from the collection according to the strategy
+ # specified by the +:dependent+ option. If no +:dependent+ option is given,
+ # then it will follow the default strategy.
#
- # If no <tt>:dependent</tt> option is given, then it will follow the
- # default strategy. The default strategy is <tt>:nullify</tt>. This
- # sets the foreign keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>,
- # the default strategy is +delete_all+.
+ # For +has_many :through+ associations, the default deletion strategy is
+ # +:delete_all+.
+ #
+ # For +has_many+ associations, the default deletion strategy is +:nullify+.
+ # This sets the foreign keys to +NULL+.
#
# class Person < ActiveRecord::Base
# has_many :pets # dependent: :nullify option by default
@@ -366,9 +395,9 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: nil>
# # ]
#
- # If it is set to <tt>:destroy</tt> all the objects from the collection
- # are removed by calling their +destroy+ method. See +destroy+ for more
- # information.
+ # Both +has_many+ and +has_many :through+ dependencies default to the
+ # +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+.
+ # Records are not instantiated and callbacks will not be fired.
#
# class Person < ActiveRecord::Base
# has_many :pets, dependent: :destroy
@@ -383,11 +412,6 @@ module ActiveRecord
# # ]
#
# person.pets.delete_all
- # # => [
- # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
- # # #<Pet id: 2, name: "Spook", person_id: 1>,
- # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
- # # ]
#
# Pet.find(1, 2, 3)
# # => ActiveRecord::RecordNotFound
@@ -408,21 +432,17 @@ module ActiveRecord
# # ]
#
# person.pets.delete_all
- # # => [
- # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
- # # #<Pet id: 2, name: "Spook", person_id: 1>,
- # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
- # # ]
#
# Pet.find(1, 2, 3)
# # => ActiveRecord::RecordNotFound
- def delete_all
- @association.delete_all
+ def delete_all(dependent = nil)
+ @association.delete_all(dependent)
end
- # Deletes the records of the collection directly from the database.
- # This will _always_ remove the records ignoring the +:dependent+
- # option.
+ # Deletes the records of the collection directly from the database
+ # ignoring the +:dependent+ option. Records are instantiated and it
+ # invokes +before_remove+, +after_remove+ , +before_destroy+ and
+ # +after_destroy+ callbacks.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -648,11 +668,12 @@ module ActiveRecord
# # #<Pet name: "Fancy-Fancy">
# # ]
#
- # person.pets.select(:name).uniq
+ # person.pets.select(:name).distinct
# # => [#<Pet name: "Fancy-Fancy">]
- def uniq
- @association.uniq
+ def distinct
+ @association.distinct
end
+ alias uniq distinct
# Count all records using SQL.
#
@@ -667,8 +688,8 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook", person_id: 1>,
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
- def count(column_name = nil, options = {})
- @association.count(column_name, options)
+ def count(column_name = nil)
+ @association.count(column_name)
end
# Returns the size of the collection. If the collection hasn't been loaded,
@@ -724,7 +745,7 @@ module ActiveRecord
end
# Returns +true+ if the collection is empty. If the collection has been
- # loaded or the <tt>:counter_sql</tt> option is provided, it is equivalent
+ # loaded it is equivalent
# to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
# it is equivalent to <tt>collection.exists?</tt>. If the collection has
# not already been loaded and you are going to fetch the records anyway it
@@ -758,7 +779,7 @@ module ActiveRecord
# person.pets.count # => 0
# person.pets.any? # => true
#
- # You can also pass a block to define criteria. The behaviour
+ # You can also pass a +block+ to define criteria. The behavior
# is the same, it returns true if the collection based on the
# criteria is not empty.
#
@@ -785,15 +806,15 @@ module ActiveRecord
# has_many :pets
# end
#
- # person.pets.count #=> 1
- # person.pets.many? #=> false
+ # person.pets.count # => 1
+ # person.pets.many? # => false
#
# person.pets << Pet.new(name: 'Snoopy')
- # person.pets.count #=> 2
- # person.pets.many? #=> true
+ # person.pets.count # => 2
+ # person.pets.many? # => true
#
- # You can also pass a block to define criteria. The
- # behaviour is the same, it returns true if the collection
+ # You can also pass a +block+ to define criteria. The
+ # behavior is the same, it returns true if the collection
# based on the criteria has more than one record.
#
# person.pets
@@ -816,7 +837,7 @@ module ActiveRecord
@association.many?(&block)
end
- # Returns +true+ if the given object is present in the collection.
+ # Returns +true+ if the given +record+ is present in the collection.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -827,10 +848,12 @@ module ActiveRecord
# person.pets.include?(Pet.find(20)) # => true
# person.pets.include?(Pet.find(21)) # => false
def include?(record)
- @association.include?(record)
+ !!@association.include?(record)
end
- alias_method :new, :build
+ def arel
+ scope.arel
+ end
def proxy_association
@association
@@ -846,19 +869,13 @@ module ActiveRecord
# Returns a <tt>Relation</tt> object for the records in this association
def scope
- association = @association
-
- @association.scope.extending! do
- define_method(:proxy_association) { association }
- end
+ @association.scope
end
-
- # :nodoc:
alias spawn scope
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
# contain the same number of elements and if each element is equal
- # to the corresponding element in the other array, otherwise returns
+ # to the corresponding element in the +other+ array, otherwise returns
# +false+.
#
# class Person < ActiveRecord::Base
@@ -923,7 +940,7 @@ module ActiveRecord
alias_method :to_a, :to_ary
# Adds one or more +records+ to the collection by setting their foreign keys
- # to the association‘s primary key. Returns +self+, so several appends may be
+ # to the association's primary key. Returns +self+, so several appends may be
# chained together.
#
# class Person < ActiveRecord::Base
@@ -946,6 +963,11 @@ module ActiveRecord
proxy_association.concat(records) && self
end
alias_method :push, :<<
+ alias_method :append, :<<
+
+ def prepend(*args)
+ raise NoMethodError, "prepend on association is not defined. Please use << or append"
+ end
# Equivalent to +delete_all+. The difference is that returns +self+, instead
# of an array with the deleted objects, so methods can be chained. See
@@ -977,6 +999,28 @@ module ActiveRecord
proxy_association.reload
self
end
+
+ # Unloads the association. Returns +self+.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets # fetches pets from the database
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ #
+ # person.pets # uses the pets cache
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ #
+ # person.pets.reset # clears the pets cache
+ #
+ # person.pets # fetches pets from the database
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ def reset
+ proxy_association.reset
+ proxy_association.reset_scope
+ self
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb
new file mode 100644
index 0000000000..fe48ecec29
--- /dev/null
+++ b/activerecord/lib/active_record/associations/foreign_association.rb
@@ -0,0 +1,11 @@
+module ActiveRecord::Associations
+ module ForeignAssociation
+ def foreign_key_present?
+ if reflection.klass.primary_key
+ owner.attribute_present?(reflection.active_record_primary_key)
+ else
+ false
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
deleted file mode 100644
index 93618721bb..0000000000
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module ActiveRecord
- # = Active Record Has And Belongs To Many Association
- module Associations
- class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
- attr_reader :join_table
-
- def initialize(owner, reflection)
- @join_table = Arel::Table.new(reflection.join_table)
- super
- end
-
- def insert_record(record, validate = true, raise = false)
- if record.new_record?
- if raise
- record.save!(:validate => validate)
- else
- return unless record.save(:validate => validate)
- end
- end
-
- if options[:insert_sql]
- owner.connection.insert(interpolate(options[:insert_sql], record))
- else
- stmt = join_table.compile_insert(
- join_table[reflection.foreign_key] => owner.id,
- join_table[reflection.association_foreign_key] => record.id
- )
-
- owner.connection.insert stmt
- end
-
- record
- end
-
- private
-
- def count_records
- load_target.size
- end
-
- def delete_records(records, method)
- if sql = options[:delete_sql]
- records = load_target if records == :all
- records.each { |record| owner.connection.delete(interpolate(sql, record)) }
- else
- relation = join_table
- condition = relation[reflection.foreign_key].eq(owner.id)
-
- unless records == :all
- condition = condition.and(
- relation[reflection.association_foreign_key]
- .in(records.map { |x| x.id }.compact)
- )
- end
-
- owner.connection.delete(relation.where(condition).compile_delete)
- end
- end
-
- def invertible_for?(record)
- false
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index f59565ae77..d7f655d00c 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -6,10 +6,11 @@ module ActiveRecord
# If the association has a <tt>:through</tt> option further specialization
# is provided by its child HasManyThroughAssociation.
class HasManyAssociation < CollectionAssociation #:nodoc:
+ include ForeignAssociation
def handle_dependency
case options[:dependent]
- when :restrict, :restrict_with_exception
+ when :restrict_with_exception
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
when :restrict_with_error
@@ -22,15 +23,17 @@ module ActiveRecord
else
if options[:dependent] == :destroy
# No point in executing the counter update since we're going to destroy the parent anyway
- load_target.each(&:mark_for_destruction)
+ load_target.each { |t| t.destroyed_by_association = reflection }
+ destroy_all
+ else
+ delete_all
end
-
- delete_all
end
end
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
+ set_inverse_instance(record)
if raise
record.save!(:validate => validate)
@@ -39,6 +42,14 @@ module ActiveRecord
end
end
+ def empty?
+ if has_cached_counter?
+ size.zero?
+ else
+ super
+ end
+ end
+
private
# Returns the number of records in this collection.
@@ -56,9 +67,7 @@ module ActiveRecord
# the loaded flag is set to true as well.
def count_records
count = if has_cached_counter?
- owner.send(:read_attribute, cached_counter_attribute_name)
- elsif options[:counter_sql] || options[:finder_sql]
- reflection.klass.count_by_sql(custom_counter_sql)
+ owner._read_attribute cached_counter_attribute_name
else
scope.count
end
@@ -71,20 +80,31 @@ module ActiveRecord
[association_scope.limit_value, count].compact.min
end
- def has_cached_counter?(reflection = reflection)
+ def has_cached_counter?(reflection = reflection())
owner.attribute_present?(cached_counter_attribute_name(reflection))
end
- def cached_counter_attribute_name(reflection = reflection)
+ def cached_counter_attribute_name(reflection = reflection())
options[:counter_cache] || "#{reflection.name}_count"
end
- def update_counter(difference, reflection = reflection)
+ def update_counter(difference, reflection = reflection())
+ update_counter_in_database(difference, reflection)
+ update_counter_in_memory(difference, reflection)
+ end
+
+ def update_counter_in_database(difference, reflection = reflection())
if has_cached_counter?(reflection)
counter = cached_counter_attribute_name(reflection)
owner.class.update_counters(owner.id, counter => difference)
+ end
+ end
+
+ def update_counter_in_memory(difference, reflection = reflection())
+ if has_cached_counter?(reflection)
+ counter = cached_counter_attribute_name(reflection)
owner[counter] += difference
- owner.changed_attributes.delete(counter) # eww
+ owner.send(:clear_attribute_changes, counter) # eww
end
end
@@ -98,36 +118,59 @@ module ActiveRecord
# it will be decremented twice.
#
# Hence this method.
- def inverse_updates_counter_cache?(reflection = reflection)
+ def inverse_updates_counter_cache?(reflection = reflection())
counter_name = cached_counter_attribute_name(reflection)
- reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
+ inverse_updates_counter_named?(counter_name, reflection)
+ end
+
+ def inverse_updates_counter_named?(counter_name, reflection = reflection())
+ reflection.klass._reflections.values.any? { |inverse_reflection|
+ inverse_reflection.belongs_to? &&
inverse_reflection.counter_cache_column == counter_name
}
end
+ def delete_count(method, scope)
+ if method == :delete_all
+ scope.delete_all
+ else
+ scope.update_all(reflection.foreign_key => nil)
+ end
+ end
+
+ def delete_or_nullify_all_records(method)
+ count = delete_count(method, self.scope)
+ update_counter(-count)
+ end
+
# Deletes the records according to the <tt>:dependent</tt> option.
def delete_records(records, method)
if method == :destroy
- records.each { |r| r.destroy }
+ records.each(&:destroy!)
update_counter(-records.length) unless inverse_updates_counter_cache?
else
- if records == :all
- scope = self.scope
- else
- keys = records.map { |r| r[reflection.association_primary_key] }
- scope = self.scope.where(reflection.association_primary_key => keys)
- end
-
- if method == :delete_all
- update_counter(-scope.delete_all)
- else
- update_counter(-scope.update_all(reflection.foreign_key => nil))
- end
+ scope = self.scope.where(reflection.klass.primary_key => records)
+ update_counter(-delete_count(method, scope))
end
end
- def foreign_key_present?
- owner.attribute_present?(reflection.association_primary_key)
+ def concat_records(records, *)
+ update_counter_if_success(super, records.length)
+ end
+
+ def _create_record(attributes, *)
+ if attributes.is_a?(Array)
+ super
+ else
+ update_counter_if_success(super, 1)
+ end
+ end
+
+ def update_counter_if_success(saved_successfully, difference)
+ if saved_successfully
+ update_counter_in_memory(difference)
+ end
+ saved_successfully
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index c3266f2bb4..7a050ca224 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -1,3 +1,4 @@
+require 'active_support/core_ext/string/filters'
module ActiveRecord
# = Active Record Has Many Through Association
@@ -12,25 +13,10 @@ module ActiveRecord
@through_association = nil
end
- # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
- # loaded and calling collection.size if it has. If it's more likely than not that the collection does
- # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
- # SELECT query if you use #length.
- def size
- if has_cached_counter?
- owner.send(:read_attribute, cached_counter_attribute_name)
- elsif loaded?
- target.size
- else
- count
- end
- end
-
def concat(*records)
unless owner.new_record?
records.flatten.each do |record|
- raise_on_type_mismatch(record)
- record.save! if record.new_record?
+ raise_on_type_mismatch!(record)
end
end
@@ -40,7 +26,7 @@ module ActiveRecord
def concat_records(records)
ensure_not_nested
- records = super
+ records = super(records, true)
if owner.new_record? && records
records.flatten.each do |record|
@@ -63,7 +49,16 @@ module ActiveRecord
end
save_through_record(record)
- update_counter(1)
+ if has_cached_counter? && !through_reflection_updates_counter_cache?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Automatic updating of counter caches on through associations has been
+ deprecated, and will be removed in Rails 5. Instead, please set the
+ appropriate `counter_cache` options on the `has_many` and `belongs_to`
+ for your associations to #{through_reflection.name}.
+ MSG
+
+ update_counter_in_database(1)
+ end
record
end
@@ -73,23 +68,31 @@ module ActiveRecord
@through_association ||= owner.association(through_reflection.name)
end
- # We temporarily cache through record that has been build, because if we build a
- # through record in build_record and then subsequently call insert_record, then we
- # want to use the exact same object.
+ # The through record (built with build_record) is temporarily cached
+ # so that it may be reused if insert_record is subsequently called.
#
- # However, after insert_record has been called, we clear the cache entry because
- # we want it to be possible to have multiple instances of the same record in an
- # association
+ # However, after insert_record has been called, the cache is cleared in
+ # order to allow multiple instances of the same record in an association.
def build_through_record(record)
@through_records[record.object_id] ||= begin
ensure_mutable
- through_record = through_association.build
+ through_record = through_association.build(*options_for_through_record)
through_record.send("#{source_reflection.name}=", record)
through_record
end
end
+ def options_for_through_record
+ [through_scope_attributes]
+ end
+
+ def through_scope_attributes
+ scope.where_values_hash(through_association.reflection.name.to_s).
+ except!(through_association.reflection.foreign_key,
+ through_association.reflection.klass.inheritance_column)
+ end
+
def save_through_record(record)
build_through_record(record).save!
ensure
@@ -103,9 +106,9 @@ module ActiveRecord
inverse = source_reflection.inverse_of
if inverse
- if inverse.macro == :has_many
+ if inverse.collection?
record.send(inverse.name) << build_through_record(record)
- elsif inverse.macro == :has_one
+ elsif inverse.has_one?
record.send("#{inverse.name}=", build_through_record(record))
end
end
@@ -114,11 +117,7 @@ module ActiveRecord
end
def target_reflection_has_associated_record?
- if through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?
- false
- else
- true
- end
+ !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?)
end
def update_through_counter?(method)
@@ -132,19 +131,31 @@ module ActiveRecord
end
end
+ def delete_or_nullify_all_records(method)
+ delete_records(load_target, method)
+ end
+
def delete_records(records, method)
ensure_not_nested
- # This is unoptimised; it will load all the target records
- # even when we just want to delete everything.
- records = load_target if records == :all
-
scope = through_association.scope
scope.where! construct_join_attributes(*records)
case method
when :destroy
- count = scope.destroy_all.length
+ if scope.klass.primary_key
+ count = scope.destroy_all.length
+ else
+ scope.each(&:_run_destroy_callbacks)
+
+ arel = scope.arel
+
+ stmt = Arel::DeleteManager.new
+ stmt.from scope.klass.arel_table
+ stmt.wheres = arel.constraints
+
+ count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values)
+ end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
else
@@ -153,12 +164,12 @@ module ActiveRecord
delete_through_records(records)
- if source_reflection.options[:counter_cache]
+ if source_reflection.options[:counter_cache] && method != :destroy
counter = source_reflection.counter_cache_column
klass.decrement_counter counter, records.map(&:id)
end
- if through_reflection.macro == :has_many && update_through_counter?(method)
+ if through_reflection.collection? && update_through_counter?(method)
update_counter(-count, through_reflection)
end
@@ -168,14 +179,18 @@ module ActiveRecord
def through_records_for(record)
attributes = construct_join_attributes(record)
candidates = Array.wrap(through_association.target)
- candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
+ candidates.find_all do |c|
+ attributes.all? do |key, value|
+ c.public_send(key) == value
+ end
+ end
end
def delete_through_records(records)
records.each do |record|
through_records = through_records_for(record)
- if through_reflection.macro == :has_many
+ if through_reflection.collection?
through_records.each { |r| through_association.target.delete(r) }
else
if through_records.include?(through_association.target)
@@ -189,13 +204,18 @@ module ActiveRecord
def find_target
return [] unless target_reflection_has_associated_record?
- scope.to_a
+ get_records
end
# NOTE - not sure that we can actually cope with inverses here
def invertible_for?(record)
false
end
+
+ def through_reflection_updates_counter_cache?
+ counter_name = cached_counter_attribute_name
+ inverse_updates_counter_named?(counter_name, through_reflection)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index ee816d2392..74b8c53758 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,12 +1,12 @@
-
module ActiveRecord
# = Active Record Belongs To Has One Association
module Associations
class HasOneAssociation < SingularAssociation #:nodoc:
+ include ForeignAssociation
def handle_dependency
case options[:dependent]
- when :restrict, :restrict_with_exception
+ when :restrict_with_exception
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target
when :restrict_with_error
@@ -22,20 +22,23 @@ module ActiveRecord
end
def replace(record, save = true)
- raise_on_type_mismatch(record) if record
+ raise_on_type_mismatch!(record) if record
load_target
- # If target and record are nil, or target is equal to record,
- # we don't need to have transaction.
- if (target || record) && target != record
+ return self.target if !(target || record)
+
+ assigning_another_record = target != record
+ if assigning_another_record || record.changed?
+ save &&= owner.persisted?
+
transaction_if(save) do
- remove_target!(options[:dependent]) if target && !target.destroyed?
+ remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
if record
set_owner_attributes(record)
set_inverse_instance(record)
- if owner.persisted? && save && !record.save
+ if save && !record.save
nullify_owner_attributes(record)
set_owner_attributes(target) if target
raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index fdf8ae1453..08e0ec691f 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -23,7 +23,7 @@ module ActiveRecord
attributes = construct_join_attributes(record)
if through_record
- through_record.update_attributes(attributes)
+ through_record.update(attributes)
elsif owner.new_record?
through_proxy.build(attributes)
else
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index cd366ac8b7..66e997c3c8 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -1,213 +1,286 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
- autoload :JoinPart, 'active_record/associations/join_dependency/join_part'
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
- attr_reader :join_parts, :reflections, :alias_tracker, :active_record
-
- def initialize(base, associations, joins)
- @active_record = base
- @table_joins = joins
- @join_parts = [JoinBase.new(base)]
- @associations = {}
- @reflections = []
- @alias_tracker = AliasTracker.new(base.connection, joins)
- @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
- build(associations)
- end
-
- def graft(*associations)
- associations.each do |association|
- join_associations.detect {|a| association == a} ||
- build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
- end
- self
- end
-
- def join_associations
- join_parts.last(join_parts.length - 1)
- end
-
- def join_base
- join_parts.first
- end
-
- def columns
- join_parts.collect { |join_part|
- table = join_part.aliased_table
- join_part.column_names_with_alias.collect{ |column_name, aliased_name|
- table[column_name].as Arel.sql(aliased_name)
+ class Aliases # :nodoc:
+ def initialize(tables)
+ @tables = tables
+ @alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.each_with_object({}) { |column,i|
+ i[column.name] = column.alias
+ }
}
- }.flatten
- end
+ @name_and_alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.map { |column|
+ [column.name, column.alias]
+ }
+ }
+ end
- def instantiate(rows)
- primary_key = join_base.aliased_primary_key
- parents = {}
+ def columns
+ @tables.flat_map(&:column_aliases)
+ end
- records = rows.map { |model|
- primary_id = model[primary_key]
- parent = parents[primary_id] ||= join_base.instantiate(model)
- construct(parent, @associations, join_associations, model)
- parent
- }.uniq
+ # An array of [column_name, alias] pairs for the table
+ def column_aliases(node)
+ @name_and_alias_cache[node]
+ end
- remove_duplicate_results!(active_record, records, @associations)
- records
- end
+ def column_alias(node, column)
+ @alias_cache[node][column]
+ end
- def remove_duplicate_results!(base, records, associations)
- case associations
- when Symbol, String
- reflection = base.reflections[associations]
- remove_uniq_by_reflection(reflection, records)
- when Array
- associations.each do |association|
- remove_duplicate_results!(base, records, association)
+ class Table < Struct.new(:node, :columns)
+ def table
+ Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
end
- when Hash
- associations.keys.each do |name|
- reflection = base.reflections[name]
- remove_uniq_by_reflection(reflection, records)
-
- parent_records = []
- records.each do |record|
- if descendant = record.send(reflection.name)
- if reflection.collection?
- parent_records.concat descendant.target.uniq
- else
- parent_records << descendant
- end
- end
- end
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
+ def column_aliases
+ t = table
+ columns.map { |column| t[column.name].as Arel.sql column.alias }
end
end
+ Column = Struct.new(:name, :alias)
end
- protected
+ attr_reader :alias_tracker, :base_klass, :join_root
- def cache_joined_association(association)
- associations = []
- parent = association.parent
- while parent != join_base
- associations.unshift(parent.reflection.name)
- parent = parent.parent
- end
- ref = @associations
- associations.each do |key|
- ref = ref[key]
- end
- ref[association.reflection.name] ||= {}
+ def self.make_tree(associations)
+ hash = {}
+ walk_tree associations, hash
+ hash
end
- def build(associations, parent = nil, join_type = Arel::InnerJoin)
- parent ||= join_parts.last
+ def self.walk_tree(associations, hash)
case associations
when Symbol, String
- reflection = parent.reflections[associations.to_s.intern] or
- raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
- unless join_association = find_join_association(reflection, parent)
- @reflections << reflection
- join_association = build_join_association(reflection, parent)
- join_association.join_type = join_type
- @join_parts << join_association
- cache_joined_association(join_association)
- end
- join_association
+ hash[associations.to_sym] ||= {}
when Array
- associations.each do |association|
- build(association, parent, join_type)
+ associations.each do |assoc|
+ walk_tree assoc, hash
end
when Hash
- associations.keys.sort_by { |a| a.to_s }.each do |name|
- join_association = build(name, parent, join_type)
- build(associations[name], join_association, join_type)
+ associations.each do |k,v|
+ cache = hash[k] ||= {}
+ walk_tree v, cache
end
else
raise ConfigurationError, associations.inspect
end
end
- def find_join_association(name_or_reflection, parent)
- if String === name_or_reflection
- name_or_reflection = name_or_reflection.to_sym
- end
+ # base is the base class on which operation is taking place.
+ # associations is the list of associations which are joined using hash, symbol or array.
+ # joins is the list of all string join commands and arel nodes.
+ #
+ # Example :
+ #
+ # class Physician < ActiveRecord::Base
+ # has_many :appointments
+ # has_many :patients, through: :appointments
+ # end
+ #
+ # If I execute `@physician.patients.to_a` then
+ # base # => Physician
+ # associations # => []
+ # joins # => [#<Arel::Nodes::InnerJoin: ...]
+ #
+ # However if I execute `Physician.joins(:appointments).to_a` then
+ # base # => Physician
+ # associations # => [:appointments]
+ # joins # => []
+ #
+ def initialize(base, associations, joins)
+ @alias_tracker = AliasTracker.create(base.connection, joins)
+ @alias_tracker.aliased_table_for(base.table_name, base.table_name, type_caster: base.type_caster) # Updates the count for base.table_name to 1
+ tree = self.class.make_tree associations
+ @join_root = JoinBase.new base, build(tree, base)
+ @join_root.children.each { |child| construct_tables! @join_root, child }
+ end
+
+ def reflections
+ join_root.drop(1).map!(&:reflection)
+ end
+
+ def join_constraints(outer_joins)
+ joins = join_root.children.flat_map { |child|
+ make_inner_joins join_root, child
+ }
- join_associations.detect { |j|
- j.reflection == name_or_reflection && j.parent == parent
+ joins.concat outer_joins.flat_map { |oj|
+ if join_root.match? oj.join_root
+ walk join_root, oj.join_root
+ else
+ oj.join_root.children.flat_map { |child|
+ make_outer_joins oj.join_root, child
+ }
+ end
+ }
+ end
+
+ def aliases
+ Aliases.new join_root.each_with_index.map { |join_part,i|
+ columns = join_part.column_names.each_with_index.map { |column_name,j|
+ Aliases::Column.new column_name, "t#{i}_r#{j}"
+ }
+ Aliases::Table.new(join_part, columns)
}
end
- def remove_uniq_by_reflection(reflection, records)
- if reflection && reflection.collection?
- records.each { |record| record.send(reflection.name).target.uniq! }
+ def instantiate(result_set, aliases)
+ primary_key = aliases.column_alias(join_root, join_root.primary_key)
+
+ seen = Hash.new { |h,parent_klass|
+ h[parent_klass] = Hash.new { |i,parent_id|
+ i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} }
+ }
+ }
+
+ model_cache = Hash.new { |h,klass| h[klass] = {} }
+ parents = model_cache[join_root]
+ column_aliases = aliases.column_aliases join_root
+
+ message_bus = ActiveSupport::Notifications.instrumenter
+
+ payload = {
+ record_count: result_set.length,
+ class_name: join_root.base_klass.name
+ }
+
+ message_bus.instrument('instantiation.active_record', payload) do
+ result_set.each { |row_hash|
+ parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases)
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
+ }
end
+
+ parents.values
end
- def build_join_association(reflection, parent)
- JoinAssociation.new(reflection, self, parent)
+ private
+
+ def make_constraints(parent, child, tables, join_type)
+ chain = child.reflection.chain
+ foreign_table = parent.table
+ foreign_klass = parent.base_klass
+ child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
end
- def construct(parent, associations, join_parts, row)
- case associations
- when Symbol, String
- name = associations.to_s
+ def make_outer_joins(parent, child)
+ tables = table_aliases_for(parent, child)
+ join_type = Arel::Nodes::OuterJoin
+ info = make_constraints parent, child, tables, join_type
- join_part = join_parts.detect { |j|
- j.reflection.name.to_s == name &&
- j.parent_table_name == parent.class.table_name }
+ [info] + child.children.flat_map { |c| make_outer_joins(child, c) }
+ end
- raise(ConfigurationError, "No such association") unless join_part
+ def make_inner_joins(parent, child)
+ tables = child.tables
+ join_type = Arel::Nodes::InnerJoin
+ info = make_constraints parent, child, tables, join_type
- join_parts.delete(join_part)
- construct_association(parent, join_part, row)
- when Array
- associations.each do |association|
- construct(parent, association, join_parts, row)
+ [info] + child.children.flat_map { |c| make_inner_joins(child, c) }
+ end
+
+ def table_aliases_for(parent, node)
+ node.reflection.chain.map { |reflection|
+ if reflection.klass
+ type_caster = reflection.klass.type_caster
end
- when Hash
- associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
- association = construct(parent, association_name, join_parts, row)
- construct(association, assoc, join_parts, row) if association
+ alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, parent, reflection != node.reflection),
+ type_caster: type_caster,
+ )
+ }
+ end
+
+ def construct_tables!(parent, node)
+ node.tables = table_aliases_for(parent, node)
+ node.children.each { |child| construct_tables! node, child }
+ end
+
+ def table_alias_for(reflection, parent, join)
+ name = "#{reflection.plural_name}_#{parent.table_name}"
+ name << "_join" if join
+ name
+ end
+
+ def walk(left, right)
+ intersection, missing = right.children.map { |node1|
+ [left.children.find { |node2| node1.match? node2 }, node1]
+ }.partition(&:first)
+
+ ojs = missing.flat_map { |_,n| make_outer_joins left, n }
+ intersection.flat_map { |l,r| walk l, r }.concat ojs
+ end
+
+ def find_reflection(klass, name)
+ klass._reflect_on_association(name) or
+ raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
+ end
+
+ def build(associations, base_klass)
+ associations.map do |name, right|
+ reflection = find_reflection base_klass, name
+ reflection.check_validity!
+ reflection.check_eager_loadable!
+
+ if reflection.polymorphic?
+ raise EagerLoadPolymorphicError.new(reflection)
end
- else
- raise ConfigurationError, associations.inspect
+
+ JoinAssociation.new reflection, build(right, reflection.klass)
end
end
- def construct_association(record, join_part, row)
- return if record.id.to_s != join_part.parent.record_id(row).to_s
+ def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
+ primary_id = ar_parent.id
- macro = join_part.reflection.macro
- if macro == :has_one
- return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name)
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- set_target_and_inverse(join_part, association, record)
- else
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- case macro
- when :has_many, :has_and_belongs_to_many
- other = record.association(join_part.reflection.name)
+ parent.children.each do |node|
+ if node.reflection.collection?
+ other = ar_parent.association(node.reflection.name)
other.loaded!
- other.target.push(association) if association
- other.set_inverse_instance(association)
- when :belongs_to
- set_target_and_inverse(join_part, association, record)
else
- raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
+ if ar_parent.association_cache.key?(node.reflection.name)
+ model = ar_parent.association(node.reflection.name).target
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ next
+ end
+ end
+
+ key = aliases.column_alias(node, node.primary_key)
+ id = row[key]
+ next if id.nil?
+
+ model = seen[parent.base_klass][primary_id][node.base_klass][id]
+
+ if model
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ else
+ model = construct_model(ar_parent, node, row, model_cache, id, aliases)
+ model.readonly!
+ seen[parent.base_klass][primary_id][node.base_klass][id] = model
+ construct(model, node, row, rs, seen, model_cache, aliases)
end
end
- association
end
- def set_target_and_inverse(join_part, association, record)
- other = record.association(join_part.reflection.name)
- other.target = association
- other.set_inverse_instance(association)
+ def construct_model(record, node, row, model_cache, id, aliases)
+ model = model_cache[node][id] ||= node.instantiate(row,
+ aliases.column_aliases(node))
+ other = record.association(node.reflection.name)
+
+ if node.reflection.collection?
+ other.target.push(model)
+ else
+ other.target = model
+ end
+
+ other.set_inverse_instance(model)
+ model
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index 0d3b4dbab1..c1ef86a95b 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -1,153 +1,128 @@
+require 'active_record/associations/join_dependency/join_part'
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
- include JoinHelper
-
# The reflection of the association represented
attr_reader :reflection
- # The JoinDependency object which this JoinAssociation exists within. This is mainly
- # relevant for generating aliases which do not conflict with other joins which are
- # part of the query.
- attr_reader :join_dependency
-
- # A JoinBase instance representing the active record we are joining onto.
- # (So in Author.has_many :posts, the Author would be that base record.)
- attr_reader :parent
-
- # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
- attr_accessor :join_type
-
- # These implement abstract methods from the superclass
- attr_reader :aliased_prefix
-
- attr_reader :tables
-
- delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
- delegate :table, :table_name, :to => :parent, :prefix => :parent
- delegate :alias_tracker, :to => :join_dependency
+ attr_accessor :tables
- alias :alias_suffix :parent_table_name
-
- def initialize(reflection, join_dependency, parent = nil)
- reflection.check_validity!
-
- if reflection.options[:polymorphic]
- raise EagerLoadPolymorphicError.new(reflection)
- end
-
- super(reflection.klass)
+ def initialize(reflection, children)
+ super(reflection.klass, children)
@reflection = reflection
- @join_dependency = join_dependency
- @parent = parent
- @join_type = Arel::InnerJoin
- @aliased_prefix = "t#{ join_dependency.join_parts.size }"
- @tables = construct_tables.reverse
+ @tables = nil
end
- def ==(other)
- other.class == self.class &&
- other.reflection == reflection &&
- other.parent == parent
+ def match?(other)
+ return true if self == other
+ super && reflection == other.reflection
end
- def find_parent_in(other_join_dependency)
- other_join_dependency.join_parts.detect do |join_part|
- parent == join_part
- end
- end
+ JoinInformation = Struct.new :joins, :binds
- def join_to(relation)
- tables = @tables.dup
- foreign_table = parent_table
- foreign_klass = parent.active_record
+ def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
+ joins = []
+ bind_values = []
+ tables = tables.reverse
+
+ scope_chain_index = 0
+ scope_chain = scope_chain.reverse
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
- chain.reverse.each_with_index do |reflection, i|
+ chain.reverse_each do |reflection|
table = tables.shift
+ klass = reflection.klass
+
+ join_keys = reflection.join_keys(klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
- case reflection.source_macro
- when :belongs_to
- key = reflection.association_primary_key
- foreign_key = reflection.foreign_key
- when :has_and_belongs_to_many
- # Join the join table first...
- relation.from(join(
- table,
- table[reflection.foreign_key].
- eq(foreign_table[reflection.active_record_primary_key])
- ))
-
- foreign_table, table = table, tables.shift
-
- key = reflection.association_primary_key
- foreign_key = reflection.association_foreign_key
- else
- key = reflection.foreign_key
- foreign_key = reflection.active_record_primary_key
+ constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
+
+ predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table))
+ scope_chain_items = scope_chain[scope_chain_index].map do |item|
+ if item.is_a?(Relation)
+ item
+ else
+ ActiveRecord::Relation.create(klass, table, predicate_builder)
+ .instance_exec(node, &item)
+ end
end
+ scope_chain_index += 1
- constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
+ relation = ActiveRecord::Relation.create(
+ klass,
+ table,
+ predicate_builder,
+ )
+ scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact
- scope_chain_items = scope_chain[i]
+ rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
+ left.merge right
+ end
- if reflection.type
- scope_chain_items += [
- ActiveRecord::Relation.new(reflection.klass, table)
- .where(reflection.type => foreign_klass.base_class.name)
- ]
+ if rel && !rel.arel.constraints.empty?
+ bind_values.concat rel.bind_values
+ constraint = constraint.and rel.arel.constraints
end
- scope_chain_items.each do |item|
- unless item.is_a?(Relation)
- item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item)
- end
+ if reflection.type
+ value = foreign_klass.base_class.name
+ column = klass.columns_hash[reflection.type.to_s]
- constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty?
+ substitute = klass.connection.substitute_at(column)
+ bind_values.push [column, value]
+ constraint = constraint.and table[reflection.type].eq substitute
end
- relation.from(join(table, constraint))
+ joins << table.create_join(table, table.create_on(constraint), join_type)
# The current table in this iteration becomes the foreign table in the next
- foreign_table, foreign_klass = table, reflection.klass
+ foreign_table, foreign_klass = table, klass
end
- relation
+ JoinInformation.new joins, bind_values
end
- def build_constraint(reflection, table, key, foreign_table, foreign_key)
+ # Builds equality condition.
+ #
+ # Example:
+ #
+ # class Physician < ActiveRecord::Base
+ # has_many :appointments
+ # end
+ #
+ # If I execute `Physician.joins(:appointments).to_a` then
+ # klass # => Physician
+ # table # => #<Arel::Table @name="appointments" ...>
+ # key # => physician_id
+ # foreign_table # => #<Arel::Table @name="physicians" ...>
+ # foreign_key # => id
+ #
+ def build_constraint(klass, table, key, foreign_table, foreign_key)
constraint = table[key].eq(foreign_table[foreign_key])
- if reflection.klass.finder_needs_type_condition?
+ if klass.finder_needs_type_condition?
constraint = table.create_and([
constraint,
- reflection.klass.send(:type_condition, table)
+ klass.send(:type_condition, table)
])
end
constraint
end
- def join_relation(joining_relation)
- self.join_type = Arel::OuterJoin
- joining_relation.joins(self)
- end
-
def table
- tables.last
+ tables.first
end
def aliased_table_name
table.table_alias || table.name
end
-
- def scope_chain
- @scope_chain ||= reflection.scope_chain.reverse
- end
-
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
index 3920e84976..3a26c25737 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -1,22 +1,20 @@
+require 'active_record/associations/join_dependency/join_part'
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinBase < JoinPart # :nodoc:
- def ==(other)
- other.class == self.class &&
- other.active_record == active_record
- end
-
- def aliased_prefix
- "t0"
+ def match?(other)
+ return true if self == other
+ super && base_klass == other.base_klass
end
def table
- Arel::Table.new(table_name, arel_engine)
+ base_klass.arel_table
end
def aliased_table_name
- active_record.table_name
+ base_klass.table_name
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
index 711f7b3ce1..9c6573f913 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -1,41 +1,42 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
- # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
+ # A JoinPart represents a part of a JoinDependency. It is inherited
# by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
# everything else is being joined onto. A JoinAssociation represents an association which
# is joining to the base. A JoinAssociation may result in more than one actual join
# operations (for example a has_and_belongs_to_many JoinAssociation would result in
# two; one for the join table and one for the target table).
class JoinPart # :nodoc:
+ include Enumerable
+
# The Active Record class which this join part is associated 'about'; for a JoinBase
# this is the actual base model, for a JoinAssociation this is the target model of the
# association.
- attr_reader :active_record
+ attr_reader :base_klass, :children
- delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
+ delegate :table_name, :column_names, :primary_key, :to => :base_klass
- def initialize(active_record)
- @active_record = active_record
- @cached_record = {}
- @column_names_with_alias = nil
+ def initialize(base_klass, children)
+ @base_klass = base_klass
+ @children = children
end
- def aliased_table
- Arel::Nodes::TableAlias.new table, aliased_table_name
+ def name
+ reflection.name
end
- def ==(other)
- raise NotImplementedError
+ def match?(other)
+ self.class == other.class
end
- # An Arel::Table for the active_record
- def table
- raise NotImplementedError
+ def each(&block)
+ yield self
+ children.each { |child| child.each(&block) }
end
- # The prefix to be used when aliasing columns in the active_record's table
- def aliased_prefix
+ # An Arel::Table for the active_record
+ def table
raise NotImplementedError
end
@@ -44,33 +45,25 @@ module ActiveRecord
raise NotImplementedError
end
- # The alias for the primary key of the active_record's table
- def aliased_primary_key
- "#{aliased_prefix}_r0"
- end
+ def extract_record(row, column_names_with_alias)
+ # This code is performance critical as it is called per row.
+ # see: https://github.com/rails/rails/pull/12185
+ hash = {}
- # An array of [column_name, alias] pairs for the table
- def column_names_with_alias
- unless @column_names_with_alias
- @column_names_with_alias = []
+ index = 0
+ length = column_names_with_alias.length
- ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i|
- @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
- end
+ while index < length
+ column_name, alias_name = column_names_with_alias[index]
+ hash[column_name] = row[alias_name]
+ index += 1
end
- @column_names_with_alias
- end
-
- def extract_record(row)
- Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
- end
- def record_id(row)
- row[aliased_primary_key]
+ hash
end
- def instantiate(row)
- @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
+ def instantiate(row, aliases)
+ base_klass.instantiate(extract_record(row, aliases))
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb
deleted file mode 100644
index 5a41b40c8f..0000000000
--- a/activerecord/lib/active_record/associations/join_helper.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-module ActiveRecord
- module Associations
- # Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope
- module JoinHelper #:nodoc:
-
- def join_type
- Arel::InnerJoin
- end
-
- private
-
- def construct_tables
- tables = []
- chain.each do |reflection|
- tables << alias_tracker.aliased_table_for(
- table_name_for(reflection),
- table_alias_for(reflection, reflection != self.reflection)
- )
-
- if reflection.source_macro == :has_and_belongs_to_many
- tables << alias_tracker.aliased_table_for(
- (reflection.source_reflection || reflection).join_table,
- table_alias_for(reflection, true)
- )
- end
- end
- tables
- end
-
- def table_name_for(reflection)
- reflection.table_name
- end
-
- def table_alias_for(reflection, join = false)
- name = "#{reflection.plural_name}_#{alias_suffix}"
- name << "_join" if join
- name
- end
-
- def join(table, constraint)
- table.create_join(table, table.create_on(constraint), join_type)
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 0848e7afb3..4358f3b581 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -2,33 +2,42 @@ module ActiveRecord
module Associations
# Implements the details of eager loading of Active Record associations.
#
- # Note that 'eager loading' and 'preloading' are actually the same thing.
- # However, there are two different eager loading strategies.
+ # Suppose that you have the following two Active Record models:
#
- # The first one is by using table joins. This was only strategy available
- # prior to Rails 2.1. Suppose that you have an Author model with columns
- # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
- # this strategy, Active Record would try to retrieve all data for an author
- # and all of its books via a single query:
+ # class Author < ActiveRecord::Base
+ # # columns: name, age
+ # has_many :books
+ # end
#
- # SELECT * FROM authors
- # LEFT OUTER JOIN books ON authors.id = books.author_id
- # WHERE authors.name = 'Ken Akamatsu'
+ # class Book < ActiveRecord::Base
+ # # columns: title, sales
+ # end
#
- # However, this could result in many rows that contain redundant data. After
- # having received the first row, we already have enough data to instantiate
- # the Author object. In all subsequent rows, only the data for the joined
- # 'books' table is useful; the joined 'authors' data is just redundant, and
- # processing this redundant data takes memory and CPU time. The problem
- # quickly becomes worse and worse as the level of eager loading increases
- # (i.e. if Active Record is to eager load the associations' associations as
- # well).
+ # When you load an author with all associated books Active Record will make
+ # multiple queries like this:
+ #
+ # Author.includes(:books).where(:name => ['bell hooks', 'Homer').to_a
+ #
+ # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer')
+ # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5)
+ #
+ # Active Record saves the ids of the records from the first query to use in
+ # the second. Depending on the number of associations involved there can be
+ # arbitrarily many SQL queries made.
+ #
+ # However, if there is a WHERE clause that spans across tables Active
+ # Record will fall back to a slightly more resource-intensive single query:
+ #
+ # Author.includes(:books).where(books: {title: 'Illiad'}).to_a
+ # => SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2,
+ # `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`sales` AS t1_r2
+ # FROM `authors`
+ # LEFT OUTER JOIN `books` ON `authors`.`id` = `books`.`author_id`
+ # WHERE `books`.`title` = 'Illiad'
+ #
+ # This could result in many rows that contain redundant data and it performs poorly at scale
+ # and is therefore only used when necessary.
#
- # The second strategy is to use multiple database queries, one for each
- # level of association. Since Rails 2.1, this is the default strategy. In
- # situations where a table join is necessary (e.g. when the +:conditions+
- # option references an association's column), it will fallback to the table
- # join strategy.
class Preloader #:nodoc:
extend ActiveSupport::Autoload
@@ -42,12 +51,9 @@ module ActiveRecord
autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
autoload :HasOne, 'active_record/associations/preloader/has_one'
autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
end
- attr_reader :records, :associations, :preload_scope, :model
-
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -82,36 +88,48 @@ module ActiveRecord
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
- def initialize(records, associations, preload_scope = nil)
- @records = Array.wrap(records).compact.uniq
- @associations = Array.wrap(associations)
- @preload_scope = preload_scope || Relation.new(nil, nil)
- end
- def run
- unless records.empty?
- associations.each { |association| preload(association) }
+ NULL_RELATION = Struct.new(:values, :bind_values).new({}, [])
+
+ def preload(records, associations, preload_scope = nil)
+ records = Array.wrap(records).compact.uniq
+ associations = Array.wrap(associations)
+ preload_scope = preload_scope || NULL_RELATION
+
+ if records.empty?
+ []
+ else
+ associations.flat_map { |association|
+ preloaders_on association, records, preload_scope
+ }
end
end
private
- def preload(association)
+ def preloaders_on(association, records, scope)
case association
when Hash
- preload_hash(association)
- when String, Symbol
- preload_one(association.to_sym)
+ preloaders_for_hash(association, records, scope)
+ when Symbol
+ preloaders_for_one(association, records, scope)
+ when String
+ preloaders_for_one(association.to_sym, records, scope)
else
raise ArgumentError, "#{association.inspect} was not recognised for preload"
end
end
- def preload_hash(association)
- association.each do |parent, child|
- Preloader.new(records, parent, preload_scope).run
- Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
- end
+ def preloaders_for_hash(association, records, scope)
+ association.flat_map { |parent, child|
+ loaders = preloaders_for_one parent, records, scope
+
+ recs = loaders.flat_map(&:preloaded_records).uniq
+ loaders.concat Array.wrap(child).flat_map { |assoc|
+ preloaders_on assoc, recs, scope
+ }
+ loaders
+ }
end
# Not all records have the same class, so group then preload group on the reflection
@@ -121,52 +139,61 @@ module ActiveRecord
# Additionally, polymorphic belongs_to associations can have multiple associated
# classes, depending on the polymorphic_type field. So we group by the classes as
# well.
- def preload_one(association)
- grouped_records(association).each do |reflection, klasses|
- klasses.each do |klass, records|
- preloader_for(reflection).new(klass, records, reflection, preload_scope).run
+ def preloaders_for_one(association, records, scope)
+ grouped_records(association, records).flat_map do |reflection, klasses|
+ klasses.map do |rhs_klass, rs|
+ loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
+ loader.run self
+ loader
end
end
end
- def grouped_records(association)
- Hash[
- records_by_reflection(association).map do |reflection, records|
- [reflection, records.group_by { |record| association_klass(reflection, record) }]
- end
- ]
+ def grouped_records(association, records)
+ h = {}
+ records.each do |record|
+ next unless record
+ assoc = record.association(association)
+ klasses = h[assoc.reflection] ||= {}
+ (klasses[assoc.klass] ||= []) << record
+ end
+ h
end
- def records_by_reflection(association)
- records.group_by do |record|
- reflection = record.class.reflections[association]
+ class AlreadyLoaded
+ attr_reader :owners, :reflection
- unless reflection
- raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
- "perhaps you misspelled it?"
- end
+ def initialize(klass, owners, reflection, preload_scope)
+ @owners = owners
+ @reflection = reflection
+ end
+
+ def run(preloader); end
- reflection
+ def preloaded_records
+ owners.flat_map { |owner| owner.association(reflection.name).target }
end
end
- def association_klass(reflection, record)
- if reflection.macro == :belongs_to && reflection.options[:polymorphic]
- klass = record.send(reflection.foreign_type)
- klass && klass.constantize
- else
- reflection.klass
- end
+ class NullPreloader
+ def self.new(klass, owners, reflection, preload_scope); self; end
+ def self.run(preloader); end
+ def self.preloaded_records; []; end
end
- def preloader_for(reflection)
+ def preloader_for(reflection, owners, rhs_klass)
+ return NullPreloader unless rhs_klass
+
+ if owners.first.association(reflection.name).loaded?
+ return AlreadyLoaded
+ end
+ reflection.check_preloadable!
+
case reflection.macro
when :has_many
reflection.options[:through] ? HasManyThrough : HasMany
when :has_one
reflection.options[:through] ? HasOneThrough : HasOne
- when :has_and_belongs_to_many
- HasAndBelongsToMany
when :belongs_to
BelongsTo
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index cbf5e734ea..a6e1a24360 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -3,6 +3,7 @@ module ActiveRecord
class Preloader
class Association #:nodoc:
attr_reader :owners, :reflection, :preload_scope, :model, :klass
+ attr_reader :preloaded_records
def initialize(klass, owners, reflection, preload_scope)
@klass = klass
@@ -12,15 +13,14 @@ module ActiveRecord
@model = owners.first && owners.first.class
@scope = nil
@owners_by_key = nil
+ @preloaded_records = []
end
- def run
- unless owners.first.association(reflection.name).loaded?
- preload
- end
+ def run(preloader)
+ preload(preloader)
end
- def preload
+ def preload(preloader)
raise NotImplementedError
end
@@ -29,7 +29,11 @@ module ActiveRecord
end
def records_for(ids)
- scope.where(association_key.in(ids))
+ query_scope(ids)
+ end
+
+ def query_scope(ids)
+ scope.where(association_key_name => ids)
end
def table
@@ -52,13 +56,16 @@ module ActiveRecord
raise NotImplementedError
end
- # We're converting to a string here because postgres will return the aliased association
- # key in a habtm as a string (for whatever reason)
def owners_by_key
- @owners_by_key ||= owners.group_by do |owner|
- key = owner[owner_key_name]
- key && key.to_s
- end
+ @owners_by_key ||= if key_conversion_required?
+ owners.group_by do |owner|
+ owner[owner_key_name].to_s
+ end
+ else
+ owners.group_by do |owner|
+ owner[owner_key_name]
+ end
+ end
end
def options
@@ -67,53 +74,87 @@ module ActiveRecord
private
- def associated_records_by_owner
+ def associated_records_by_owner(preloader)
owners_map = owners_by_key
owner_keys = owners_map.keys.compact
- if klass.nil? || owner_keys.empty?
- records = []
- else
- # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
- # Make several smaller queries if necessary or make one query if the adapter supports it
- sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
- records = sliced.map { |slice| records_for(slice).to_a }.flatten
+ # Each record may have multiple owners, and vice-versa
+ records_by_owner = owners.each_with_object({}) do |owner,h|
+ h[owner] = []
end
- # Each record may have multiple owners, and vice-versa
- records_by_owner = Hash[owners.map { |owner| [owner, []] }]
- records.each do |record|
- owner_key = record[association_key_name].to_s
+ if owner_keys.any?
+ # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
+ # Make several smaller queries if necessary or make one query if the adapter supports it
+ sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
- owners_map[owner_key].each do |owner|
- records_by_owner[owner] << record
+ records = load_slices sliced
+ records.each do |record, owner_key|
+ owners_map[owner_key].each do |owner|
+ records_by_owner[owner] << record
+ end
end
end
+
records_by_owner
end
+ def key_conversion_required?
+ association_key_type != owner_key_type
+ end
+
+ def association_key_type
+ @klass.type_for_attribute(association_key_name.to_s).type
+ end
+
+ def owner_key_type
+ @model.type_for_attribute(owner_key_name.to_s).type
+ end
+
+ def load_slices(slices)
+ @preloaded_records = slices.flat_map { |slice|
+ records_for(slice)
+ }
+
+ @preloaded_records.map { |record|
+ key = record[association_key_name]
+ key = key.to_s if key_conversion_required?
+
+ [record, key]
+ }
+ end
+
def reflection_scope
@reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
end
def build_scope
scope = klass.unscoped
- scope.default_scoped = true
values = reflection_scope.values
+ reflection_binds = reflection_scope.bind_values
preload_values = preload_scope.values
+ preload_binds = preload_scope.bind_values
scope.where_values = Array(values[:where]) + Array(preload_values[:where])
scope.references_values = Array(values[:references]) + Array(preload_values[:references])
+ scope.bind_values = (reflection_binds + preload_binds)
- scope.select! preload_values[:select] || values[:select] || table[Arel.star]
+ scope._select! preload_values[:select] || values[:select] || table[Arel.star]
scope.includes! preload_values[:includes] || values[:includes]
+ scope.joins! preload_values[:joins] || values[:joins]
+ scope.order! preload_values[:order] || values[:order]
+
+ if preload_values[:readonly] || values[:readonly]
+ scope.readonly!
+ end
if options[:as]
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
- scope
+ scope.unscope_values = Array(values[:unscope])
+ klass.default_scoped.merge(scope)
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
index e6cd35e7a1..5adffcd831 100644
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -9,8 +9,8 @@ module ActiveRecord
super.order(preload_scope.values[:order] || reflection_scope.values[:order])
end
- def preload
- associated_records_by_owner.each do |owner, records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, records|
association = owner.association(reflection.name)
association.loaded!
association.target.concat(records)
diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
deleted file mode 100644
index 8e8925f0a9..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class HasAndBelongsToMany < CollectionAssociation #:nodoc:
- attr_reader :join_table
-
- def initialize(klass, records, reflection, preload_options)
- super
- @join_table = Arel::Table.new(reflection.join_table).alias('t0')
- end
-
- # Unlike the other associations, we want to get a raw array of rows so that we can
- # access the aliased column on the join table
- def records_for(ids)
- scope = super
- klass.connection.select_all(scope.arel, 'SQL', scope.bind_values)
- end
-
- def owner_key_name
- reflection.active_record_primary_key
- end
-
- def association_key_name
- 'ar_association_key_name'
- end
-
- def association_key
- join_table[reflection.foreign_key]
- end
-
- private
-
- # Once we have used the join table column (in super), we manually instantiate the
- # actual records, ensuring that we don't create more than one instances of the same
- # record
- def associated_records_by_owner
- records = {}
- super.each do |owner_key, rows|
- rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) }
- end
- end
-
- def build_scope
- super.joins(join).select(join_select)
- end
-
- def join_select
- association_key.as(Arel.sql(association_key_name))
- end
-
- def join
- condition = table[reflection.association_primary_key].eq(
- join_table[reflection.association_foreign_key])
-
- table.create_join(join_table, table.create_on(condition))
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
index 9a662d3f53..2029871f39 100644
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -4,10 +4,14 @@ module ActiveRecord
class HasManyThrough < CollectionAssociation #:nodoc:
include ThroughAssociation
- def associated_records_by_owner
- super.each do |owner, records|
- records.uniq! if reflection_scope.uniq_value
+ def associated_records_by_owner(preloader)
+ records_by_owner = super
+
+ if reflection_scope.distinct_value
+ records_by_owner.each_value(&:uniq!)
end
+
+ records_by_owner
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
index 44e804d785..f60647a81e 100644
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb
@@ -5,13 +5,13 @@ module ActiveRecord
private
- def preload
- associated_records_by_owner.each do |owner, associated_records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, associated_records|
record = associated_records.first
association = owner.association(reflection.name)
association.target = record
- association.set_inverse_instance(record)
+ association.set_inverse_instance(record) if record
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 1c1ba11c44..12bf3ef138 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -2,7 +2,6 @@ module ActiveRecord
module Associations
class Preloader
module ThroughAssociation #:nodoc:
-
def through_reflection
reflection.through_reflection
end
@@ -11,50 +10,85 @@ module ActiveRecord
reflection.source_reflection
end
- def associated_records_by_owner
- through_records = through_records_by_owner
+ def associated_records_by_owner(preloader)
+ preloader.preload(owners,
+ through_reflection.name,
+ through_scope)
- Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run
+ through_records = owners.map do |owner|
+ association = owner.association through_reflection.name
- through_records.each do |owner, records|
- records.map! { |r| r.send(source_reflection.name) }.flatten!
- records.compact!
+ [owner, Array(association.reader)]
end
- end
- private
+ reset_association owners, through_reflection.name
+
+ middle_records = through_records.flat_map { |(_,rec)| rec }
+
+ preloaders = preloader.preload(middle_records,
+ source_reflection.name,
+ reflection_scope)
- def through_records_by_owner
- Preloader.new(owners, through_reflection.name, through_scope).run
+ @preloaded_records = preloaders.flat_map(&:preloaded_records)
+
+ middle_to_pl = preloaders.each_with_object({}) do |pl,h|
+ pl.owners.each { |middle|
+ h[middle] = pl
+ }
+ end
+
+ record_offset = {}
+ @preloaded_records.each_with_index do |record,i|
+ record_offset[record] = i
+ end
- Hash[owners.map do |owner|
- through_records = Array.wrap(owner.send(through_reflection.name))
+ through_records.each_with_object({}) { |(lhs,center),records_by_owner|
+ pl_to_middle = center.group_by { |record| middle_to_pl[record] }
- # Dont cache the association - we would only be caching a subset
- if reflection.options[:source_type] && through_reflection.collection?
- owner.association(through_reflection.name).reset
+ records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
+ rhs_records = middles.flat_map { |r|
+ association = r.association source_reflection.name
+
+ association.reader
+ }.compact
+
+ rhs_records.sort_by { |rhs| record_offset[rhs] }
end
+ }
+ end
+
+ private
+
+ def reset_association(owners, association_name)
+ should_reset = (through_scope != through_reflection.klass.unscoped) ||
+ (reflection.options[:source_type] && through_reflection.collection?)
- [owner, through_records]
- end]
+ # Don't cache the association - we would only be caching a subset
+ if should_reset
+ owners.each { |owner|
+ owner.association(association_name).reset
+ }
+ end
end
+
def through_scope
- through_scope = through_reflection.klass.unscoped
+ scope = through_reflection.klass.unscoped
if options[:source_type]
- through_scope.where! reflection.foreign_type => options[:source_type]
+ scope.where! reflection.foreign_type => options[:source_type]
else
unless reflection_scope.where_values.empty?
- through_scope.includes_values = reflection_scope.values[:includes] || options[:source]
- through_scope.where_values = reflection_scope.values[:where]
+ scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
+ scope.where_values = reflection_scope.values[:where]
+ scope.bind_values = reflection_scope.bind_values
end
- through_scope.order! reflection_scope.values[:order]
- through_scope.references! reflection_scope.values[:references]
+ scope.references! reflection_scope.values[:references]
+ scope = scope.order reflection_scope.values[:order] if scope.eager_loading?
end
- through_scope
+ scope
end
end
end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 10238555f0..c44242a0f0 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -18,11 +18,11 @@ module ActiveRecord
end
def create(attributes = {}, &block)
- create_record(attributes, &block)
+ _create_record(attributes, &block)
end
def create!(attributes = {}, &block)
- create_record(attributes, true, &block)
+ _create_record(attributes, true, &block)
end
def build(attributes = {})
@@ -38,11 +38,33 @@ module ActiveRecord
scope.scope_for_create.stringify_keys.except(klass.primary_key)
end
+ def get_records
+ if reflection.scope_chain.any?(&:any?) ||
+ scope.eager_loading? ||
+ klass.current_scope ||
+ klass.default_scopes.any?
+
+ return scope.limit(1).to_a
+ end
+
+ conn = klass.connection
+ sc = reflection.association_scope_cache(conn, owner) do
+ StatementCache.create(conn) { |params|
+ as = AssociationScope.create { params.bind }
+ target_scope.merge(as.scope(self, conn)).limit(1)
+ }
+ end
+
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
+ sc.execute binds, klass, klass.connection
+ end
+
def find_target
- scope.first.tap { |record| set_inverse_instance(record) }
+ if record = get_records.first
+ set_inverse_instance record
+ end
end
- # Implemented by subclasses
def replace(record)
raise NotImplementedError, "Subclasses must implement a replace(record) method"
end
@@ -51,7 +73,7 @@ module ActiveRecord
replace(record)
end
- def create_record(attributes, raise_error = false)
+ def _create_record(attributes, raise_error = false)
record = build_record(attributes)
yield(record) if block_given?
saved = record.save
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index 43520142bf..09828dbd9b 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -3,7 +3,7 @@ module ActiveRecord
module Associations
module ThroughAssociation #:nodoc:
- delegate :source_reflection, :through_reflection, :chain, :to => :reflection
+ delegate :source_reflection, :through_reflection, :to => :reflection
protected
@@ -13,10 +13,16 @@ module ActiveRecord
# 2. To get the type conditions for any STI models in the chain
def target_scope
scope = super
- chain[1..-1].each do |reflection|
- scope = scope.merge(
- reflection.klass.all.with_default_scope.
- except(:select, :create_with, :includes, :preload, :joins, :eager_load)
+ reflection.chain.drop(1).each do |reflection|
+ relation = reflection.klass.all
+
+ reflection_scope = reflection.scope
+ if reflection_scope && reflection_scope.arity.zero?
+ relation.merge!(reflection_scope)
+ end
+
+ scope.merge!(
+ relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
end
scope
@@ -39,12 +45,16 @@ module ActiveRecord
def construct_join_attributes(*records)
ensure_mutable
- join_attributes = {
- source_reflection.foreign_key =>
- records.map { |record|
- record.send(source_reflection.association_primary_key(reflection.klass))
- }
- }
+ if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key
+ join_attributes = { source_reflection.name => records }
+ else
+ join_attributes = {
+ source_reflection.foreign_key =>
+ records.map { |record|
+ record.send(source_reflection.association_primary_key(reflection.klass))
+ }
+ }
+ end
if options[:source_type]
join_attributes[source_reflection.foreign_type] =
@@ -61,18 +71,17 @@ module ActiveRecord
# Note: this does not capture all cases, for example it would be crazy to try to
# properly support stale-checking for nested associations.
def stale_state
- if through_reflection.macro == :belongs_to
+ if through_reflection.belongs_to?
owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s
end
end
def foreign_key_present?
- through_reflection.macro == :belongs_to &&
- !owner[through_reflection.foreign_key].nil?
+ through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil?
end
def ensure_mutable
- if source_reflection.macro != :belongs_to
+ unless source_reflection.belongs_to?
raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
end
end
@@ -82,6 +91,17 @@ module ActiveRecord
raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
end
end
+
+ def build_record(attributes)
+ inverse = source_reflection.inverse_of
+ target = through_association.target
+
+ if inverse && target && !target.is_a?(Array)
+ attributes[inverse.foreign_key] = target.id
+ end
+
+ super(attributes)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
new file mode 100644
index 0000000000..88536eaac0
--- /dev/null
+++ b/activerecord/lib/active_record/attribute.rb
@@ -0,0 +1,149 @@
+module ActiveRecord
+ class Attribute # :nodoc:
+ class << self
+ def from_database(name, value, type)
+ FromDatabase.new(name, value, type)
+ end
+
+ def from_user(name, value, type)
+ FromUser.new(name, value, type)
+ end
+
+ def with_cast_value(name, value, type)
+ WithCastValue.new(name, value, type)
+ end
+
+ def null(name)
+ Null.new(name)
+ end
+
+ def uninitialized(name, type)
+ Uninitialized.new(name, type)
+ end
+ end
+
+ attr_reader :name, :value_before_type_cast, :type
+
+ # This method should not be called directly.
+ # Use #from_database or #from_user
+ def initialize(name, value_before_type_cast, type)
+ @name = name
+ @value_before_type_cast = value_before_type_cast
+ @type = type
+ end
+
+ def value
+ # `defined?` is cheaper than `||=` when we get back falsy values
+ @value = original_value unless defined?(@value)
+ @value
+ end
+
+ def original_value
+ type_cast(value_before_type_cast)
+ end
+
+ def value_for_database
+ type.type_cast_for_database(value)
+ end
+
+ def changed_from?(old_value)
+ type.changed?(old_value, value, value_before_type_cast)
+ end
+
+ def changed_in_place_from?(old_value)
+ type.changed_in_place?(old_value, value)
+ end
+
+ def with_value_from_user(value)
+ self.class.from_user(name, value, type)
+ end
+
+ def with_value_from_database(value)
+ self.class.from_database(name, value, type)
+ end
+
+ def with_cast_value(value)
+ self.class.with_cast_value(name, value, type)
+ end
+
+ def type_cast(*)
+ raise NotImplementedError
+ end
+
+ def initialized?
+ true
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ name == other.name &&
+ value_before_type_cast == other.value_before_type_cast &&
+ type == other.type
+ end
+
+ protected
+
+ def initialize_dup(other)
+ if defined?(@value) && @value.duplicable?
+ @value = @value.dup
+ end
+ end
+
+ class FromDatabase < Attribute # :nodoc:
+ def type_cast(value)
+ type.type_cast_from_database(value)
+ end
+ end
+
+ class FromUser < Attribute # :nodoc:
+ def type_cast(value)
+ type.type_cast_from_user(value)
+ end
+ end
+
+ class WithCastValue < Attribute # :nodoc:
+ def type_cast(value)
+ value
+ end
+
+ def changed_in_place_from?(old_value)
+ false
+ end
+ end
+
+ class Null < Attribute # :nodoc:
+ def initialize(name)
+ super(name, nil, Type::Value.new)
+ end
+
+ def value
+ nil
+ end
+
+ def with_value_from_database(value)
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
+ end
+ alias_method :with_value_from_user, :with_value_from_database
+ end
+
+ class Uninitialized < Attribute # :nodoc:
+ def initialize(name, type)
+ super(name, nil, type)
+ end
+
+ def value
+ if block_given?
+ yield name
+ end
+ end
+
+ def value_for_database
+ end
+
+ def initialized?
+ false
+ end
+ end
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index ecfa556ab4..bf64830417 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -1,8 +1,8 @@
+require 'active_model/forbidden_attributes_protection'
module ActiveRecord
module AttributeAssignment
extend ActiveSupport::Concern
- include ActiveModel::DeprecatedMassAssignmentSecurity
include ActiveModel::ForbiddenAttributesProtection
# Allows you to set all the attributes by passing in a hash of attributes with
@@ -11,7 +11,19 @@ module ActiveRecord
# If the passed hash responds to <tt>permitted?</tt> method and the return value
# of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
# exception is raised.
+ #
+ # cat = Cat.new(name: "Gorby", status: "yawning")
+ # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil}
+ # cat.assign_attributes(status: "sleeping")
+ # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil }
+ #
+ # New attributes will be persisted in the database when the object is saved.
+ #
+ # Aliased to <tt>attributes=</tt>.
def assign_attributes(new_attributes)
+ if !new_attributes.respond_to?(:stringify_keys)
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
+ end
return if new_attributes.blank?
attributes = new_attributes.stringify_keys
@@ -44,7 +56,7 @@ module ActiveRecord
if respond_to?("#{k}=")
raise
else
- raise UnknownAttributeError, "unknown attribute: #{k}"
+ raise UnknownAttributeError.new(self, k)
end
end
@@ -75,13 +87,13 @@ module ActiveRecord
end
end
unless errors.empty?
- error_descriptions = errors.map { |ex| ex.message }.join(",")
+ error_descriptions = errors.map(&:message).join(",")
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
end
end
def extract_callstack_for_multiparameter_attributes(pairs)
- attributes = { }
+ attributes = {}
pairs.each do |(multiparameter_name, value)|
attribute_name = multiparameter_name.split("(").first
@@ -103,7 +115,7 @@ module ActiveRecord
end
class MultiparameterAttribute #:nodoc:
- attr_reader :object, :name, :values, :column
+ attr_reader :object, :name, :values, :cast_type
def initialize(object, name, values)
@object = object
@@ -114,22 +126,22 @@ module ActiveRecord
def read_value
return if values.values.compact.empty?
- @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name)
- klass = column.klass
+ @cast_type = object.type_for_attribute(name)
+ klass = cast_type.klass
if klass == Time
read_time
elsif klass == Date
read_date
else
- read_other(klass)
+ read_other
end
end
private
def instantiate_time_object(set_values)
- if object.class.send(:create_time_zone_conversion_attribute?, name, column)
+ if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type)
Time.zone.local(*set_values)
else
Time.send(object.class.default_timezone, *set_values)
@@ -137,16 +149,16 @@ module ActiveRecord
end
def read_time
- # If column is a :time (and not :date or :timestamp) there is no need to validate if
+ # If column is a :time (and not :date or :datetime) there is no need to validate if
# there are year/month/day fields
- if column.type == :time
+ if cast_type.type == :time
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
{ 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
values[key] ||= value
end
else
# else column is a timestamp, so if Date bits were not provided, error
- validate_missing_parameters!([1,2,3])
+ validate_required_parameters!([1,2,3])
# If Date bits were provided but blank, then return nil
return if blank_date_parameter?
@@ -169,17 +181,16 @@ module ActiveRecord
end
end
- def read_other(klass)
+ def read_other
max_position = extract_max_param
positions = (1..max_position)
- validate_missing_parameters!(positions)
+ validate_required_parameters!(positions)
- set_values = values.values_at(*positions)
- klass.new(*set_values)
+ values.slice(*positions)
end
# Checks whether some blank date parameter exists. Note that this is different
- # than the validate_missing_parameters! method, since it just checks for blank
+ # than the validate_required_parameters! method, since it just checks for blank
# positions instead of missing ones, and does not raise in case one blank position
# exists. The caller is responsible to handle the case of this returning true.
def blank_date_parameter?
@@ -187,7 +198,7 @@ module ActiveRecord
end
# If some position is not provided, it errors out a missing parameter exception.
- def validate_missing_parameters!(positions)
+ def validate_required_parameters!(positions)
if missing_parameter = positions.detect { |position| !values.key?(position) }
raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
end
diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
new file mode 100644
index 0000000000..5b96623b6e
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -0,0 +1,66 @@
+module ActiveRecord
+ module AttributeDecorators # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :attribute_type_decorations, instance_accessor: false # :internal:
+ self.attribute_type_decorations = TypeDecorator.new
+ end
+
+ module ClassMethods # :nodoc:
+ def decorate_attribute_type(column_name, decorator_name, &block)
+ matcher = ->(name, _) { name == column_name.to_s }
+ key = "_#{column_name}_#{decorator_name}"
+ decorate_matching_attribute_types(matcher, key, &block)
+ end
+
+ def decorate_matching_attribute_types(matcher, decorator_name, &block)
+ clear_caches_calculated_from_columns
+ decorator_name = decorator_name.to_s
+
+ # Create new hashes so we don't modify parent classes
+ self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
+ end
+
+ private
+
+ def add_user_provided_columns(*)
+ super.map do |column|
+ decorated_type = attribute_type_decorations.apply(column.name, column.cast_type)
+ column.with_type(decorated_type)
+ end
+ end
+ end
+
+ class TypeDecorator # :nodoc:
+ delegate :clear, to: :@decorations
+
+ def initialize(decorations = {})
+ @decorations = decorations
+ end
+
+ def merge(*args)
+ TypeDecorator.new(@decorations.merge(*args))
+ end
+
+ def apply(name, type)
+ decorations = decorators_for(name, type)
+ decorations.inject(type) do |new_type, block|
+ block.call(new_type)
+ end
+ end
+
+ private
+
+ def decorators_for(name, type)
+ matching(name, type).map(&:last)
+ end
+
+ def matching(name, type)
+ @decorations.values.select do |(matcher, _)|
+ matcher.call(name, type)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index e0bfdb8f3e..b7edac791e 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,4 +1,7 @@
require 'active_support/core_ext/enumerable'
+require 'active_support/core_ext/string/filters'
+require 'mutex_m'
+require 'thread_safe'
module ActiveRecord
# = Active Record Attribute Methods
@@ -7,6 +10,7 @@ module ActiveRecord
include ActiveModel::AttributeMethods
included do
+ initialize_generated_modules
include Read
include Write
include BeforeTypeCast
@@ -15,29 +19,81 @@ module ActiveRecord
include TimeZoneConversion
include Dirty
include Serialization
+
+ delegate :column_for_attribute, to: :class
end
+ AttrNames = Module.new {
+ def self.set_name_cache(name, value)
+ const_name = "ATTR_#{name}"
+ unless const_defined? const_name
+ const_set const_name, value.dup.freeze
+ end
+ end
+ }
+
+ BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
+
+ class AttributeMethodCache
+ def initialize
+ @module = Module.new
+ @method_cache = ThreadSafe::Cache.new
+ end
+
+ def [](name)
+ @method_cache.compute_if_absent(name) do
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+ @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__
+ @module.instance_method temp_method
+ end
+ end
+
+ private
+
+ # Override this method in the subclasses for method body.
+ def method_body(method_name, const_name)
+ raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method."
+ end
+ end
+
+ class GeneratedAttributeMethods < Module; end # :nodoc:
+
module ClassMethods
+ def inherited(child_class) #:nodoc:
+ child_class.initialize_generated_modules
+ super
+ end
+
+ def initialize_generated_modules # :nodoc:
+ @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m }
+ @attribute_methods_generated = false
+ include @generated_attribute_methods
+
+ super
+ end
+
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
- # Use a mutex; we don't want two thread simaltaneously trying to define
+ return false if @attribute_methods_generated
+ # Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
- @attribute_methods_mutex.synchronize do
- return if attribute_methods_generated?
+ generated_attribute_methods.synchronize do
+ return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(column_names)
@attribute_methods_generated = true
end
- end
-
- def attribute_methods_generated? # :nodoc:
- @attribute_methods_generated ||= false
+ true
end
def undefine_attribute_methods # :nodoc:
- super if attribute_methods_generated?
- @attribute_methods_generated = false
+ generated_attribute_methods.synchronize do
+ super if defined?(@attribute_methods_generated) && @attribute_methods_generated
+ @attribute_methods_generated = false
+ end
end
# Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
@@ -56,28 +112,48 @@ module ActiveRecord
# # => false
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
- raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
+ raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name."
end
if superclass == Base
super
else
- # If B < A and A defines its own attribute method, then we don't want to overwrite that.
- defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
- defined && !ActiveRecord::Base.method_defined?(method_name) || super
+ # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
+ # defines its own attribute method, then we don't want to overwrite that.
+ defined = method_defined_within?(method_name, superclass, Base) &&
+ ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
+ defined || super
end
end
- # A method name is 'dangerous' if it is already defined by Active Record, but
+ # A method name is 'dangerous' if it is already (re)defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
def dangerous_attribute_method?(name) # :nodoc:
method_defined_within?(name, Base)
end
- def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc:
+ def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
if klass.method_defined?(name) || klass.private_method_defined?(name)
- if sup.method_defined?(name) || sup.private_method_defined?(name)
- klass.instance_method(name).owner != sup.instance_method(name).owner
+ if superklass.method_defined?(name) || superklass.private_method_defined?(name)
+ klass.instance_method(name).owner != superklass.instance_method(name).owner
+ else
+ true
+ end
+ else
+ false
+ end
+ end
+
+ # A class method is 'dangerous' if it is already (re)defined by Active Record, but
+ # not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
+ def dangerous_class_method?(method_name)
+ BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
+ end
+
+ def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
+ if klass.respond_to?(name, true)
+ if superklass.respond_to?(name, true)
+ klass.method(name).owner != superklass.method(name).owner
else
true
end
@@ -114,41 +190,35 @@ module ActiveRecord
[]
end
end
- end
-
- # If we haven't generated any methods yet, generate them, then
- # see if we've created the method we're looking for.
- def method_missing(method, *args, &block) # :nodoc:
- unless self.class.attribute_methods_generated?
- self.class.define_attribute_methods
- if respond_to_without_attributes?(method)
- send(method, *args, &block)
- else
- super
+ # Returns the column object for the named attribute.
+ # Returns nil if the named attribute does not exist.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter
+ # # => #<ActiveRecord::ConnectionAdapters::Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
+ #
+ # person.column_for_attribute(:nothing)
+ # # => nil
+ def column_for_attribute(name)
+ column = columns_hash[name.to_s]
+ if column.nil?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `#column_for_attribute` will return a null object for non-existent
+ columns in Rails 5. Use `#has_attribute?` if you need to check for
+ an attribute's existence.
+ MSG
end
- else
- super
- end
- end
-
- def attribute_missing(match, *args, &block) # :nodoc:
- if self.class.columns_hash[match.attr_name]
- ActiveSupport::Deprecation.warn(
- "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
- "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \
- "is a column of the table. If this error has happened through normal usage of Active " \
- "Record (rather than through your own code or external libraries), please report it as " \
- "a bug."
- )
+ column
end
-
- super
end
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
- # which will all return +true+. It also define the attribute methods if they have
+ # which will all return +true+. It also defines the attribute methods if they have
# not been generated.
#
# class Person < ActiveRecord::Base
@@ -163,8 +233,18 @@ module ActiveRecord
# person.respond_to('age?') # => true
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
- self.class.define_attribute_methods unless self.class.attribute_methods_generated?
- super
+ return false unless super
+ name = name.to_s
+
+ # If the result is true then check for the select case.
+ # For queries selecting a subset of columns, return false for unselected columns.
+ # We check defined?(@attributes) not to issue warnings if called on objects that
+ # have been allocated but not yet initialized.
+ if defined?(@attributes) && self.class.column_names.include?(name)
+ return has_attribute?(name)
+ end
+
+ return true
end
# Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
@@ -177,7 +257,7 @@ module ActiveRecord
# person.has_attribute?('age') # => true
# person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
- @attributes.has_key?(attr_name.to_s)
+ @attributes.key?(attr_name.to_s)
end
# Returns an array of names for the attributes available on this object.
@@ -201,31 +281,36 @@ module ActiveRecord
# person.attributes
# # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
def attributes
- attribute_names.each_with_object({}) { |name, attrs|
- attrs[name] = read_attribute(name)
- }
+ @attributes.to_hash
end
# Returns an <tt>#inspect</tt>-like string for the value of the
- # attribute +attr_name+. String attributes are truncated upto 50
- # characters, and Date and Time attributes are returned in the
- # <tt>:db</tt> format. Other attributes return the value of
- # <tt>#inspect</tt> without modification.
+ # attribute +attr_name+. String attributes are truncated up to 50
+ # characters, Date and Time attributes are returned in the
+ # <tt>:db</tt> format, Array attributes are truncated up to 10 values.
+ # Other attributes return the value of <tt>#inspect</tt> without
+ # modification.
#
# person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
#
# person.attribute_for_inspect(:name)
- # # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\""
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
#
# person.attribute_for_inspect(:created_at)
# # => "\"2012-10-22 00:15:07\""
+ #
+ # person.attribute_for_inspect(:tag_ids)
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]"
def attribute_for_inspect(attr_name)
value = read_attribute(attr_name)
if value.is_a?(String) && value.length > 50
- "#{value[0..50]}...".inspect
+ "#{value[0, 50]}...".inspect
elsif value.is_a?(Date) || value.is_a?(Time)
%("#{value.to_s(:db)}")
+ elsif value.is_a?(Array) && value.size > 10
+ inspected = value.first(10).inspect
+ %(#{inspected[0...-1]}, ...])
else
value.inspect
end
@@ -239,39 +324,24 @@ module ActiveRecord
# class Task < ActiveRecord::Base
# end
#
- # person = Task.new(title: '', is_done: false)
- # person.attribute_present?(:title) # => false
- # person.attribute_present?(:is_done) # => true
- # person.name = 'Francesco'
- # person.is_done = true
- # person.attribute_present?(:title) # => true
- # person.attribute_present?(:is_done) # => true
+ # task = Task.new(title: '', is_done: false)
+ # task.attribute_present?(:title) # => false
+ # task.attribute_present?(:is_done) # => true
+ # task.title = 'Buy milk'
+ # task.is_done = true
+ # task.attribute_present?(:title) # => true
+ # task.attribute_present?(:is_done) # => true
def attribute_present?(attribute)
- value = read_attribute(attribute)
+ value = _read_attribute(attribute)
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
end
- # Returns the column object for the named attribute. Returns +nil+ if the
- # named attribute not exists.
- #
- # class Person < ActiveRecord::Base
- # end
- #
- # person = Person.new
- # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter
- # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
- #
- # person.column_for_attribute(:nothing)
- # # => nil
- def column_for_attribute(name)
- # FIXME: should this return a null object for columns that don't exist?
- self.class.columns_hash[name.to_s]
- end
-
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
- # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). It raises
+ # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises
# <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
#
+ # Note: +:id+ is always present.
+ #
# Alias for the <tt>read_attribute</tt> method.
#
# class Person < ActiveRecord::Base
@@ -305,13 +375,6 @@ module ActiveRecord
protected
- def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc:
- attribute_names.each do |name|
- attributes[name] = clone_attribute_value(reader_method, name)
- end
- attributes
- end
-
def clone_attribute_value(reader_method, attribute_name) # :nodoc:
value = send(reader_method, attribute_name)
value.duplicable? ? value.clone : value
@@ -328,13 +391,14 @@ module ActiveRecord
end
def attribute_method?(attr_name) # :nodoc:
- defined?(@attributes) && @attributes.include?(attr_name)
+ # We check defined? because Syck calls respond_to? before actually calling initialize.
+ defined?(@attributes) && @attributes.key?(attr_name)
end
private
# Returns a Hash of the Arel::Attributes and attribute values that have been
- # type casted for use in an Arel insert/update method.
+ # typecasted for use in an Arel insert/update method.
def arel_attributes_with_values(attribute_names)
attrs = {}
arel_table = self.class.arel_table
@@ -347,16 +411,16 @@ module ActiveRecord
# Filters the primary keys and readonly attributes from the attribute names.
def attributes_for_update(attribute_names)
- attribute_names.select do |name|
- column_for_attribute(name) && !pk_attribute?(name) && !readonly_attribute?(name)
+ attribute_names.reject do |name|
+ readonly_attribute?(name)
end
end
# Filters out the primary keys, from the attribute names, when the primary
# key is to be generated (e.g. the id attribute has no value).
def attributes_for_create(attribute_names)
- attribute_names.select do |name|
- column_for_attribute(name) && !(pk_attribute?(name) && id.nil?)
+ attribute_names.reject do |name|
+ pk_attribute?(name) && id.nil?
end
end
@@ -365,14 +429,11 @@ module ActiveRecord
end
def pk_attribute?(name)
- column_for_attribute(name).primary
+ name == self.class.primary_key
end
def typecasted_attribute_value(name)
- # FIXME: we need @attributes to be used consistently.
- # If the values stored in @attributes were already typecasted, this code
- # could be simplified
- read_attribute(name)
+ _read_attribute(name)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index a23baeaced..fd61febd57 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -41,8 +41,9 @@ module ActiveRecord
# task.read_attribute_before_type_cast('id') # => '1'
# task.read_attribute('completed_on') # => Sun, 21 Oct 2012
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
+ # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
def read_attribute_before_type_cast(attr_name)
- @attributes[attr_name]
+ @attributes[attr_name.to_s].value_before_type_cast
end
# Returns a hash of attributes before typecasting and deserialization.
@@ -56,7 +57,7 @@ module ActiveRecord
# task.attributes_before_type_cast
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
def attributes_before_type_cast
- @attributes
+ @attributes.values_before_type_cast
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 0333605eac..d5702accaf 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/deprecation'
module ActiveRecord
module AttributeMethods
@@ -15,24 +14,12 @@ module ActiveRecord
class_attribute :partial_writes, instance_writer: false
self.partial_writes = true
-
- def self.partial_updates=(v); self.partial_writes = v; end
- def self.partial_updates?; partial_writes?; end
- def self.partial_updates; partial_writes; end
-
- ActiveSupport::Deprecation.deprecate_methods(
- singleton_class,
- :partial_updates= => :partial_writes=,
- :partial_updates? => :partial_writes?,
- :partial_updates => :partial_writes
- )
end
# Attempts to +save+ the record and clears changed attributes if successful.
def save(*)
if status = super
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
status
end
@@ -40,75 +27,158 @@ module ActiveRecord
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
def save!(*)
super.tap do
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
end
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- @previously_changed.clear
- @changed_attributes.clear
+ clear_changes_information
+ end
+ end
+
+ def initialize_dup(other) # :nodoc:
+ super
+ calculate_changes_from_defaults
+ end
+
+ def changes_applied
+ super
+ store_original_raw_attributes
+ end
+
+ def clear_changes_information
+ super
+ original_raw_attributes.clear
+ end
+
+ def changed_attributes
+ # This should only be set by methods which will call changed_attributes
+ # multiple times when it is known that the computed value cannot change.
+ if defined?(@cached_changed_attributes)
+ @cached_changed_attributes
+ else
+ super.reverse_merge(attributes_changed_in_place).freeze
+ end
+ end
+
+ def changes
+ cache_changed_attributes do
+ super
+ end
+ end
+
+ def attribute_changed_in_place?(attr_name)
+ old_value = original_raw_attribute(attr_name)
+ @attributes[attr_name].changed_in_place_from?(old_value)
+ end
+
+ private
+
+ def changes_include?(attr_name)
+ super || attribute_changed_in_place?(attr_name)
+ end
+
+ def calculate_changes_from_defaults
+ @changed_attributes = nil
+ self.class.column_defaults.each do |attr, orig_value|
+ set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value)
end
end
- private
# Wrap write_attribute to remember original attribute value.
def write_attribute(attr, value)
attr = attr.to_s
- # The attribute already has an unsaved change.
+ old_value = old_attribute_value(attr)
+
+ result = super
+ store_original_raw_attribute(attr)
+ save_changed_attribute(attr, old_value)
+ result
+ end
+
+ def raw_write_attribute(attr, value)
+ attr = attr.to_s
+
+ result = super
+ original_raw_attributes[attr] = value
+ result
+ end
+
+ def save_changed_attribute(attr, old_value)
if attribute_changed?(attr)
- old = @changed_attributes[attr]
- @changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
+ clear_attribute_changes(attr) unless _field_changed?(attr, old_value)
else
- old = clone_attribute_value(:read_attribute, attr)
- @changed_attributes[attr] = old if _field_changed?(attr, old, value)
+ set_attribute_was(attr, old_value) if _field_changed?(attr, old_value)
end
+ end
- # Carry on.
- super(attr, value)
+ def old_attribute_value(attr)
+ if attribute_changed?(attr)
+ changed_attributes[attr]
+ else
+ clone_attribute_value(:_read_attribute, attr)
+ end
end
- def update(*)
+ def _update_record(*)
partial_writes? ? super(keys_for_partial_write) : super
end
- def create(*)
+ def _create_record(*)
partial_writes? ? super(keys_for_partial_write) : super
end
# Serialized attributes should always be written in case they've been
# changed in place.
def keys_for_partial_write
- changed | (attributes.keys & self.class.serialized_attributes.keys)
+ changed
end
- def _field_changed?(attr, old, value)
- if column = column_for_attribute(attr)
- if column.number? && (changes_from_nil_to_empty_string?(column, old, value) ||
- changes_from_zero_to_string?(old, value))
- value = nil
- else
- value = column.type_cast(value)
- end
+ def _field_changed?(attr, old_value)
+ @attributes[attr].changed_from?(old_value)
+ end
+
+ def attributes_changed_in_place
+ changed_in_place.each_with_object({}) do |attr_name, h|
+ orig = @attributes[attr_name].original_value
+ h[attr_name] = orig
+ end
+ end
+
+ def changed_in_place
+ self.class.attribute_names.select do |attr_name|
+ attribute_changed_in_place?(attr_name)
end
+ end
- old != value
+ def original_raw_attribute(attr_name)
+ original_raw_attributes.fetch(attr_name) do
+ read_attribute_before_type_cast(attr_name)
+ end
end
- def changes_from_nil_to_empty_string?(column, old, value)
- # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
- # Hence we don't record it as a change if the value changes from nil to ''.
- # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
- # be typecast back to 0 (''.to_i => 0)
- column.null && (old.nil? || old == 0) && value.blank?
+ def original_raw_attributes
+ @original_raw_attributes ||= {}
+ end
+
+ def store_original_raw_attribute(attr_name)
+ original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database
+ end
+
+ def store_original_raw_attributes
+ attribute_names.each do |attr|
+ store_original_raw_attribute(attr)
+ end
end
- def changes_from_zero_to_string?(old, value)
- # For columns with old 0 and value non-empty string
- old == 0 && value.is_a?(String) && value.present? && value != '0'
+ def cache_changed_attributes
+ @cached_changed_attributes = changed_attributes
+ yield
+ ensure
+ remove_instance_variable(:@cached_changed_attributes)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 0857b02c8e..c28374e4ab 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -8,30 +8,43 @@ module ActiveRecord
# Returns this record's primary key value wrapped in an Array if one is
# available.
def to_key
+ sync_with_transaction_state
key = self.id
[key] if key
end
# Returns the primary key value.
def id
- read_attribute(self.class.primary_key)
+ if pk = self.class.primary_key
+ sync_with_transaction_state
+ _read_attribute(pk)
+ end
end
# Sets the primary key value.
def id=(value)
+ sync_with_transaction_state
write_attribute(self.class.primary_key, value) if self.class.primary_key
end
# Queries the primary key value.
def id?
+ sync_with_transaction_state
query_attribute(self.class.primary_key)
end
# Returns the primary key value before type cast.
def id_before_type_cast
+ sync_with_transaction_state
read_attribute_before_type_cast(self.class.primary_key)
end
+ # Returns the primary key previous value.
+ def id_was
+ sync_with_transaction_state
+ attribute_was(self.class.primary_key)
+ end
+
protected
def attribute_method?(attr_name)
@@ -47,7 +60,7 @@ module ActiveRecord
end
end
- ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast).to_set
+ ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set
def dangerous_attribute_method?(method_name)
super && !ID_ATTRIBUTE_METHODS.include?(method_name)
@@ -76,16 +89,13 @@ module ActiveRecord
end
def get_primary_key(base_name) #:nodoc:
- return 'id' unless base_name && !base_name.blank?
-
- case primary_key_prefix_type
- when :table_name
+ if base_name && primary_key_prefix_type == :table_name
base_name.foreign_key(false)
- when :table_name_with_underscore
+ elsif base_name && primary_key_prefix_type == :table_name_with_underscore
base_name.foreign_key
else
if ActiveRecord::Base != self && table_exists?
- connection.schema_cache.primary_keys[table_name]
+ connection.schema_cache.primary_keys(table_name)
else
'id'
end
@@ -110,6 +120,7 @@ module ActiveRecord
def primary_key=(value)
@primary_key = value && value.to_s
@quoted_primary_key = nil
+ @attributes_builder = nil
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 0f9723febb..dc689f399a 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -8,7 +8,7 @@ module ActiveRecord
end
def query_attribute(attr_name)
- value = read_attribute(attr_name) { |n| missing_attribute(n, caller) }
+ value = self[attr_name]
case value
when true then true
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 3c03cce838..20f0936e52 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,40 +1,16 @@
+require 'active_support/core_ext/module/method_transplanting'
+
module ActiveRecord
module AttributeMethods
module Read
- extend ActiveSupport::Concern
-
- ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
-
- included do
- class_attribute :attribute_types_cached_by_default, instance_writer: false
- self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
- end
-
- module ClassMethods
- # +cache_attributes+ allows you to declare which converted attribute
- # values should be cached. Usually caching only pays off for attributes
- # with expensive conversion methods, like time related columns (e.g.
- # +created_at+, +updated_at+).
- def cache_attributes(*attribute_names)
- cached_attributes.merge attribute_names.map { |attr| attr.to_s }
- end
-
- # Returns the attributes which are cached. By default time related columns
- # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
- def cached_attributes
- @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set
- end
-
- # Returns +true+ if the provided attribute is being cached.
- def cache_attribute?(attr_name)
- cached_attributes.include?(attr_name)
- end
-
- protected
-
+ ReaderMethodCache = Class.new(AttributeMethodCache) {
+ private
# We want to generate the methods via module_eval rather than
- # define_method, because define_method is slower on dispatch and
- # uses more memory (because it creates a closure).
+ # define_method, because define_method is slower on dispatch.
+ # Evaluating many similar methods may use more memory as the instruction
+ # sequences are duplicated and cached (in MRI). define_method may
+ # be slower on dispatch, but if you're careful about the closure
+ # created, then define_method will consume much less memory.
#
# But sometimes the database might return columns with
# characters that are not allowed in normal method names (like
@@ -46,61 +22,81 @@ module ActiveRecord
# the attribute name. Using a constant means that we do not have
# to allocate an object on each call to the attribute method.
# Making it frozen means that it doesn't get duped when used to
- # key the @attributes_cache in read_attribute.
- def define_method_attribute(name)
- safe_name = name.unpack('h*').first
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}
- read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) }
- end
- alias_method #{name.inspect}, :__temp__#{safe_name}
- undef_method :__temp__#{safe_name}
- STR
+ # key the @attributes in read_attribute.
+ def method_body(method_name, const_name)
+ <<-EOMETHOD
+ def #{method_name}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
+ _read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ EOMETHOD
end
+ }.new
- private
+ extend ActiveSupport::Concern
- def cacheable_column?(column)
- if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
- ! serialized_attributes.include? column.name
- else
- attribute_types_cached_by_default.include?(column.type)
+ module ClassMethods
+ [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name|
+ define_method method_name do |*|
+ cached_attributes_deprecation_warning(method_name)
+ true
+ end
+ end
+
+ protected
+
+ def cached_attributes_deprecation_warning(method_name)
+ ActiveSupport::Deprecation.warn "Calling `#{method_name}` is no longer necessary. All attributes are cached."
+ end
+
+ if Module.methods_transplantable?
+ def define_method_attribute(name)
+ method = ReaderMethodCache[name]
+ generated_attribute_methods.module_eval { define_method name, method }
+ end
+ else
+ def define_method_attribute(name)
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
+
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def #{temp_method}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ _read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ STR
+
+ generated_attribute_methods.module_eval do
+ alias_method name, temp_method
+ undef_method temp_method
+ end
end
end
end
+ ID = 'id'.freeze
+
# Returns the value of the attribute identified by <tt>attr_name</tt> after
- # it has been typecast (for example, "2004-12-12" in a data column is cast
+ # it has been typecast (for example, "2004-12-12" in a date column is cast
# to a date object, like Date.new(2004, 12, 12)).
- def read_attribute(attr_name)
- # If it's cached, just return it
- # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/3552829.
+ def read_attribute(attr_name, &block)
name = attr_name.to_s
- @attributes_cache[name] || @attributes_cache.fetch(name) {
- column = @columns_hash.fetch(name) {
- return @attributes.fetch(name) {
- if name == 'id' && self.class.primary_key != name
- read_attribute(self.class.primary_key)
- end
- }
- }
-
- value = @attributes.fetch(name) {
- return block_given? ? yield(name) : nil
- }
+ name = self.class.primary_key if name == ID
+ _read_attribute(name, &block)
+ end
- if self.class.cache_attribute?(name)
- @attributes_cache[name] = column.type_cast(value)
- else
- column.type_cast value
- end
- }
+ # This method exists to avoid the expensive primary_key check internally, without
+ # breaking compatibility with the read_attribute API
+ def _read_attribute(attr_name) # :nodoc:
+ @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
end
private
def attribute(attribute_name)
- read_attribute(attribute_name)
+ _read_attribute(attribute_name)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 25d62fdb85..e5ec5ddca5 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -1,27 +1,23 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module AttributeMethods
module Serialization
extend ActiveSupport::Concern
- included do
- # Returns a hash of all the attributes that have been specified for
- # serialization as keys and their class restriction as values.
- class_attribute :serialized_attributes, instance_accessor: false
- self.serialized_attributes = {}
- end
-
module ClassMethods
# If you have an attribute that needs to be saved to the database as an
# object, and retrieved as the same object, then specify the name of that
# attribute using this method and it will be handled automatically. The
# serialization is done through YAML. If +class_name+ is specified, the
- # serialized object must be of that class on retrieval or
- # <tt>SerializationTypeMismatch</tt> will be raised.
+ # serialized object must be of that class on assignment and retrieval.
+ # Otherwise <tt>SerializationTypeMismatch</tt> will be raised.
#
# ==== Parameters
#
# * +attr_name+ - The field name that should be serialized.
- # * +class_name+ - Optional, class name that the object type should be equal to.
+ # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump`
+ # or a class name that the object type should be equal to.
#
# ==== Example
#
@@ -29,121 +25,44 @@ module ActiveRecord
# class User < ActiveRecord::Base
# serialize :preferences
# end
- def serialize(attr_name, class_name = Object)
- include Behavior
-
- coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
- class_name
+ #
+ # # Serialize preferences using JSON as coder.
+ # class User < ActiveRecord::Base
+ # serialize :preferences, JSON
+ # end
+ #
+ # # Serialize preferences as Hash using YAML coder.
+ # class User < ActiveRecord::Base
+ # serialize :preferences, Hash
+ # end
+ def serialize(attr_name, class_name_or_coder = Object)
+ # When ::JSON is used, force it to go through the Active Support JSON encoder
+ # to ensure special objects (e.g. Active Record models) are dumped correctly
+ # using the #as_json hook.
+ coder = if class_name_or_coder == ::JSON
+ Coders::JSON
+ elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
+ class_name_or_coder
else
- Coders::YAMLColumn.new(class_name)
+ Coders::YAMLColumn.new(class_name_or_coder)
end
- # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
- # has its own hash of own serialized attributes
- self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
- end
- end
-
- def serialized_attributes
- message = "Instance level serialized_attributes method is deprecated, please use class level method."
- ActiveSupport::Deprecation.warn message
- defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes
- end
-
- class Type # :nodoc:
- def initialize(column)
- @column = column
- end
-
- def type_cast(value)
- value.unserialized_value
- end
-
- def type
- @column.type
- end
- end
-
- class Attribute < Struct.new(:coder, :value, :state) # :nodoc:
- def unserialized_value
- state == :serialized ? unserialize : value
- end
-
- def serialized_value
- state == :unserialized ? serialize : value
- end
-
- def unserialize
- self.state = :unserialized
- self.value = coder.load(value)
- end
-
- def serialize
- self.state = :serialized
- self.value = coder.dump(value)
- end
- end
-
- # This is only added to the model when serialize is called, which
- # ensures we do not make things slower when serialization is not used.
- module Behavior #:nodoc:
- extend ActiveSupport::Concern
-
- module ClassMethods
- def initialize_attributes(attributes, options = {})
- serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
- super(attributes, options)
-
- serialized_attributes.each do |key, coder|
- if attributes.key?(key)
- attributes[key] = Attribute.new(coder, attributes[key], serialized)
- end
- end
-
- attributes
+ decorate_attribute_type(attr_name, :serialize) do |type|
+ Type::Serialized.new(type, coder)
end
end
- def type_cast_attribute_for_write(column, value)
- if column && coder = self.class.serialized_attributes[column.name]
- Attribute.new(coder, value, :unserialized)
- else
- super
- end
- end
-
- def _field_changed?(attr, old, value)
- if self.class.serialized_attributes.include?(attr)
- old != value
- else
- super
- end
- end
-
- def read_attribute_before_type_cast(attr_name)
- if self.class.serialized_attributes.include?(attr_name)
- super.unserialized_value
- else
- super
- end
- end
-
- def attributes_before_type_cast
- super.dup.tap do |attributes|
- self.class.serialized_attributes.each_key do |key|
- if attributes.key?(key)
- attributes[key] = attributes[key].unserialized_value
- end
- end
- end
- end
-
- def typecasted_attribute_value(name)
- if self.class.serialized_attributes.include?(name)
- @attributes[name].serialized_value
- else
- super
- end
+ def serialized_attributes
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `serialized_attributes` is deprecated without replacement, and will
+ be removed in Rails 5.0.
+ MSG
+
+ @serialized_attributes ||= Hash[
+ columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c|
+ [c.name, c.cast_type.coder]
+ }
+ ]
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 47a8b576c0..777f7ab4d7 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,18 +1,33 @@
module ActiveRecord
module AttributeMethods
module TimeZoneConversion
- class Type # :nodoc:
- def initialize(column)
- @column = column
+ class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc:
+ include Type::Decorator
+
+ def type_cast_from_database(value)
+ convert_time_to_time_zone(super)
end
- def type_cast(value)
- value = @column.type_cast(value)
- value.acts_like?(:time) ? value.in_time_zone : value
+ def type_cast_from_user(value)
+ if value.is_a?(Array)
+ value.map { |v| type_cast_from_user(v) }
+ elsif value.respond_to?(:in_time_zone)
+ begin
+ value.in_time_zone || super
+ rescue ArgumentError
+ nil
+ end
+ end
end
- def type
- @column.type
+ def convert_time_to_time_zone(value)
+ if value.is_a?(Array)
+ value.map { |v| convert_time_to_time_zone(v) }
+ elsif value.acts_like?(:time)
+ value.in_time_zone
+ else
+ value
+ end
end
end
@@ -27,46 +42,28 @@ module ActiveRecord
end
module ClassMethods
- protected
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
- def define_method_attribute=(attr_name)
- if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
- method_body, line = <<-EOV, __LINE__ + 1
- def #{attr_name}=(original_time)
- original_time = nil if original_time.blank?
- time = original_time
- unless time.acts_like?(:time)
- time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
- end
- zoned_time = time && time.in_time_zone rescue nil
- rounded_time = round_usec(zoned_time)
- rounded_value = round_usec(read_attribute("#{attr_name}"))
- if (rounded_value != rounded_time) || (!rounded_value && original_time)
- write_attribute("#{attr_name}", original_time)
- #{attr_name}_will_change!
- @attributes_cache["#{attr_name}"] = zoned_time
- end
- end
- EOV
- generated_attribute_methods.module_eval(method_body, __FILE__, line)
- else
- super
+ private
+
+ def inherited(subclass)
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or
+ # `skip_time_zone_conversion_for_attributes` would not be picked up.
+ subclass.class_eval do
+ matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
+ decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type|
+ TimeZoneConverter.new(type)
+ end
end
+ super
end
- private
- def create_time_zone_conversion_attribute?(name, column)
+ def create_time_zone_conversion_attribute?(name, cast_type)
time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- [:datetime, :timestamp].include?(column.type)
+ (:datetime == cast_type.type)
end
end
-
- private
- def round_usec(value)
- value.change(usec: 0) if value
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index cd33494cc3..16804f86bf 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -1,6 +1,21 @@
+require 'active_support/core_ext/module/method_transplanting'
+
module ActiveRecord
module AttributeMethods
module Write
+ WriterMethodCache = Class.new(AttributeMethodCache) {
+ private
+
+ def method_body(method_name, const_name)
+ <<-EOMETHOD
+ def #{method_name}(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
+ write_attribute(name, value)
+ end
+ EOMETHOD
+ end
+ }.new
+
extend ActiveSupport::Concern
included do
@@ -10,17 +25,27 @@ module ActiveRecord
module ClassMethods
protected
- # See define_method_attribute in read.rb for an explanation of
- # this code.
- def define_method_attribute=(name)
- safe_name = name.unpack('h*').first
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}=(value)
- write_attribute(AttrNames::ATTR_#{safe_name}, value)
- end
- alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
- undef_method :__temp__#{safe_name}=
- STR
+ if Module.methods_transplantable?
+ def define_method_attribute=(name)
+ method = WriterMethodCache[name]
+ generated_attribute_methods.module_eval {
+ define_method "#{name}=", method
+ }
+ end
+ else
+ def define_method_attribute=(name)
+ safe_name = name.unpack('h*').first
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__#{safe_name}=(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ write_attribute(name, value)
+ end
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
+ undef_method :__temp__#{safe_name}=
+ STR
+ end
end
end
@@ -28,24 +53,12 @@ module ActiveRecord
# specified +value+. Empty strings for fixnum and float columns are
# turned into +nil+.
def write_attribute(attr_name, value)
- attr_name = attr_name.to_s
- attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
- @attributes_cache.delete(attr_name)
- column = column_for_attribute(attr_name)
-
- # If we're dealing with a binary column, write the data to the cache
- # so we don't attempt to typecast multiple times.
- if column && column.binary?
- @attributes_cache[attr_name] = value
- end
+ write_attribute_with_type_cast(attr_name, value, true)
+ end
- if column || @attributes.has_key?(attr_name)
- @attributes[attr_name] = type_cast_attribute_for_write(column, value)
- else
- raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
- end
+ def raw_write_attribute(attr_name, value)
+ write_attribute_with_type_cast(attr_name, value, false)
end
- alias_method :raw_write_attribute, :write_attribute
private
# Handle *= for method_missing.
@@ -53,10 +66,17 @@ module ActiveRecord
write_attribute(attribute_name, value)
end
- def type_cast_attribute_for_write(column, value)
- return value unless column
+ def write_attribute_with_type_cast(attr_name, value, should_type_cast)
+ attr_name = attr_name.to_s
+ attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
+
+ if should_type_cast
+ @attributes.write_from_user(attr_name, value)
+ else
+ @attributes.write_cast_value(attr_name, value)
+ end
- column.type_cast_for_write value
+ value
end
end
end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
new file mode 100644
index 0000000000..66fcaf6945
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -0,0 +1,77 @@
+require 'active_record/attribute_set/builder'
+
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def [](name)
+ attributes[name] || Attribute.null(name)
+ end
+
+ def values_before_type_cast
+ attributes.transform_values(&:value_before_type_cast)
+ end
+
+ def to_hash
+ initialized_attributes.transform_values(&:value)
+ end
+ alias_method :to_h, :to_hash
+
+ def key?(name)
+ attributes.key?(name) && self[name].initialized?
+ end
+
+ def keys
+ attributes.initialized_keys
+ end
+
+ def fetch_value(name)
+ self[name].value { |n| yield n if block_given? }
+ end
+
+ def write_from_database(name, value)
+ attributes[name] = self[name].with_value_from_database(value)
+ end
+
+ def write_from_user(name, value)
+ attributes[name] = self[name].with_value_from_user(value)
+ end
+
+ def write_cast_value(name, value)
+ attributes[name] = self[name].with_cast_value(value)
+ end
+
+ def freeze
+ @attributes.freeze
+ super
+ end
+
+ def initialize_dup(_)
+ @attributes = attributes.dup
+ super
+ end
+
+ def initialize_clone(_)
+ @attributes = attributes.clone
+ super
+ end
+
+ def reset(key)
+ if key?(key)
+ write_from_database(key, nil)
+ end
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def initialized_attributes
+ attributes.select { |_, attr| attr.initialized? }
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
new file mode 100644
index 0000000000..3a76f5262d
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -0,0 +1,86 @@
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ class Builder # :nodoc:
+ attr_reader :types, :always_initialized
+
+ def initialize(types, always_initialized = nil)
+ @types = types
+ @always_initialized = always_initialized
+ end
+
+ def build_from_database(values = {}, additional_types = {})
+ if always_initialized && !values.key?(always_initialized)
+ values[always_initialized] = nil
+ end
+
+ attributes = LazyAttributeHash.new(types, values, additional_types)
+ AttributeSet.new(attributes)
+ end
+ end
+ end
+
+ class LazyAttributeHash # :nodoc:
+ delegate :select, :transform_values, to: :materialize
+
+ def initialize(types, values, additional_types)
+ @types = types
+ @values = values
+ @additional_types = additional_types
+ @materialized = false
+ @delegate_hash = {}
+ end
+
+ def key?(key)
+ delegate_hash.key?(key) || values.key?(key) || types.key?(key)
+ end
+
+ def [](key)
+ delegate_hash[key] || assign_default_value(key)
+ end
+
+ def []=(key, value)
+ if frozen?
+ raise RuntimeError, "Can't modify frozen hash"
+ end
+ delegate_hash[key] = value
+ end
+
+ def initialized_keys
+ delegate_hash.keys | values.keys
+ end
+
+ def initialize_dup(_)
+ @delegate_hash = delegate_hash.transform_values(&:dup)
+ super
+ end
+
+ protected
+
+ attr_reader :types, :values, :additional_types, :delegate_hash
+
+ private
+
+ def assign_default_value(name)
+ type = additional_types.fetch(name, types[name])
+ value_present = true
+ value = values.fetch(name) { value_present = false }
+
+ if value_present
+ delegate_hash[name] = Attribute.from_database(name, value, type)
+ elsif types.key?(name)
+ delegate_hash[name] = Attribute.uninitialized(name, type)
+ end
+ end
+
+ def materialize
+ unless @materialized
+ values.each_key { |key| self[key] }
+ types.each_key { |key| self[key] }
+ unless frozen?
+ @materialized = true
+ end
+ end
+ delegate_hash
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
new file mode 100644
index 0000000000..aafb990bc1
--- /dev/null
+++ b/activerecord/lib/active_record/attributes.rb
@@ -0,0 +1,140 @@
+module ActiveRecord
+ module Attributes # :nodoc:
+ extend ActiveSupport::Concern
+
+ Type = ActiveRecord::Type
+
+ included do
+ class_attribute :user_provided_columns, instance_accessor: false # :internal:
+ class_attribute :user_provided_defaults, instance_accessor: false # :internal:
+ self.user_provided_columns = {}
+ self.user_provided_defaults = {}
+ end
+
+ module ClassMethods # :nodoc:
+ # Defines or overrides a attribute on this model. This allows customization of
+ # Active Record's type casting behavior, as well as adding support for user defined
+ # types.
+ #
+ # +name+ The name of the methods to define attribute methods for, and the column which
+ # this will persist to.
+ #
+ # +cast_type+ A type object that contains information about how to type cast the value.
+ # See the examples section for more information.
+ #
+ # ==== Options
+ # The options hash accepts the following options:
+ #
+ # +default+ is the default value that the column should use on a new record.
+ #
+ # ==== Examples
+ #
+ # The type detected by Active Record can be overridden.
+ #
+ # # db/schema.rb
+ # create_table :store_listings, force: true do |t|
+ # t.decimal :price_in_cents
+ # end
+ #
+ # # app/models/store_listing.rb
+ # class StoreListing < ActiveRecord::Base
+ # end
+ #
+ # store_listing = StoreListing.new(price_in_cents: '10.1')
+ #
+ # # before
+ # store_listing.price_in_cents # => BigDecimal.new(10.1)
+ #
+ # class StoreListing < ActiveRecord::Base
+ # attribute :price_in_cents, Type::Integer.new
+ # end
+ #
+ # # after
+ # store_listing.price_in_cents # => 10
+ #
+ # Users may also define their own custom types, as long as they respond to the methods
+ # defined on the value type. The +type_cast+ method on your type object will be called
+ # with values both from the database, and from your controllers. See
+ # +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your
+ # type objects inherit from an existing type, or the base value type.
+ #
+ # class MoneyType < ActiveRecord::Type::Integer
+ # def type_cast(value)
+ # if value.include?('$')
+ # price_in_dollars = value.gsub(/\$/, '').to_f
+ # price_in_dollars * 100
+ # else
+ # value.to_i
+ # end
+ # end
+ # end
+ #
+ # class StoreListing < ActiveRecord::Base
+ # attribute :price_in_cents, MoneyType.new
+ # end
+ #
+ # store_listing = StoreListing.new(price_in_cents: '$10.00')
+ # store_listing.price_in_cents # => 1000
+ def attribute(name, cast_type, options = {})
+ name = name.to_s
+ clear_caches_calculated_from_columns
+ # Assign a new hash to ensure that subclasses do not share a hash
+ self.user_provided_columns = user_provided_columns.merge(name => cast_type)
+
+ if options.key?(:default)
+ self.user_provided_defaults = user_provided_defaults.merge(name => options[:default])
+ end
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns
+ @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name))
+ end
+
+ # Returns a hash of column objects for the table associated with this class.
+ def columns_hash
+ @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ end
+
+ def reset_column_information # :nodoc:
+ super
+ clear_caches_calculated_from_columns
+ end
+
+ private
+
+ def add_user_provided_columns(schema_columns)
+ existing_columns = schema_columns.map do |column|
+ new_type = user_provided_columns[column.name]
+ if new_type
+ column.with_type(new_type)
+ else
+ column
+ end
+ end
+
+ existing_column_names = existing_columns.map(&:name)
+ new_columns = user_provided_columns.except(*existing_column_names).map do |(name, type)|
+ connection.new_column(name, nil, type)
+ end
+
+ existing_columns + new_columns
+ end
+
+ def clear_caches_calculated_from_columns
+ @arel_table = nil
+ @attributes_builder = nil
+ @column_names = nil
+ @column_types = nil
+ @columns = nil
+ @columns_hash = nil
+ @content_columns = nil
+ @default_attributes = nil
+ end
+
+ def raw_default_values
+ super.merge(user_provided_defaults)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 704998301c..c39b045a5e 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -17,7 +17,8 @@ module ActiveRecord
# be destroyed directly. They will however still be marked for destruction.
#
# Note that <tt>autosave: false</tt> is not same as not declaring <tt>:autosave</tt>.
- # When the <tt>:autosave</tt> option is not present new associations are saved.
+ # When the <tt>:autosave</tt> option is not present then new association records are
+ # saved but the updated association records are not saved.
#
# == Validation
#
@@ -34,7 +35,7 @@ module ActiveRecord
#
# === One-to-one Example
#
- # class Post
+ # class Post < ActiveRecord::Base
# has_one :author, autosave: true
# end
#
@@ -62,20 +63,20 @@ module ActiveRecord
# Note that the model is _not_ yet removed from the database:
#
# id = post.author.id
- # Author.find_by_id(id).nil? # => false
+ # Author.find_by(id: id).nil? # => false
#
# post.save
# post.reload.author # => nil
#
# Now it _is_ removed from the database:
#
- # Author.find_by_id(id).nil? # => true
+ # Author.find_by(id: id).nil? # => true
#
# === One-to-many Example
#
# When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
#
- # class Post
+ # class Post < ActiveRecord::Base
# has_many :comments # :autosave option is not declared
# end
#
@@ -94,97 +95,100 @@ module ActiveRecord
# When <tt>:autosave</tt> is true all children are saved, no matter whether they
# are new records or not:
#
- # class Post
+ # class Post < ActiveRecord::Base
# has_many :comments, autosave: true
# end
#
# post = Post.create(title: 'ruby rocks')
# post.comments.create(body: 'hello world')
# post.comments[0].body = 'hi everyone'
- # post.save # => saves both post and comment, with 'hi everyone' as body
+ # post.comments.build(body: "good morning.")
+ # post.title += "!"
+ # post.save # => saves both post and comments.
#
# Destroying one of the associated models as part of the parent's save action
# is as simple as marking it for destruction:
#
- # post.comments.last.mark_for_destruction
- # post.comments.last.marked_for_destruction? # => true
+ # post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]>
+ # post.comments[1].mark_for_destruction
+ # post.comments[1].marked_for_destruction? # => true
# post.comments.length # => 2
#
# Note that the model is _not_ yet removed from the database:
#
# id = post.comments.last.id
- # Comment.find_by_id(id).nil? # => false
+ # Comment.find_by(id: id).nil? # => false
#
# post.save
# post.reload.comments.length # => 1
#
# Now it _is_ removed from the database:
#
- # Comment.find_by_id(id).nil? # => true
+ # Comment.find_by(id: id).nil? # => true
module AutosaveAssociation
extend ActiveSupport::Concern
module AssociationBuilderExtension #:nodoc:
- def build
+ def self.build(model, reflection)
model.send(:add_autosave_association_callbacks, reflection)
- super
+ end
+
+ def self.valid_options
+ [ :autosave ]
end
end
included do
- Associations::Builder::Association.class_eval do
- self.valid_options << :autosave
- include AssociationBuilderExtension
- end
+ Associations::Builder::Association.extensions << AssociationBuilderExtension
end
module ClassMethods
private
- def define_non_cyclic_method(name, reflection, &block)
- define_method(name) do |*args|
- result = true; @_already_called ||= {}
- # Loop prevention for validation of associations
- unless @_already_called[[name, reflection.name]]
- begin
- @_already_called[[name, reflection.name]]=true
- result = instance_eval(&block)
- ensure
- @_already_called[[name, reflection.name]]=false
+ def define_non_cyclic_method(name, &block)
+ return if method_defined?(name)
+ define_method(name) do |*args|
+ result = true; @_already_called ||= {}
+ # Loop prevention for validation of associations
+ unless @_already_called[name]
+ begin
+ @_already_called[name]=true
+ result = instance_eval(&block)
+ ensure
+ @_already_called[name]=false
+ end
end
- end
- result
+ result
+ end
end
- end
- # Adds validation and save callbacks for the association as specified by
- # the +reflection+.
- #
- # For performance reasons, we don't check whether to validate at runtime.
- # However the validation and callback methods are lazy and those methods
- # get created when they are invoked for the very first time. However,
- # this can change, for instance, when using nested attributes, which is
- # called _after_ the association has been defined. Since we don't want
- # the callbacks to get defined multiple times, there are guards that
- # check if the save or validation methods have already been defined
- # before actually defining them.
- def add_autosave_association_callbacks(reflection)
- save_method = :"autosave_associated_records_for_#{reflection.name}"
- validation_method = :"validate_associated_records_for_#{reflection.name}"
- collection = reflection.collection?
-
- unless method_defined?(save_method)
+ # Adds validation and save callbacks for the association as specified by
+ # the +reflection+.
+ #
+ # For performance reasons, we don't check whether to validate at runtime.
+ # However the validation and callback methods are lazy and those methods
+ # get created when they are invoked for the very first time. However,
+ # this can change, for instance, when using nested attributes, which is
+ # called _after_ the association has been defined. Since we don't want
+ # the callbacks to get defined multiple times, there are guards that
+ # check if the save or validation methods have already been defined
+ # before actually defining them.
+ def add_autosave_association_callbacks(reflection)
+ save_method = :"autosave_associated_records_for_#{reflection.name}"
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
+ collection = reflection.collection?
+
if collection
before_save :before_save_collection_association
- define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) }
+ define_non_cyclic_method(save_method) { save_collection_association(reflection) }
# Doesn't use after_save as that would save associations added in after_create/after_update twice
after_create save_method
after_update save_method
- elsif reflection.macro == :has_one
- define_method(save_method) { save_has_one_association(reflection) }
+ elsif reflection.has_one?
+ define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method)
# Configures two callbacks instead of a single after_save so that
# the model may rely on their execution order relative to its
# own callbacks.
@@ -196,22 +200,22 @@ module ActiveRecord
after_create save_method
after_update save_method
else
- define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
+ define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) }
before_save save_method
end
- end
- if reflection.validate? && !method_defined?(validation_method)
- method = (collection ? :validate_collection_association : :validate_single_association)
- define_non_cyclic_method(validation_method, reflection) { send(method, reflection) }
- validate validation_method
+ if reflection.validate? && !method_defined?(validation_method)
+ method = (collection ? :validate_collection_association : :validate_single_association)
+ define_non_cyclic_method(validation_method) { send(method, reflection) }
+ validate validation_method
+ end
end
- end
end
# Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
def reload(options = nil)
@marked_for_destruction = false
+ @destroyed_by_association = nil
super
end
@@ -231,6 +235,19 @@ module ActiveRecord
@marked_for_destruction
end
+ # Records the association that is being destroyed and destroying this
+ # record in the process.
+ def destroyed_by_association=(reflection)
+ @destroyed_by_association = reflection
+ end
+
+ # Returns the association for the parent being destroyed.
+ #
+ # Used to avoid updating the counter cache unnecessarily.
+ def destroyed_by_association
+ @destroyed_by_association
+ end
+
# Returns whether or not this record has been changed in any way (including whether
# any of its nested autosave associations are likewise changed)
def changed_for_autosave?
@@ -239,173 +256,184 @@ module ActiveRecord
private
- # Returns the record for an association collection that should be validated
- # or saved. If +autosave+ is +false+ only new records will be returned,
- # unless the parent is/was a new record itself.
- def associated_records_to_validate_or_save(association, new_record, autosave)
- if new_record
- association && association.target
- elsif autosave
- association.target.find_all { |record| record.changed_for_autosave? }
- else
- association.target.find_all { |record| record.new_record? }
+ # Returns the record for an association collection that should be validated
+ # or saved. If +autosave+ is +false+ only new records will be returned,
+ # unless the parent is/was a new record itself.
+ def associated_records_to_validate_or_save(association, new_record, autosave)
+ if new_record
+ association && association.target
+ elsif autosave
+ association.target.find_all(&:changed_for_autosave?)
+ else
+ association.target.find_all(&:new_record?)
+ end
end
- end
- # go through nested autosave associations that are loaded in memory (without loading
- # any new ones), and return true if is changed for autosave
- def nested_records_changed_for_autosave?
- self.class.reflect_on_all_autosave_associations.any? do |reflection|
- association = association_instance_get(reflection.name)
- association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ # go through nested autosave associations that are loaded in memory (without loading
+ # any new ones), and return true if is changed for autosave
+ def nested_records_changed_for_autosave?
+ self.class._reflections.values.any? do |reflection|
+ if reflection.options[:autosave]
+ association = association_instance_get(reflection.name)
+ association && Array.wrap(association.target).any?(&:changed_for_autosave?)
+ end
+ end
end
- end
- # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
- # turned on for the association.
- def validate_single_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.reader
- association_valid?(reflection, record) if record
- end
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
+ # turned on for the association.
+ def validate_single_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.reader
+ association_valid?(reflection, record) if record
+ end
- # Validate the associated records if <tt>:validate</tt> or
- # <tt>:autosave</tt> is turned on for the association specified by
- # +reflection+.
- def validate_collection_association(reflection)
- if association = association_instance_get(reflection.name)
- if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
- records.each { |record| association_valid?(reflection, record) }
+ # Validate the associated records if <tt>:validate</tt> or
+ # <tt>:autosave</tt> is turned on for the association specified by
+ # +reflection+.
+ def validate_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
+ records.each { |record| association_valid?(reflection, record) }
+ end
end
end
- end
- # Returns whether or not the association is valid and applies any errors to
- # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
- # enabled records if they're marked_for_destruction? or destroyed.
- def association_valid?(reflection, record)
- return true if record.destroyed? || record.marked_for_destruction?
-
- unless valid = record.valid?(validation_context)
- if reflection.options[:autosave]
- record.errors.each do |attribute, message|
- attribute = "#{reflection.name}.#{attribute}"
- errors[attribute] << message
- errors[attribute].uniq!
+ # Returns whether or not the association is valid and applies any errors to
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
+ # enabled records if they're marked_for_destruction? or destroyed.
+ def association_valid?(reflection, record)
+ return true if record.destroyed? || record.marked_for_destruction?
+
+ validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
+ unless valid = record.valid?(validation_context)
+ if reflection.options[:autosave]
+ record.errors.each do |attribute, message|
+ attribute = "#{reflection.name}.#{attribute}"
+ errors[attribute] << message
+ errors[attribute].uniq!
+ end
+ else
+ errors.add(reflection.name)
end
- else
- errors.add(reflection.name)
end
+ valid
end
- valid
- end
- # Is used as a before_save callback to check while saving a collection
- # association whether or not the parent was a new record before saving.
- def before_save_collection_association
- @new_record_before_save = new_record?
- true
- end
+ # Is used as a before_save callback to check while saving a collection
+ # association whether or not the parent was a new record before saving.
+ def before_save_collection_association
+ @new_record_before_save = new_record?
+ true
+ end
- # Saves any new associated records, or all loaded autosave associations if
- # <tt>:autosave</tt> is enabled on the association.
- #
- # In addition, it destroys all children that were marked for destruction
- # with mark_for_destruction.
- #
- # This all happens inside a transaction, _if_ the Transactions module is included into
- # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
- def save_collection_association(reflection)
- if association = association_instance_get(reflection.name)
- autosave = reflection.options[:autosave]
-
- if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
- records_to_destroy = []
- records.each do |record|
- next if record.destroyed?
-
- saved = true
-
- if autosave && record.marked_for_destruction?
- records_to_destroy << record
- elsif autosave != false && (@new_record_before_save || record.new_record?)
- if autosave
- saved = association.insert_record(record, false)
- else
- association.insert_record(record) unless reflection.nested?
- end
- elsif autosave
- saved = record.save(:validate => false)
+ # Saves any new associated records, or all loaded autosave associations if
+ # <tt>:autosave</tt> is enabled on the association.
+ #
+ # In addition, it destroys all children that were marked for destruction
+ # with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ autosave = reflection.options[:autosave]
+
+ if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+ if autosave
+ records_to_destroy = records.select(&:marked_for_destruction?)
+ records_to_destroy.each { |record| association.destroy(record) }
+ records -= records_to_destroy
end
- raise ActiveRecord::Rollback unless saved
- end
+ records.each do |record|
+ next if record.destroyed?
+
+ saved = true
+
+ if autosave != false && (@new_record_before_save || record.new_record?)
+ if autosave
+ saved = association.insert_record(record, false)
+ else
+ association.insert_record(record) unless reflection.nested?
+ end
+ elsif autosave
+ saved = record.save(:validate => false)
+ end
- records_to_destroy.each do |record|
- association.destroy(record)
+ raise ActiveRecord::Rollback unless saved
+ end
end
- end
- # reconstruct the scope now that we know the owner's id
- association.send(:reset_scope) if association.respond_to?(:reset_scope)
+ # reconstruct the scope now that we know the owner's id
+ association.reset_scope if association.respond_to?(:reset_scope)
+ end
end
- end
- # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
- # on the association.
- #
- # In addition, it will destroy the association if it was marked for
- # destruction with mark_for_destruction.
- #
- # This all happens inside a transaction, _if_ the Transactions module is included into
- # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
- def save_has_one_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- record.destroy
- else
- key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
- if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
- unless reflection.through_reflection
- record[reflection.foreign_key] = key
- end
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+ # on the association.
+ #
+ # In addition, it will destroy the association if it was marked for
+ # destruction with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_has_one_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
- saved = record.save(:validate => !autosave)
- raise ActiveRecord::Rollback if !saved && autosave
- saved
+ if autosave && record.marked_for_destruction?
+ record.destroy
+ elsif autosave != false
+ key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
+
+ if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key)
+ unless reflection.through_reflection
+ record[reflection.foreign_key] = key
+ end
+
+ saved = record.save(:validate => !autosave)
+ raise ActiveRecord::Rollback if !saved && autosave
+ saved
+ end
end
end
end
- end
- # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
- #
- # In addition, it will destroy the association if it was marked for destruction.
- def save_belongs_to_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- self[reflection.foreign_key] = nil
- record.destroy
- elsif autosave != false
- saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
-
- if association.updated?
- association_id = record.send(reflection.options[:primary_key] || :id)
- self[reflection.foreign_key] = association_id
- association.loaded!
- end
+ # If the record is new or it has changed, returns true.
+ def record_changed?(reflection, record, key)
+ record.new_record? ||
+ (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) ||
+ record.attribute_changed?(reflection.foreign_key)
+ end
+
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
+ #
+ # In addition, it will destroy the association if it was marked for destruction.
+ def save_belongs_to_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
+
+ if autosave && record.marked_for_destruction?
+ self[reflection.foreign_key] = nil
+ record.destroy
+ elsif autosave != false
+ saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
+
+ if association.updated?
+ association_id = record.send(reflection.options[:primary_key] || :id)
+ self[reflection.foreign_key] = association_id
+ association.loaded!
+ end
- saved if autosave
+ saved if autosave
+ end
end
end
- end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index aab832c2f7..bb01231bca 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -4,20 +4,25 @@ require 'active_support/benchmarkable'
require 'active_support/dependencies'
require 'active_support/descendants_tracker'
require 'active_support/time'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/hash/transform_values'
require 'active_support/core_ext/string/behavior'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/class/subclasses'
require 'arel'
+require 'active_record/attribute_decorators'
require 'active_record/errors'
require 'active_record/log_subscriber'
require 'active_record/explain_subscriber'
+require 'active_record/relation/delegation'
+require 'active_record/attributes'
+require 'active_record/type_caster'
module ActiveRecord #:nodoc:
# = Active Record
@@ -137,6 +142,7 @@ module ActiveRecord #:nodoc:
#
# In addition to the basic accessors, query methods are also automatically available on the Active Record object.
# Query methods allow you to test whether an attribute value is present.
+ # Additionally, when dealing with numeric values, a query method will return false if the value is zero.
#
# For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call
# to determine whether the user has a name:
@@ -160,14 +166,11 @@ module ActiveRecord #:nodoc:
#
# == Dynamic attribute-based finders
#
- # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects
+ # Dynamic attribute-based finders are a mildly deprecated way of getting (and/or creating) objects
# by simple queries without turning to SQL. They work by appending the name of an attribute
- # to <tt>find_by_</tt>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt> and thus produces finders
- # like <tt>Person.find_by_user_name</tt>, <tt>Person.find_all_by_last_name</tt>, and
- # <tt>Payment.find_by_transaction_id</tt>. Instead of writing
- # <tt>Person.where(user_name: user_name).first</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
- # And instead of writing <tt>Person.where(last_name: last_name).all</tt>, you just do
- # <tt>Person.find_all_by_last_name(last_name)</tt>.
+ # to <tt>find_by_</tt> like <tt>Person.find_by_user_name</tt>.
+ # Instead of writing <tt>Person.find_by(user_name: user_name)</tt>, you can use
+ # <tt>Person.find_by_user_name(user_name)</tt>.
#
# It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
# <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records,
@@ -175,51 +178,12 @@ module ActiveRecord #:nodoc:
#
# It's also possible to use multiple attributes in the same find by separating them with "_and_".
#
- # Person.where(user_name: user_name, password: password).first
+ # Person.find_by(user_name: user_name, password: password)
# Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
#
# It's even possible to call these dynamic finder methods on relations and named scopes.
#
- # Payment.order("created_on").find_all_by_amount(50)
- # Payment.pending.find_last_by_amount(100)
- #
- # The same dynamic finder style can be used to create the object if it doesn't already exist.
- # This dynamic finder is called with <tt>find_or_create_by_</tt> and will return the object if
- # it already exists and otherwise creates it, then returns it. Protected attributes won't be set
- # unless they are given in a block.
- #
- # # No 'Summer' tag exists
- # Tag.find_or_create_by_name("Summer") # equal to Tag.create(name: "Summer")
- #
- # # Now the 'Summer' tag does exist
- # Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
- #
- # # Now 'Bob' exist and is an 'admin'
- # User.find_or_create_by_name('Bob', age: 40) { |u| u.admin = true }
- #
- # Adding an exclamation point (!) on to the end of <tt>find_or_create_by_</tt> will
- # raise an <tt>ActiveRecord::RecordInvalid</tt> error if the new record is invalid.
- #
- # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without
- # saving it first. Protected attributes won't be set unless they are given in a block.
- #
- # # No 'Winter' tag exists
- # winter = Tag.find_or_initialize_by_name("Winter")
- # winter.persisted? # false
- #
- # To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of
- # a list of parameters.
- #
- # Tag.find_or_create_by_name(name: "rails", creator: current_user)
- #
- # That will either find an existing tag named "rails", or create a new one while setting the
- # user that created it.
- #
- # Just like <tt>find_by_*</tt>, you can also use <tt>scoped_by_*</tt> to retrieve data. The good thing about
- # using this feature is that the very first time result is returned using <tt>method_missing</tt> technique
- # but after that the method is declared on the class. Henceforth <tt>method_missing</tt> will not be hit.
- #
- # User.scoped_by_user_name('David')
+ # Payment.order("created_on").find_by_amount(50)
#
# == Saving arrays, hashes, and other non-mappable objects in text columns
#
@@ -258,25 +222,9 @@ module ActiveRecord #:nodoc:
#
# == Single table inheritance
#
- # Active Record allows inheritance by storing the name of the class in a column that by
- # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
- # This means that an inheritance looking like this:
- #
- # class Company < ActiveRecord::Base; end
- # class Firm < Company; end
- # class Client < Company; end
- # class PriorityClient < Client; end
- #
- # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
- # the companies table with type = "Firm". You can then fetch this row again using
- # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
- #
- # If you don't have a type column defined in your table, single-table inheritance won't
- # be triggered. In that case, it'll work just like normal subclasses with no special magic
- # for differentiating between them or reloading the right type with find.
- #
- # Note, all the attributes for all the cases are kept in the same table. Read more:
- # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ # Active Record allows inheritance by storing the name of the class in a
+ # column that is named "type" by default. See ActiveRecord::Inheritance for
+ # more details.
#
# == Connection to multiple databases in different models
#
@@ -332,7 +280,10 @@ module ActiveRecord #:nodoc:
extend Translation
extend DynamicMatchers
extend Explain
+ extend Enum
+ extend Delegation::DelegateCache
+ include Core
include Persistence
include ReadonlyAttributes
include ModelSchema
@@ -344,6 +295,8 @@ module ActiveRecord #:nodoc:
include Integration
include Validations
include CounterCache
+ include Attributes
+ include AttributeDecorators
include Locking::Optimistic
include Locking::Pessimistic
include AttributeMethods
@@ -355,10 +308,10 @@ module ActiveRecord #:nodoc:
include NestedAttributes
include Aggregations
include Transactions
+ include NoTouching
include Reflection
include Serialization
include Store
- include Core
end
ActiveSupport.run_load_hooks(:active_record, Base)
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 1c9c627090..497ce8c15c 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -23,11 +23,14 @@ module ActiveRecord
# Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and
# <tt>after_rollback</tt>.
#
+ # Additionally, an <tt>after_touch</tt> callback is triggered whenever an
+ # object is touched.
+ #
# Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
# is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
# are instantiated as well.
#
- # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the
+ # There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the
# Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar,
# except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
#
@@ -83,7 +86,7 @@ module ActiveRecord
#
# In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
# So, use the callback macros when you want to ensure that a certain callback is called for the entire
- # hierarchy, and use the regular overwriteable methods when you want to leave it up to each descendant
+ # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant
# to decide whether they want to call +super+ and trigger the inherited callbacks.
#
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
@@ -125,7 +128,7 @@ module ActiveRecord
# record.credit_card_number = decrypt(record.credit_card_number)
# end
#
- # alias_method :after_find, :after_save
+ # alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
@@ -160,7 +163,7 @@ module ActiveRecord
# record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
# end
#
- # alias_method :after_find, :after_save
+ # alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
@@ -286,25 +289,25 @@ module ActiveRecord
end
def destroy #:nodoc:
- run_callbacks(:destroy) { super }
+ _run_destroy_callbacks { super }
end
def touch(*) #:nodoc:
- run_callbacks(:touch) { super }
+ _run_touch_callbacks { super }
end
private
- def create_or_update #:nodoc:
- run_callbacks(:save) { super }
+ def create_or_update(*) #:nodoc:
+ _run_save_callbacks { super }
end
- def create #:nodoc:
- run_callbacks(:create) { super }
+ def _create_record #:nodoc:
+ _run_create_callbacks { super }
end
- def update(*) #:nodoc:
- run_callbacks(:update) { super }
+ def _update_record(*) #:nodoc:
+ _run_update_callbacks { super }
end
end
end
diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb
new file mode 100644
index 0000000000..75d3bfe625
--- /dev/null
+++ b/activerecord/lib/active_record/coders/json.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module Coders # :nodoc:
+ class JSON # :nodoc:
+ def self.dump(obj)
+ ActiveSupport::JSON.encode(obj)
+ end
+
+ def self.load(json)
+ ActiveSupport::JSON.decode(json) unless json.nil?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index f6cdc67b4d..9ea22ed798 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -3,12 +3,12 @@ require 'yaml'
module ActiveRecord
module Coders # :nodoc:
class YAMLColumn # :nodoc:
- RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ]
attr_accessor :object_class
def initialize(object_class = Object)
@object_class = object_class
+ check_arity_of_constructor
end
def dump(obj)
@@ -24,18 +24,24 @@ module ActiveRecord
def load(yaml)
return object_class.new if object_class != Object && yaml.nil?
return yaml unless yaml.is_a?(String) && yaml =~ /^---/
- begin
- obj = YAML.load(yaml)
+ obj = YAML.load(yaml)
- unless obj.is_a?(object_class) || obj.nil?
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
- end
- obj ||= object_class.new if object_class != Object
+ unless obj.is_a?(object_class) || obj.nil?
+ raise SerializationTypeMismatch,
+ "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
+ end
+ obj ||= object_class.new if object_class != Object
- obj
- rescue *RESCUE_ERRORS
- yaml
+ obj
+ end
+
+ private
+
+ def check_arity_of_constructor
+ begin
+ load(nil)
+ rescue ArgumentError
+ raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor."
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 82d0cf7e2e..6235745fb2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -2,7 +2,7 @@ require 'thread'
require 'thread_safe'
require 'monitor'
require 'set'
-require 'active_support/deprecation'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -59,13 +59,11 @@ module ActiveRecord
# * +checkout_timeout+: number of seconds to block and wait for a connection
# before giving up and raising a timeout error (default 5 seconds).
# * +reaping_frequency+: frequency in seconds to periodically run the
- # Reaper, which attempts to find and close dead connections, which can
- # occur if a programmer forgets to close a connection at the end of a
- # thread or a thread dies unexpectedly. (Default nil, which means don't
- # run the Reaper).
- # * +dead_connection_timeout+: number of seconds from last checkout
- # after which the Reaper will consider a connection reapable. (default
- # 5 seconds).
+ # Reaper, which attempts to find and recover connections from dead
+ # threads, which can occur if a programmer forgets to close a
+ # connection at the end of a thread or a thread dies unexpectedly.
+ # Regardless of this setting, the Reaper will be invoked before every
+ # blocking wait. (Default nil, which means don't schedule the Reaper).
class ConnectionPool
# Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
# with which it shares a Monitor. But could be a generic Queue.
@@ -87,7 +85,7 @@ module ActiveRecord
end
end
- # Return the number of threads currently waiting on this
+ # Returns the number of threads currently waiting on this
# queue.
def num_waiting
synchronize do
@@ -124,13 +122,13 @@ module ActiveRecord
# greater than the number of threads currently waiting (that
# is, don't jump ahead in line). Otherwise, return nil.
#
- # If +timeout+ is given, block if it there is no element
+ # If +timeout+ is given, block if there is no element
# available, waiting up to +timeout+ seconds for an element to
# become available.
#
# Raises:
# - ConnectionTimeoutError if +timeout+ is given and no element
- # becomes available after +timeout+ seconds,
+ # becomes available within +timeout+ seconds,
def poll(timeout = nil)
synchronize do
if timeout
@@ -153,7 +151,7 @@ module ActiveRecord
end
# A thread can remove an element from the queue without
- # waiting if an only if the number of currently available
+ # waiting if and only if the number of currently available
# connections is strictly greater than the number of waiting
# threads.
def can_remove_no_wait?
@@ -223,7 +221,7 @@ module ActiveRecord
include MonitorMixin
- attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout
+ attr_accessor :automatic_reconnect, :checkout_timeout
attr_reader :spec, :connections, :size, :reaper
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
@@ -237,9 +235,8 @@ module ActiveRecord
@spec = spec
- @checkout_timeout = spec.config[:checkout_timeout] || 5
- @dead_connection_timeout = spec.config[:dead_connection_timeout]
- @reaper = Reaper.new self, spec.config[:reaping_frequency]
+ @checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5
+ @reaper = Reaper.new(self, (spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f))
@reaper.run
# default max pool size to 5
@@ -254,14 +251,6 @@ module ActiveRecord
@available = Queue.new self
end
- # Hack for tests to be able to add connections. Do not call outside of tests
- def insert_connection_for_test!(c) #:nodoc:
- synchronize do
- @connections << c
- @available.add c
- end
- end
-
# Retrieve the connection associated with the current thread, or call
# #checkout to obtain one if necessary.
#
@@ -331,9 +320,7 @@ module ActiveRecord
checkin conn
conn.disconnect! if conn.requires_reloading?
end
- @connections.delete_if do |conn|
- conn.requires_reloading?
- end
+ @connections.delete_if(&:requires_reloading?)
@available.clear
@connections.each do |conn|
@available.add conn
@@ -341,11 +328,6 @@ module ActiveRecord
end
end
- def clear_stale_cached_connections! # :nodoc:
- reap
- end
- deprecate :clear_stale_cached_connections! => "Please use #reap instead"
-
# Check-out a database connection from the pool, indicating that you want
# to use it. You should call #checkin when you no longer need this.
#
@@ -375,11 +357,13 @@ module ActiveRecord
# calling +checkout+ on this pool.
def checkin(conn)
synchronize do
- conn.run_callbacks :checkin do
+ owner = conn.owner
+
+ conn._run_checkin_callbacks do
conn.expire
end
- release conn
+ release conn, owner
@available.add conn
end
@@ -392,22 +376,30 @@ module ActiveRecord
@connections.delete conn
@available.delete conn
- # FIXME: we might want to store the key on the connection so that removing
- # from the reserved hash will be a little easier.
- release conn
+ release conn, conn.owner
@available.add checkout_new_connection if @available.any_waiting?
end
end
- # Removes dead connections from the pool. A dead connection can occur
- # if a programmer forgets to close a connection at the end of a thread
+ # Recover lost connections for the pool. A lost connection can occur if
+ # a programmer forgets to checkin a connection at the end of a thread
# or a thread dies unexpectedly.
def reap
- synchronize do
- stale = Time.now - @dead_connection_timeout
- connections.dup.each do |conn|
- remove conn if conn.in_use? && stale > conn.last_use && !conn.active?
+ stale_connections = synchronize do
+ @connections.select do |conn|
+ conn.in_use? && !conn.owner.alive?
+ end
+ end
+
+ stale_connections.each do |conn|
+ synchronize do
+ if conn.active?
+ conn.reset!
+ checkin conn
+ else
+ remove conn
+ end
end
end
end
@@ -427,20 +419,17 @@ module ActiveRecord
elsif @connections.size < @size
checkout_new_connection
else
+ reap
@available.poll(@checkout_timeout)
end
end
- def release(conn)
- thread_id = if @reserved_connections[current_connection_id] == conn
- current_connection_id
- else
- @reserved_connections.keys.find { |k|
- @reserved_connections[k] == conn
- }
- end
+ def release(conn, owner)
+ thread_id = owner.object_id
- @reserved_connections.delete thread_id if thread_id
+ if @reserved_connections[thread_id] == conn
+ @reserved_connections.delete thread_id
+ end
end
def new_connection
@@ -461,7 +450,7 @@ module ActiveRecord
end
def checkout_and_verify(c)
- c.run_callbacks :checkout do
+ c._run_checkout_callbacks do
c.verify!
end
c
@@ -474,23 +463,44 @@ module ActiveRecord
#
# For example, suppose that you have 5 models, with the following hierarchy:
#
- # |
- # +-- Book
- # | |
- # | +-- ScaryBook
- # | +-- GoodBook
- # +-- Author
- # +-- BankAccount
+ # class Author < ActiveRecord::Base
+ # end
+ #
+ # class BankAccount < ActiveRecord::Base
+ # end
+ #
+ # class Book < ActiveRecord::Base
+ # establish_connection "library_db"
+ # end
#
- # Suppose that Book is to connect to a separate database (i.e. one other
- # than the default database). Then Book, ScaryBook and GoodBook will all use
- # the same connection pool. Likewise, Author and BankAccount will use the
- # same connection pool. However, the connection pool used by Author/BankAccount
- # is not the same as the one used by Book/ScaryBook/GoodBook.
+ # class ScaryBook < Book
+ # end
#
- # Normally there is only a single ConnectionHandler instance, accessible via
- # ActiveRecord::Base.connection_handler. Active Record models use this to
- # determine the connection pool that they should use.
+ # class GoodBook < Book
+ # end
+ #
+ # And a database.yml that looked like this:
+ #
+ # development:
+ # database: my_application
+ # host: localhost
+ #
+ # library_db:
+ # database: library
+ # host: some.library.org
+ #
+ # Your primary database in the development environment is "my_application"
+ # but the Book model connects to a separate database called "library_db"
+ # (this can even be a database on a different machine).
+ #
+ # Book, ScaryBook and GoodBook will all use the same connection pool to
+ # "library_db" while Author, BankAccount, and any other models you create
+ # will use the default connection pool to "my_application".
+ #
+ # The various connection pools are managed by a single instance of
+ # ConnectionHandler accessible via ActiveRecord::Base.connection_handler.
+ # All Active Record models use this handler to determine the connection pool that they
+ # should use.
class ConnectionHandler
def initialize
# These caches are keyed by klass.name, NOT klass. Keying them by klass
@@ -509,15 +519,17 @@ module ActiveRecord
end
def connection_pools
- ActiveSupport::Deprecation.warn(
- "In the next release, this will return the same as #connection_pool_list. " \
- "(An array of pools, rather than a hash mapping specs to pools.)"
- )
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ In the next release, this will return the same as `#connection_pool_list`.
+ (An array of pools, rather than a hash mapping specs to pools.)
+ MSG
+
Hash[connection_pool_list.map { |pool| [pool.spec, pool] }]
end
def establish_connection(owner, spec)
@class_to_pool.clear
+ raise RuntimeError, "Anonymous class is not allowed." unless owner.name
owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)
end
@@ -549,7 +561,10 @@ module ActiveRecord
# for (not necessarily the current class).
def retrieve_connection(klass) #:nodoc:
pool = retrieve_connection_pool(klass)
- (pool && pool.connection) or raise ConnectionNotEstablished
+ raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool
+ conn = pool.connection
+ raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn
+ conn
end
# Returns true if a connection that's accessible to this class has
@@ -577,10 +592,10 @@ module ActiveRecord
# When a connection is established or removed, we invalidate the cache.
#
# Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil.
- # However, benchmarking (https://gist.github.com/3552829) showed that #fetch is
- # significantly slower than #[]. So in the nil case, no caching will take place,
- # but that's ok since the nil case is not the common one that we wish to optimise
- # for.
+ # However, benchmarking (https://gist.github.com/jonleighton/3552829) showed that
+ # #fetch is significantly slower than #[]. So in the nil case, no caching will
+ # take place, but that's ok since the nil case is not the common one that we wish
+ # to optimise for.
def retrieve_connection_pool(klass)
class_to_pool[klass.name] ||= begin
until pool = pool_for(klass)
@@ -627,7 +642,7 @@ module ActiveRecord
end
def call(env)
- testing = env.key?('rack.test')
+ testing = env['rack.test']
response = @app.call(env)
response[2] = ::Rack::BodyProxy.new(response[2]) do
@@ -635,7 +650,7 @@ module ActiveRecord
end
response
- rescue
+ rescue Exception
ActiveRecord::Base.clear_active_connections! unless testing
raise
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index 30ccb8f0a4..c0a2111571 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -17,6 +17,15 @@ module ActiveRecord
64
end
+ # Returns the maximum allowed length for an index name. This
+ # limit is enforced by rails and Is less than or equal to
+ # <tt>index_name_length</tt>. The gap between
+ # <tt>index_name_length</tt> is to allow internal rails
+ # operations to use prefixes in temporary operations.
+ def allowed_index_name_length
+ index_name_length
+ end
+
# Returns the maximum length of an index name.
def index_name_length
64
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index c3d15ca929..59cdd8e98c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -9,26 +9,33 @@ module ActiveRecord
# Converts an arel AST to SQL
def to_sql(arel, binds = [])
if arel.respond_to?(:ast)
- binds = binds.dup
- visitor.accept(arel.ast) do
- quote(*binds.shift.reverse)
- end
+ collected = visitor.accept(arel.ast, collector)
+ collected.compile(binds.dup, self)
else
arel
end
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # This is used in the StatementCache object. It returns an object that
+ # can be used to query the database repeatedly.
+ def cacheable_query(arel) # :nodoc:
+ if prepared_statements
+ ActiveRecord::StatementCache.query visitor, arel.ast
+ else
+ ActiveRecord::StatementCache.partial_query visitor, arel.ast, collector
+ end
+ end
+
+ # Returns an ActiveRecord::Result instance.
def select_all(arel, name = nil, binds = [])
+ arel, binds = binds_from_relation arel, binds
select(to_sql(arel, binds), name, binds)
end
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(arel, name = nil, binds = [])
- result = select_all(arel, name, binds)
- result.first if result
+ select_all(arel, name, binds).first
end
# Returns a single value from a record
@@ -41,13 +48,13 @@ module ActiveRecord
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(arel, name = nil)
- result = select_rows(to_sql(arel, []), name)
- result.map { |v| v[0] }
+ arel, binds = binds_from_relation arel, []
+ select_rows(to_sql(arel, binds), name, binds).map(&:first)
end
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
- def select_rows(sql, name = nil)
+ def select_rows(sql, name = nil, binds = [])
end
undef_method :select_rows
@@ -76,6 +83,11 @@ module ActiveRecord
exec_query(sql, name, binds)
end
+ # Executes the truncate statement.
+ def truncate(table_name, name = nil)
+ raise NotImplementedError
+ end
+
# Executes update +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
@@ -125,7 +137,8 @@ module ActiveRecord
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
# http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
- # Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
+ # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8'
+ # supports savepoints.
#
# It is safe to call this method if a database transaction is already open,
# i.e. if #transaction is called within another #transaction block. In case
@@ -185,7 +198,7 @@ module ActiveRecord
# * You are creating a nested (savepoint) transaction
#
# The mysql, mysql2 and postgresql adapters support setting the transaction
- # isolation level. However, support is disabled for mysql versions below 5,
+ # isolation level. However, support is disabled for MySQL versions below 5,
# because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170]
# which means the isolation level gets persisted outside the transaction.
def transaction(options = {})
@@ -195,58 +208,30 @@ module ActiveRecord
if options[:isolation]
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
end
-
yield
else
- within_new_transaction(options) { yield }
+ transaction_manager.within_new_transaction(options) { yield }
end
rescue ActiveRecord::Rollback
# rollbacks are silently swallowed
end
- def within_new_transaction(options = {}) #:nodoc:
- transaction = begin_transaction(options)
- yield
- rescue Exception => error
- rollback_transaction if transaction
- raise
- ensure
- begin
- commit_transaction unless error
- rescue Exception
- rollback_transaction
- raise
- end
- end
+ attr_reader :transaction_manager #:nodoc:
- def current_transaction #:nodoc:
- @transaction
- end
+ delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager
def transaction_open?
- @transaction.open?
- end
-
- def begin_transaction(options = {}) #:nodoc:
- @transaction = @transaction.begin(options)
- end
-
- def commit_transaction #:nodoc:
- @transaction = @transaction.commit
- end
-
- def rollback_transaction #:nodoc:
- @transaction = @transaction.rollback
+ current_transaction.open?
end
def reset_transaction #:nodoc:
- @transaction = ClosedTransaction.new(self)
+ @transaction_manager = TransactionManager.new(self)
end
# Register a record with the current transaction so that its after_commit and after_rollback callbacks
# can be called.
def add_transaction_record(record)
- @transaction.add_record(record)
+ current_transaction.add_record(record)
end
# Begins the transaction (and turns off auto-committing).
@@ -273,7 +258,18 @@ module ActiveRecord
# Rolls back the transaction (and turns on auto-committing). Must be
# done if the transaction block raises an exception or returns false.
- def rollback_db_transaction() end
+ def rollback_db_transaction
+ exec_rollback_db_transaction
+ end
+
+ def exec_rollback_db_transaction() end #:nodoc:
+
+ def rollback_to_savepoint(name = nil)
+ exec_rollback_to_savepoint(name)
+ end
+
+ def exec_rollback_to_savepoint(name = nil) #:nodoc:
+ end
def default_sequence_name(table, column)
nil
@@ -302,14 +298,6 @@ module ActiveRecord
"DEFAULT VALUES"
end
- def case_sensitive_equality_operator
- "="
- end
-
- def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
- "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
- end
-
# Sanitizes the given LIMIT parameter in order to prevent SQL injection.
#
# The +limit+ may be anything that can evaluate to a string via #to_s. It
@@ -322,7 +310,7 @@ module ActiveRecord
def sanitize_limit(limit)
if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral)
limit
- elsif limit.to_s =~ /,/
+ elsif limit.to_s.include?(',')
Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',')
else
Integer(limit)
@@ -330,8 +318,8 @@ module ActiveRecord
end
# The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
- # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in
- # an UPDATE statement, so in the mysql adapters we redefine this to do that.
+ # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in
+ # an UPDATE statement, so in the MySQL adapters we redefine this to do that.
def join_to_update(update, select) #:nodoc:
key = update.key
subselect = subquery_for(key, select)
@@ -347,18 +335,18 @@ module ActiveRecord
protected
- # Return a subquery for the given key using the join information.
+ # Returns a subquery for the given key using the join information.
def subquery_for(key, select)
subselect = select.clone
subselect.projections = [key]
subselect
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
+ exec_query(sql, name, binds)
end
- undef_method :select
+
# Returns the last auto-generated ID from the affected table.
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
@@ -376,14 +364,21 @@ module ActiveRecord
update_sql(sql, name)
end
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
- [sql, binds]
- end
+ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
+ [sql, binds]
+ end
- def last_inserted_id(result)
- row = result.rows.first
- row && row.first
- end
+ def last_inserted_id(result)
+ row = result.rows.first
+ row && row.first
+ end
+
+ def binds_from_relation(relation, binds)
+ if relation.is_a?(Relation) && binds.empty?
+ relation, binds = relation.arel, relation.bind_values
+ end
+ [relation, binds]
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index be6fda95b4..5e27cfe507 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -3,16 +3,16 @@ module ActiveRecord
module QueryCache
class << self
def included(base) #:nodoc:
- dirties_query_cache base, :insert, :update, :delete
+ dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction
end
def dirties_query_cache(base, *method_names)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
- def #{method_name}(*) # def update_with_query_dirty(*args)
- clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
- super # update_without_query_dirty(*args)
- end # end
+ def #{method_name}(*)
+ clear_query_cache if @query_cache_enabled
+ super
+ end
end_code
end
end
@@ -20,13 +20,19 @@ module ActiveRecord
attr_reader :query_cache, :query_cache_enabled
+ def initialize(*)
+ super
+ @query_cache = Hash.new { |h,sql| h[sql] = {} }
+ @query_cache_enabled = false
+ end
+
# Enable the query cache within the block.
def cache
old, @query_cache_enabled = @query_cache_enabled, true
yield
ensure
- clear_query_cache
@query_cache_enabled = old
+ clear_query_cache unless @query_cache_enabled
end
def enable_query_cache!
@@ -57,6 +63,7 @@ module ActiveRecord
def select_all(arel, name = nil, binds = [])
if @query_cache_enabled && !locked?(arel)
+ arel, binds = binds_from_relation arel, binds
sql = to_sql(arel, binds)
cache_sql(sql, binds) { super(sql, name, binds) }
else
@@ -75,16 +82,11 @@ module ActiveRecord
else
@query_cache[sql][binds] = yield
end
-
- # FIXME: we should guarantee that all cached items are Result
- # objects. Then we can avoid this conditional
- if ActiveRecord::Result === result
- result.dup
- else
- result.collect { |row| row.dup }
- end
+ result.dup
end
+ # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such
+ # queries should not be cached.
def locked?(arel)
arel.respond_to?(:locked) && arel.locked
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 60a9eee7c7..143d7d9574 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -9,72 +9,29 @@ module ActiveRecord
# records are quoted as their primary key
return value.quoted_id if value.respond_to?(:quoted_id)
- case value
- when String, ActiveSupport::Multibyte::Chars
- value = value.to_s
- return "'#{quote_string(value)}'" unless column
-
- case column.type
- when :binary then "'#{quote_string(column.string_to_binary(value))}'"
- when :integer then value.to_i.to_s
- when :float then value.to_f.to_s
- else
- "'#{quote_string(value)}'"
- end
-
- when true, false
- if column && column.type == :integer
- value ? '1' : '0'
- else
- value ? quoted_true : quoted_false
- end
- # BigDecimals need to be put in a non-normalized form and quoted.
- when nil then "NULL"
- when BigDecimal then value.to_s('F')
- when Numeric, ActiveSupport::Duration 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))}'"
+ if column
+ value = column.cast_type.type_cast_for_database(value)
end
+
+ _quote(value)
end
# Cast a +value+ to a type that the database understands. For example,
# SQLite does not understand dates, so this method will convert a Date
# to a String.
- def type_cast(value, column)
- return value.id if value.respond_to?(:quoted_id)
-
- case value
- when String, ActiveSupport::Multibyte::Chars
- value = value.to_s
- return value unless column
-
- case column.type
- when :binary then value
- when :integer then value.to_i
- when :float then value.to_f
- else
- value
- end
+ def type_cast(value, column = nil)
+ if value.respond_to?(:quoted_id) && value.respond_to?(:id)
+ return value.id
+ end
- when true, false
- if column && column.type == :integer
- value ? 1 : 0
- else
- value ? 't' : 'f'
- end
- # BigDecimals need to be put in a non-normalized form and quoted.
- when nil then nil
- when BigDecimal then value.to_s('F')
- when Numeric then value
- when Date, Time then quoted_date(value)
- when Symbol then value.to_s
- else
- to_type = column ? " to #{column.type}" : ""
- raise TypeError, "can't cast #{value.class}#{to_type}"
+ if column
+ value = column.cast_type.type_cast_for_database(value)
end
+
+ _type_cast(value)
+ rescue TypeError
+ to_type = column ? " to #{column.type}" : ""
+ raise TypeError, "can't cast #{value.class}#{to_type}"
end
# Quotes a string, escaping any ' (single quote) and \ (backslash)
@@ -93,14 +50,34 @@ module ActiveRecord
quote_column_name(table_name)
end
+ # Override to return the quoted table name for assignment. Defaults to
+ # table quoting.
+ #
+ # This works for mysql and mysql2 where table.column can be used to
+ # resolve ambiguity.
+ #
+ # We override this in the sqlite3 and postgresql adapters to use only
+ # the column name (as per syntax requirements).
+ def quote_table_name_for_assignment(table, attr)
+ quote_table_name("#{table}.#{attr}")
+ end
+
def quoted_true
"'t'"
end
+ def unquoted_true
+ 't'
+ end
+
def quoted_false
"'f'"
end
+ def unquoted_false
+ 'f'
+ end
+
def quoted_date(value)
if value.acts_like?(:time)
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
@@ -112,6 +89,45 @@ module ActiveRecord
value.to_s(:db)
end
+
+ private
+
+ def types_which_need_no_typecasting
+ [nil, Numeric, String]
+ end
+
+ def _quote(value)
+ case value
+ when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data
+ "'#{quote_string(value.to_s)}'"
+ when true then quoted_true
+ when false then quoted_false
+ when nil then "NULL"
+ # BigDecimals need to be put in a non-normalized form and quoted.
+ when BigDecimal then value.to_s('F')
+ when Numeric, ActiveSupport::Duration then value.to_s
+ when Date, Time then "'#{quoted_date(value)}'"
+ when Symbol then "'#{quote_string(value.to_s)}'"
+ when Class then "'#{value}'"
+ else
+ "'#{quote_string(YAML.dump(value))}'"
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data
+ value.to_s
+ when true then unquoted_true
+ when false then unquoted_false
+ # BigDecimals need to be put in a non-normalized form and quoted.
+ when BigDecimal then value.to_s('F')
+ when Date, Time then quoted_date(value)
+ when *types_which_need_no_typecasting
+ value
+ else raise TypeError
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
new file mode 100644
index 0000000000..c0662f8473
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module Savepoints #:nodoc:
+ def supports_savepoints?
+ true
+ end
+
+ def create_savepoint(name = current_savepoint_name)
+ execute("SAVEPOINT #{name}")
+ end
+
+ def exec_rollback_to_savepoint(name = current_savepoint_name)
+ execute("ROLLBACK TO SAVEPOINT #{name}")
+ end
+
+ def release_savepoint(name = current_savepoint_name)
+ execute("RELEASE SAVEPOINT #{name}")
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
new file mode 100644
index 0000000000..18ff869ea6
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -0,0 +1,130 @@
+require 'active_support/core_ext/string/strip'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class AbstractAdapter
+ class SchemaCreation # :nodoc:
+ def initialize(conn)
+ @conn = conn
+ @cache = {}
+ end
+
+ def accept(o)
+ m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
+ send m, o
+ end
+
+ def visit_AddColumn(o)
+ "ADD #{accept(o)}"
+ end
+
+ private
+
+ def visit_AlterTable(o)
+ sql = "ALTER TABLE #{quote_table_name(o.name)} "
+ sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ')
+ sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ')
+ end
+
+ def visit_ColumnDefinition(o)
+ sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
+ column_sql = "#{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key
+ column_sql
+ end
+
+ def visit_TableDefinition(o)
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
+ create_sql << "#{quote_table_name(o.name)} "
+ create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as
+ create_sql << "#{o.options}"
+ create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
+ create_sql
+ end
+
+ def visit_AddForeignKey(o)
+ sql = <<-SQL.strip_heredoc
+ ADD CONSTRAINT #{quote_column_name(o.name)}
+ FOREIGN KEY (#{quote_column_name(o.column)})
+ REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
+ SQL
+ sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete
+ sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update
+ sql
+ end
+
+ def visit_DropForeignKey(name)
+ "DROP CONSTRAINT #{quote_column_name(name)}"
+ end
+
+ def column_options(o)
+ column_options = {}
+ column_options[:null] = o.null unless o.null.nil?
+ column_options[:default] = o.default unless o.default.nil?
+ column_options[:column] = o
+ column_options[:first] = o.first
+ column_options[:after] = o.after
+ column_options[:auto_increment] = o.auto_increment
+ column_options[:primary_key] = o.primary_key
+ column_options
+ end
+
+ def quote_column_name(name)
+ @conn.quote_column_name name
+ end
+
+ def quote_table_name(name)
+ @conn.quote_table_name name
+ end
+
+ def type_to_sql(type, limit, precision, scale)
+ @conn.type_to_sql type.to_sym, limit, precision, scale
+ end
+
+ def add_column_options!(sql, options)
+ sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
+ # must explicitly check for :null to allow change_column to work on migrations
+ if options[:null] == false
+ sql << " NOT NULL"
+ end
+ if options[:auto_increment] == true
+ sql << " AUTO_INCREMENT"
+ end
+ if options[:primary_key] == true
+ sql << " PRIMARY KEY"
+ end
+ sql
+ end
+
+ def quote_default_expression(value, column)
+ column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale)
+ value = type_for_column(column).type_cast_for_database(value)
+
+ @conn.quote(value)
+ end
+
+ def options_include_default?(options)
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ end
+
+ def action_sql(action, dependency)
+ case dependency
+ when :nullify then "ON #{action} SET NULL"
+ when :cascade then "ON #{action} CASCADE"
+ when :restrict then "ON #{action} RESTRICT"
+ else
+ raise ArgumentError, <<-MSG.strip_heredoc
+ '#{dependency}' is not supported for :on_update or :on_delete.
+ Supported values are: :nullify, :cascade, :restrict
+ MSG
+ end
+ end
+
+ def type_for_column(column)
+ @conn.lookup_cast_type(column.sql_type)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index b1ec33d06c..68d8a2cd6a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -8,44 +8,149 @@ module ActiveRecord
# Abstract representation of an index definition on a table. Instances of
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where) #:nodoc:
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc:
end
# Abstract representation of a column definition. Instances of this type
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type, :cast_type) #:nodoc:
- def string_to_binary(value)
- value
+ def primary_key?
+ primary_key || type.to_sym == :primary_key
end
+ end
+
+ class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc:
+ end
+
+ class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
+ def name
+ options[:name]
+ end
+
+ def column
+ options[:column]
+ end
+
+ def primary_key
+ options[:primary_key] || default_primary_key
+ end
+
+ def on_delete
+ options[:on_delete]
+ end
+
+ def on_update
+ options[:on_update]
+ end
+
+ def custom_primary_key?
+ options[:primary_key] != default_primary_key
+ end
+
+ private
+ def default_primary_key
+ "id"
+ end
+ end
+
+ module TimestampDefaultDeprecation # :nodoc:
+ def emit_warning_if_null_unspecified(options)
+ return if options.key?(:null)
+
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `#timestamp` was called without specifying an option for `null`. In Rails 5,
+ this behavior will change to `null: false`. You should manually specify
+ `null: true` to prevent the behavior of your existing migrations from changing.
+ MSG
+ end
+ end
- def sql_type
- base.type_to_sql(type.to_sym, limit, precision, scale)
+ class ReferenceDefinition # :nodoc:
+ def initialize(
+ name,
+ polymorphic: false,
+ index: false,
+ foreign_key: false,
+ type: :integer,
+ **options
+ )
+ @name = name
+ @polymorphic = polymorphic
+ @index = index
+ @foreign_key = foreign_key
+ @type = type
+ @options = options
+
+ if polymorphic && foreign_key
+ raise ArgumentError, "Cannot add a foreign key to a polymorphic relation"
+ end
end
- def to_sql
- column_sql = "#{base.quote_column_name(name)} #{sql_type}"
- column_options = {}
- column_options[:null] = null unless null.nil?
- column_options[:default] = default unless default.nil?
- add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key
- column_sql
+ def add_to(table)
+ columns.each do |column_options|
+ table.column(*column_options)
+ end
+
+ if index
+ table.index(column_names, index_options)
+ end
+
+ if foreign_key
+ table.foreign_key(foreign_table_name, foreign_key_options)
+ end
end
+ protected
+
+ attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options
+
private
- def add_column_options!(sql, options)
- base.add_column_options!(sql, options.merge(:column => self))
+ def as_options(value, default = {})
+ if value.is_a?(Hash)
+ value
+ else
+ default
+ end
+ end
+
+ def polymorphic_options
+ as_options(polymorphic, options)
+ end
+
+ def index_options
+ as_options(index)
+ end
+
+ def foreign_key_options
+ as_options(foreign_key)
+ end
+
+ def columns
+ result = [["#{name}_id", type, options]]
+ if polymorphic
+ result.unshift(["#{name}_type", :string, polymorphic_options])
end
+ result
+ end
+
+ def column_names
+ columns.map(&:first)
+ end
+
+ def foreign_table_name
+ name.to_s.pluralize
+ end
end
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
- # Inside migration files, the +t+ object in +create_table+ and
- # +change_table+ is actually of this type:
+ # Inside migration files, the +t+ object in +create_table+
+ # is actually of this type:
#
# class SomeMigration < ActiveRecord::Migration
# def up
@@ -62,30 +167,30 @@ module ActiveRecord
# The table definitions
# The Columns are stored as a ColumnDefinition in the +columns+ attribute.
class TableDefinition
+ include TimestampDefaultDeprecation
+
# An array of ColumnDefinition objects, representing the column changes
# that have been defined.
- attr_accessor :columns, :indexes
+ attr_accessor :indexes
+ attr_reader :name, :temporary, :options, :as, :foreign_keys
- def initialize(base)
- @columns = []
+ def initialize(types, name, temporary, options, as = nil)
@columns_hash = {}
@indexes = {}
- @base = base
+ @foreign_keys = {}
+ @native = types
+ @temporary = temporary
+ @options = options
+ @as = as
+ @name = name
end
- def xml(*args)
- raise NotImplementedError unless %w{
- sqlite mysql mysql2
- }.include? @base.adapter_name.downcase
-
- options = args.extract_options!
- column(args[0], :text, options)
- end
+ def columns; @columns_hash.values; end
# Appends a primary key definition to the table definition.
# Can be called multiple times, but this is probably not a good idea.
- def primary_key(name)
- column(name, :primary_key)
+ def primary_key(name, type = :primary_key, options = {})
+ column(name, type, options.merge(:primary_key => true))
end
# Returns a ColumnDefinition for the column with name +name+.
@@ -98,8 +203,8 @@ module ActiveRecord
# which is one of the following:
# <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
# <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
- # <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
- # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>.
+ # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>,
+ # <tt>:binary</tt>, <tt>:boolean</tt>.
#
# You may use a type not in this list as long as it is supported by your
# database (for example, "polygon" in MySQL), but this will not be database
@@ -118,9 +223,11 @@ module ActiveRecord
# Specifies the precision for a <tt>:decimal</tt> column.
# * <tt>:scale</tt> -
# Specifies the scale for a <tt>:decimal</tt> column.
+ # * <tt>:index</tt> -
+ # Create an index for the column. Can be either <tt>true</tt> or an options hash.
#
- # For clarity's sake: the precision is the number of significant digits,
- # while the scale is the number of digits that can be stored following
+ # Note: The precision is the total number of significant digits
+ # and the scale is the number of digits that can be stored following
# the decimal point. For example, the number 123.45 has a precision of 5
# and a scale of 2. A decimal with a precision of 5 and a scale of 2 can
# range from -999.99 to 999.99.
@@ -142,17 +249,8 @@ module ActiveRecord
# Default is (38,0).
# * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
# Default unknown.
- # * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18].
- # Default (9,0). Internal types NUMERIC and DECIMAL have different
- # storage rules, decimal being better.
- # * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
- # Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for
- # NUMERIC is 19, and DECIMAL is 38.
# * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
# Default (38,0).
- # * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
- # Default (38,0).
- # * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>.
#
# This method returns <tt>self</tt>.
#
@@ -191,20 +289,23 @@ module ActiveRecord
# What can be written like this with the regular calls to column:
#
# create_table :products do |t|
- # t.column :shop_id, :integer
- # t.column :creator_id, :integer
- # t.column :name, :string, default: "Untitled"
- # t.column :value, :string, default: "Untitled"
- # t.column :created_at, :datetime
- # t.column :updated_at, :datetime
+ # t.column :shop_id, :integer
+ # t.column :creator_id, :integer
+ # t.column :item_number, :string
+ # t.column :name, :string, default: "Untitled"
+ # t.column :value, :string, default: "Untitled"
+ # t.column :created_at, :datetime
+ # t.column :updated_at, :datetime
# end
+ # add_index :products, :item_number
#
# can also be written as follows using the short-hand:
#
# create_table :products do |t|
# t.integer :shop_id, :creator_id
+ # t.string :item_number, index: true
# t.string :name, :value, default: "Untitled"
- # t.timestamps
+ # t.timestamps null: false
# end
#
# There's a short-hand method for each of the type values declared at the top. And then there's
@@ -234,33 +335,26 @@ module ActiveRecord
name = name.to_s
type = type.to_sym
- if primary_key_column_name == name
+ if @columns_hash[name] && @columns_hash[name].primary_key?
raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
end
- column = self[name] || new_column_definition(@base, name, type)
-
- limit = options.fetch(:limit) do
- native[type][:limit] if native[type].is_a?(Hash)
- end
-
- column.limit = limit
- column.precision = options[:precision]
- column.scale = options[:scale]
- column.default = options[:default]
- column.null = options[:null]
+ index_options = options.delete(:index)
+ index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options
+ @columns_hash[name] = new_column_definition(name, type, options)
self
end
- %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
- class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{column_type}(*args) # def string(*args)
- options = args.extract_options! # options = args.extract_options!
- column_names = args # column_names = args
- type = :'#{column_type}' # type = :string
- column_names.each { |name| column(name, type, options) } # column_names.each { |name| column(name, type, options) }
- end # end
- EOV
+ def remove_column(name)
+ @columns_hash.delete name.to_s
+ end
+
+ [:string, :text, :integer, :bigint, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type|
+ define_method column_type do |*args|
+ options = args.extract_options!
+ column_names = args
+ column_names.each { |name| column(name, column_type, options) }
+ end
end
# Adds index options to the indexes hash, keyed by column name
@@ -271,48 +365,98 @@ module ActiveRecord
indexes[column_name] = options
end
+ def foreign_key(table_name, options = {}) # :nodoc:
+ foreign_keys[table_name] = options
+ end
+
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
- # <tt>:updated_at</tt> to the table.
+ # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps
+ #
+ # t.timestamps null: false
def timestamps(*args)
options = args.extract_options!
+ emit_warning_if_null_unspecified(options)
column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end
- def references(*args)
- options = args.extract_options!
- polymorphic = options.delete(:polymorphic)
- index_options = options.delete(:index)
+ # Adds a reference. Optionally adds a +type+ column, if the
+ # +:polymorphic+ option is provided. +references+ and +belongs_to+
+ # are acceptable. The reference column will be an +integer+ by default,
+ # the +:type+ option can be used to specify a different type. A foreign
+ # key will be created if the +:foreign_key+ option is passed.
+ #
+ # t.references(:user)
+ # t.references(:user, type: "string")
+ # t.belongs_to(:supplier, polymorphic: true)
+ #
+ # See SchemaStatements#add_reference
+ def references(*args, **options)
args.each do |col|
- column("#{col}_id", :integer, options)
- column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
- index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options
+ ReferenceDefinition.new(col, **options).add_to(self)
end
end
alias :belongs_to :references
- # Returns a String whose contents are the column definitions
- # concatenated together. This string can then be prepended and appended to
- # to generate the final SQL to create the table.
- def to_sql
- @columns.map { |c| c.to_sql } * ', '
+ def new_column_definition(name, type, options) # :nodoc:
+ type = aliased_types(type.to_s, type)
+ column = create_column_definition name, type
+ limit = options.fetch(:limit) do
+ native[type][:limit] if native[type].is_a?(Hash)
+ end
+
+ column.limit = limit
+ column.precision = options[:precision]
+ column.scale = options[:scale]
+ column.default = options[:default]
+ column.null = options[:null]
+ column.first = options[:first]
+ column.after = options[:after]
+ column.auto_increment = options[:auto_increment]
+ column.primary_key = type == :primary_key || options[:primary_key]
+ column
end
private
- def new_column_definition(base, name, type)
- definition = ColumnDefinition.new base, name, type
- @columns << definition
- @columns_hash[name] = definition
- definition
+ def create_column_definition(name, type)
+ ColumnDefinition.new name, type
end
- def primary_key_column_name
- primary_key_column = columns.detect { |c| c.type == :primary_key }
- primary_key_column && primary_key_column.name
+ def native
+ @native
end
- def native
- @base.native_database_types
+ def aliased_types(name, fallback)
+ 'timestamp' == name ? :datetime : fallback
+ end
+ end
+
+ class AlterTable # :nodoc:
+ attr_reader :adds
+ attr_reader :foreign_key_adds
+ attr_reader :foreign_key_drops
+
+ def initialize(td)
+ @td = td
+ @adds = []
+ @foreign_key_adds = []
+ @foreign_key_drops = []
+ end
+
+ def name; @td.name; end
+
+ def add_foreign_key(to_table, options)
+ @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options)
+ end
+
+ def drop_foreign_key(name)
+ @foreign_key_drops << name
+ end
+
+ def add_column(name, type, options)
+ name = name.to_s
+ type = type.to_sym
+ @adds << @td.new_column_definition(name, type, options)
end
end
@@ -350,151 +494,173 @@ module ActiveRecord
# end
#
class Table
+ attr_reader :name
+
def initialize(table_name, base)
- @table_name = table_name
+ @name = table_name
@base = base
end
# Adds a new column to the named table.
- # See TableDefinition#column for details of the options you can use.
#
- # ====== Creating a simple column
# t.column(:name, :string)
+ #
+ # See TableDefinition#column for details of the options you can use.
def column(column_name, type, options = {})
- @base.add_column(@table_name, column_name, type, options)
+ @base.add_column(name, column_name, type, options)
end
- # Checks to see if a column exists. See SchemaStatements#column_exists?
+ # Checks to see if a column exists.
+ #
+ # See SchemaStatements#column_exists?
def column_exists?(column_name, type = nil, options = {})
- @base.column_exists?(@table_name, column_name, type, options)
+ @base.column_exists?(name, column_name, type, options)
end
# Adds a new index to the table. +column_name+ can be a single Symbol, or
- # an Array of Symbols. See SchemaStatements#add_index
+ # an Array of Symbols.
#
- # ====== Creating a simple index
# t.index(:name)
- # ====== Creating a unique index
# t.index([:branch_id, :party_id], unique: true)
- # ====== Creating a named index
# t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party')
+ #
+ # See SchemaStatements#add_index for details of the options you can use.
def index(column_name, options = {})
- @base.add_index(@table_name, column_name, options)
+ @base.add_index(name, column_name, options)
end
- # Checks to see if an index exists. See SchemaStatements#index_exists?
+ # Checks to see if an index exists.
+ #
+ # See SchemaStatements#index_exists?
def index_exists?(column_name, options = {})
- @base.index_exists?(@table_name, column_name, options)
+ @base.index_exists?(name, column_name, options)
end
# Renames the given index on the table.
#
# t.rename_index(:user_id, :account_id)
+ #
+ # See SchemaStatements#rename_index
def rename_index(index_name, new_index_name)
- @base.rename_index(@table_name, index_name, new_index_name)
+ @base.rename_index(name, index_name, new_index_name)
end
- # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps
+ # Adds timestamps (+created_at+ and +updated_at+) columns to the table.
+ #
+ # t.timestamps(null: false)
#
- # t.timestamps
- def timestamps
- @base.add_timestamps(@table_name)
+ # See SchemaStatements#add_timestamps
+ def timestamps(options = {})
+ @base.add_timestamps(name, options)
end
# Changes the column's definition according to the new options.
- # See TableDefinition#column for details of the options you can use.
#
# t.change(:name, :string, limit: 80)
# t.change(:description, :text)
+ #
+ # See TableDefinition#column for details of the options you can use.
def change(column_name, type, options = {})
- @base.change_column(@table_name, column_name, type, options)
+ @base.change_column(name, column_name, type, options)
end
- # Sets a new default value for a column. See SchemaStatements#change_column_default
+ # Sets a new default value for a column.
#
# t.change_default(:qualification, 'new')
# t.change_default(:authorized, 1)
+ #
+ # See SchemaStatements#change_column_default
def change_default(column_name, default)
- @base.change_column_default(@table_name, column_name, default)
+ @base.change_column_default(name, column_name, default)
end
# Removes the column(s) from the table definition.
#
# t.remove(:qualification)
# t.remove(:qualification, :experience)
+ #
+ # See SchemaStatements#remove_columns
def remove(*column_names)
- @base.remove_columns(@table_name, *column_names)
+ @base.remove_columns(name, *column_names)
end
# Removes the given index from the table.
#
- # ====== 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
- # t.remove_index column: :branch_id
- # ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table
- # t.remove_index column: [:branch_id, :party_id]
- # ====== Remove the index named by_branch_party in the table_name table
- # t.remove_index name: :by_branch_party
+ # t.remove_index(:branch_id)
+ # t.remove_index(column: [:branch_id, :party_id])
+ # t.remove_index(name: :by_branch_party)
+ #
+ # See SchemaStatements#remove_index
def remove_index(options = {})
- @base.remove_index(@table_name, options)
+ @base.remove_index(name, options)
end
# Removes the timestamp columns (+created_at+ and +updated_at+) from the table.
#
# t.remove_timestamps
- def remove_timestamps
- @base.remove_timestamps(@table_name)
+ #
+ # See SchemaStatements#remove_timestamps
+ def remove_timestamps(options = {})
+ @base.remove_timestamps(name, options)
end
# Renames a column.
#
# t.rename(:description, :name)
+ #
+ # See SchemaStatements#rename_column
def rename(column_name, new_column_name)
- @base.rename_column(@table_name, column_name, new_column_name)
+ @base.rename_column(name, column_name, new_column_name)
end
- # 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.
+ # Adds a reference. Optionally adds a +type+ column, if
+ # <tt>:polymorphic</tt> option is provided.
#
# t.references(:user)
+ # t.references(:user, type: "string")
# t.belongs_to(:supplier, polymorphic: true)
+ # t.belongs_to(:supplier, foreign_key: true)
#
+ # See SchemaStatements#add_reference
def references(*args)
options = args.extract_options!
args.each do |ref_name|
- @base.add_reference(@table_name, ref_name, options)
+ @base.add_reference(name, ref_name, options)
end
end
alias :belongs_to :references
# Removes a reference. Optionally removes a +type+ column.
- # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable.
#
# t.remove_references(:user)
# t.remove_belongs_to(:supplier, polymorphic: true)
#
+ # See SchemaStatements#remove_reference
def remove_references(*args)
options = args.extract_options!
args.each do |ref_name|
- @base.remove_reference(@table_name, ref_name, options)
+ @base.remove_reference(name, ref_name, options)
end
end
alias :remove_belongs_to :remove_references
- # Adds a column or columns of a specified type
+ # Adds a column or columns of a specified type.
#
# t.string(:goat)
# t.string(:goat, :sheep)
- %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
- class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{column_type}(*args) # def string(*args)
- options = args.extract_options! # options = args.extract_options!
- args.each do |name| # column_names.each do |name|
- @base.add_column(@table_name, name, :#{column_type}, options) # @base.add_column(@table_name, name, :string, options)
- end # end
- end # end
- EOV
+ #
+ # See SchemaStatements#add_column
+ [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type|
+ define_method column_type do |*args|
+ options = args.extract_options!
+ args.each do |column_name|
+ @base.add_column(name, column_name, column_type, options)
+ end
+ end
+ end
+
+ def foreign_key(*args) # :nodoc:
+ @base.add_foreign_key(name, *args)
end
private
@@ -502,6 +668,5 @@ module ActiveRecord
@base.native_database_types
end
end
-
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index 9d6111b51e..42ea599a74 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -3,34 +3,38 @@ module ActiveRecord
# The goal of this module is to move Adapter specific column
# definitions to the Adapter instead of having it in the schema
# dumper itself. This code represents the normal case.
- # We can then redefine how certain data types may be handled in the schema dumper on the
- # Adapter level by over-writing this code inside the database spececific adapters
+ # We can then redefine how certain data types may be handled in the schema dumper on the
+ # Adapter level by over-writing this code inside the database specific adapters
module ColumnDumper
- def column_spec(column, types)
- spec = prepare_column_options(column, types)
- (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")}
+ def column_spec(column)
+ spec = prepare_column_options(column)
+ (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k}: ")}
spec
end
+ def column_spec_for_primary_key(column)
+ return if column.type == :integer
+ spec = { id: column.type.inspect }
+ spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) })
+ end
+
# This can be overridden on a Adapter level basis to support other
# extended datatypes (Example: Adding an array option in the
# PostgreSQLAdapter)
- def prepare_column_options(column, types)
+ def prepare_column_options(column)
spec = {}
spec[:name] = column.name.inspect
+ spec[:type] = column.type.to_s
+ spec[:null] = 'false' unless column.null
- # AR has an optimization which handles zero-scale decimals as integers. This
- # code ensures that the dumper still dumps the column as a decimal.
- spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type
- 'decimal'
- else
- column.type.to_s
- end
- spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal'
+ limit = column.limit || native_database_types[column.type][:limit]
+ spec[:limit] = limit.inspect if limit
spec[:precision] = column.precision.inspect if column.precision
spec[:scale] = column.scale.inspect if column.scale
- spec[:null] = 'false' unless column.null
- spec[:default] = default_string(column.default) if column.has_default?
+
+ default = schema_default(column) if column.has_default?
+ spec[:default] = default unless default.nil?
+
spec
end
@@ -41,16 +45,12 @@ module ActiveRecord
private
- def default_string(value)
- case value
- when BigDecimal
- value.to_s
- when Date, DateTime, Time
- "'#{value.to_s(:db)}'"
- else
- value.inspect
- end
+ def schema_default(column)
+ default = column.type_cast_from_database(column.default)
+ unless default.nil?
+ column.type_cast_for_schema(default)
end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index cdc8433185..34d60493ea 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module SchemaStatements
include ActiveRecord::Migration::JoinTable
- # Returns a Hash of mappings from the abstract data types to the native
+ # Returns a hash of mappings from the abstract data types to the native
# database types. See TableDefinition#column for details on the recognized
# abstract data types.
def native_database_types
@@ -20,6 +20,7 @@ module ActiveRecord
# Checks to see if the table +table_name+ exists on the database.
#
# table_exists?(:developers)
+ #
def table_exists?(table_name)
tables.include?(table_name.to_s)
end
@@ -29,25 +30,27 @@ module ActiveRecord
# Checks to see if an index exists on a table for a given index definition.
#
- # # Check an index exists
- # index_exists?(:suppliers, :company_id)
+ # # Check an index exists
+ # index_exists?(:suppliers, :company_id)
+ #
+ # # Check an index on multiple columns exists
+ # index_exists?(:suppliers, [:company_id, :company_type])
#
- # # Check an index on multiple columns exists
- # index_exists?(:suppliers, [:company_id, :company_type])
+ # # Check a unique index exists
+ # index_exists?(:suppliers, :company_id, unique: true)
#
- # # Check a unique index exists
- # index_exists?(:suppliers, :company_id, unique: true)
+ # # Check an index with a custom name exists
+ # index_exists?(:suppliers, :company_id, name: "idx_company_id")
#
- # # Check an index with a custom name exists
- # index_exists?(:suppliers, :company_id, name: "idx_company_id"
def index_exists?(table_name, column_name, options = {})
- column_names = Array(column_name)
- index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names)
- if options[:unique]
- indexes(table_name).any?{ |i| i.unique && i.name == index_name }
- else
- indexes(table_name).any?{ |i| i.name == index_name }
- end
+ column_names = Array(column_name).map(&:to_s)
+ index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names)
+ checks = []
+ checks << lambda { |i| i.name == index_name }
+ checks << lambda { |i| i.columns == column_names }
+ checks << lambda { |i| i.unique } if options[:unique]
+
+ indexes(table_name).any? { |i| checks.all? { |check| check[i] } }
end
# Returns an array of Column objects for the table specified by +table_name+.
@@ -56,19 +59,21 @@ module ActiveRecord
# Checks to see if a column exists in a given table.
#
- # # Check a column exists
- # column_exists?(:suppliers, :name)
+ # # Check a column exists
+ # column_exists?(:suppliers, :name)
#
- # # Check a column exists of a particular type
- # column_exists?(:suppliers, :name, :string)
+ # # Check a column exists of a particular type
+ # column_exists?(:suppliers, :name, :string)
+ #
+ # # Check a column exists with a specific definition
+ # column_exists?(:suppliers, :name, :string, limit: 100)
+ # column_exists?(:suppliers, :name, :string, default: 'default')
+ # column_exists?(:suppliers, :name, :string, null: false)
+ # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2)
#
- # # Check a column exists with a specific definition
- # column_exists?(:suppliers, :name, :string, limit: 100)
- # column_exists?(:suppliers, :name, :string, default: 'default')
- # column_exists?(:suppliers, :name, :string, null: false)
- # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2)
def column_exists?(table_name, column_name, type = nil, options = {})
- columns(table_name).any?{ |c| c.name == column_name.to_s &&
+ column_name = column_name.to_s
+ columns(table_name).any?{ |c| c.name == column_name &&
(!type || c.type == type) &&
(!options.key?(:limit) || c.limit == options[:limit]) &&
(!options.key?(:precision) || c.precision == options[:precision]) &&
@@ -84,27 +89,30 @@ module ActiveRecord
# form or the regular form, like this:
#
# === Block form
- # # create_table() passes a TableDefinition object to the block.
- # # This form will not only create the table, but also columns for the
- # # table.
#
- # create_table(:suppliers) do |t|
- # t.column :name, :string, limit: 60
- # # Other fields here
- # end
+ # # create_table() passes a TableDefinition object to the block.
+ # # This form will not only create the table, but also columns for the
+ # # table.
+ #
+ # create_table(:suppliers) do |t|
+ # t.column :name, :string, limit: 60
+ # # Other fields here
+ # end
#
# === Block form, with shorthand
- # # You can also use the column types as method calls, rather than calling the column method.
- # create_table(:suppliers) do |t|
- # t.string :name, limit: 60
- # # Other fields here
- # end
+ #
+ # # You can also use the column types as method calls, rather than calling the column method.
+ # create_table(:suppliers) do |t|
+ # t.string :name, limit: 60
+ # # Other fields here
+ # end
#
# === Regular form
- # # Creates a table called 'suppliers' with no columns.
- # create_table(:suppliers)
- # # Add a column to 'suppliers'.
- # add_column(:suppliers, :name, :string, {limit: 60})
+ #
+ # # Creates a table called 'suppliers' with no columns.
+ # create_table(:suppliers)
+ # # Add a column to 'suppliers'.
+ # add_column(:suppliers, :name, :string, {limit: 60})
#
# The +options+ hash can include the following keys:
# [<tt>:id</tt>]
@@ -114,9 +122,9 @@ module ActiveRecord
# The name of the primary key, if one is to be added automatically.
# Defaults to +id+. If <tt>:id</tt> is false this option is ignored.
#
- # Also note that this just sets the primary key in the table. You additionally
- # need to configure the primary key in the model via +self.primary_key=+.
- # Models do NOT auto-detect the primary key from their table definition.
+ # Note that Active Record models will automatically detect their
+ # primary key. This can be avoided by using +self.primary_key=+ on the model
+ # to define the key explicitly.
#
# [<tt>:options</tt>]
# Any extra options you want appended to the table definition.
@@ -124,40 +132,70 @@ module ActiveRecord
# Make a temporary table.
# [<tt>:force</tt>]
# Set to true to drop the table before creating it.
+ # Set to +:cascade+ to drop dependent objects as well.
# Defaults to false.
+ # [<tt>:as</tt>]
+ # SQL to use to generate the table. When this option is used, the block is
+ # ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options.
#
# ====== Add a backend specific option to the generated SQL (MySQL)
- # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
+ # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
# generates:
- # CREATE TABLE suppliers (
- # id int(11) DEFAULT NULL auto_increment PRIMARY KEY
- # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ #
+ # CREATE TABLE suppliers (
+ # id int(11) DEFAULT NULL auto_increment PRIMARY KEY
+ # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
# ====== Rename the primary key column
- # create_table(:objects, primary_key: 'guid') do |t|
- # t.column :name, :string, limit: 80
- # end
+ #
+ # create_table(:objects, primary_key: 'guid') do |t|
+ # t.column :name, :string, limit: 80
+ # end
+ #
# generates:
- # CREATE TABLE objects (
- # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
- # name varchar(80)
- # )
+ #
+ # CREATE TABLE objects (
+ # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ # name varchar(80)
+ # )
#
# ====== Do not add a primary key column
- # create_table(:categories_suppliers, id: false) do |t|
- # t.column :category_id, :integer
- # t.column :supplier_id, :integer
- # end
+ #
+ # create_table(:categories_suppliers, id: false) do |t|
+ # t.column :category_id, :integer
+ # t.column :supplier_id, :integer
+ # end
+ #
# generates:
- # CREATE TABLE categories_suppliers (
- # category_id int,
- # supplier_id int
- # )
+ #
+ # CREATE TABLE categories_suppliers (
+ # category_id int,
+ # supplier_id int
+ # )
+ #
+ # ====== Create a temporary table based on a query
+ #
+ # create_table(:long_query, temporary: true,
+ # as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id")
+ #
+ # generates:
+ #
+ # CREATE TEMPORARY TABLE long_query AS
+ # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
#
# See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {})
- td = table_definition
- td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
+ td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
+
+ if options[:id] != false && !options[:as]
+ pk = options.fetch(:primary_key) do
+ Base.get_primary_key table_name.to_s.singularize
+ end
+
+ td.primary_key pk, options.fetch(:id, :primary_key), options
+ end
yield td if block_given?
@@ -165,19 +203,26 @@ module ActiveRecord
drop_table(table_name, options)
end
- create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
- create_sql << "#{quote_table_name(table_name)} ("
- create_sql << td.to_sql
- create_sql << ") #{options[:options]}"
- execute create_sql
- td.indexes.each_pair { |c,o| add_index table_name, c, o }
+ result = execute schema_creation.accept td
+
+ unless supports_indexes_in_create?
+ td.indexes.each_pair do |column_name, index_options|
+ add_index(table_name, column_name, index_options)
+ end
+ end
+
+ td.foreign_keys.each_pair do |other_table_name, foreign_key_options|
+ add_foreign_key(table_name, other_table_name, foreign_key_options)
+ end
+
+ result
end
# Creates a new join table with the name created using the lexical order of the first two
# arguments. These arguments can be a String or a Symbol.
#
- # # Creates a table called 'assemblies_parts' with no id.
- # create_join_table(:assemblies, :parts)
+ # # Creates a table called 'assemblies_parts' with no id.
+ # create_join_table(:assemblies, :parts)
#
# You can pass a +options+ hash can include the following keys:
# [<tt>:table_name</tt>]
@@ -192,13 +237,25 @@ module ActiveRecord
# Set to true to drop the table before creating it.
# Defaults to false.
#
+ # Note that +create_join_table+ does not create any indices by default; you can use
+ # its block form to do so yourself:
+ #
+ # create_join_table :products, :categories do |t|
+ # t.index :product_id
+ # t.index :category_id
+ # end
+ #
# ====== Add a backend specific option to the generated SQL (MySQL)
- # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
+ # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
# generates:
- # CREATE TABLE assemblies_parts (
- # assembly_id int NOT NULL,
- # part_id int NOT NULL,
- # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ #
+ # CREATE TABLE assemblies_parts (
+ # assembly_id int NOT NULL,
+ # part_id int NOT NULL,
+ # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ #
def create_join_table(table_1, table_2, options = {})
join_table_name = find_join_table_name(table_1, table_2, options)
@@ -215,7 +272,7 @@ module ActiveRecord
end
# Drops the join table specified by the given arguments.
- # See create_join_table for details.
+ # See +create_join_table+ for details.
#
# Although this command ignores the block if one is given, it can be helpful
# to provide one in a migration's +change+ method so it can be reverted.
@@ -227,87 +284,100 @@ module ActiveRecord
# A block for changing columns in +table+.
#
- # # change_table() yields a Table instance
- # change_table(:suppliers) do |t|
- # t.column :name, :string, limit: 60
- # # Other column alterations here
- # end
+ # # change_table() yields a Table instance
+ # change_table(:suppliers) do |t|
+ # t.column :name, :string, limit: 60
+ # # Other column alterations here
+ # end
#
# The +options+ hash can include the following keys:
# [<tt>:bulk</tt>]
# Set this to true to make this a bulk alter query, such as
- # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
+ #
+ # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
#
# Defaults to false.
#
# ====== Add a column
- # change_table(:suppliers) do |t|
- # t.column :name, :string, limit: 60
- # end
+ #
+ # change_table(:suppliers) do |t|
+ # t.column :name, :string, limit: 60
+ # end
#
# ====== Add 2 integer columns
- # change_table(:suppliers) do |t|
- # t.integer :width, :height, null: false, default: 0
- # end
+ #
+ # change_table(:suppliers) do |t|
+ # t.integer :width, :height, null: false, default: 0
+ # end
#
# ====== Add created_at/updated_at columns
- # change_table(:suppliers) do |t|
- # t.timestamps
- # end
+ #
+ # change_table(:suppliers) do |t|
+ # t.timestamps
+ # end
#
# ====== Add a foreign key column
- # change_table(:suppliers) do |t|
- # t.references :company
- # end
#
- # Creates a <tt>company_id(integer)</tt> column
+ # change_table(:suppliers) do |t|
+ # t.references :company
+ # end
+ #
+ # Creates a <tt>company_id(integer)</tt> column.
#
# ====== Add a polymorphic foreign key column
+ #
# change_table(:suppliers) do |t|
# t.belongs_to :company, polymorphic: true
# end
#
- # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns
+ # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns.
#
# ====== Remove a column
+ #
# change_table(:suppliers) do |t|
# t.remove :company
# end
#
# ====== Remove several columns
+ #
# change_table(:suppliers) do |t|
# t.remove :company_id
# t.remove :width, :height
# end
#
# ====== Remove an index
+ #
# change_table(:suppliers) do |t|
# t.remove_index :company_id
# end
#
- # See also Table for details on
- # all of the various column transformation
+ # See also Table for details on all of the various column transformation.
def change_table(table_name, options = {})
if supports_bulk_alter? && options[:bulk]
recorder = ActiveRecord::Migration::CommandRecorder.new(self)
- yield Table.new(table_name, recorder)
+ yield update_table_definition(table_name, recorder)
bulk_change_table(table_name, recorder.commands)
else
- yield Table.new(table_name, self)
+ yield update_table_definition(table_name, self)
end
end
# Renames a table.
#
- # rename_table('octopuses', 'octopi')
+ # rename_table('octopuses', 'octopi')
+ #
def rename_table(table_name, new_name)
raise NotImplementedError, "rename_table is not implemented"
end
# Drops a table from the database.
#
- # Although this command ignores +options+ and the block if one is given, it can be helpful
- # to provide these in a migration's +change+ method so it can be reverted.
+ # [<tt>:force</tt>]
+ # Set to +:cascade+ to drop dependent objects as well.
+ # Defaults to false.
+ #
+ # Although this command ignores most +options+ and the block if one is given,
+ # it can be helpful to provide these in a migration's +change+ method so it can be reverted.
# In that case, +options+ and the block will be used by create_table.
def drop_table(table_name, options = {})
execute "DROP TABLE #{quote_table_name(table_name)}"
@@ -316,14 +386,15 @@ module ActiveRecord
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
- execute(add_column_sql)
+ at = create_alter_table table_name
+ at.add_column(column_name, type, options)
+ execute schema_creation.accept at
end
# Removes the given columns from the table definition.
#
- # remove_columns(:suppliers, :qualification, :experience)
+ # remove_columns(:suppliers, :qualification, :experience)
+ #
def remove_columns(table_name, *column_names)
raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.empty?
column_names.each do |column_name|
@@ -333,7 +404,7 @@ module ActiveRecord
# Removes the column from the table definition.
#
- # remove_column(:suppliers, :qualification)
+ # remove_column(:suppliers, :qualification)
#
# The +type+ and +options+ parameters will be ignored if present. It can be helpful
# to provide these in a migration's +change+ method so it can be reverted.
@@ -345,24 +416,50 @@ module ActiveRecord
# Changes the column's definition according to the new options.
# See TableDefinition#column for details of the options you can use.
#
- # change_column(:suppliers, :name, :string, limit: 80)
- # change_column(:accounts, :description, :text)
+ # change_column(:suppliers, :name, :string, limit: 80)
+ # change_column(:accounts, :description, :text)
+ #
def change_column(table_name, column_name, type, options = {})
raise NotImplementedError, "change_column is not implemented"
end
- # Sets a new default value for a column.
+ # Sets a new default value for a column:
+ #
+ # change_column_default(:suppliers, :qualification, 'new')
+ # change_column_default(:accounts, :authorized, 1)
+ #
+ # Setting the default to +nil+ effectively drops the default:
+ #
+ # change_column_default(:users, :email, nil)
#
- # change_column_default(:suppliers, :qualification, 'new')
- # change_column_default(:accounts, :authorized, 1)
- # change_column_default(:users, :email, nil)
def change_column_default(table_name, column_name, default)
raise NotImplementedError, "change_column_default is not implemented"
end
+ # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag
+ # indicates whether the value can be +NULL+. For example
+ #
+ # change_column_null(:users, :nickname, false)
+ #
+ # says nicknames cannot be +NULL+ (adds the constraint), whereas
+ #
+ # change_column_null(:users, :nickname, true)
+ #
+ # allows them to be +NULL+ (drops the constraint).
+ #
+ # The method accepts an optional fourth argument to replace existing
+ # +NULL+s with some other value. Use that one when enabling the
+ # constraint if needed, since otherwise those rows would not be valid.
+ #
+ # Please note the fourth argument does not set a column's default.
+ def change_column_null(table_name, column_name, null, default = nil)
+ raise NotImplementedError, "change_column_null is not implemented"
+ end
+
# Renames a column.
#
- # rename_column(:suppliers, :description, :name)
+ # rename_column(:suppliers, :description, :name)
+ #
def rename_column(table_name, column_name, new_column_name)
raise NotImplementedError, "rename_column is not implemented"
end
@@ -374,60 +471,106 @@ module ActiveRecord
# you pass <tt>:name</tt> as an option.
#
# ====== Creating a simple index
- # add_index(:suppliers, :name)
- # generates
- # CREATE INDEX suppliers_name_index ON suppliers(name)
+ #
+ # add_index(:suppliers, :name)
+ #
+ # generates:
+ #
+ # CREATE INDEX suppliers_name_index ON suppliers(name)
#
# ====== Creating a unique index
- # add_index(:accounts, [:branch_id, :party_id], unique: true)
- # generates
- # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
+ #
+ # add_index(:accounts, [:branch_id, :party_id], unique: true)
+ #
+ # generates:
+ #
+ # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
#
# ====== Creating a named index
- # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party')
- # generates
+ #
+ # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party')
+ #
+ # generates:
+ #
# CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id)
#
# ====== Creating an index with specific key length
- # add_index(:accounts, :name, name: 'by_name', length: 10)
- # generates
- # CREATE INDEX by_name ON accounts(name(10))
#
- # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15})
- # generates
- # CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
+ # add_index(:accounts, :name, name: 'by_name', length: 10)
+ #
+ # generates:
#
- # Note: SQLite doesn't support index length
+ # CREATE INDEX by_name ON accounts(name(10))
+ #
+ # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15})
+ #
+ # generates:
+ #
+ # CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
+ #
+ # Note: SQLite doesn't support index length.
#
# ====== Creating an index with a sort order (desc or asc, asc is the default)
- # add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc})
- # generates
- # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
#
- # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it)
+ # add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc})
+ #
+ # generates:
+ #
+ # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
+ #
+ # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it).
#
# ====== Creating a partial index
- # add_index(:accounts, [:branch_id, :party_id], unique: true, where: "active")
- # generates
- # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active
#
- # Note: only supported by PostgreSQL
+ # add_index(:accounts, [:branch_id, :party_id], unique: true, where: "active")
+ #
+ # generates:
+ #
+ # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active
+ #
+ # ====== Creating an index with a specific method
+ #
+ # add_index(:developers, :name, using: 'btree')
#
+ # generates:
+ #
+ # CREATE INDEX index_developers_on_name ON developers USING btree (name) -- PostgreSQL
+ # CREATE INDEX index_developers_on_name USING btree ON developers (name) -- MySQL
+ #
+ # Note: only supported by PostgreSQL and MySQL
+ #
+ # ====== Creating an index with a specific type
+ #
+ # add_index(:developers, :name, type: :fulltext)
+ #
+ # generates:
+ #
+ # CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL
+ #
+ # Note: only supported by MySQL. Supported: <tt>:fulltext</tt> and <tt>:spatial</tt> on MyISAM tables.
def add_index(table_name, column_name, options = {})
index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}"
end
- # Remove the given index from the table.
+ # Removes the given index from the table.
+ #
+ # Removes the +index_accounts_on_column+ in the +accounts+ table.
#
- # Remove the index_accounts_on_column in the accounts table.
# remove_index :accounts, :column
- # Remove the index named index_accounts_on_branch_id in the accounts table.
+ #
+ # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table.
+ #
# remove_index :accounts, column: :branch_id
- # Remove the index named index_accounts_on_branch_id_and_party_id in the accounts table.
+ #
+ # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table.
+ #
# remove_index :accounts, column: [:branch_id, :party_id]
- # Remove the index named by_branch_party in the accounts table.
+ #
+ # Removes the index named +by_branch_party+ in the +accounts+ table.
+ #
# remove_index :accounts, name: :by_branch_party
+ #
def remove_index(table_name, options = {})
remove_index!(table_name, index_name_for_remove(table_name, options))
end
@@ -436,16 +579,20 @@ module ActiveRecord
execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
end
- # Rename an index.
+ # Renames an index.
+ #
+ # Rename the +index_people_on_last_name+ index to +index_users_on_last_name+:
#
- # Rename the index_people_on_last_name index to index_users_on_last_name
# rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name'
+ #
def rename_index(table_name, old_name, new_name)
+ validate_index_length!(table_name, new_name)
+
# this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
return unless old_index_def
- remove_index(table_name, :name => old_name)
- add_index(table_name, old_index_def.columns, :name => new_name, :unique => old_index_def.unique)
+ add_index(table_name, old_index_def.columns, name: new_name, unique: old_index_def.unique)
+ remove_index(table_name, name: old_name)
end
def index_name(table_name, options) #:nodoc:
@@ -462,7 +609,7 @@ module ActiveRecord
end
end
- # Verify the existence of an index with a given name.
+ # Verifies the existence of an index with a given name.
#
# The default argument is returned if the underlying implementation does not define the indexes method,
# as there's no way to determine the correct answer in that case.
@@ -473,23 +620,32 @@ module ActiveRecord
end
# Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
+ # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify
+ # a different type.
# <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable.
#
- # ====== Create a user_id column
- # add_reference(:products, :user)
+ # ====== Create a user_id integer column
+ #
+ # add_reference(:products, :user)
+ #
+ # ====== Create a user_id string column
+ #
+ # add_reference(:products, :user, type: :string)
#
# ====== Create a supplier_id and supplier_type columns
- # add_belongs_to(:products, :supplier, polymorphic: true)
#
- # ====== Create a supplier_id, supplier_type columns and appropriate index
- # add_reference(:products, :supplier, polymorphic: true, index: true)
+ # add_belongs_to(:products, :supplier, polymorphic: true)
+ #
+ # ====== Create supplier_id, supplier_type columns and appropriate index
+ #
+ # add_reference(:products, :supplier, polymorphic: true, index: true)
+ #
+ # ====== Create a supplier_id column and appropriate foreign key
#
- def add_reference(table_name, ref_name, options = {})
- polymorphic = options.delete(:polymorphic)
- index_options = options.delete(:index)
- add_column(table_name, "#{ref_name}_id", :integer, options)
- add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
- add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options
+ # add_reference(:products, :supplier, foreign_key: true)
+ #
+ def add_reference(table_name, *args)
+ ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self))
end
alias :add_belongs_to :add_reference
@@ -497,10 +653,12 @@ module ActiveRecord
# <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable.
#
# ====== Remove the reference
- # remove_reference(:products, :user, index: true)
+ #
+ # remove_reference(:products, :user, index: true)
#
# ====== Remove polymorphic reference
- # remove_reference(:products, :supplier, polymorphic: true)
+ #
+ # remove_reference(:products, :supplier, polymorphic: true)
#
def remove_reference(table_name, ref_name, options = {})
remove_column(table_name, "#{ref_name}_id")
@@ -508,9 +666,113 @@ module ActiveRecord
end
alias :remove_belongs_to :remove_reference
- # Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the
- # entire structure of the database.
- def structure_dump
+ # Returns an array of foreign keys for the given table.
+ # The foreign keys are represented as +ForeignKeyDefinition+ objects.
+ def foreign_keys(table_name)
+ raise NotImplementedError, "foreign_keys is not implemented"
+ end
+
+ # Adds a new foreign key. +from_table+ is the table with the key column,
+ # +to_table+ contains the referenced primary key.
+ #
+ # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>.
+ # +identifier+ is a 10 character long random string. A custom name can be specified with
+ # the <tt>:name</tt> option.
+ #
+ # ====== Creating a simple foreign key
+ #
+ # add_foreign_key :articles, :authors
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
+ #
+ # ====== Creating a foreign key on a specific column
+ #
+ # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id")
+ #
+ # ====== Creating a cascading foreign key
+ #
+ # add_foreign_key :articles, :authors, on_delete: :cascade
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
+ #
+ # The +options+ hash can include the following keys:
+ # [<tt>:column</tt>]
+ # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt>
+ # [<tt>:primary_key</tt>]
+ # The primary key column name on +to_table+. Defaults to +id+.
+ # [<tt>:name</tt>]
+ # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>.
+ # [<tt>:on_delete</tt>]
+ # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ # [<tt>:on_update</tt>]
+ # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ def add_foreign_key(from_table, to_table, options = {})
+ return unless supports_foreign_keys?
+
+ options[:column] ||= foreign_key_column_for(to_table)
+
+ options = {
+ column: options[:column],
+ primary_key: options[:primary_key],
+ name: foreign_key_name(from_table, options),
+ on_delete: options[:on_delete],
+ on_update: options[:on_update]
+ }
+ at = create_alter_table from_table
+ at.add_foreign_key to_table, options
+
+ execute schema_creation.accept(at)
+ end
+
+ # Removes the given foreign key from the table.
+ #
+ # Removes the foreign key on +accounts.branch_id+.
+ #
+ # remove_foreign_key :accounts, :branches
+ #
+ # Removes the foreign key on +accounts.owner_id+.
+ #
+ # remove_foreign_key :accounts, column: :owner_id
+ #
+ # Removes the foreign key named +special_fk_name+ on the +accounts+ table.
+ #
+ # remove_foreign_key :accounts, name: :special_fk_name
+ #
+ def remove_foreign_key(from_table, options_or_to_table = {})
+ return unless supports_foreign_keys?
+
+ if options_or_to_table.is_a?(Hash)
+ options = options_or_to_table
+ else
+ options = { column: foreign_key_column_for(options_or_to_table) }
+ end
+
+ fk_name_to_delete = options.fetch(:name) do
+ fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column].to_s }
+
+ if fk_to_delete
+ fk_to_delete.name
+ else
+ raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'"
+ end
+ end
+
+ at = create_alter_table from_table
+ at.drop_foreign_key fk_name_to_delete
+
+ execute schema_creation.accept(at)
+ end
+
+ def foreign_key_column_for(table_name) # :nodoc:
+ "#{table_name.to_s.singularize}_id"
end
def dump_schema_information #:nodoc:
@@ -532,7 +794,7 @@ module ActiveRecord
version = version.to_i
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
- migrated = select_values("SELECT version FROM #{sm_table}").map { |v| v.to_i }
+ migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
paths = migrations_paths.map {|p| "#{p}/[0-9]*_*.rb" }
versions = Dir[*paths].map do |filename|
filename.split('/').last.split('_').first.to_i
@@ -576,42 +838,78 @@ module ActiveRecord
column_type_sql
else
- type
- end
- end
-
- def add_column_options!(sql, options) #:nodoc:
- sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
- # must explicitly check for :null to allow change_column to work on migrations
- if options[:null] == false
- sql << " NOT NULL"
+ type.to_s
end
end
- # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
- # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax.
+ # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT.
+ # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they
+ # require the order columns appear in the SELECT.
#
- # distinct("posts.id", "posts.created_at desc")
- def distinct(columns, order_by)
- "DISTINCT #{columns}"
+ # columns_for_distinct("posts.id", ["posts.created_at desc"])
+ def columns_for_distinct(columns, orders) #:nodoc:
+ columns
end
- # Adds timestamps (created_at and updated_at) columns to the named table.
+ include TimestampDefaultDeprecation
+ # Adds timestamps (+created_at+ and +updated_at+) columns to +table_name+.
+ # Additional options (like <tt>null: false</tt>) are forwarded to #add_column.
#
- # add_timestamps(:suppliers)
- def add_timestamps(table_name)
- add_column table_name, :created_at, :datetime
- add_column table_name, :updated_at, :datetime
+ # add_timestamps(:suppliers, null: false)
+ #
+ def add_timestamps(table_name, options = {})
+ emit_warning_if_null_unspecified(options)
+ add_column table_name, :created_at, :datetime, options
+ add_column table_name, :updated_at, :datetime, options
end
- # Removes the timestamp columns (created_at and updated_at) from the table definition.
+ # Removes the timestamp columns (+created_at+ and +updated_at+) from the table definition.
#
# remove_timestamps(:suppliers)
- def remove_timestamps(table_name)
+ #
+ def remove_timestamps(table_name, options = {})
remove_column table_name, :updated_at
remove_column table_name, :created_at
end
+ def update_table_definition(table_name, base) #:nodoc:
+ Table.new(table_name, base)
+ end
+
+ def add_index_options(table_name, column_name, options = {}) #:nodoc:
+ column_names = Array(column_name)
+ index_name = index_name(table_name, column: column_names)
+
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
+
+ index_type = options[:unique] ? "UNIQUE" : ""
+ index_type = options[:type].to_s if options.key?(:type)
+ index_name = options[:name].to_s if options.key?(:name)
+ max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
+
+ if options.key?(:algorithm)
+ algorithm = index_algorithms.fetch(options[:algorithm]) {
+ raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
+ }
+ end
+
+ using = "USING #{options[:using]}" if options[:using].present?
+
+ if supports_partial_index?
+ index_options = options[:where] ? " WHERE #{options[:where]}" : ""
+ end
+
+ if index_name.length > max_index_length
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
+ end
+ if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
+ end
+ index_columns = quoted_columns_for_index(column_names, options).join(", ")
+
+ [index_name, index_type, index_columns, index_options, algorithm, using]
+ end
+
protected
def add_index_sort_order(option_strings, column_names, options = {})
if options.is_a?(Hash) && order = options[:order]
@@ -626,7 +924,7 @@ module ActiveRecord
return option_strings
end
- # Overridden by the mysql adapter for supporting index lengths
+ # Overridden by the MySQL adapter for supporting index lengths
def quoted_columns_for_index(column_names, options = {})
option_strings = Hash[column_names.map {|name| [name, '']}]
@@ -642,61 +940,65 @@ module ActiveRecord
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
- def add_index_options(table_name, column_name, options = {})
- column_names = Array(column_name)
- index_name = index_name(table_name, column: column_names)
-
- if Hash === options # legacy support, since this param was a string
- options.assert_valid_keys(:unique, :order, :name, :where, :length)
-
- index_type = options[:unique] ? "UNIQUE" : ""
- index_name = options[:name].to_s if options.key?(:name)
+ def index_name_for_remove(table_name, options = {})
+ index_name = index_name(table_name, options)
- if supports_partial_index?
- index_options = options[:where] ? " WHERE #{options[:where]}" : ""
- end
- else
- if options
- message = "Passing a string as third argument of `add_index` is deprecated and will" +
- " be removed in Rails 4.1." +
- " Use add_index(#{table_name.inspect}, #{column_name.inspect}, unique: true) instead"
+ unless index_name_exists?(table_name, index_name, true)
+ if options.is_a?(Hash) && options.has_key?(:name)
+ options_without_column = options.dup
+ options_without_column.delete :column
+ index_name_without_column = index_name(table_name, options_without_column)
- ActiveSupport::Deprecation.warn message
+ return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false)
end
- index_type = options
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
end
- if index_name.length > index_name_length
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
- end
- if index_name_exists?(table_name, index_name, false)
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
+ index_name
+ end
+
+ def rename_table_indexes(table_name, new_name)
+ indexes(new_name).each do |index|
+ generated_index_name = index_name(table_name, column: index.columns)
+ if generated_index_name == index.name
+ rename_index new_name, generated_index_name, index_name(new_name, column: index.columns)
+ end
end
- index_columns = quoted_columns_for_index(column_names, options).join(", ")
+ end
- [index_name, index_type, index_columns, index_options]
+ def rename_column_indexes(table_name, column_name, new_column_name)
+ column_name, new_column_name = column_name.to_s, new_column_name.to_s
+ indexes(table_name).each do |index|
+ next unless index.columns.include?(new_column_name)
+ old_columns = index.columns.dup
+ old_columns[old_columns.index(new_column_name)] = column_name
+ generated_index_name = index_name(table_name, column: old_columns)
+ if generated_index_name == index.name
+ rename_index table_name, generated_index_name, index_name(table_name, column: index.columns)
+ end
+ end
end
- def index_name_for_remove(table_name, options = {})
- index_name = index_name(table_name, options)
+ private
+ def create_table_definition(name, temporary = false, options = nil, as = nil)
+ TableDefinition.new native_database_types, name, temporary, options, as
+ end
- unless index_name_exists?(table_name, index_name, true)
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
- end
+ def create_alter_table(name)
+ AlterTable.new create_table_definition(name)
+ end
- index_name
+ def foreign_key_name(table_name, options) # :nodoc:
+ options.fetch(:name) do
+ "fk_rails_#{SecureRandom.hex(5)}"
end
+ end
- def columns_for_remove(table_name, *column_names)
- ActiveSupport::Deprecation.warn("columns_for_remove is deprecated and will be removed in the future")
- raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.blank?
- column_names.map {|column_name| quote_column_name(column_name) }
+ def validate_index_length!(table_name, new_name)
+ if new_name.length > allowed_index_name_length
+ raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters"
end
-
- private
- def table_definition
- TableDefinition.new(self)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 4cca94e40b..fd666c8c39 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -1,128 +1,143 @@
module ActiveRecord
module ConnectionAdapters
- class Transaction #:nodoc:
- attr_reader :connection
+ class TransactionState
+ attr_reader :parent
- def initialize(connection)
- @connection = connection
- end
- end
+ VALID_STATES = Set.new([:committed, :rolledback, nil])
- class ClosedTransaction < Transaction #:nodoc:
- def number
- 0
+ def initialize(state = nil)
+ @state = state
+ @parent = nil
end
- def begin(options = {})
- RealTransaction.new(connection, self, options)
+ def finalized?
+ @state
end
- def closed?
- true
+ def committed?
+ @state == :committed
end
- def open?
- false
+ def rolledback?
+ @state == :rolledback
end
- def joinable?
- false
+ def completed?
+ committed? || rolledback?
end
- # This is a noop when there are no open transactions
- def add_record(record)
+ def set_state(state)
+ if !VALID_STATES.include?(state)
+ raise ArgumentError, "Invalid transaction state: #{state}"
+ end
+ @state = state
end
end
- class OpenTransaction < Transaction #:nodoc:
- attr_reader :parent, :records
- attr_writer :joinable
-
- def initialize(connection, parent, options = {})
- super connection
-
- @parent = parent
- @records = []
- @finishing = false
- @joinable = options.fetch(:joinable, true)
- end
+ class NullTransaction #:nodoc:
+ def initialize; end
+ def closed?; true; end
+ def open?; false; end
+ def joinable?; false; end
+ def add_record(record); end
+ end
- # This state is necesarry so that we correctly handle stuff that might
- # happen in a commit/rollback. But it's kinda distasteful. Maybe we can
- # find a better way to structure it in the future.
- def finishing?
- @finishing
- end
+ class Transaction #:nodoc:
- def joinable?
- @joinable && !finishing?
- end
+ attr_reader :connection, :state, :records, :savepoint_name
+ attr_writer :joinable
- def number
- if finishing?
- parent.number
- else
- parent.number + 1
- end
+ def initialize(connection, options)
+ @connection = connection
+ @state = TransactionState.new
+ @records = []
+ @joinable = options.fetch(:joinable, true)
end
- def begin(options = {})
- if finishing?
- parent.begin
+ def add_record(record)
+ if record.has_transactional_callbacks?
+ records << record
else
- SavepointTransaction.new(connection, self, options)
+ record.set_transaction_state(@state)
end
end
def rollback
- @finishing = true
- perform_rollback
- parent
- end
-
- def commit
- @finishing = true
- perform_commit
- parent
- end
-
- def add_record(record)
- records << record
+ @state.set_state(:rolledback)
end
def rollback_records
- records.uniq.each do |record|
+ ite = records.uniq
+ while record = ite.shift
begin
- record.rolledback!(parent.closed?)
+ record.rolledback! full_rollback?
rescue => e
+ raise if ActiveRecord::Base.raise_in_transactional_callbacks
record.logger.error(e) if record.respond_to?(:logger) && record.logger
end
end
+ ensure
+ ite.each do |i|
+ i.rolledback!(full_rollback?, false)
+ end
+ end
+
+ def commit
+ @state.set_state(:committed)
end
def commit_records
- records.uniq.each do |record|
+ ite = records.uniq
+ while record = ite.shift
begin
record.committed!
rescue => e
+ raise if ActiveRecord::Base.raise_in_transactional_callbacks
record.logger.error(e) if record.respond_to?(:logger) && record.logger
end
end
+ ensure
+ ite.each do |i|
+ i.committed!(false)
+ end
end
- def closed?
- false
+ def full_rollback?; true; end
+ def joinable?; @joinable; end
+ def closed?; false; end
+ def open?; !closed?; end
+ end
+
+ class SavepointTransaction < Transaction
+
+ def initialize(connection, savepoint_name, options)
+ super(connection, options)
+ if options[:isolation]
+ raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
+ end
+ connection.create_savepoint(@savepoint_name = savepoint_name)
end
- def open?
- true
+ def rollback
+ connection.rollback_to_savepoint(savepoint_name)
+ super
+ rollback_records
end
- end
- class RealTransaction < OpenTransaction #:nodoc:
- def initialize(connection, parent, options = {})
+ def commit
+ connection.release_savepoint(savepoint_name)
super
+ parent = connection.transaction_manager.current_transaction
+ records.each { |r| parent.add_record(r) }
+ end
+
+ def full_rollback?; false; end
+ end
+
+ class RealTransaction < Transaction
+ def initialize(connection, options)
+ super
if options[:isolation]
connection.begin_isolated_db_transaction(options[:isolation])
else
@@ -130,36 +145,75 @@ module ActiveRecord
end
end
- def perform_rollback
+ def rollback
connection.rollback_db_transaction
+ super
rollback_records
end
- def perform_commit
+ def commit
connection.commit_db_transaction
+ super
commit_records
end
end
- class SavepointTransaction < OpenTransaction #:nodoc:
- def initialize(connection, parent, options = {})
- if options[:isolation]
- raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
- end
+ class TransactionManager #:nodoc:
+ def initialize(connection)
+ @stack = []
+ @connection = connection
+ end
- super
- connection.create_savepoint
+ def begin_transaction(options = {})
+ transaction =
+ if @stack.empty?
+ RealTransaction.new(@connection, options)
+ else
+ SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options)
+ end
+ @stack.push(transaction)
+ transaction
+ end
+
+ def commit_transaction
+ @stack.pop.commit
+ end
+
+ def rollback_transaction
+ @stack.pop.rollback
+ end
+
+ def within_new_transaction(options = {})
+ transaction = begin_transaction options
+ yield
+ rescue Exception => error
+ rollback_transaction if transaction
+ raise
+ ensure
+ unless error
+ if Thread.current.status == 'aborting'
+ rollback_transaction
+ else
+ begin
+ commit_transaction
+ rescue Exception
+ transaction.rollback unless transaction.state.completed?
+ raise
+ end
+ end
+ end
end
- def perform_rollback
- connection.rollback_to_savepoint
- rollback_records
+ def open_transactions
+ @stack.size
end
- def perform_commit
- connection.release_savepoint
- records.each { |r| parent.add_record(r) }
+ def current_transaction
+ @stack.last || NULL_TRANSACTION
end
+
+ private
+ NULL_TRANSACTION = NullTransaction.new
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 8517ce5fc5..fa24d9b43f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,11 +1,14 @@
require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
+require 'active_record/type'
require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/schema_cache'
require 'active_record/connection_adapters/abstract/schema_dumper'
+require 'active_record/connection_adapters/abstract/schema_creation'
require 'monitor'
-require 'active_support/deprecation'
+require 'arel/collectors/bind'
+require 'arel/collectors/sql_string'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -17,8 +20,12 @@ module ActiveRecord
autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do
autoload :IndexDefinition
autoload :ColumnDefinition
+ autoload :ChangeColumnDefinition
+ autoload :ForeignKeyDefinition
autoload :TableDefinition
autoload :Table
+ autoload :AlterTable
+ autoload :TimestampDefaultDeprecation
end
autoload_at 'active_record/connection_adapters/abstract/connection_pool' do
@@ -33,12 +40,15 @@ module ActiveRecord
autoload :Quoting
autoload :ConnectionPool
autoload :QueryCache
+ autoload :Savepoints
end
autoload_at 'active_record/connection_adapters/abstract/transaction' do
- autoload :ClosedTransaction
+ autoload :TransactionManager
+ autoload :NullTransaction
autoload :RealTransaction
autoload :SavepointTransaction
+ autoload :TransactionState
end
# Active Record supports multiple database systems. AbstractAdapter and
@@ -55,6 +65,7 @@ module ActiveRecord
# Most of the methods in the adapter are useful during migrations. Most
# notably, the instance methods provided by SchemaStatement are very useful.
class AbstractAdapter
+ ADAPTER_NAME = 'Abstract'.freeze
include Quoting, DatabaseStatements, SchemaStatements
include DatabaseLimits
include QueryCache
@@ -62,32 +73,77 @@ module ActiveRecord
include MonitorMixin
include ColumnDumper
+ SIMPLE_INT = /\A\d+\z/
+
define_callbacks :checkout, :checkin
attr_accessor :visitor, :pool
- attr_reader :schema_cache, :last_use, :in_use, :logger
- alias :in_use? :in_use
+ attr_reader :schema_cache, :owner, :logger
+ alias :in_use? :owner
+
+ def self.type_cast_config_to_integer(config)
+ if config =~ SIMPLE_INT
+ config.to_i
+ else
+ config
+ end
+ end
+
+ def self.type_cast_config_to_boolean(config)
+ if config == "false"
+ false
+ else
+ config
+ end
+ end
+
+ attr_reader :prepared_statements
def initialize(connection, logger = nil, pool = nil) #:nodoc:
super()
@connection = connection
- @in_use = false
+ @owner = nil
@instrumenter = ActiveSupport::Notifications.instrumenter
- @last_use = false
@logger = logger
@pool = pool
- @query_cache = Hash.new { |h,sql| h[sql] = {} }
- @query_cache_enabled = false
@schema_cache = SchemaCache.new self
@visitor = nil
+ @prepared_statements = false
+ end
+
+ class BindCollector < Arel::Collectors::Bind
+ def compile(bvs, conn)
+ super(bvs.map { |bv| conn.quote(*bv.reverse) })
+ end
+ end
+
+ class SQLString < Arel::Collectors::SQLString
+ def compile(bvs, conn)
+ super(bvs)
+ end
+ end
+
+ def collector
+ if prepared_statements
+ SQLString.new
+ else
+ BindCollector.new
+ end
+ end
+
+ def valid_type?(type)
+ true
+ end
+
+ def schema_creation
+ SchemaCreation.new self
end
def lease
synchronize do
- unless in_use
- @in_use = true
- @last_use = Time.now
+ unless in_use?
+ @owner = Thread.current
end
end
end
@@ -98,37 +154,35 @@ module ActiveRecord
end
def expire
- @in_use = false
+ @owner = nil
+ end
+
+ def unprepared_statement
+ old_prepared_statements, @prepared_statements = @prepared_statements, false
+ yield
+ ensure
+ @prepared_statements = old_prepared_statements
end
# Returns the human-readable name of the adapter. Use mixed case - one
# can always use downcase if needed.
def adapter_name
- 'Abstract'
+ self.class::ADAPTER_NAME
end
- # Does this adapter support migrations? Backend specific, as the
- # abstract adapter always returns +false+.
+ # Does this adapter support migrations?
def supports_migrations?
false
end
# Can this adapter determine the primary key for tables not attached
- # to an Active Record class, such as join tables? Backend specific, as
- # the abstract adapter always returns +false+.
+ # to an Active Record class, such as join tables?
def supports_primary_key?
false
end
- # Does this adapter support using DISTINCT within COUNT? This is +true+
- # for all adapters except sqlite.
- def supports_count_distinct?
- true
- end
-
# Does this adapter support DDL rollbacks in transactions? That is, would
- # CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
- # SQL Server, and others support this. MySQL and others do not.
+ # CREATE TABLE or ALTER TABLE get rolled back by a transaction?
def supports_ddl_transactions?
false
end
@@ -137,8 +191,7 @@ module ActiveRecord
false
end
- # Does this adapter support savepoints? PostgreSQL and MySQL do,
- # SQLite < 3.6.8 does not.
+ # Does this adapter support savepoints?
def supports_savepoints?
false
end
@@ -146,7 +199,6 @@ module ActiveRecord
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
# is called before each insert to set the record's primary key.
- # This is false for all adapters but Firebird.
def prefetch_primary_key?(table_name = nil)
false
end
@@ -161,8 +213,7 @@ module ActiveRecord
false
end
- # Does this adapter support explain? As of this writing sqlite3,
- # mysql2, and postgresql are the only ones that do.
+ # Does this adapter support explain?
def supports_explain?
false
end
@@ -172,12 +223,61 @@ module ActiveRecord
false
end
+ # Does this adapter support database extensions?
+ def supports_extensions?
+ false
+ end
+
+ # Does this adapter support creating indexes in the same statement as
+ # creating the table?
+ def supports_indexes_in_create?
+ false
+ end
+
+ # Does this adapter support creating foreign key constraints?
+ def supports_foreign_keys?
+ false
+ end
+
+ # Does this adapter support views?
+ def supports_views?
+ false
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def disable_extension(name)
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def enable_extension(name)
+ end
+
+ # A list of extensions, to be filled in by adapters that support them.
+ def extensions
+ []
+ end
+
+ # A list of index algorithms, to be filled by adapters that support them.
+ def index_algorithms
+ {}
+ end
+
# QUOTING ==================================================
- # Returns a bind substitution value given a +column+ and list of current
- # +binds+
- def substitute_at(column, index)
- Arel::Nodes::BindParam.new '?'
+ # Quote date/time values for use in SQL input. Includes microseconds
+ # if the value is a Time responding to usec.
+ def quoted_date(value) #:nodoc:
+ if value.acts_like?(:time) && value.respond_to?(:usec)
+ "#{super}.#{sprintf("%06d", value.usec)}"
+ else
+ super
+ end
+ end
+
+ # Returns a bind substitution value given a bind +column+
+ # NOTE: The column param is currently being used by the sqlserver-adapter
+ def substitute_at(column, _unused = 0)
+ Arel::Nodes::BindParam.new
end
# REFERENTIAL INTEGRITY ====================================
@@ -228,7 +328,6 @@ module ActiveRecord
end
# Returns true if its required to reload the connection between requests for development mode.
- # This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
def requires_reloading?
false
end
@@ -250,70 +349,151 @@ module ActiveRecord
@connection
end
- def open_transactions
- @transaction.number
+ def create_savepoint(name = nil)
end
- def increment_open_transactions
- ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect"
+ def release_savepoint(name = nil)
end
- def decrement_open_transactions
- ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect"
+ def case_sensitive_modifier(node, table_attribute)
+ node
end
- def transaction_joinable=(joinable)
- message = "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead."
- ActiveSupport::Deprecation.warn message
- @transaction.joinable = joinable
+ def case_sensitive_comparison(table, attribute, column, value)
+ table_attr = table[attribute]
+ value = case_sensitive_modifier(value, table_attr) unless value.nil?
+ table_attr.eq(value)
end
- def create_savepoint
+ def case_insensitive_comparison(table, attribute, column, value)
+ table[attribute].lower.eq(table.lower(value))
end
- def rollback_to_savepoint
+ def current_savepoint_name
+ current_transaction.savepoint_name
end
- def release_savepoint
+ # Check the connection back in to the connection pool
+ def close
+ pool.checkin self
end
- def case_sensitive_modifier(node)
- node
+ def type_map # :nodoc:
+ @type_map ||= Type::TypeMap.new.tap do |mapping|
+ initialize_type_map(mapping)
+ end
end
- def case_insensitive_comparison(table, attribute, column, value)
- table[attribute].lower.eq(table.lower(value))
+ def new_column(name, default, cast_type, sql_type = nil, null = true)
+ Column.new(name, default, cast_type, sql_type, null)
end
- def current_savepoint_name
- "active_record_#{open_transactions}"
+ def lookup_cast_type(sql_type) # :nodoc:
+ type_map.lookup(sql_type)
end
- # Check the connection back in to the connection pool
- def close
- pool.checkin self
+ def column_name_for_operation(operation, node) # :nodoc:
+ node.to_sql
end
protected
- def log(sql, name = "SQL", binds = [])
- @instrumenter.instrument(
- "sql.active_record",
- :sql => sql,
- :name => name,
- :connection_id => object_id,
- :binds => binds) { yield }
- rescue => e
- message = "#{e.class.name}: #{e.message}: #{sql}"
+ def initialize_type_map(m) # :nodoc:
+ register_class_with_limit m, %r(boolean)i, Type::Boolean
+ register_class_with_limit m, %r(char)i, Type::String
+ register_class_with_limit m, %r(binary)i, Type::Binary
+ register_class_with_limit m, %r(text)i, Type::Text
+ register_class_with_limit m, %r(date)i, Type::Date
+ register_class_with_limit m, %r(time)i, Type::Time
+ register_class_with_limit m, %r(datetime)i, Type::DateTime
+ register_class_with_limit m, %r(float)i, Type::Float
+ register_class_with_limit m, %r(int)i, Type::Integer
+
+ m.alias_type %r(blob)i, 'binary'
+ m.alias_type %r(clob)i, 'text'
+ m.alias_type %r(timestamp)i, 'datetime'
+ m.alias_type %r(numeric)i, 'decimal'
+ m.alias_type %r(number)i, 'decimal'
+ m.alias_type %r(double)i, 'float'
+
+ m.register_type(%r(decimal)i) do |sql_type|
+ scale = extract_scale(sql_type)
+ precision = extract_precision(sql_type)
+
+ if scale == 0
+ # FIXME: Remove this class as well
+ Type::DecimalWithoutScale.new(precision: precision)
+ else
+ Type::Decimal.new(precision: precision, scale: scale)
+ end
+ end
+ end
+
+ def reload_type_map # :nodoc:
+ type_map.clear
+ initialize_type_map(type_map)
+ end
+
+ def register_class_with_limit(mapping, key, klass) # :nodoc:
+ mapping.register_type(key) do |*args|
+ limit = extract_limit(args.last)
+ klass.new(limit: limit)
+ end
+ end
+
+ def extract_scale(sql_type) # :nodoc:
+ case sql_type
+ when /\((\d+)\)/ then 0
+ when /\((\d+)(,(\d+))\)/ then $3.to_i
+ end
+ end
+
+ def extract_precision(sql_type) # :nodoc:
+ $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/
+ end
+
+ def extract_limit(sql_type) # :nodoc:
+ $1.to_i if sql_type =~ /\((.*)\)/
+ end
+
+ def translate_exception_class(e, sql)
+ begin
+ message = "#{e.class.name}: #{e.message}: #{sql}"
+ rescue Encoding::CompatibilityError
+ message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}"
+ end
+
@logger.error message if @logger
exception = translate_exception(e, message)
exception.set_backtrace e.backtrace
- raise exception
+ exception
+ end
+
+ def log(sql, name = "SQL", binds = [], statement_name = nil)
+ @instrumenter.instrument(
+ "sql.active_record",
+ :sql => sql,
+ :name => name,
+ :connection_id => object_id,
+ :statement_name => statement_name,
+ :binds => binds) { yield }
+ rescue => e
+ raise translate_exception_class(e, sql)
end
def translate_exception(exception, message)
# override in derived class
- ActiveRecord::StatementInvalid.new(message)
+ ActiveRecord::StatementInvalid.new(message, exception)
+ end
+
+ def without_prepared_statement?(binds)
+ !prepared_statements || binds.empty?
+ end
+
+ def column_for(table_name, column_name) # :nodoc:
+ column_name = column_name.to_s
+ columns(table_name).detect { |c| c.name == column_name } ||
+ raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}")
end
end
end
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 52b0b3fe79..e9a3c26c32 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,88 +1,119 @@
require 'arel/visitors/bind_visitor'
+require 'active_support/core_ext/string/strip'
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
+ include Savepoints
+
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ def primary_key(name, type = :primary_key, options = {})
+ options[:auto_increment] ||= type == :bigint
+ super
+ end
+ end
+
+ class SchemaCreation < AbstractAdapter::SchemaCreation
+ def visit_AddColumn(o)
+ add_column_position!(super, column_options(o))
+ end
+
+ private
+
+ def visit_DropForeignKey(name)
+ "DROP FOREIGN KEY #{name}"
+ end
+
+ def visit_TableDefinition(o)
+ name = o.name
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
+
+ statements = o.columns.map { |c| accept c }
+ statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })
+
+ create_sql << "(#{statements.join(', ')}) " if statements.present?
+ create_sql << "#{o.options}"
+ create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
+ create_sql
+ end
+
+ def visit_ChangeColumnDefinition(o)
+ change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}"
+ add_column_position!(change_column_sql, column_options(o.column))
+ end
+
+ def add_column_position!(sql, options)
+ if options[:first]
+ sql << " FIRST"
+ elsif options[:after]
+ sql << " AFTER #{quote_column_name(options[:after])}"
+ end
+ sql
+ end
+
+ def index_in_create(table_name, column_name, options)
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options)
+ "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}"
+ end
+ end
+
+ def schema_creation
+ SchemaCreation.new self
+ end
+
+ def column_spec_for_primary_key(column)
+ spec = {}
+ if column.extra == 'auto_increment'
+ return unless column.limit == 8
+ spec[:id] = ':bigint'
+ else
+ spec[:id] = column.type.inspect
+ spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) })
+ end
+ spec
+ end
+
class Column < ConnectionAdapters::Column # :nodoc:
- attr_reader :collation, :strict
+ attr_reader :collation, :strict, :extra
- def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false)
+ def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
@strict = strict
@collation = collation
-
- super(name, default, sql_type, null)
+ @extra = extra
+ super(name, default, cast_type, sql_type, null)
+ assert_valid_default(default)
+ extract_default
end
- def extract_default(default)
+ def extract_default
if blob_or_text_column?
- if default.blank?
- null || strict ? nil : ''
- else
- raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
- end
- elsif missing_default_forged_as_empty_string?(default)
- nil
- else
- super
+ @default = null || strict ? nil : ''
+ elsif missing_default_forged_as_empty_string?(@default)
+ @default = nil
end
end
def has_default?
- return false if blob_or_text_column? #mysql forbids defaults on blob and text columns
+ return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns
super
end
-
+
def blob_or_text_column?
sql_type =~ /blob/i || type == :text
end
- # Must return the relevant concrete adapter
- def adapter
- raise NotImplementedError
- end
-
def case_sensitive?
collation && !collation.match(/_ci$/)
end
- private
-
- def simplified_type(field_type)
- return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
-
- case field_type
- when /enum/i, /set/i then :string
- when /year/i then :integer
- when /bit/i then :binary
- else
- super
- end
+ def ==(other)
+ super &&
+ collation == other.collation &&
+ strict == other.strict &&
+ extra == other.extra
end
- def extract_limit(sql_type)
- case sql_type
- when /blob|text/i
- case sql_type
- when /tiny/i
- 255
- when /medium/i
- 16777215
- when /long/i
- 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
- else
- super # we could return 65535 here, but we leave it undecorated by default
- end
- when /^bigint/i; 8
- when /^int/i; 4
- when /^mediumint/i; 3
- when /^smallint/i; 2
- when /^tinyint/i; 1
- when /^enum\((.+)\)/i
- $1.split(',').map{|enum| enum.strip.length - 2}.max
- else
- super
- end
- end
+ private
# MySQL misreports NOT NULL column default when none is given.
# We can't detect this for columns which may have a legitimate ''
@@ -94,6 +125,16 @@ module ActiveRecord
def missing_default_forged_as_empty_string?(default)
type != :string && !null && default == ''
end
+
+ def assert_valid_default(default)
+ if blob_or_text_column? && default.present?
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
+ end
+ end
+
+ def attributes_for_hash
+ super + [collation, strict, extra]
+ end
end
##
@@ -116,23 +157,21 @@ module ActiveRecord
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
- :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
+ :primary_key => "int(11) auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 4 },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
:boolean => { :name => "tinyint", :limit => 1 }
}
- class BindSubstitution < Arel::Visitors::MySQL # :nodoc:
- include Arel::Visitors::BindVisitor
- end
+ INDEX_TYPES = [:fulltext, :spatial]
+ INDEX_USINGS = [:btree, :hash]
# FIXME: Make the first parameter more similar for the two adapters
def initialize(connection, logger, connection_options, config)
@@ -140,17 +179,15 @@ module ActiveRecord
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
- if config.fetch(:prepared_statements) { true }
- @visitor = Arel::Visitors::MySQL.new self
+ @visitor = Arel::Visitors::MySQL.new self
+
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
else
- @visitor = BindSubstitution.new self
+ @prepared_statements = false
end
end
- def adapter_name #:nodoc:
- self.class::ADAPTER_NAME
- end
-
# Returns true, since this connection adapter supports migrations.
def supports_migrations?
true
@@ -160,11 +197,6 @@ module ActiveRecord
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
- end
-
def supports_bulk_alter? #:nodoc:
true
end
@@ -183,10 +215,26 @@ module ActiveRecord
version[0] >= 5
end
+ def supports_indexes_in_create?
+ true
+ end
+
+ def supports_foreign_keys?
+ true
+ end
+
+ def supports_views?
+ version[0] >= 5
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
+ def index_algorithms
+ { default: 'ALGORITHM = DEFAULT', copy: 'ALGORITHM = COPY', inplace: 'ALGORITHM = INPLACE' }
+ end
+
# HELPER METHODS ===========================================
# The two drivers have slightly different ways of yielding hashes of results, so
@@ -195,12 +243,11 @@ module ActiveRecord
raise NotImplementedError
end
- # Overridden by the adapters to instantiate their specific Column type.
- def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation)
+ def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc:
+ Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra)
end
- # Must return the Mysql error number from the exception, if the exception has an
+ # Must return the MySQL error number from the exception, if the exception has an
# error number.
def error_number(exception) # :nodoc:
raise NotImplementedError
@@ -208,12 +255,9 @@ module ActiveRecord
# QUOTING ==================================================
- def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
- "x'#{s}'"
- elsif value.kind_of?(BigDecimal)
- value.to_s("F")
+ def _quote(value) # :nodoc:
+ if value.is_a?(Type::Binary::Data)
+ "x'#{value.hex}'"
else
super
end
@@ -231,13 +275,21 @@ module ActiveRecord
QUOTED_TRUE
end
+ def unquoted_true
+ 1
+ end
+
def quoted_false
QUOTED_FALSE
end
+ def unquoted_false
+ 0
+ end
+
# REFERENTIAL INTEGRITY ====================================
- def disable_referential_integrity(&block) #:nodoc:
+ def disable_referential_integrity #:nodoc:
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
@@ -248,21 +300,18 @@ module ActiveRecord
end
end
+ #--
# DATABASE STATEMENTS ======================================
+ #++
+
+ def clear_cache!
+ super
+ reload_type_map
+ end
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
- if name == :skip_logging
- @connection.query(sql)
- else
- log(sql, name) { @connection.query(sql) }
- end
- rescue ActiveRecord::StatementInvalid => exception
- if exception.message.split(":").first =~ /Packets out of order/
- raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
- else
- raise
- end
+ log(sql, name) { @connection.query(sql) }
end
# MysqlAdapter has to free a result after using it, so we use this method to write
@@ -279,39 +328,19 @@ module ActiveRecord
def begin_db_transaction
execute "BEGIN"
- rescue
- # Transactions aren't supported
end
def begin_isolated_db_transaction(isolation)
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
begin_db_transaction
- rescue
- # Transactions aren't supported
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
- rescue
- # Transactions aren't supported
end
- def rollback_db_transaction #:nodoc:
+ def exec_rollback_db_transaction #:nodoc:
execute "ROLLBACK"
- rescue
- # Transactions aren't supported
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
@@ -332,25 +361,13 @@ module ActiveRecord
# SCHEMA STATEMENTS ========================================
- def structure_dump #:nodoc:
- if supports_views?
- sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
- else
- sql = "SHOW TABLES"
- end
-
- select_all(sql, 'SCHEMA').map { |table|
- table.delete('Table_type')
- sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
- exec_query(sql, 'SCHEMA').first['Create Table'] + ";\n\n"
- }.join
- end
-
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
def recreate_database(name, options = {})
drop_database(name)
- create_database(name, options)
+ sql = create_database(name, options)
+ reconnect!
+ sql
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
@@ -396,12 +413,16 @@ module ActiveRecord
sql << "LIKE #{quote(like)}" if like
execute_and_free(sql, 'SCHEMA') do |result|
- result.collect { |field| field.first }
+ result.collect(&:first)
end
end
+ def truncate(table_name, name = nil)
+ execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
+ end
+
def table_exists?(name)
- return false unless name
+ return false unless name.present?
return true if tables(nil, nil, name).any?
name = name.to_s
@@ -424,7 +445,11 @@ module ActiveRecord
if current_index != row[:Key_name]
next if row[:Key_name] == 'PRIMARY' # skip the primary key
current_index = row[:Key_name]
- indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [])
+
+ mysql_index_type = row[:Index_type].downcase.to_sym
+ index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil
+ index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil
+ indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using)
end
indexes.last.columns << row[:Column_name]
@@ -440,7 +465,10 @@ module ActiveRecord
sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field|
- new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation])
+ field_name = set_field_encoding(field[:Field])
+ sql_type = field[:Type]
+ cast_type = lookup_cast_type(sql_type)
+ new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra])
end
end
end
@@ -450,7 +478,7 @@ module ActiveRecord
end
def bulk_change_table(table_name, operations) #:nodoc:
- sqls = operations.map do |command, args|
+ sqls = operations.flat_map do |command, args|
table, arguments = args.shift, args
method = :"#{command}_sql"
@@ -459,7 +487,7 @@ module ActiveRecord
else
raise "Unknown method called : #{method}(#{arguments.inspect})"
end
- end.flatten.join(", ")
+ end.join(", ")
execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
end
@@ -470,13 +498,24 @@ module ActiveRecord
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
+ rename_table_indexes(table_name, new_name)
end
- def add_column(table_name, column_name, type, options = {})
- execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
+ def drop_table(table_name, options = {})
+ execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
- def change_column_default(table_name, column_name, default)
+ def rename_index(table_name, old_name, new_name)
+ if supports_rename_index?
+ validate_index_length!(table_name, new_name)
+
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
+ else
+ super
+ end
+ end
+
+ def change_column_default(table_name, column_name, default) #:nodoc:
column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
end
@@ -497,6 +536,40 @@ module ActiveRecord
def rename_column(table_name, column_name, new_column_name) #:nodoc:
execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
+ rename_column_indexes(table_name, column_name, new_column_name)
+ end
+
+ def add_index(table_name, column_name, options = {}) #:nodoc:
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}"
+ end
+
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT fk.referenced_table_name as 'to_table'
+ ,fk.referenced_column_name as 'primary_key'
+ ,fk.column_name as 'column'
+ ,fk.constraint_name as 'name'
+ FROM information_schema.key_column_usage fk
+ WHERE fk.referenced_column_name is not null
+ AND fk.table_schema = '#{@config[:database]}'
+ AND fk.table_name = '#{table_name}'
+ SQL
+
+ create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE")
+ options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE")
+
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
end
# Maps logical Rails types to MySQL-specific data types.
@@ -526,19 +599,18 @@ module ActiveRecord
when 0x1000000..0xffffffff; 'longtext'
else raise(ActiveRecordError, "No text type has character length #{limit}")
end
+ when 'datetime'
+ return super unless precision
+
+ case precision
+ when 0..6; "datetime(#{precision})"
+ else raise(ActiveRecordError, "No datetime type has precision of #{precision}. The allowed range of precision is from 0 to 6.")
+ end
else
super
end
end
- def add_column_position!(sql, options)
- if options[:first]
- sql << " FIRST"
- elsif options[:after]
- sql << " AFTER #{quote_column_name(options[:after])}"
- end
- end
-
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA')
@@ -564,10 +636,19 @@ module ActiveRecord
pk_and_sequence && pk_and_sequence.first
end
- def case_sensitive_modifier(node)
+ def case_sensitive_modifier(node, table_attribute)
+ node = Arel::Nodes.build_quoted node, table_attribute
Arel::Nodes::Bin.new(node)
end
+ def case_sensitive_comparison(table, attribute, column, value)
+ if column.case_sensitive?
+ table[attribute].eq(value)
+ else
+ super
+ end
+ end
+
def case_insensitive_comparison(table, attribute, column, value)
if column.case_sensitive?
super
@@ -576,16 +657,65 @@ module ActiveRecord
end
end
- def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
- where_sql
+ def strict_mode?
+ self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
end
- def strict_mode?
- @config.fetch(:strict, true)
+ def valid_type?(type)
+ !native_database_types[type].nil?
end
protected
+ def initialize_type_map(m) # :nodoc:
+ super
+
+ register_class_with_limit m, %r(char)i, MysqlString
+
+ m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1)
+ m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1)
+ m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1)
+ m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1)
+ m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1)
+ m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1)
+ m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1)
+ m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1)
+ m.register_type %r(^float)i, Type::Float.new(limit: 24)
+ m.register_type %r(^double)i, Type::Float.new(limit: 53)
+
+ register_integer_type m, %r(^bigint)i, limit: 8
+ register_integer_type m, %r(^int)i, limit: 4
+ register_integer_type m, %r(^mediumint)i, limit: 3
+ register_integer_type m, %r(^smallint)i, limit: 2
+ register_integer_type m, %r(^tinyint)i, limit: 1
+
+ m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans
+ m.alias_type %r(set)i, 'varchar'
+ m.alias_type %r(year)i, 'integer'
+ m.alias_type %r(bit)i, 'binary'
+
+ m.register_type(%r(datetime)i) do |sql_type|
+ precision = extract_precision(sql_type)
+ MysqlDateTime.new(precision: precision)
+ end
+
+ m.register_type(%r(enum)i) do |sql_type|
+ limit = sql_type[/^enum\((.+)\)/i, 1]
+ .split(',').map{|enum| enum.strip.length - 2}.max
+ MysqlString.new(limit: limit)
+ end
+ end
+
+ def register_integer_type(mapping, key, options) # :nodoc:
+ mapping.register_type(key) do |sql_type|
+ if /unsigned/i =~ sql_type
+ Type::UnsignedInteger.new(options)
+ else
+ Type::Integer.new(options)
+ end
+ end
+ end
+
# MySQL is too stupid to create a temporary table for use subquery, so we have
# to give it some prompting in the form of a subsubquery. Ugh!
def subquery_for(key, select)
@@ -634,10 +764,9 @@ module ActiveRecord
end
def add_column_sql(table_name, column_name, type, options = {})
- add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
- add_column_position!(add_column_sql, options)
- add_column_sql
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(column_name, type, options)
+ schema_creation.visit_AddColumn cd
end
def change_column_sql(table_name, column_name, type, options = {})
@@ -651,26 +780,23 @@ module ActiveRecord
options[:null] = column.null
end
- change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(change_column_sql, options)
- add_column_position!(change_column_sql, options)
- change_column_sql
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(column.name, type, options)
+ schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
end
def rename_column_sql(table_name, column_name, new_column_name)
- options = {}
-
- if column = columns(table_name).find { |c| c.name == column_name.to_s }
- options[:default] = column.default
- options[:null] = column.null
- else
- raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
- end
+ column = column_for(table_name, column_name)
+ options = {
+ default: column.default,
+ null: column.null,
+ auto_increment: column.extra == "auto_increment"
+ }
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
- rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
- add_column_options!(rename_column_sql, options)
- rename_column_sql
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(new_column_name, current_type, options)
+ schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
end
def remove_column_sql(table_name, column_name, type = nil, options = {})
@@ -691,65 +817,118 @@ module ActiveRecord
"DROP INDEX #{index_name}"
end
- def add_timestamps_sql(table_name)
- [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
+ def add_timestamps_sql(table_name, options = {})
+ [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)]
end
- def remove_timestamps_sql(table_name)
+ def remove_timestamps_sql(table_name, options = {})
[remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
end
private
- def supports_views?
- version[0] >= 5
+ def version
+ @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i)
end
- def column_for(table_name, column_name)
- unless column = columns(table_name).find { |c| c.name == column_name.to_s }
- raise "No such column: #{table_name}.#{column_name}"
- end
- column
+ def mariadb?
+ full_version =~ /mariadb/i
+ end
+
+ def supports_rename_index?
+ mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6
end
def configure_connection
- variables = @config[:variables] || {}
+ variables = @config.fetch(:variables, {}).stringify_keys
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
- variables[:sql_auto_is_null] = 0
+ variables['sql_auto_is_null'] = 0
# Increase timeout so the server doesn't disconnect us.
wait_timeout = @config[:wait_timeout]
wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
- variables[:wait_timeout] = wait_timeout
+ variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout)
# Make MySQL reject illegal values rather than truncating or blanking them, see
# http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables
# If the user has provided another value for sql_mode, don't replace it.
- if strict_mode? && !variables.has_key?(:sql_mode)
- variables[:sql_mode] = 'STRICT_ALL_TABLES'
+ unless variables.has_key?('sql_mode')
+ variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
end
# NAMES does not have an equals sign, see
# http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430
# (trailing comma because variable_assignments will always have content)
- encoding = "NAMES #{@config[:encoding]}, " if @config[:encoding]
+ if @config[:encoding]
+ encoding = "NAMES #{@config[:encoding]}"
+ encoding << " COLLATE #{@config[:collation]}" if @config[:collation]
+ encoding << ", "
+ end
# Gather up all of the SET variables...
variable_assignments = variables.map do |k, v|
if v == ':default' || v == :default
- "@@SESSION.#{k.to_s} = DEFAULT" # Sets the value to the global or compile default
+ "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default
elsif !v.nil?
- "@@SESSION.#{k.to_s} = #{quote(v)}"
+ "@@SESSION.#{k} = #{quote(v)}"
end
# or else nil; compact to clear nils out
end.compact.join(', ')
# ...and send them all in one query
- execute("SET #{encoding} #{variable_assignments}", :skip_logging)
+ @connection.query "SET #{encoding} #{variable_assignments}"
end
+ def extract_foreign_key_action(structure, name, action) # :nodoc:
+ if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/
+ case $1
+ when 'CASCADE'; :cascade
+ when 'SET NULL'; :nullify
+ end
+ end
+ end
+
+ def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
+ TableDefinition.new(native_database_types, name, temporary, options, as)
+ end
+
+ class MysqlDateTime < Type::DateTime # :nodoc:
+ def type_cast_for_database(value)
+ if value.acts_like?(:time) && value.respond_to?(:usec)
+ result = super.to_s(:db)
+ case precision
+ when 1..6
+ "#{result}.#{sprintf("%0#{precision}d", value.usec / 10 ** (6 - precision))}"
+ else
+ result
+ end
+ else
+ super
+ end
+ end
+ end
+
+ class MysqlString < Type::String # :nodoc:
+ def type_cast_for_database(value)
+ case value
+ when true then "1"
+ when false then "0"
+ else super
+ end
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then "1"
+ when false then "0"
+ else super
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index fd36a5b075..af307b57a4 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -13,124 +13,36 @@ module ActiveRecord
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
end
- attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
- attr_accessor :primary, :coder
+ attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function
- alias :encoded? :coder
+ delegate :type, :precision, :scale, :limit, :klass, :accessor,
+ :text?, :number?, :binary?, :changed?,
+ :type_cast_from_user, :type_cast_from_database, :type_cast_for_database,
+ :type_cast_for_schema,
+ to: :cast_type
# Instantiates a new column in the table.
#
# +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
+ # +cast_type+ is the object used for type casting and type information.
# +sql_type+ is used to extract the column's length, if necessary. For example +60+ in
# <tt>company_name varchar(60)</tt>.
# It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type = nil, null = true)
- @name = name
- @sql_type = sql_type
- @null = null
- @limit = extract_limit(sql_type)
- @precision = extract_precision(sql_type)
- @scale = extract_scale(sql_type)
- @type = simplified_type(sql_type)
- @default = extract_default(default)
- @primary = nil
- @coder = nil
- end
-
- # Returns +true+ if the column is either of type string or text.
- def text?
- type == :string || type == :text
- end
-
- # Returns +true+ if the column is either of type integer, float or decimal.
- def number?
- type == :integer || type == :float || type == :decimal
+ def initialize(name, default, cast_type, sql_type = nil, null = true)
+ @name = name
+ @cast_type = cast_type
+ @sql_type = sql_type
+ @null = null
+ @default = default
+ @default_function = nil
end
def has_default?
!default.nil?
end
- # Returns the Ruby class that corresponds to the abstract data type.
- def klass
- case type
- when :integer then Fixnum
- when :float then Float
- when :decimal then BigDecimal
- when :datetime, :timestamp, :time then Time
- when :date then Date
- when :text, :string, :binary then String
- when :boolean then Object
- end
- end
-
- def binary?
- type == :binary
- end
-
- # Casts a Ruby value to something appropriate for writing to the database.
- def type_cast_for_write(value)
- return value unless number?
-
- if value == false
- 0
- elsif value == true
- 1
- elsif value.is_a?(String) && value.blank?
- nil
- else
- value
- end
- end
-
- # Casts value (which is a String) to an appropriate instance.
- def type_cast(value)
- return nil if value.nil?
- return coder.load(value) if encoded?
-
- klass = self.class
-
- case type
- when :string, :text then value
- when :integer then klass.value_to_integer(value)
- when :float then value.to_f
- when :decimal then klass.value_to_decimal(value)
- when :datetime, :timestamp then klass.string_to_time(value)
- when :time then klass.string_to_dummy_time(value)
- when :date then klass.value_to_date(value)
- when :binary then klass.binary_to_string(value)
- when :boolean then klass.value_to_boolean(value)
- else value
- end
- end
-
- def type_cast_code(var_name)
- message = "Column#type_cast_code is deprecated in favor of using Column#type_cast only, " \
- "and it is going to be removed in future Rails versions."
- ActiveSupport::Deprecation.warn message
-
- klass = self.class.name
-
- case type
- when :string, :text then var_name
- when :integer then "#{klass}.value_to_integer(#{var_name})"
- when :float then "#{var_name}.to_f"
- when :decimal then "#{klass}.value_to_decimal(#{var_name})"
- when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
- when :time then "#{klass}.string_to_dummy_time(#{var_name})"
- when :date then "#{klass}.value_to_date(#{var_name})"
- when :binary then "#{klass}.binary_to_string(#{var_name})"
- when :boolean then "#{klass}.value_to_boolean(#{var_name})"
- when :hstore then "#{klass}.string_to_hstore(#{var_name})"
- when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})"
- when :json then "#{klass}.string_to_json(#{var_name})"
- when :intrange then "#{klass}.string_to_intrange(#{var_name})"
- else var_name
- end
- end
-
# Returns the human name of the column name.
#
# ===== Examples
@@ -139,179 +51,31 @@ module ActiveRecord
Base.human_attribute_name(@name)
end
- def extract_default(default)
- type_cast(default)
+ def with_type(type)
+ dup.tap do |clone|
+ clone.instance_variable_set('@cast_type', type)
+ end
end
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- self.class.string_to_binary(value)
+ def ==(other)
+ other.name == name &&
+ other.default == default &&
+ other.cast_type == cast_type &&
+ other.sql_type == sql_type &&
+ other.null == null &&
+ other.default_function == default_function
end
+ alias :eql? :==
- class << self
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- value
- end
-
- # Used to convert from BLOBs to Strings
- def binary_to_string(value)
- value
- end
-
- def value_to_date(value)
- if value.is_a?(String)
- return nil if value.blank?
- fast_string_to_date(value) || fallback_string_to_date(value)
- elsif value.respond_to?(:to_date)
- value.to_date
- else
- value
- end
- end
-
- def string_to_time(string)
- return string unless string.is_a?(String)
- return nil if string.blank?
-
- fast_string_to_time(string) || fallback_string_to_time(string)
- end
-
- def string_to_dummy_time(string)
- return string unless string.is_a?(String)
- return nil if string.blank?
-
- dummy_time_string = "2000-01-01 #{string}"
-
- fast_string_to_time(dummy_time_string) || begin
- time_hash = Date._parse(dummy_time_string)
- return nil if time_hash[:hour].nil?
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
- end
- end
-
- # convert something to a boolean
- def value_to_boolean(value)
- if value.is_a?(String) && value.blank?
- nil
- else
- TRUE_VALUES.include?(value)
- end
- end
-
- # Used to convert values to integer.
- # handle the case when an integer column is used to store boolean values
- def value_to_integer(value)
- case value
- when TrueClass, FalseClass
- value ? 1 : 0
- else
- value.to_i
- end
- end
-
- # convert something to a BigDecimal
- def value_to_decimal(value)
- # Using .class is faster than .is_a? and
- # subclasses of BigDecimal will be handled
- # in the else clause
- if value.class == BigDecimal
- value
- elsif value.respond_to?(:to_d)
- value.to_d
- else
- value.to_s.to_d
- end
- end
-
- protected
- # '0.123456' -> 123456
- # '1.123456' -> 123456
- def microseconds(time)
- time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
- end
-
- def new_date(year, mon, mday)
- if year && year != 0
- Date.new(year, mon, mday) rescue nil
- end
- end
-
- def new_time(year, mon, mday, hour, min, sec, microsec)
- # Treat 0000-00-00 00:00:00 as nil.
- return nil if year.nil? || (year == 0 && mon == 0 && mday == 0)
-
- Time.send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
- end
-
- def fast_string_to_date(string)
- if string =~ Format::ISO_DATE
- new_date $1.to_i, $2.to_i, $3.to_i
- end
- end
-
- # Doesn't handle time zones.
- def fast_string_to_time(string)
- if string =~ Format::ISO_DATETIME
- microsec = ($7.to_r * 1_000_000).to_i
- new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
- end
- end
-
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- end
-
- def fallback_string_to_time(string)
- time_hash = Date._parse(string)
- time_hash[:sec_fraction] = microseconds(time_hash)
-
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
- end
+ def hash
+ attributes_for_hash.hash
end
private
- def extract_limit(sql_type)
- $1.to_i if sql_type =~ /\((.*)\)/
- end
-
- def extract_precision(sql_type)
- $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
- end
- def extract_scale(sql_type)
- case sql_type
- when /^(numeric|decimal|number)\((\d+)\)/i then 0
- when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
- end
- end
-
- def simplified_type(field_type)
- case field_type
- when /int/i
- :integer
- when /float|double/i
- :float
- when /decimal|numeric|number/i
- extract_scale(field_type) == 0 ? :integer : :decimal
- when /datetime/i
- :datetime
- when /timestamp/i
- :timestamp
- when /time/i
- :time
- when /date/i
- :date
- when /clob/i, /text/i
- :text
- when /blob/i, /binary/i
- :binary
- when /char/i, /string/i
- :string
- when /boolean/i
- :boolean
- end
- end
+ def attributes_for_hash
+ [self.class, name, default, cast_type, sql_type, null, default_function]
+ end
end
end
# :startdoc:
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 09250d3c01..e54e3199ff 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -1,4 +1,5 @@
require 'uri'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
module ConnectionAdapters
@@ -13,74 +14,261 @@ module ActiveRecord
@config = original.config.dup
end
+ # Expands a connection string into a hash.
+ class ConnectionUrlResolver # :nodoc:
+
+ # == Example
+ #
+ # url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000"
+ # ConnectionUrlResolver.new(url).to_hash
+ # # => {
+ # "adapter" => "postgresql",
+ # "host" => "localhost",
+ # "port" => 9000,
+ # "database" => "foo_test",
+ # "username" => "foo",
+ # "password" => "bar",
+ # "pool" => "5",
+ # "timeout" => "3000"
+ # }
+ def initialize(url)
+ raise "Database URL cannot be empty" if url.blank?
+ @uri = uri_parser.parse(url)
+ @adapter = @uri.scheme.tr('-', '_')
+ @adapter = "postgresql" if @adapter == "postgres"
+
+ if @uri.opaque
+ @uri.opaque, @query = @uri.opaque.split('?', 2)
+ else
+ @query = @uri.query
+ end
+ end
+
+ # Converts the given URL to a full connection hash.
+ def to_hash
+ config = raw_config.reject { |_,value| value.blank? }
+ config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String }
+ config
+ end
+
+ private
+
+ def uri
+ @uri
+ end
+
+ def uri_parser
+ @uri_parser ||= URI::Parser.new
+ end
+
+ # Converts the query parameters of the URI into a hash.
+ #
+ # "localhost?pool=5&reaping_frequency=2"
+ # # => { "pool" => "5", "reaping_frequency" => "2" }
+ #
+ # returns empty hash if no query present.
+ #
+ # "localhost"
+ # # => {}
+ def query_hash
+ Hash[(@query || '').split("&").map { |pair| pair.split("=") }]
+ end
+
+ def raw_config
+ if uri.opaque
+ query_hash.merge({
+ "adapter" => @adapter,
+ "database" => uri.opaque })
+ else
+ query_hash.merge({
+ "adapter" => @adapter,
+ "username" => uri.user,
+ "password" => uri.password,
+ "port" => uri.port,
+ "database" => database_from_path,
+ "host" => uri.hostname })
+ end
+ end
+
+ # Returns name of the database.
+ def database_from_path
+ if @adapter == 'sqlite3'
+ # 'sqlite3:/foo' is absolute, because that makes sense. The
+ # corresponding relative version, 'sqlite3:foo', is handled
+ # elsewhere, as an "opaque".
+
+ uri.path
+ else
+ # Only SQLite uses a filename as the "database" name; for
+ # anything else, a leading slash would be silly.
+
+ uri.path.sub(%r{^/}, "")
+ end
+ end
+ end
+
##
- # Builds a ConnectionSpecification from user input
+ # Builds a ConnectionSpecification from user input.
class Resolver # :nodoc:
- attr_reader :config, :klass, :configurations
+ attr_reader :configurations
- def initialize(config, configurations)
- @config = config
+ # Accepts a hash two layers deep, keys on the first layer represent
+ # environments such as "production". Keys must be strings.
+ def initialize(configurations)
@configurations = configurations
end
- def spec
- case config
- when nil
- raise AdapterNotSpecified unless defined?(Rails.env)
- resolve_string_connection Rails.env
- when Symbol, String
- resolve_string_connection config.to_s
- when Hash
- resolve_hash_connection config
+ # Returns a hash with database connection information.
+ #
+ # == Examples
+ #
+ # Full hash Configuration.
+ #
+ # configurations = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
+ # Resolver.new(configurations).resolve(:production)
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3"}
+ #
+ # Initialized with URL configuration strings.
+ #
+ # configurations = { "production" => "postgresql://localhost/foo" }
+ # Resolver.new(configurations).resolve(:production)
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve(config)
+ if config
+ resolve_connection config
+ elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call
+ resolve_symbol_connection env.to_sym
+ else
+ raise AdapterNotSpecified
end
end
- private
- def resolve_string_connection(spec) # :nodoc:
- hash = configurations.fetch(spec) do |k|
- connection_url_to_hash(k)
+ # Expands each key in @configurations hash into fully resolved hash
+ def resolve_all
+ config = configurations.dup
+ config.each do |key, value|
+ config[key] = resolve(value) if value
end
-
- raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
-
- resolve_hash_connection hash
+ config
end
- def resolve_hash_connection(spec) # :nodoc:
- spec = spec.symbolize_keys
+ # Returns an instance of ConnectionSpecification for a given adapter.
+ # Accepts a hash one layer deep that contains all connection information.
+ #
+ # == Example
+ #
+ # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
+ # spec = Resolver.new(config).spec(:production)
+ # spec.adapter_method
+ # # => "sqlite3_connection"
+ # spec.config
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" }
+ #
+ def spec(config)
+ spec = resolve(config).symbolize_keys
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
+ path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
begin
- require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
+ require path_to_adapter
+ rescue Gem::LoadError => e
+ raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
rescue LoadError => e
- raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace
+ raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
end
adapter_method = "#{spec[:adapter]}_connection"
-
ConnectionSpecification.new(spec, adapter_method)
end
- def connection_url_to_hash(url) # :nodoc:
- config = URI.parse url
- adapter = config.scheme
- adapter = "postgresql" if adapter == "postgres"
- spec = { :adapter => adapter,
- :username => config.user,
- :password => config.password,
- :port => config.port,
- :database => config.path.sub(%r{^/},""),
- :host => config.host }
- spec.reject!{ |_,value| value.blank? }
- uri_parser = URI::Parser.new
- spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) }
- if config.query
- options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
- spec.merge!(options)
+ private
+
+ # Returns fully resolved connection, accepts hash, string or symbol.
+ # Always returns a hash.
+ #
+ # == Examples
+ #
+ # Symbol representing current environment.
+ #
+ # Resolver.new("production" => {}).resolve_connection(:production)
+ # # => {}
+ #
+ # One layer deep hash of connection values.
+ #
+ # Resolver.new({}).resolve_connection("adapter" => "sqlite3")
+ # # => { "adapter" => "sqlite3" }
+ #
+ # Connection URL.
+ #
+ # Resolver.new({}).resolve_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_connection(spec)
+ case spec
+ when Symbol
+ resolve_symbol_connection spec
+ when String
+ resolve_string_connection spec
+ when Hash
+ resolve_hash_connection spec
+ end
+ end
+
+ def resolve_string_connection(spec)
+ # Rails has historically accepted a string to mean either
+ # an environment key or a URL spec, so we have deprecated
+ # this ambiguous behaviour and in the future this function
+ # can be removed in favor of resolve_url_connection.
+ if configurations.key?(spec) || spec !~ /:/
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing a string to ActiveRecord::Base.establish_connection for a
+ configuration lookup is deprecated, please pass a symbol
+ (#{spec.to_sym.inspect}) instead.
+ MSG
+
+ resolve_symbol_connection(spec)
+ else
+ resolve_url_connection(spec)
+ end
+ end
+
+ # Takes the environment such as +:production+ or +:development+.
+ # This requires that the @configurations was initialized with a key that
+ # matches.
+ #
+ # Resolver.new("production" => {}).resolve_symbol_connection(:production)
+ # # => {}
+ #
+ def resolve_symbol_connection(spec)
+ if config = configurations[spec.to_s]
+ resolve_connection(config)
+ else
+ raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}")
+ end
+ end
+
+ # Accepts a hash. Expands the "url" key that contains a
+ # URL database connection to a full connection
+ # hash and merges with the rest of the hash.
+ # Connection details inside of the "url" key win any merge conflicts
+ def resolve_hash_connection(spec)
+ if spec["url"] && spec["url"] !~ /^jdbc:/
+ connection_hash = resolve_url_connection(spec.delete("url"))
+ spec.merge!(connection_hash)
end
spec
end
+
+ # Takes a connection URL.
+ #
+ # Resolver.new({}).resolve_url_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_url_connection(url)
+ ConnectionUrlResolver.new(url).to_hash
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index a6013f754a..75f244b3f3 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,41 +1,51 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
-gem 'mysql2', '~> 0.3.10'
+gem 'mysql2', '~> 0.3.13'
require 'mysql2'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# Establishes a connection to the database that's used by all Active Record objects.
def mysql2_connection(config)
+ config = config.symbolize_keys
+
config[:username] = 'root' if config[:username].nil?
if Mysql2::Client.const_defined? :FOUND_ROWS
config[:flags] = Mysql2::Client::FOUND_ROWS
end
- client = Mysql2::Client.new(config.symbolize_keys)
+ client = Mysql2::Client.new(config)
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
+ rescue Mysql2::Error => error
+ if error.message.include?("Unknown database")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
end
module ConnectionAdapters
class Mysql2Adapter < AbstractMysqlAdapter
-
- class Column < AbstractMysqlAdapter::Column # :nodoc:
- def adapter
- Mysql2Adapter
- end
- end
-
- ADAPTER_NAME = 'Mysql2'
+ ADAPTER_NAME = 'Mysql2'.freeze
def initialize(connection, logger, connection_options, config)
super
- @visitor = BindSubstitution.new self
+ @prepared_statements = false
configure_connection
end
+ MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191
+ def initialize_schema_migrations_table
+ if @config[:encoding] == 'utf8mb4'
+ ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4)
+ else
+ ActiveRecord::SchemaMigration.create_table
+ end
+ end
+
def supports_explain?
true
end
@@ -52,21 +62,21 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation, strict_mode?)
- end
-
def error_number(exception)
exception.error_number if exception.respond_to?(:error_number)
end
+ #--
# QUOTING ==================================================
+ #++
def quote_string(string)
@connection.escape(string)
end
+ #--
# CONNECTION MANAGEMENT ====================================
+ #++
def active?
return false unless @connection
@@ -90,7 +100,9 @@ module ActiveRecord
end
end
+ #--
# DATABASE STATEMENTS ======================================
+ #++
def explain(arel, binds = [])
sql = "EXPLAIN #{to_sql(arel, binds.dup)}"
@@ -196,15 +208,17 @@ module ActiveRecord
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
- def select_rows(sql, name = nil)
+ def select_rows(sql, name = nil, binds = [])
execute(sql, name).to_a
end
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
- # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
- # made since we established the connection
- @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+ if @connection
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
+ # made since we established the connection
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+ end
super
end
@@ -216,12 +230,6 @@ module ActiveRecord
alias exec_without_stmt exec_query
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
- def select(sql, name = nil, binds = [])
- exec_query(sql, name)
- end
-
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
super
id_value || @connection.last_id
@@ -254,8 +262,12 @@ module ActiveRecord
super
end
- def version
- @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ def full_version
+ @full_version ||= @connection.info[:version]
+ end
+
+ def set_field_encoding field_name
+ field_name
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 631f646f58..23d8389abb 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -16,9 +16,9 @@ class Mysql
end
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# Establishes a connection to the database that's used by all Active Record objects.
- def mysql_connection(config) # :nodoc:
+ def mysql_connection(config)
config = config.symbolize_keys
host = config[:host]
port = config[:port]
@@ -34,6 +34,12 @@ module ActiveRecord
default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
options = [host, username, password, database, port, socket, default_flags]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
+ rescue Mysql::Error => error
+ if error.message.include?("Unknown database")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
end
@@ -52,7 +58,7 @@ module ActiveRecord
# * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
# * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
# * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html)
- # * <tt>:variables</tt> - (Optional) A hash session variables to send as `SET @@SESSION.key = value` on each database connection. Use the value `:default` to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html).
+ # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html).
# * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
@@ -60,36 +66,7 @@ module ActiveRecord
# * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
#
class MysqlAdapter < AbstractMysqlAdapter
-
- class Column < AbstractMysqlAdapter::Column #:nodoc:
- def self.string_to_time(value)
- return super unless Mysql::Time === value
- new_time(
- value.year,
- value.month,
- value.day,
- value.hour,
- value.minute,
- value.second,
- value.second_part)
- end
-
- def self.string_to_dummy_time(v)
- return super unless Mysql::Time === v
- new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)
- end
-
- def self.string_to_date(v)
- return super unless Mysql::Time === v
- new_date(v.year, v.month, v.day)
- end
-
- def adapter
- MysqlAdapter
- end
- end
-
- ADAPTER_NAME = 'MySQL'
+ ADAPTER_NAME = 'MySQL'.freeze
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max = 1000)
@@ -111,7 +88,7 @@ module ActiveRecord
end
def clear
- cache.values.each do |hash|
+ cache.each_value do |hash|
hash[:stmt].close
end
cache.clear
@@ -126,7 +103,7 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super
@statements = StatementPool.new(@connection,
- config.fetch(:statement_limit) { 1000 })
+ self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@client_encoding = nil
connect
end
@@ -150,27 +127,19 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation, strict_mode?)
- end
-
def error_number(exception) # :nodoc:
exception.errno if exception.respond_to?(:errno)
end
# QUOTING ==================================================
- def type_cast(value, column)
- return super unless value == true || value == false
-
- value ? 1 : 0
- end
-
def quote_string(string) #:nodoc:
@connection.quote(string)
end
+ #--
# CONNECTION MANAGEMENT ====================================
+ #++
def active?
if @connection.respond_to?(:stat)
@@ -211,17 +180,20 @@ module ActiveRecord
end
end
+ #--
# DATABASE STATEMENTS ======================================
+ #++
- def select_rows(sql, name = nil)
+ def select_rows(sql, name = nil, binds = [])
@connection.query_with_result = true
- rows = exec_query(sql, name).rows
+ rows = exec_query(sql, name, binds).rows
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
# Clears the prepared statements cache.
def clear_cache!
+ super
@statements.clear
end
@@ -279,11 +251,7 @@ module ActiveRecord
end
def exec_query(sql, name = 'SQL', binds = [])
- # If the configuration sets prepared_statements:false, binds will
- # always be empty, since the bind variables will have been already
- # substituted and removed from binds by BindVisitor, so this will
- # effectively disable prepared statement usage completely.
- if binds.empty?
+ if without_prepared_statement?(binds)
result_set, affected_rows = exec_without_stmt(sql, name)
else
result_set, affected_rows = exec_stmt(sql, name, binds)
@@ -298,118 +266,70 @@ module ActiveRecord
@connection.insert_id
end
- module Fields
- class Type
- def type; end
-
- def type_cast_for_write(value)
- value
- end
- end
-
- class Identity < Type
- def type_cast(value); value; end
- end
-
- class Integer < Type
- def type_cast(value)
- return if value.nil?
-
- value.to_i rescue value ? 1 : 0
- end
- end
-
- class Date < Type
- def type; :date; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.value_to_date value
- end
- end
-
- class DateTime < Type
- def type; :datetime; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.string_to_time value
+ module Fields # :nodoc:
+ class DateTime < Type::DateTime # :nodoc:
+ def cast_value(value)
+ if Mysql::Time === value
+ new_time(
+ value.year,
+ value.month,
+ value.day,
+ value.hour,
+ value.minute,
+ value.second,
+ value.second_part)
+ else
+ super
+ end
end
end
- class Time < Type
- def type; :time; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.string_to_dummy_time value
+ class Time < Type::Time # :nodoc:
+ def cast_value(value)
+ if Mysql::Time === value
+ new_time(
+ 2000,
+ 01,
+ 01,
+ value.hour,
+ value.minute,
+ value.second,
+ value.second_part)
+ else
+ super
+ end
end
end
- class Float < Type
- def type; :float; end
+ class << self
+ TYPES = Type::HashLookupTypeMap.new # :nodoc:
- def type_cast(value)
- return if value.nil?
+ delegate :register_type, :alias_type, to: :TYPES
- value.to_f
+ def find_type(field)
+ if field.type == Mysql::Field::TYPE_TINY && field.length > 1
+ TYPES.lookup(Mysql::Field::TYPE_LONG)
+ else
+ TYPES.lookup(field.type)
+ end
end
end
- class Decimal < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_decimal value
- end
- end
-
- class Boolean < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_boolean value
- end
- end
-
- TYPES = {}
-
- # Register an MySQL +type_id+ with a typcasting object in
- # +type+.
- def self.register_type(type_id, type)
- TYPES[type_id] = type
- end
-
- def self.alias_type(new, old)
- TYPES[new] = TYPES[old]
- end
-
- register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new
- register_type Mysql::Field::TYPE_LONG, Fields::Integer.new
+ register_type Mysql::Field::TYPE_TINY, Type::Boolean.new
+ register_type Mysql::Field::TYPE_LONG, Type::Integer.new
alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG
alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG
- register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new
- register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new
- register_type Mysql::Field::TYPE_DATE, Fields::Date.new
+ register_type Mysql::Field::TYPE_DATE, Type::Date.new
register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new
register_type Mysql::Field::TYPE_TIME, Fields::Time.new
- register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new
+ register_type Mysql::Field::TYPE_FLOAT, Type::Float.new
+ end
- Mysql::Field.constants.grep(/TYPE/).map { |class_name|
- Mysql::Field.const_get class_name
- }.reject { |const| TYPES.key? const }.each do |const|
- register_type const, Fields::Identity.new
- end
+ def initialize_type_map(m) # :nodoc:
+ super
+ m.register_type %r(datetime)i, Fields::DateTime.new
+ m.register_type %r(time)i, Fields::Time.new
end
def exec_without_stmt(sql, name = 'SQL') # :nodoc:
@@ -421,16 +341,19 @@ module ActiveRecord
if result
types = {}
+ fields = []
result.fetch_fields.each { |field|
+ field_name = field.name
+ fields << field_name
+
if field.decimals > 0
- types[field.name] = Fields::Decimal.new
+ types[field_name] = Type::Decimal.new
else
- types[field.name] = Fields::TYPES.fetch(field.type) {
- Fields::Identity.new
- }
+ types[field_name] = Fields.find_type field
end
}
- result_set = ActiveRecord::Result.new(types.keys, result.to_a, types)
+
+ result_set = ActiveRecord::Result.new(fields, result.to_a, types)
result.free
else
result_set = ActiveRecord::Result.new([], [])
@@ -440,7 +363,7 @@ module ActiveRecord
end
end
- def execute_and_free(sql, name = nil)
+ def execute_and_free(sql, name = nil) # :nodoc:
result = execute(sql, name)
ret = yield result
result.free
@@ -453,7 +376,7 @@ module ActiveRecord
end
alias :create :insert_sql
- def exec_delete(sql, name, binds)
+ def exec_delete(sql, name, binds) # :nodoc:
affected_rows = 0
exec_query(sql, name, binds) do |n|
@@ -466,15 +389,17 @@ module ActiveRecord
def begin_db_transaction #:nodoc:
exec_query "BEGIN"
- rescue Mysql::Error
- # Transactions aren't supported
end
private
def exec_stmt(sql, name, binds)
cache = {}
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+
+ log(sql, name, type_casted_binds) do
if binds.empty?
stmt = @connection.prepare(sql)
else
@@ -485,10 +410,10 @@ module ActiveRecord
end
begin
- stmt.execute(*binds.map { |col, val| type_cast(val, col) })
+ stmt.execute(*type_casted_binds.map { |_, val| val })
rescue Mysql::Error => e
# Older versions of MySQL leave the prepared statement in a bad
- # place when an error occurs. To support older mysql versions, we
+ # place when an error occurs. To support older MySQL versions, we
# need to close the statement and delete the statement from the
# cache.
stmt.close
@@ -498,15 +423,13 @@ module ActiveRecord
cols = nil
if metadata = stmt.result_metadata
- cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
- field.name
- }
+ cols = cache[:cols] ||= metadata.fetch_fields.map(&:name)
+ metadata.free
end
result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols
affected_rows = stmt.affected_rows
- stmt.result_metadata.free if cols
stmt.free_result
stmt.close if binds.empty?
@@ -544,14 +467,22 @@ module ActiveRecord
def select(sql, name = nil, binds = [])
@connection.query_with_result = true
- rows = exec_query(sql, name, binds)
+ rows = super
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
- # Returns the version of the connected MySQL server.
- def version
- @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ # Returns the full version of the connected MySQL server.
+ def full_version
+ @full_version ||= @connection.server_info
+ end
+
+ def set_field_encoding field_name
+ field_name.force_encoding(client_encoding)
+ if internal_enc = Encoding.default_internal
+ field_name = field_name.encode!(internal_enc)
+ end
+ field_name
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
index b7d24f2bb3..1b74c039ce 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
@@ -1,41 +1,36 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLColumn < Column
- module ArrayParser
- private
- # Loads pg_array_parser if available. String parsing can be
- # performed quicker by a native extension, which will not create
- # a large amount of Ruby objects that will need to be garbage
- # collected. pg_array_parser has a C and Java extension
- begin
- require 'pg_array_parser'
- include PgArrayParser
- rescue LoadError
- def parse_pg_array(string)
- parse_data(string, 0)
+ module PostgreSQL
+ module ArrayParser # :nodoc:
+
+ DOUBLE_QUOTE = '"'
+ BACKSLASH = "\\"
+ COMMA = ','
+ BRACKET_OPEN = '{'
+ BRACKET_CLOSE = '}'
+
+ def parse_pg_array(string) # :nodoc:
+ local_index = 0
+ array = []
+ while(local_index < string.length)
+ case string[local_index]
+ when BRACKET_OPEN
+ local_index,array = parse_array_contents(array, string, local_index + 1)
+ when BRACKET_CLOSE
+ return array
end
+ local_index += 1
end
- def parse_data(string, index)
- local_index = index
- array = []
- while(local_index < string.length)
- case string[local_index]
- when '{'
- local_index,array = parse_array_contents(array, string, local_index + 1)
- when '}'
- return array
- end
- local_index += 1
- end
+ array
+ end
- array
- end
+ private
def parse_array_contents(array, string, index)
- is_escaping = false
- is_quoted = false
- was_quoted = false
+ is_escaping = false
+ is_quoted = false
+ was_quoted = false
current_item = ''
local_index = index
@@ -47,29 +42,29 @@ module ActiveRecord
else
if is_quoted
case token
- when '"'
+ when DOUBLE_QUOTE
is_quoted = false
was_quoted = true
- when "\\"
+ when BACKSLASH
is_escaping = true
else
current_item << token
end
else
case token
- when "\\"
+ when BACKSLASH
is_escaping = true
- when ','
+ when COMMA
add_item_to_array(array, current_item, was_quoted)
current_item = ''
was_quoted = false
- when '"'
+ when DOUBLE_QUOTE
is_quoted = true
- when '{'
+ when BRACKET_OPEN
internal_items = []
local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1)
array.push(internal_items)
- when '}'
+ when BRACKET_CLOSE
add_item_to_array(array, current_item, was_quoted)
return local_index,array
else
@@ -84,8 +79,9 @@ module ActiveRecord
end
def add_item_to_array(array, current_item, quoted)
- if current_item.length == 0
- elsif !quoted && current_item == 'NULL'
+ return if !quoted && current_item.length == 0
+
+ if !quoted && current_item == 'NULL'
array.push nil
else
array.push current_item
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
deleted file mode 100644
index f7d734a2f1..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ /dev/null
@@ -1,156 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- class PostgreSQLColumn < Column
- module Cast
- def string_to_time(string)
- return string unless String === string
-
- case string
- when 'infinity'; 1.0 / 0.0
- when '-infinity'; -1.0 / 0.0
- when / BC$/
- super("-" + string.sub(/ BC$/, ""))
- else
- super
- end
- end
-
- def hstore_to_string(object)
- if Hash === object
- object.map { |k,v|
- "#{escape_hstore(k)}=>#{escape_hstore(v)}"
- }.join ','
- else
- object
- end
- end
-
- def string_to_hstore(string)
- if string.nil?
- nil
- elsif String === string
- Hash[string.scan(HstorePair).map { |k,v|
- v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
- k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
- [k,v]
- }]
- else
- string
- end
- end
-
- def json_to_string(object)
- if Hash === object
- ActiveSupport::JSON.encode(object)
- else
- object
- end
- end
-
- def array_to_string(value, column, adapter, should_be_quoted = false)
- casted_values = value.map do |val|
- if String === val
- if val == "NULL"
- "\"#{val}\""
- else
- quote_and_escape(adapter.type_cast(val, column, true))
- end
- else
- adapter.type_cast(val, column, true)
- end
- end
- "{#{casted_values.join(',')}}"
- end
-
- def string_to_json(string)
- if String === string
- ActiveSupport::JSON.decode(string)
- else
- string
- 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
-
- def string_to_array(string, oid)
- parse_pg_array(string).map{|val| oid.type_cast val}
- end
-
- def string_to_intrange(string)
- if string.nil?
- nil
- elsif "empty" == string
- (nil..nil)
- elsif String === string && (matches = /^(\(|\[)([0-9]+),(\s?)([0-9]+)(\)|\])$/i.match(string))
- lower_bound = ("(" == matches[1] ? (matches[2].to_i + 1) : matches[2].to_i)
- upper_bound = (")" == matches[5] ? (matches[4].to_i - 1) : matches[4].to_i)
- (lower_bound..upper_bound)
- else
- string
- end
- end
-
- def intrange_to_string(object)
- if object.nil?
- nil
- elsif Range === object
- if [object.first, object.last].all? { |el| Integer === el }
- "[#{object.first.to_i},#{object.exclude_end? ? object.last.to_i : object.last.to_i + 1})"
- elsif [object.first, object.last].all? { |el| NilClass === el }
- "empty"
- else
- nil
- end
- else
- object
- end
- end
-
- private
-
- HstorePair = begin
- quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
- unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
- /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
- end
-
- def escape_hstore(value)
- if value.nil?
- 'NULL'
- else
- if value == ""
- '""'
- else
- '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
- end
- end
- end
-
- def quote_and_escape(value)
- case value
- when "NULL"
- value
- else
- "\"#{value.gsub(/"/,"\\\"")}\""
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
new file mode 100644
index 0000000000..1458fbf496
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -0,0 +1,24 @@
+module ActiveRecord
+ module ConnectionAdapters
+ # PostgreSQL-specific extensions to column definitions in a table.
+ class PostgreSQLColumn < Column #:nodoc:
+ attr_accessor :array
+
+ def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil)
+ if sql_type =~ /\[\]$/
+ @array = true
+ super(name, default, cast_type, sql_type[0..sql_type.length - 3], null)
+ else
+ @array = false
+ super(name, default, cast_type, sql_type, null)
+ end
+
+ @default_function = default_function
+ end
+
+ def serial?
+ default_function && default_function =~ /\Anextval\(.*\)\z/
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 34d7a246b2..11d3f5301a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -1,8 +1,6 @@
-require 'active_support/deprecation'
-
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
module DatabaseStatements
def explain(arel, binds = [])
sql = "EXPLAIN #{to_sql(arel, binds)}"
@@ -46,10 +44,32 @@ module ActiveRecord
end
end
+ def select_value(arel, name = nil, binds = [])
+ arel, binds = binds_from_relation arel, binds
+ sql = to_sql(arel, binds)
+ execute_and_clear(sql, name, binds) do |result|
+ result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0
+ end
+ end
+
+ def select_values(arel, name = nil)
+ arel, binds = binds_from_relation arel, []
+ sql = to_sql(arel, binds)
+ execute_and_clear(sql, name, binds) do |result|
+ if result.nfields > 0
+ result.column_values(0)
+ else
+ []
+ end
+ end
+ end
+
# Executes a SELECT query and returns an array of rows. Each row is an
# array of field values.
- def select_rows(sql, name = nil)
- select_raw(sql, name).last
+ def select_rows(sql, name = nil, binds = [])
+ execute_and_clear(sql, name, binds) do |result|
+ result.values
+ end
end
# Executes an INSERT query and returns the new record's ID
@@ -74,6 +94,11 @@ module ActiveRecord
super.insert
end
+ # The internal PostgreSQL identifier of the money data type.
+ MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
+ # The internal PostgreSQL identifier of the BYTEA data type.
+ BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
+
# create a 2D array representing the result set
def result_as_array(res) #:nodoc:
# check if we have any binary column and if they need escaping
@@ -131,39 +156,21 @@ module ActiveRecord
end
end
- def substitute_at(column, index)
- Arel::Nodes::BindParam.new "$#{index + 1}"
- end
-
def exec_query(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
-
+ execute_and_clear(sql, name, binds) do |result|
types = {}
- result.fields.each_with_index do |fname, i|
+ fields = result.fields
+ fields.each_with_index do |fname, i|
ftype = result.ftype i
fmod = result.fmod i
- types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod|
- warn "unknown OID: #{fname}(#{oid}) (#{sql})"
- OID::Identity.new
- }
+ types[fname] = get_oid_type(ftype, fmod, fname)
end
-
- ret = ActiveRecord::Result.new(result.fields, result.values, types)
- result.clear
- return ret
+ ActiveRecord::Result.new(fields, result.values, types)
end
end
def exec_delete(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
- affected = result.cmd_tuples
- result.clear
- affected
- end
+ execute_and_clear(sql, name, binds) {|result| result.cmd_tuples }
end
alias :exec_update :exec_delete
@@ -216,28 +223,9 @@ module ActiveRecord
end
# Aborts a transaction.
- def rollback_db_transaction
+ def exec_rollback_db_transaction
execute "ROLLBACK"
end
-
- def outside_transaction?
- message = "#outside_transaction? is deprecated. This method was only really used " \
- "internally, but you can use #transaction_open? instead."
- ActiveSupport::Deprecation.warn message
- @connection.transaction_status == PGconn::PQTRANS_IDLE
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
end
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 18ea83ce42..d28a2b4fa0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -1,287 +1,35 @@
-require 'active_record/connection_adapters/abstract_adapter'
+require 'active_record/connection_adapters/postgresql/oid/infinity'
+
+require 'active_record/connection_adapters/postgresql/oid/array'
+require 'active_record/connection_adapters/postgresql/oid/bit'
+require 'active_record/connection_adapters/postgresql/oid/bit_varying'
+require 'active_record/connection_adapters/postgresql/oid/bytea'
+require 'active_record/connection_adapters/postgresql/oid/cidr'
+require 'active_record/connection_adapters/postgresql/oid/date'
+require 'active_record/connection_adapters/postgresql/oid/date_time'
+require 'active_record/connection_adapters/postgresql/oid/decimal'
+require 'active_record/connection_adapters/postgresql/oid/enum'
+require 'active_record/connection_adapters/postgresql/oid/float'
+require 'active_record/connection_adapters/postgresql/oid/hstore'
+require 'active_record/connection_adapters/postgresql/oid/inet'
+require 'active_record/connection_adapters/postgresql/oid/integer'
+require 'active_record/connection_adapters/postgresql/oid/json'
+require 'active_record/connection_adapters/postgresql/oid/jsonb'
+require 'active_record/connection_adapters/postgresql/oid/money'
+require 'active_record/connection_adapters/postgresql/oid/point'
+require 'active_record/connection_adapters/postgresql/oid/range'
+require 'active_record/connection_adapters/postgresql/oid/specialized_string'
+require 'active_record/connection_adapters/postgresql/oid/time'
+require 'active_record/connection_adapters/postgresql/oid/uuid'
+require 'active_record/connection_adapters/postgresql/oid/vector'
+require 'active_record/connection_adapters/postgresql/oid/xml'
+
+require 'active_record/connection_adapters/postgresql/oid/type_map_initializer'
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- module OID
- class Type
- def type; end
-
- def type_cast_for_write(value)
- value
- end
- end
-
- class Identity < Type
- def type_cast(value)
- value
- end
- end
-
- class Bytea < Type
- def type_cast(value)
- PGconn.unescape_bytea value
- end
- end
-
- class Money < Type
- def type_cast(value)
- return if value.nil?
-
- # Because money output is formatted according to the locale, there are two
- # cases to consider (note the decimal separators):
- # (1) $12,345,678.12
- # (2) $12.345.678,12
-
- case value
- when /^-?\D+[\d,]+\.\d{2}$/ # (1)
- value.gsub!(/[^-\d.]/, '')
- when /^-?\D+[\d.]+,\d{2}$/ # (2)
- value.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
- end
-
- ConnectionAdapters::Column.value_to_decimal value
- end
- end
-
- class Vector < Type
- attr_reader :delim, :subtype
-
- # +delim+ corresponds to the `typdelim` column in the pg_types
- # table. +subtype+ is derived from the `typelem` column in the
- # pg_types table.
- def initialize(delim, subtype)
- @delim = delim
- @subtype = subtype
- end
-
- # FIXME: this should probably split on +delim+ and use +subtype+
- # to cast the values. Unfortunately, the current Rails behavior
- # is to just return the string.
- def type_cast(value)
- value
- end
- end
-
- class Array < Type
- attr_reader :subtype
- def initialize(subtype)
- @subtype = subtype
- end
-
- def type_cast(value)
- if String === value
- ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype
- else
- value
- end
- end
- end
-
- class Integer < Type
- def type_cast(value)
- return if value.nil?
-
- value.to_i rescue value ? 1 : 0
- end
- end
-
- class Boolean < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_boolean value
- end
- end
-
- class Timestamp < Type
- def type; :timestamp; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::PostgreSQLColumn.string_to_time value
- end
- end
-
- class Date < Type
- def type; :datetime; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::Column.value_to_date value
- end
- end
-
- class Time < Type
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::Column.string_to_dummy_time value
- end
- end
-
- class Float < Type
- def type_cast(value)
- return if value.nil?
-
- value.to_f
- end
- end
-
- class Decimal < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_decimal value
- end
- end
-
- class Hstore < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::PostgreSQLColumn.string_to_hstore value
- end
- end
-
- class Cidr < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::PostgreSQLColumn.string_to_cidr value
- end
- end
-
- class Json < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::PostgreSQLColumn.string_to_json value
- end
- end
-
- class IntRange < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::PostgreSQLColumn.string_to_intrange value
- end
- end
-
- class TypeMap
- def initialize
- @mapping = {}
- end
-
- def []=(oid, type)
- @mapping[oid] = type
- end
-
- def [](oid)
- @mapping[oid]
- end
-
- def key?(oid)
- @mapping.key? oid
- end
-
- def fetch(ftype, fmod)
- # The type for the numeric depends on the width of the field,
- # so we'll do something special here.
- #
- # When dealing with decimal columns:
- #
- # places after decimal = fmod - 4 & 0xffff
- # places before decimal = (fmod - 4) >> 16 & 0xffff
- if ftype == 1700 && (fmod - 4 & 0xffff).zero?
- ftype = 23
- end
-
- @mapping.fetch(ftype) { |oid| yield oid, fmod }
- end
- end
-
- TYPE_MAP = TypeMap.new # :nodoc:
-
- # When the PG adapter connects, the pg_type table is queried. The
- # key of this hash maps to the `typname` column from the table.
- # TYPE_MAP is then dynamically built with oids as the key and type
- # objects as values.
- NAMES = Hash.new { |h,k| # :nodoc:
- h[k] = OID::Identity.new
- }
-
- # Register an OID type named +name+ with a typcasting object in
- # +type+. +name+ should correspond to the `typname` column in
- # the `pg_type` table.
- def self.register_type(name, type)
- NAMES[name] = type
- end
-
- # Alias the +old+ type to the +new+ type.
- def self.alias_type(new, old)
- NAMES[new] = NAMES[old]
- end
-
- # Is +name+ a registered type?
- def self.registered_type?(name)
- NAMES.key? name
- end
-
- register_type 'int2', OID::Integer.new
- alias_type 'int4', 'int2'
- alias_type 'int8', 'int2'
- alias_type 'oid', 'int2'
-
- register_type 'numeric', OID::Decimal.new
- register_type 'text', OID::Identity.new
- alias_type 'varchar', 'text'
- alias_type 'char', 'text'
- alias_type 'bpchar', 'text'
- alias_type 'xml', 'text'
-
- # FIXME: why are we keeping these types as strings?
- alias_type 'tsvector', 'text'
- alias_type 'interval', 'text'
- alias_type 'bit', 'text'
- alias_type 'varbit', 'text'
- alias_type 'macaddr', 'text'
- alias_type 'uuid', 'text'
-
- # FIXME: I don't think this is correct. We should probably be returning a parsed date,
- # but the tests pass with a string returned.
- register_type 'timestamptz', OID::Identity.new
-
- register_type 'money', OID::Money.new
- register_type 'bytea', OID::Bytea.new
- register_type 'bool', OID::Boolean.new
-
- register_type 'float4', OID::Float.new
- alias_type 'float8', 'float4'
-
- register_type 'timestamp', OID::Timestamp.new
- register_type 'date', OID::Date.new
- register_type 'time', OID::Time.new
-
- register_type 'path', OID::Identity.new
- register_type 'polygon', OID::Identity.new
- register_type 'circle', OID::Identity.new
- register_type 'hstore', OID::Hstore.new
- register_type 'json', OID::Json.new
-
- register_type 'int4range', OID::IntRange.new
- alias_type 'int8range', 'int4range'
-
- register_type 'cidr', OID::Cidr.new
- alias_type 'inet', 'cidr'
+ module PostgreSQL
+ module OID # :nodoc:
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
new file mode 100644
index 0000000000..c203e6c604
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -0,0 +1,99 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Array < Type::Value # :nodoc:
+ include Type::Mutable
+
+ # Loads pg_array_parser if available. String parsing can be
+ # performed quicker by a native extension, which will not create
+ # a large amount of Ruby objects that will need to be garbage
+ # collected. pg_array_parser has a C and Java extension
+ begin
+ require 'pg_array_parser'
+ include PgArrayParser
+ rescue LoadError
+ require 'active_record/connection_adapters/postgresql/array_parser'
+ include PostgreSQL::ArrayParser
+ end
+
+ attr_reader :subtype, :delimiter
+ delegate :type, to: :subtype
+
+ def initialize(subtype, delimiter = ',')
+ @subtype = subtype
+ @delimiter = delimiter
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ type_cast_array(parse_pg_array(value), :type_cast_from_database)
+ else
+ super
+ end
+ end
+
+ def type_cast_from_user(value)
+ if value.is_a?(::String)
+ value = parse_pg_array(value)
+ end
+ type_cast_array(value, :type_cast_from_user)
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ cast_value_for_database(value)
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_array(value, method)
+ if value.is_a?(::Array)
+ value.map { |item| type_cast_array(item, method) }
+ else
+ @subtype.public_send(method, value)
+ end
+ end
+
+ def cast_value_for_database(value)
+ if value.is_a?(::Array)
+ casted_values = value.map { |item| cast_value_for_database(item) }
+ "{#{casted_values.join(delimiter)}}"
+ else
+ quote_and_escape(subtype.type_cast_for_database(value))
+ end
+ end
+
+ ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
+
+ def quote_and_escape(value)
+ case value
+ when ::String
+ if string_requires_quoting?(value)
+ value = value.gsub(/\\/, ARRAY_ESCAPE)
+ value.gsub!(/"/,"\\\"")
+ %("#{value}")
+ else
+ value
+ end
+ when nil then "NULL"
+ else value
+ end
+ end
+
+ # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO
+ # for a list of all cases in which strings will be quoted.
+ def string_requires_quoting?(string)
+ string.empty? ||
+ string == "NULL" ||
+ string =~ /[\{\}"\\\s]/ ||
+ string.include?(delimiter)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
new file mode 100644
index 0000000000..1dbb40ca1d
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -0,0 +1,52 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Bit < Type::Value # :nodoc:
+ def type
+ :bit
+ end
+
+ def type_cast(value)
+ if ::String === value
+ case value
+ when /^0x/i
+ value[2..-1].hex.to_s(2) # Hexadecimal notation
+ else
+ value # Bit-string notation
+ end
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ Data.new(super) if value
+ end
+
+ class Data
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ value
+ end
+
+ def binary?
+ /\A[01]*\Z/ === value
+ end
+
+ def hex?
+ /\A[0-9A-F]*\Z/i === value
+ end
+
+ protected
+
+ attr_reader :value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
new file mode 100644
index 0000000000..4c21097d48
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class BitVarying < OID::Bit # :nodoc:
+ def type
+ :bit_varying
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
new file mode 100644
index 0000000000..6bd1b8ecae
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Bytea < Type::Binary # :nodoc:
+ def type_cast_from_database(value)
+ return if value.nil?
+ return value.to_s if value.is_a?(Type::Binary::Data)
+ PGconn.unescape_bytea(super)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
new file mode 100644
index 0000000000..222f10fa8f
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
@@ -0,0 +1,46 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Cidr < Type::Value # :nodoc:
+ def type
+ :cidr
+ end
+
+ def type_cast_for_schema(value)
+ subnet_mask = value.instance_variable_get(:@mask_addr)
+
+ # If the subnet mask is equal to /32, don't output it
+ if subnet_mask == (2**32 - 1)
+ "\"#{value}\""
+ else
+ "\"#{value}/#{subnet_mask.to_s(2).count('1')}\""
+ end
+ end
+
+ def type_cast_for_database(value)
+ if IPAddr === value
+ "#{value}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
+ else
+ value
+ end
+ end
+
+ def cast_value(value)
+ if value.nil?
+ nil
+ elsif String === value
+ begin
+ IPAddr.new(value)
+ rescue ArgumentError
+ nil
+ end
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
new file mode 100644
index 0000000000..1d8d264530
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Date < Type::Date # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
new file mode 100644
index 0000000000..b9e7894e5c
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
@@ -0,0 +1,27 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class DateTime < Type::DateTime # :nodoc:
+ include Infinity
+
+ def cast_value(value)
+ if value.is_a?(::String)
+ case value
+ when 'infinity' then ::Float::INFINITY
+ when '-infinity' then -::Float::INFINITY
+ when / BC$/
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
+ else
+ super
+ end
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
new file mode 100644
index 0000000000..43d22c8daf
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Decimal < Type::Decimal # :nodoc:
+ def infinity(options = {})
+ BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
new file mode 100644
index 0000000000..77d5038efd
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Enum < Type::Value # :nodoc:
+ def type
+ :enum
+ end
+
+ def type_cast(value)
+ value.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
new file mode 100644
index 0000000000..78ef94b912
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Float < Type::Float # :nodoc:
+ include Infinity
+
+ def cast_value(value)
+ case value
+ when ::Float then value
+ when 'Infinity' then ::Float::INFINITY
+ when '-Infinity' then -::Float::INFINITY
+ when 'NaN' then ::Float::NAN
+ else value.to_f
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
new file mode 100644
index 0000000000..be4525c94f
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -0,0 +1,59 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Hstore < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :hstore
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ ::Hash[value.scan(HstorePair).map { |k, v|
+ v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ [k, v]
+ }]
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Hash)
+ value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ')
+ else
+ value
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+
+ private
+
+ HstorePair = begin
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
+ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
+ /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
+ end
+
+ def escape_hstore(value)
+ if value.nil?
+ 'NULL'
+ else
+ if value == ""
+ '""'
+ else
+ '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
new file mode 100644
index 0000000000..96486fa65b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Inet < Cidr # :nodoc:
+ def type
+ :inet
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
new file mode 100644
index 0000000000..e47780399a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ module Infinity # :nodoc:
+ def infinity(options = {})
+ options[:negative] ? -::Float::INFINITY : ::Float::INFINITY
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
new file mode 100644
index 0000000000..59abdc0009
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Integer < Type::Integer # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
new file mode 100644
index 0000000000..e12ddd9901
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
@@ -0,0 +1,35 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Json < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :json
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ ::ActiveSupport::JSON.decode(value)
+ else
+ super
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array) || value.is_a?(::Hash)
+ ::ActiveSupport::JSON.encode(value)
+ else
+ super
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
new file mode 100644
index 0000000000..380c50fc14
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
@@ -0,0 +1,23 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Jsonb < Json # :nodoc:
+ def type
+ :jsonb
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ # Postgres does not preserve insignificant whitespaces when
+ # roundtripping jsonb columns. This causes some false positives for
+ # the comparison here. Therefore, we need to parse and re-dump the
+ # raw value here to ensure the insignificant whitespaces are
+ # consistent with our encoder's output.
+ raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value))
+ super(raw_old_value, new_value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
new file mode 100644
index 0000000000..df890c2ed6
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Money < Type::Decimal # :nodoc:
+ include Infinity
+
+ class_attribute :precision
+
+ def type
+ :money
+ end
+
+ def scale
+ 2
+ end
+
+ def cast_value(value)
+ return value unless ::String === value
+
+ # Because money output is formatted according to the locale, there are two
+ # cases to consider (note the decimal separators):
+ # (1) $12,345,678.12
+ # (2) $12.345.678,12
+ # Negative values are represented as follows:
+ # (3) -$2.55
+ # (4) ($2.55)
+
+ value.sub!(/^\((.+)\)$/, '-\1') # (4)
+ case value
+ when /^-?\D+[\d,]+\.\d{2}$/ # (1)
+ value.gsub!(/[^-\d.]/, '')
+ when /^-?\D+[\d.]+,\d{2}$/ # (2)
+ value.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
+ end
+
+ super(value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
new file mode 100644
index 0000000000..bac8b01d6b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Point < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :point
+ end
+
+ def type_cast(value)
+ case value
+ when ::String
+ if value[0] == '(' && value[-1] == ')'
+ value = value[1...-1]
+ end
+ type_cast(value.split(','))
+ when ::Array
+ value.map { |v| Float(v) }
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ "(#{number_for_point(value[0])},#{number_for_point(value[1])})"
+ else
+ super
+ end
+ end
+
+ private
+
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, '')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
new file mode 100644
index 0000000000..961e6224c4
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -0,0 +1,79 @@
+require 'active_support/core_ext/string/filters'
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Range < Type::Value # :nodoc:
+ attr_reader :subtype, :type
+
+ def initialize(subtype, type)
+ @subtype = subtype
+ @type = type
+ end
+
+ def type_cast_for_schema(value)
+ value.inspect.gsub('Infinity', '::Float::INFINITY')
+ end
+
+ def cast_value(value)
+ return if value == 'empty'
+ return value if value.is_a?(::Range)
+
+ extracted = extract_bounds(value)
+ from = type_cast_single extracted[:from]
+ to = type_cast_single extracted[:to]
+
+ if !infinity?(from) && extracted[:exclude_start]
+ if from.respond_to?(:succ)
+ from = from.succ
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Excluding the beginning of a Range is only partialy supported
+ through `#succ`. This is not reliable and will be removed in
+ the future.
+ MSG
+ else
+ raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')"
+ end
+ end
+ ::Range.new(from, to, extracted[:exclude_end])
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Range)
+ from = type_cast_single_for_database(value.begin)
+ to = type_cast_single_for_database(value.end)
+ "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}"
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_single(value)
+ infinity?(value) ? value : @subtype.type_cast_from_database(value)
+ end
+
+ def type_cast_single_for_database(value)
+ infinity?(value) ? '' : @subtype.type_cast_for_database(value)
+ end
+
+ def extract_bounds(value)
+ from, to = value[1..-2].split(',')
+ {
+ from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
+ to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
+ exclude_start: (value[0] == '('),
+ exclude_end: (value[-1] == ')')
+ }
+ end
+
+ def infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
new file mode 100644
index 0000000000..b2a42e9ebb
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
@@ -0,0 +1,19 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class SpecializedString < Type::String # :nodoc:
+ attr_reader :type
+
+ def initialize(type)
+ @type = type
+ end
+
+ def text?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
new file mode 100644
index 0000000000..8f0246eddb
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Time < Type::Time # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
new file mode 100644
index 0000000000..9b3de41fab
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -0,0 +1,97 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ # This class uses the data from PostgreSQL pg_type table to build
+ # the OID -> Type mapping.
+ # - OID is an integer representing the type.
+ # - Type is an OID::Type object.
+ # This class has side effects on the +store+ passed during initialization.
+ class TypeMapInitializer # :nodoc:
+ def initialize(store)
+ @store = store
+ end
+
+ def run(records)
+ nodes = records.reject { |row| @store.key? row['oid'].to_i }
+ mapped, nodes = nodes.partition { |row| @store.key? row['typname'] }
+ ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' }
+ enums, nodes = nodes.partition { |row| row['typtype'] == 'e' }
+ domains, nodes = nodes.partition { |row| row['typtype'] == 'd' }
+ arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
+ composites, nodes = nodes.partition { |row| row['typelem'] != '0' }
+
+ mapped.each { |row| register_mapped_type(row) }
+ enums.each { |row| register_enum_type(row) }
+ domains.each { |row| register_domain_type(row) }
+ arrays.each { |row| register_array_type(row) }
+ ranges.each { |row| register_range_type(row) }
+ composites.each { |row| register_composite_type(row) }
+ end
+
+ private
+ def register_mapped_type(row)
+ alias_type row['oid'], row['typname']
+ end
+
+ def register_enum_type(row)
+ register row['oid'], OID::Enum.new
+ end
+
+ def register_array_type(row)
+ register_with_subtype(row['oid'], row['typelem'].to_i) do |subtype|
+ OID::Array.new(subtype, row['typdelim'])
+ end
+ end
+
+ def register_range_type(row)
+ register_with_subtype(row['oid'], row['rngsubtype'].to_i) do |subtype|
+ OID::Range.new(subtype, row['typname'].to_sym)
+ end
+ end
+
+ def register_domain_type(row)
+ if base_type = @store.lookup(row["typbasetype"].to_i)
+ register row['oid'], base_type
+ else
+ warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}."
+ end
+ end
+
+ def register_composite_type(row)
+ if subtype = @store.lookup(row['typelem'].to_i)
+ register row['oid'], OID::Vector.new(row['typdelim'], subtype)
+ end
+ end
+
+ def register(oid, oid_type = nil, &block)
+ oid = assert_valid_registration(oid, oid_type || block)
+ if block_given?
+ @store.register_type(oid, &block)
+ else
+ @store.register_type(oid, oid_type)
+ end
+ end
+
+ def alias_type(oid, target)
+ oid = assert_valid_registration(oid, target)
+ @store.alias_type(oid, target)
+ end
+
+ def register_with_subtype(oid, target_oid)
+ if @store.key?(target_oid)
+ register(oid) do |_, *args|
+ yield @store.lookup(target_oid, *args)
+ end
+ end
+ end
+
+ def assert_valid_registration(oid, oid_type)
+ raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil?
+ oid.to_i
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
new file mode 100644
index 0000000000..97b4fd3d08
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Uuid < Type::Value # :nodoc:
+ ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x
+
+ alias_method :type_cast_for_database, :type_cast_from_database
+
+ def type
+ :uuid
+ end
+
+ def type_cast(value)
+ value.to_s[ACCEPTABLE_UUID, 0]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
new file mode 100644
index 0000000000..de4187b028
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Vector < Type::Value # :nodoc:
+ attr_reader :delim, :subtype
+
+ # +delim+ corresponds to the `typdelim` column in the pg_types
+ # table. +subtype+ is derived from the `typelem` column in the
+ # pg_types table.
+ def initialize(delim, subtype)
+ @delim = delim
+ @subtype = subtype
+ end
+
+ # FIXME: this should probably split on +delim+ and use +subtype+
+ # to cast the values. Unfortunately, the current Rails behavior
+ # is to just return the string.
+ def type_cast(value)
+ value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
new file mode 100644
index 0000000000..334af7c598
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
@@ -0,0 +1,28 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Xml < Type::String # :nodoc:
+ def type
+ :xml
+ end
+
+ def type_cast_for_database(value)
+ return unless value
+ Data.new(super)
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ @value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index c2fcef94da..607848884b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -1,110 +1,17 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
module Quoting
# Escapes binary strings for bytea input to the database.
def escape_bytea(value)
- PGconn.escape_bytea(value) if value
+ @connection.escape_bytea(value) if value
end
# Unescapes bytea output from a database to the binary string it represents.
# NOTE: This is NOT an inverse of escape_bytea! This is only to be used
# on escaped binary output from database drive.
def unescape_bytea(value)
- PGconn.unescape_bytea(value) if value
- end
-
- # Quotes PostgreSQL-specific data types for SQL input.
- def quote(value, column = nil) #:nodoc:
- return super unless column
-
- case value
- when Array
- if column.array
- "'#{PostgreSQLColumn.array_to_string(value, column, self)}'"
- else
- super
- end
- when Hash
- case column.sql_type
- when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column)
- when 'json' then super(PostgreSQLColumn.json_to_string(value), column)
- else super
- end
- when Range
- case column.sql_type
- when 'int4range', 'int8range' then super(PostgreSQLColumn.intrange_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}'"
- elsif value.infinite? || value.nan?
- "'#{value.to_s}'"
- else
- super
- end
- when Numeric
- return super unless column.sql_type == 'money'
- # Not truly string input, so doesn't require (or allow) escape string syntax.
- "'#{value}'"
- when String
- case column.sql_type
- when 'bytea' then "'#{escape_bytea(value)}'"
- when 'xml' then "xml '#{quote_string(value)}'"
- when /^bit/
- case value
- when /^[01]*$/ then "B'#{value}'" # Bit-string notation
- when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
- end
- else
- super
- end
- else
- super
- end
- end
-
- def type_cast(value, column, array_member = false)
- return super(value, column) unless column
-
- case value
- when NilClass
- if column.array && array_member
- 'NULL'
- elsif column.array
- value
- else
- super(value, column)
- end
- when Array
- return super(value, column) unless column.array
- PostgreSQLColumn.array_to_string(value, column, self)
- when String
- return super(value, column) unless 'bytea' == column.sql_type
- { :value => value, :format => 1 }
- when Hash
- case column.sql_type
- when 'hstore' then PostgreSQLColumn.hstore_to_string(value)
- when 'json' then PostgreSQLColumn.json_to_string(value)
- else super(value, column)
- end
- when Range
- case column.sql_type
- when 'int4range', 'int8range' then PostgreSQLColumn.intrange_to_string(value)
- else super(value, column)
- end
- when IPAddr
- return super(value, column) unless ['inet','cidr'].include? column.sql_type
- PostgreSQLColumn.cidr_to_string(value)
- else
- super(value, column)
- end
+ @connection.unescape_bytea(value) if value
end
# Quotes strings for use in SQL input.
@@ -121,14 +28,11 @@ module ActiveRecord
# - "schema.name".table_name
# - "schema.name"."table.name"
def quote_table_name(name)
- schema, name_part = extract_pg_identifier_from_name(name.to_s)
+ Utils.extract_schema_qualified_name(name.to_s).quoted
+ end
- unless name_part
- quote_column_name(schema)
- else
- table_name, name_part = extract_pg_identifier_from_name(name_part)
- "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
- end
+ def quote_table_name_for_assignment(table, attr)
+ quote_column_name(attr)
end
# Quotes column names for use in SQL queries.
@@ -139,15 +43,60 @@ module ActiveRecord
# Quote date/time values for use in SQL input. Includes microseconds
# if the value is a Time responding to usec.
def quoted_date(value) #:nodoc:
- result = super
- if value.acts_like?(:time) && value.respond_to?(:usec)
- result = "#{result}.#{sprintf("%06d", value.usec)}"
+ if value.year <= 0
+ bce_year = format("%04d", -value.year + 1)
+ super.sub(/^-?\d+/, bce_year) + " BC"
+ else
+ super
end
+ end
- if value.year < 0
- result = result.sub(/^-/, "") + " BC"
+ # Does not quote function default values for UUID columns
+ def quote_default_value(value, column) #:nodoc:
+ if column.type == :uuid && value =~ /\(\)/
+ value
+ else
+ quote(value, column)
+ end
+ end
+
+ private
+
+ def _quote(value)
+ case value
+ when Type::Binary::Data
+ "'#{escape_bytea(value.to_s)}'"
+ when OID::Xml::Data
+ "xml '#{quote_string(value.to_s)}'"
+ when OID::Bit::Data
+ if value.binary?
+ "B'#{value}'"
+ elsif value.hex?
+ "X'#{value}'"
+ end
+ when Float
+ if value.infinite? || value.nan?
+ "'#{value}'"
+ else
+ super
+ end
+ else
+ super
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when Type::Binary::Data
+ # Return a bind param hash with format as binary.
+ # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
+ # for more information
+ { value: value.to_s, format: 1 }
+ when OID::Xml::Data, OID::Bit::Data
+ value.to_s
+ else
+ super
end
- result
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
index bc775394a6..52b307c432 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -1,12 +1,12 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- module ReferentialIntegrity
- def supports_disable_referential_integrity? #:nodoc:
+ module PostgreSQL
+ module ReferentialIntegrity # :nodoc:
+ def supports_disable_referential_integrity? # :nodoc:
true
end
- def disable_referential_integrity #:nodoc:
+ def disable_referential_integrity # :nodoc:
if supports_disable_referential_integrity?
begin
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
new file mode 100644
index 0000000000..a9522e152f
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -0,0 +1,150 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module ColumnMethods
+ def xml(*args)
+ options = args.extract_options!
+ column(args[0], :xml, options)
+ end
+
+ def tsvector(*args)
+ options = args.extract_options!
+ column(args[0], :tsvector, options)
+ end
+
+ def int4range(name, options = {})
+ column(name, :int4range, options)
+ end
+
+ def int8range(name, options = {})
+ column(name, :int8range, options)
+ end
+
+ def tsrange(name, options = {})
+ column(name, :tsrange, options)
+ end
+
+ def tstzrange(name, options = {})
+ column(name, :tstzrange, options)
+ end
+
+ def numrange(name, options = {})
+ column(name, :numrange, options)
+ end
+
+ def daterange(name, options = {})
+ column(name, :daterange, options)
+ end
+
+ def hstore(name, options = {})
+ column(name, :hstore, options)
+ end
+
+ def ltree(name, options = {})
+ column(name, :ltree, 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
+
+ def uuid(name, options = {})
+ column(name, :uuid, options)
+ end
+
+ def json(name, options = {})
+ column(name, :json, options)
+ end
+
+ def jsonb(name, options = {})
+ column(name, :jsonb, options)
+ end
+
+ def citext(name, options = {})
+ column(name, :citext, options)
+ end
+
+ def point(name, options = {})
+ column(name, :point, options)
+ end
+
+ def bit(name, options)
+ column(name, :bit, options)
+ end
+
+ def bit_varying(name, options)
+ column(name, :bit_varying, options)
+ end
+
+ def money(name, options)
+ column(name, :money, options)
+ end
+ end
+
+ class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
+ attr_accessor :array
+ end
+
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ include ColumnMethods
+
+ # Defines the primary key field.
+ # Use of the native PostgreSQL UUID type is supported, and can be used
+ # by defining your tables as such:
+ #
+ # create_table :stuffs, id: :uuid do |t|
+ # t.string :content
+ # t.timestamps
+ # end
+ #
+ # By default, this will use the +uuid_generate_v4()+ function from the
+ # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
+ # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
+ # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
+ # set the +:default+ option to +nil+:
+ #
+ # create_table :stuffs, id: false do |t|
+ # t.primary_key :id, :uuid, default: nil
+ # t.uuid :foo_id
+ # t.timestamps
+ # end
+ #
+ # You may also pass a different UUID generation function from +uuid-ossp+
+ # or another library.
+ #
+ # Note that setting the UUID primary key default value to +nil+ will
+ # require you to assure that you always provide a UUID value before saving
+ # a record (as primary keys cannot be +nil+). This might be done via the
+ # +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
+ def primary_key(name, type = :primary_key, options = {})
+ options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid
+ super
+ end
+
+ def new_column_definition(name, type, options) # :nodoc:
+ column = super
+ column.array = options[:array]
+ column
+ end
+
+ private
+
+ def create_column_definition(name, type)
+ PostgreSQL::ColumnDefinition.new name, type
+ end
+ end
+
+ class Table < ActiveRecord::ConnectionAdapters::Table
+ include ColumnMethods
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index e10b562fa4..a90adcf4aa 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -1,6 +1,39 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
+ class SchemaCreation < AbstractAdapter::SchemaCreation
+ private
+
+ def column_options(o)
+ column_options = super
+ column_options[:array] = o.array
+ column_options
+ end
+
+ def add_column_options!(sql, options)
+ if options[:array]
+ sql << '[]'
+ end
+ super
+ end
+
+ def quote_default_expression(value, column)
+ if column.type == :uuid && value =~ /\(\)/
+ value
+ else
+ super
+ end
+ end
+
+ def type_for_column(column)
+ if column.array
+ @conn.lookup_cast_type("#{column.sql_type}[]")
+ else
+ super
+ end
+ end
+ end
+
module SchemaStatements
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
@@ -10,7 +43,7 @@ module ActiveRecord
end
# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
- # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>,
+ # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>,
# <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
#
@@ -18,10 +51,10 @@ module ActiveRecord
# create_database config[:database], config
# create_database 'foo_development', encoding: 'unicode'
def create_database(name, options = {})
- options = options.reverse_merge(:encoding => "utf8")
+ options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
- option_string = options.symbolize_keys.sum do |key, value|
- case key
+ option_string = options.inject("") do |memo, (key, value)|
+ memo += case key
when :owner
" OWNER = \"#{value}\""
when :template
@@ -65,22 +98,23 @@ module ActiveRecord
# If the schema is not specified as part of +name+ then it will only find tables within
# the current schema search path (regardless of permissions to access tables in other schemas)
def table_exists?(name)
- schema, table = Utils.extract_schema_and_table(name.to_s)
- return false unless table
-
- binds = [[nil, table]]
- binds << [nil, schema] if schema
+ name = Utils.extract_schema_qualified_name(name.to_s)
+ return false unless name.identifier
exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
SELECT COUNT(*)
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind in ('v','r')
- AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
- AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
+ WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
+ AND c.relname = '#{name.identifier}'
+ AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
+ def drop_table(table_name, options = {})
+ execute "DROP TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
+ end
+
# Returns true if schema exists.
def schema_exists?(name)
exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
@@ -90,6 +124,19 @@ module ActiveRecord
SQL
end
+ def index_name_exists?(table_name, index_name, default)
+ exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
+ SELECT COUNT(*)
+ FROM pg_class t
+ INNER JOIN pg_index d ON t.oid = d.indrelid
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
+ WHERE i.relkind = 'i'
+ AND i.relname = '#{index_name}'
+ AND t.relname = '#{table_name}'
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
+ SQL
+ end
+
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil)
result = query(<<-SQL, 'SCHEMA')
@@ -120,12 +167,15 @@ module ActiveRecord
column_names = columns.values_at(*indkey).compact
- # 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]}] : {}
- where = inddef.scan(/WHERE (.+)$/).flatten[0]
+ unless column_names.empty?
+ # 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]}] : {}
+ where = inddef.scan(/WHERE (.+)$/).flatten[0]
+ using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
- column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where)
+ IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
+ end
end.compact
end
@@ -133,13 +183,17 @@ module ActiveRecord
def columns(table_name)
# Limit, precision, and scale are all handled by the superclass.
column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
- oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) {
- OID::Identity.new
- }
- PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f')
+ oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type)
+ default_value = extract_value_from_default(oid, default)
+ default_function = extract_default_function(default_value, default)
+ new_column(column_name, default_value, oid, type, notnull == 'f', default_function)
end
end
+ def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc:
+ PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function)
+ end
+
# Returns the current database name.
def current_database
query('select current_database()', 'SCHEMA')[0][0]
@@ -224,9 +278,9 @@ module ActiveRecord
def default_sequence_name(table_name, pk = nil) #:nodoc:
result = serial_sequence(table_name, pk || 'id')
return nil unless result
- result.split('.').last
+ Utils.extract_schema_qualified_name(result).to_s
rescue ActiveRecord::StatementInvalid
- "#{table_name}_#{pk || 'id'}_seq"
+ PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq").to_s
end
def serial_sequence(table, column)
@@ -236,6 +290,23 @@ module ActiveRecord
result.rows.first.first
end
+ # Sets the sequence of a table's primary key to the specified value.
+ def set_pk_sequence!(table, value) #:nodoc:
+ pk, sequence = pk_and_sequence_for(table)
+
+ if pk
+ if sequence
+ quoted_sequence = quote_table_name(sequence)
+
+ select_value <<-end_sql, 'SCHEMA'
+ SELECT setval('#{quoted_sequence}', #{value})
+ end_sql
+ else
+ @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
+ end
+ end
+ end
+
# Resets the sequence of a table's primary key to the maximum value.
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
unless pk and sequence
@@ -263,25 +334,29 @@ module ActiveRecord
# First try looking for a sequence with a dependency on the
# given table's primary key.
result = query(<<-end_sql, 'SCHEMA')[0]
- SELECT attr.attname, seq.relname
+ SELECT attr.attname, nsp.nspname, seq.relname
FROM pg_class seq,
pg_attribute attr,
pg_depend dep,
- pg_constraint cons
+ pg_constraint cons,
+ pg_namespace nsp
WHERE seq.oid = dep.objid
AND seq.relkind = 'S'
AND attr.attrelid = dep.refobjid
AND attr.attnum = dep.refobjsubid
AND attr.attrelid = cons.conrelid
AND attr.attnum = cons.conkey[1]
+ AND seq.relnamespace = nsp.oid
AND cons.contype = 'p'
+ AND dep.classid = 'pg_class'::regclass
AND dep.refobjid = '#{quote_table_name(table)}'::regclass
end_sql
if result.nil? or result.empty?
result = query(<<-end_sql, 'SCHEMA')[0]
- SELECT attr.attname,
+ SELECT attr.attname, nsp.nspname,
CASE
+ WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
@@ -291,62 +366,76 @@ module ActiveRecord
JOIN pg_attribute attr ON (t.oid = attrelid)
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
+ JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid)
WHERE t.oid = '#{quote_table_name(table)}'::regclass
AND cons.contype = 'p'
- AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval'
+ AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
end_sql
end
- [result.first, result.last]
+ pk = result.shift
+ if result.last
+ [pk, PostgreSQL::Name.new(*result)]
+ else
+ [pk, nil]
+ end
rescue
nil
end
# Returns just a table's primary key
def primary_key(table)
- row = exec_query(<<-end_sql, 'SCHEMA').rows.first
+ pks = exec_query(<<-end_sql, 'SCHEMA').rows
SELECT attr.attname
FROM pg_attribute attr
- INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
+ INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey)
WHERE cons.contype = 'p'
AND cons.conrelid = '#{quote_table_name(table)}'::regclass
end_sql
-
- row && row.first
+ return nil unless pks.count == 1
+ pks[0][0]
end
# Renames a table.
- # Also renames a table's primary key sequence if the sequence name matches the
- # Active Record default.
+ # Also renames a table's primary key sequence if the sequence name exists and
+ # matches the Active Record default.
#
# Example:
# rename_table('octopuses', 'octopi')
- def rename_table(name, new_name)
+ def rename_table(table_name, new_name)
clear_cache!
- execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
pk, seq = pk_and_sequence_for(new_name)
- if seq == "#{name}_#{pk}_seq"
+ if seq && seq.identifier == "#{table_name}_#{pk}_seq"
new_seq = "#{new_name}_#{pk}_seq"
+ idx = "#{table_name}_pkey"
+ new_idx = "#{new_name}_pkey"
execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
+ execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"
end
+
+ rename_table_indexes(table_name, new_name)
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
clear_cache!
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
-
- execute add_column_sql
+ super
end
# Changes the column of a table.
def change_column(table_name, column_name, type, options = {})
clear_cache!
quoted_table_name = quote_table_name(table_name)
-
- execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale])
+ sql_type << "[]" if options[:array]
+ sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}"
+ sql << " USING #{options[:using]}" if options[:using]
+ if options[:cast_as]
+ sql << " USING CAST(#{quote_column_name(column_name)} AS #{type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale])})"
+ end
+ execute sql
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
@@ -355,13 +444,24 @@ module ActiveRecord
# Changes the default value of a table column.
def change_column_default(table_name, column_name, default)
clear_cache!
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
+ column = column_for(table_name, column_name)
+ return unless column
+
+ alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
+ if default.nil?
+ # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
+ # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
+ execute alter_column_query % "DROP DEFAULT"
+ else
+ execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}"
+ end
end
def change_column_null(table_name, column_name, null, default = nil)
clear_cache!
unless null || default.nil?
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
+ column = column_for(table_name, column_name)
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column
end
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
end
@@ -370,6 +470,12 @@ module ActiveRecord
def rename_column(table_name, column_name, new_column_name)
clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
+ rename_column_indexes(table_name, column_name, new_column_name)
+ end
+
+ def add_index(table_name, column_name, options = {}) #:nodoc:
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
end
def remove_index!(table_name, index_name) #:nodoc:
@@ -377,9 +483,48 @@ module ActiveRecord
end
def rename_index(table_name, old_name, new_name)
+ validate_index_length!(table_name, new_name)
+
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
+ FROM pg_constraint c
+ JOIN pg_class t1 ON c.conrelid = t1.oid
+ JOIN pg_class t2 ON c.confrelid = t2.oid
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
+ WHERE c.contype = 'f'
+ AND t1.relname = #{quote(table_name)}
+ AND t3.nspname = ANY (current_schemas(false))
+ ORDER BY c.conname
+ SQL
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
+
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
+ def extract_foreign_key_action(specifier) # :nodoc:
+ case specifier
+ when 'c'; :cascade
+ when 'n'; :nullify
+ when 'r'; :restrict
+ end
+ end
+
def index_name_length
63
end
@@ -417,35 +562,23 @@ module ActiveRecord
when 0..6; "timestamp(#{precision})"
else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6")
end
- when 'intrange'
- return 'int4range' unless limit
-
- case limit
- when 1..4; 'int4range'
- when 5..8; 'int8range'
- else raise(ActiveRecordError, "No range type has byte size #{limit}. Use a numeric with precision 0 instead.")
- end
else
super
end
end
- # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
- #
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
# requires that the ORDER BY include the distinct column.
- #
- # distinct("posts.id", ["posts.created_at desc"])
- # # => "DISTINCT posts.id, posts.created_at AS alias_0"
- def distinct(columns, orders) #:nodoc:
- order_columns = orders.map{ |s|
+ def columns_for_distinct(columns, orders) #:nodoc:
+ order_columns = orders.reject(&:blank?).map{ |s|
# Convert Arel node to string
s = s.to_sql unless s.is_a?(String)
# Remove any ASC/DESC modifiers
- s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '')
+ s.gsub(/\s+(?:ASC|DESC)\b/i, '')
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '')
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
- [super].concat(order_columns).join(', ')
+ [super, *order_columns].join(', ')
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
new file mode 100644
index 0000000000..9a0b80d7d3
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
@@ -0,0 +1,77 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ # Value Object to hold a schema qualified name.
+ # This is usually the name of a PostgreSQL relation but it can also represent
+ # schema qualified type names. +schema+ and +identifier+ are unquoted to prevent
+ # double quoting.
+ class Name # :nodoc:
+ SEPARATOR = "."
+ attr_reader :schema, :identifier
+
+ def initialize(schema, identifier)
+ @schema, @identifier = unquote(schema), unquote(identifier)
+ end
+
+ def to_s
+ parts.join SEPARATOR
+ end
+
+ def quoted
+ if schema
+ PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier)
+ else
+ PGconn.quote_ident(identifier)
+ end
+ end
+
+ def ==(o)
+ o.class == self.class && o.parts == parts
+ end
+ alias_method :eql?, :==
+
+ def hash
+ parts.hash
+ end
+
+ protected
+ def unquote(part)
+ if part && part.start_with?('"')
+ part[1..-2]
+ else
+ part
+ end
+ end
+
+ def parts
+ @parts ||= [@schema, @identifier].compact
+ end
+ end
+
+ module Utils # :nodoc:
+ extend self
+
+ # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt>
+ # extracted from +string+.
+ # +schema+ is nil if not specified in +string+.
+ # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+)
+ # +string+ supports the range of schema/table references understood by PostgreSQL, for example:
+ #
+ # * <tt>table_name</tt>
+ # * <tt>"table.name"</tt>
+ # * <tt>schema_name.table_name</tt>
+ # * <tt>schema_name."table.name"</tt>
+ # * <tt>"schema_name".table_name</tt>
+ # * <tt>"schema.name"."table name"</tt>
+ def extract_schema_qualified_name(string)
+ schema, table = string.scan(/[^".\s]+|"[^"]*"/)
+ if table.nil?
+ table = schema
+ schema = nil
+ end
+ PostgreSQL::Name.new(schema, table)
+ end
+ end
+ 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 d62a375470..13bb5c187e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,38 +1,44 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/statement_pool'
+
+require 'active_record/connection_adapters/postgresql/utils'
+require 'active_record/connection_adapters/postgresql/column'
require 'active_record/connection_adapters/postgresql/oid'
-require 'active_record/connection_adapters/postgresql/cast'
-require 'active_record/connection_adapters/postgresql/array_parser'
require 'active_record/connection_adapters/postgresql/quoting'
+require 'active_record/connection_adapters/postgresql/referential_integrity'
+require 'active_record/connection_adapters/postgresql/schema_definitions'
require 'active_record/connection_adapters/postgresql/schema_statements'
require 'active_record/connection_adapters/postgresql/database_statements'
-require 'active_record/connection_adapters/postgresql/referential_integrity'
+
require 'arel/visitors/bind_visitor'
# Make sure we're using pg high enough for PGResult#values
-gem 'pg', '~> 0.11'
+gem 'pg', '~> 0.15'
require 'pg'
require 'ipaddr'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
+ VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
+ :client_encoding, :options, :application_name, :fallback_application_name,
+ :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count,
+ :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey,
+ :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service]
+
# Establishes a connection to the database that's used by all Active Record objects
- def postgresql_connection(config) # :nodoc:
+ def postgresql_connection(config)
conn_params = config.symbolize_keys
- # Forward any unused config params to PGconn.connect.
- [:statement_limit, :encoding, :min_messages, :schema_search_path,
- :schema_order, :adapter, :pool, :checkout_timeout, :template,
- :reaping_frequency, :insert_returning, :variables].each do |key|
- conn_params.delete key
- end
- conn_params.delete_if { |k,v| v.nil? }
+ conn_params.delete_if { |_, v| v.nil? }
# Map ActiveRecords param names to PGs.
conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
+ # Forward only valid config params to PGconn.connect.
+ conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) }
+
# The postgres drivers don't allow the creation of an unconnected PGconn object,
# so just pass a nil connection object for the time being.
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config)
@@ -40,194 +46,6 @@ module ActiveRecord
end
module ConnectionAdapters
- # PostgreSQL-specific extensions to column definitions in a table.
- class PostgreSQLColumn < Column #:nodoc:
- attr_accessor :array
- # Instantiates a new PostgreSQL column definition in a table.
- def initialize(name, default, oid_type, sql_type = nil, null = true)
- @oid_type = oid_type
- if sql_type =~ /\[\]$/
- @array = true
- super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null)
- else
- @array = false
- super(name, self.class.extract_value_from_default(default), sql_type, null)
- end
- end
-
- # :stopdoc:
- class << self
- include ConnectionAdapters::PostgreSQLColumn::Cast
- include ConnectionAdapters::PostgreSQLColumn::ArrayParser
- attr_accessor :money_precision
- end
- # :startdoc:
-
- # Extracts the value from a PostgreSQL column default definition.
- def self.extract_value_from_default(default)
- # This is a performance optimization for Ruby 1.9.2 in development.
- # If the value is nil, we return nil straight away without checking
- # the regular expressions. If we check each regular expression,
- # Regexp#=== will call NilClass#to_str, which will trigger
- # method_missing (defined by whiny nil in ActiveSupport) which
- # makes this method very very slow.
- return default unless default
-
- case default
- # Numeric types
- when /\A\(?(-?\d+(\.\d*)?\)?)\z/
- $1
- # Character types
- when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
- $1
- # Binary data types
- when /\A'(.*)'::bytea\z/m
- $1
- # Date/time types
- when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
- $1
- when /\A'(.*)'::interval\z/
- $1
- # Boolean type
- when 'true'
- true
- when 'false'
- false
- # Geometric types
- when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
- $1
- # Network address types
- when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
- $1
- # Bit string types
- when /\AB'(.*)'::"?bit(?: varying)?"?\z/
- $1
- # XML type
- when /\A'(.*)'::xml\z/m
- $1
- # Arrays
- when /\A'(.*)'::"?\D+"?\[\]\z/
- $1
- # Hstore
- when /\A'(.*)'::hstore\z/
- $1
- # JSON
- when /\A'(.*)'::json\z/
- $1
- # int4range, int8range
- when /\A'(.*)'::int(4|8)range\z/
- $1
- # Object identifier types
- when /\A-?\d+\z/
- $1
- else
- # Anything else is blank, some user type, or some function
- # and we can't know the value of that, so return nil.
- nil
- end
- end
-
- def type_cast(value)
- return if value.nil?
- return super if encoded?
-
- @oid_type.type_cast value
- end
-
- private
-
- def extract_limit(sql_type)
- case sql_type
- when /^bigint/i; 8
- when /^smallint/i; 2
- when /^timestamp/i; nil
- else super
- end
- end
-
- # Extracts the scale from PostgreSQL-specific data types.
- def extract_scale(sql_type)
- # Money type has a fixed scale of 2.
- sql_type =~ /^money/ ? 2 : super
- end
-
- # Extracts the precision from PostgreSQL-specific data types.
- def extract_precision(sql_type)
- if sql_type == 'money'
- self.class.money_precision
- elsif sql_type =~ /timestamp/i
- $1.to_i if sql_type =~ /\((\d+)\)/
- else
- super
- end
- end
-
- # Maps PostgreSQL-specific data types to logical Rails types.
- def simplified_type(field_type)
- case field_type
- # Numeric and monetary types
- when /^(?:real|double precision)$/
- :float
- # Monetary types
- when 'money'
- :decimal
- when 'hstore'
- :hstore
- # Network address types
- when 'inet'
- :inet
- when 'cidr'
- :cidr
- when 'macaddr'
- :macaddr
- # Character types
- when /^(?:character varying|bpchar)(?:\(\d+\))?$/
- :string
- # Binary data types
- when 'bytea'
- :binary
- # Date/time types
- when /^timestamp with(?:out)? time zone$/
- :datetime
- when /^interval(?:|\(\d+\))$/
- :string
- # Geometric types
- when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
- :string
- # Bit strings
- when /^bit(?: varying)?(?:\(\d+\))?$/
- :string
- # XML type
- when 'xml'
- :xml
- # tsvector type
- when 'tsvector'
- :tsvector
- # Arrays
- when /^\D+\[\]$/
- :string
- # Object identifier types
- when 'oid'
- :integer
- # UUID type
- when 'uuid'
- :uuid
- # JSON type
- when 'json'
- :json
- # int4range, int8range types
- when 'int4range', 'int8range'
- :intrange
- # Small and big integer types
- when /^(?:small|big)int$/
- :integer
- # Pass through all types that are not specific to PostgreSQL.
- else
- super
- end
- end
- end
-
# The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver.
#
# Options:
@@ -256,82 +74,28 @@ module ActiveRecord
# In addition, default connection parameters of libpq can be set per environment variables.
# See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
class PostgreSQLAdapter < AbstractAdapter
- class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :array
- end
-
- class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
- def xml(*args)
- options = args.extract_options!
- column(args[0], 'xml', options)
- end
-
- def tsvector(*args)
- options = args.extract_options!
- column(args[0], 'tsvector', options)
- end
-
- 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
-
- def uuid(name, options = {})
- column(name, 'uuid', options)
- end
-
- def json(name, options = {})
- column(name, 'json', options)
- end
-
- def intrange(name, options = {})
- column(name, 'intrange', options)
- end
-
- def column(name, type = nil, options = {})
- super
- column = self[name]
- column.array = options[:array]
-
- self
- end
-
- private
-
- def new_column_definition(base, name, type)
- definition = ColumnDefinition.new base, name, type
- @columns << definition
- @columns_hash[name] = definition
- definition
- end
- end
-
- ADAPTER_NAME = 'PostgreSQL'
+ ADAPTER_NAME = 'PostgreSQL'.freeze
NATIVE_DATABASE_TYPES = {
primary_key: "serial primary key",
- string: { name: "character varying", limit: 255 },
+ bigserial: "bigserial",
+ string: { name: "character varying" },
text: { name: "text" },
integer: { name: "integer" },
float: { name: "float" },
decimal: { name: "decimal" },
datetime: { name: "timestamp" },
- timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
+ daterange: { name: "daterange" },
+ numrange: { name: "numrange" },
+ tsrange: { name: "tsrange" },
+ tstzrange: { name: "tstzrange" },
+ int4range: { name: "int4range" },
+ int8range: { name: "int8range" },
binary: { name: "bytea" },
boolean: { name: "boolean" },
+ bigint: { name: "bigint" },
xml: { name: "xml" },
tsvector: { name: "tsvector" },
hstore: { name: "hstore" },
@@ -340,28 +104,52 @@ module ActiveRecord
macaddr: { name: "macaddr" },
uuid: { name: "uuid" },
json: { name: "json" },
- intrange: { name: "int4range" }
+ jsonb: { name: "jsonb" },
+ ltree: { name: "ltree" },
+ citext: { name: "citext" },
+ point: { name: "point" },
+ bit: { name: "bit" },
+ bit_varying: { name: "bit varying" },
+ money: { name: "money" },
}
- include Quoting
- include ReferentialIntegrity
- include SchemaStatements
- include DatabaseStatements
+ OID = PostgreSQL::OID #:nodoc:
+
+ include PostgreSQL::Quoting
+ include PostgreSQL::ReferentialIntegrity
+ include PostgreSQL::SchemaStatements
+ include PostgreSQL::DatabaseStatements
+ include Savepoints
+
+ def schema_creation # :nodoc:
+ PostgreSQL::SchemaCreation.new self
+ end
- # Returns 'PostgreSQL' as adapter name for identification purposes.
- def adapter_name
- ADAPTER_NAME
+ def column_spec_for_primary_key(column)
+ spec = {}
+ if column.serial?
+ return unless column.sql_type == 'bigint'
+ spec[:id] = ':bigserial'
+ elsif column.type == :uuid
+ spec[:id] = ':uuid'
+ spec[:default] = column.default_function.inspect
+ else
+ spec[:id] = column.type.inspect
+ spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) })
+ end
+ spec
end
- # Adds `:array` option to the default set provided by the
+ # Adds +:array+ option to the default set provided by the
# AbstractAdapter
- def prepare_column_options(column, types)
+ def prepare_column_options(column) # :nodoc:
spec = super
spec[:array] = 'true' if column.respond_to?(:array) && column.array
+ spec[:default] = "\"#{column.default_function}\"" if column.default_function
spec
end
- # Adds `:array` as a valid migration key
+ # Adds +:array+ as a valid migration key
def migration_keys
super + [:array]
end
@@ -384,6 +172,18 @@ module ActiveRecord
true
end
+ def supports_foreign_keys?
+ true
+ end
+
+ def supports_views?
+ true
+ end
+
+ def index_algorithms
+ { concurrently: 'CONCURRENTLY' }
+ end
+
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
super
@@ -437,22 +237,17 @@ module ActiveRecord
end
end
- class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc:
- include Arel::Visitors::BindVisitor
- end
-
# Initializes and connects a PostgreSQL adapter.
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
- if config.fetch(:prepared_statements) { true }
- @visitor = Arel::Visitors::PostgreSQL.new self
+ @visitor = Arel::Visitors::PostgreSQL.new self
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
else
- @visitor = BindSubstitution.new self
+ @prepared_statements = false
end
- connection_parameters.delete :prepared_statements
-
@connection_parameters, @config = connection_parameters, config
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
@@ -461,15 +256,16 @@ module ActiveRecord
connect
@statements = StatementPool.new @connection,
- config.fetch(:statement_limit) { 1000 }
+ self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })
if postgresql_version < 80200
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
end
- initialize_type_map
+ @type_map = Type::HashLookupTypeMap.new
+ initialize_type_map(type_map)
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
- @use_insert_returning = @config.key?(:insert_returning) ? @config[:insert_returning] : true
+ @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
# Clears the prepared statements cache.
@@ -477,6 +273,10 @@ module ActiveRecord
@statements.clear
end
+ def truncate(table_name, name = nil)
+ exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, []
+ end
+
# Is this connection alive and ready for queries?
def active?
@connection.query 'SELECT 1'
@@ -494,7 +294,12 @@ module ActiveRecord
def reset!
clear_cache!
- super
+ reset_transaction
+ unless @connection.transaction_status == ::PG::PQTRANS_IDLE
+ @connection.query 'ROLLBACK'
+ end
+ @connection.query 'DISCARD ALL'
+ configure_connection
end
# Disconnects from the database if already connected. Otherwise, this
@@ -526,34 +331,59 @@ module ActiveRecord
self.client_min_messages = old
end
- def supports_insert_with_returning?
+ def supports_ddl_transactions?
true
end
- def supports_ddl_transactions?
+ def supports_explain?
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
+ # Returns true if pg > 9.1
+ def supports_extensions?
+ postgresql_version >= 90100
end
- # Returns true.
- def supports_explain?
- true
+ # Range datatypes weren't introduced until PostgreSQL 9.2
+ def supports_ranges?
+ postgresql_version >= 90200
end
- # Returns the configured supported identifier length supported by PostgreSQL
- def table_alias_length
- @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
+ def supports_materialized_views?
+ postgresql_version >= 90300
end
- def add_column_options!(sql, options)
- if options[:array] || options[:column].try(:array)
- sql << '[]'
+ def enable_extension(name)
+ exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap {
+ reload_type_map
+ }
+ end
+
+ def disable_extension(name)
+ exec_query("DROP EXTENSION IF EXISTS \"#{name}\" CASCADE").tap {
+ reload_type_map
+ }
+ end
+
+ def extension_enabled?(name)
+ if supports_extensions?
+ res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled",
+ 'SCHEMA'
+ res.cast_values.first
end
- super
+ end
+
+ def extensions
+ if supports_extensions?
+ exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values
+ else
+ super
+ end
+ end
+
+ # Returns the configured supported identifier length supported by PostgreSQL
+ def table_alias_length
+ @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
end
# Set the authorized user for this session
@@ -562,29 +392,33 @@ module ActiveRecord
exec_query "SET SESSION AUTHORIZATION #{user}"
end
- module Utils
- extend self
+ def use_insert_returning?
+ @use_insert_returning
+ end
+
+ def valid_type?(type)
+ !native_database_types[type].nil?
+ end
- # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
- # +schema_name+ is nil if not specified in +name+.
- # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
- # +name+ supports the range of schema/table references understood by PostgreSQL, for example:
- #
- # * <tt>table_name</tt>
- # * <tt>"table.name"</tt>
- # * <tt>schema_name.table_name</tt>
- # * <tt>schema_name."table.name"</tt>
- # * <tt>"schema.name"."table name"</tt>
- def extract_schema_and_table(name)
- table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
- [schema, table]
- end
+ def update_table_definition(table_name, base) #:nodoc:
+ PostgreSQL::Table.new(table_name, base)
end
- def use_insert_returning?
- @use_insert_returning
+ def lookup_cast_type(sql_type) # :nodoc:
+ oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i
+ super(oid)
end
+ def column_name_for_operation(operation, node) # :nodoc:
+ OPERATION_ALIASES.fetch(operation) { operation.downcase }
+ end
+
+ OPERATION_ALIASES = { # :nodoc:
+ "maximum" => "max",
+ "minimum" => "min",
+ "average" => "avg",
+ }
+
protected
# Returns the version of the connected PostgreSQL server.
@@ -597,6 +431,8 @@ module ActiveRecord
UNIQUE_VIOLATION = "23505"
def translate_exception(exception, message)
+ return exception unless exception.respond_to?(:result)
+
case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE)
when UNIQUE_VIOLATION
RecordNotUnique.new(message, exception)
@@ -609,63 +445,194 @@ module ActiveRecord
private
- def initialize_type_map
- result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
- leaves, nodes = result.partition { |row| row['typelem'] == '0' }
+ def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc:
+ if !type_map.key?(oid)
+ load_additional_types(type_map, [oid])
+ end
+
+ type_map.fetch(oid, fmod, sql_type) {
+ warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
+ Type::Value.new.tap do |cast_type|
+ type_map.register_type(oid, cast_type)
+ end
+ }
+ end
+
+ def initialize_type_map(m) # :nodoc:
+ register_class_with_limit m, 'int2', OID::Integer
+ register_class_with_limit m, 'int4', OID::Integer
+ register_class_with_limit m, 'int8', OID::Integer
+ m.alias_type 'oid', 'int2'
+ m.register_type 'float4', OID::Float.new
+ m.alias_type 'float8', 'float4'
+ m.register_type 'text', Type::Text.new
+ register_class_with_limit m, 'varchar', Type::String
+ m.alias_type 'char', 'varchar'
+ m.alias_type 'name', 'varchar'
+ m.alias_type 'bpchar', 'varchar'
+ m.register_type 'bool', Type::Boolean.new
+ register_class_with_limit m, 'bit', OID::Bit
+ register_class_with_limit m, 'varbit', OID::BitVarying
+ m.alias_type 'timestamptz', 'timestamp'
+ m.register_type 'date', OID::Date.new
+ m.register_type 'time', OID::Time.new
+
+ m.register_type 'money', OID::Money.new
+ m.register_type 'bytea', OID::Bytea.new
+ m.register_type 'point', OID::Point.new
+ m.register_type 'hstore', OID::Hstore.new
+ m.register_type 'json', OID::Json.new
+ m.register_type 'jsonb', OID::Jsonb.new
+ m.register_type 'cidr', OID::Cidr.new
+ m.register_type 'inet', OID::Inet.new
+ m.register_type 'uuid', OID::Uuid.new
+ m.register_type 'xml', OID::Xml.new
+ m.register_type 'tsvector', OID::SpecializedString.new(:tsvector)
+ m.register_type 'macaddr', OID::SpecializedString.new(:macaddr)
+ m.register_type 'citext', OID::SpecializedString.new(:citext)
+ m.register_type 'ltree', OID::SpecializedString.new(:ltree)
+
+ # FIXME: why are we keeping these types as strings?
+ m.alias_type 'interval', 'varchar'
+ m.alias_type 'path', 'varchar'
+ m.alias_type 'line', 'varchar'
+ m.alias_type 'polygon', 'varchar'
+ m.alias_type 'circle', 'varchar'
+ m.alias_type 'lseg', 'varchar'
+ m.alias_type 'box', 'varchar'
+
+ m.register_type 'timestamp' do |_, _, sql_type|
+ precision = extract_precision(sql_type)
+ OID::DateTime.new(precision: precision)
+ end
+
+ m.register_type 'numeric' do |_, fmod, sql_type|
+ precision = extract_precision(sql_type)
+ scale = extract_scale(sql_type)
+
+ # The type for the numeric depends on the width of the field,
+ # so we'll do something special here.
+ #
+ # When dealing with decimal columns:
+ #
+ # places after decimal = fmod - 4 & 0xffff
+ # places before decimal = (fmod - 4) >> 16 & 0xffff
+ if fmod && (fmod - 4 & 0xffff).zero?
+ # FIXME: Remove this class, and the second argument to
+ # lookups on PG
+ Type::DecimalWithoutScale.new(precision: precision)
+ else
+ OID::Decimal.new(precision: precision, scale: scale)
+ end
+ end
+
+ load_additional_types(m)
+ end
- # populate the leaf nodes
- leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
- OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']]
+ def extract_limit(sql_type) # :nodoc:
+ case sql_type
+ when /^bigint/i, /^int8/i
+ 8
+ when /^smallint/i
+ 2
+ else
+ super
end
+ end
+
+ # Extracts the value from a PostgreSQL column default definition.
+ def extract_value_from_default(oid, default) # :nodoc:
+ case default
+ # Quoted types
+ when /\A[\(B]?'(.*)'::/m
+ $1.gsub(/''/, "'")
+ # Boolean types
+ when 'true', 'false'
+ default
+ # Numeric types
+ when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
+ $1
+ # Object identifier types
+ when /\A-?\d+\z/
+ $1
+ else
+ # Anything else is blank, some user type, or some function
+ # and we can't know the value of that, so return nil.
+ nil
+ end
+ end
+
+ def extract_default_function(default_value, default) # :nodoc:
+ default if has_default_function?(default_value, default)
+ end
- arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
+ def has_default_function?(default_value, default) # :nodoc:
+ !default_value && (%r{\w+\(.*\)} === default)
+ end
- # populate composite types
- nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i]
- OID::TYPE_MAP[row['oid'].to_i] = vector
+ def load_additional_types(type_map, oids = nil) # :nodoc:
+ if supports_ranges?
+ query = <<-SQL
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
+ FROM pg_type as t
+ LEFT JOIN pg_range as r ON oid = rngtypid
+ SQL
+ else
+ query = <<-SQL
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype
+ FROM pg_type as t
+ SQL
end
- # populate array types
- arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i]
- OID::TYPE_MAP[row['oid'].to_i] = array
+ if oids
+ query += "WHERE t.oid::integer IN (%s)" % oids.join(", ")
end
+
+ initializer = OID::TypeMapInitializer.new(type_map)
+ records = execute(query, 'SCHEMA')
+ initializer.run(records)
end
- FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
+ FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
- def exec_no_cache(sql, binds)
- @connection.async_exec(sql)
+ def execute_and_clear(sql, name, binds)
+ result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
+ exec_cache(sql, name, binds)
+ ret = yield result
+ result.clear
+ ret
end
- def exec_cache(sql, binds)
- begin
- stmt_key = prepare_statement sql
+ def exec_no_cache(sql, name, binds)
+ log(sql, name, binds) { @connection.async_exec(sql, []) }
+ end
- # Clear the queue
- @connection.get_last_result
- @connection.send_query_prepared(stmt_key, binds.map { |col, val|
- type_cast(val, col)
- })
- @connection.block
- @connection.get_last_result
- rescue PGError => e
- # Get the PG code for the failure. Annoyingly, the code for
- # prepared statements whose return value may have changed is
- # FEATURE_NOT_SUPPORTED. Check here for more details:
- # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
- begin
- code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
- rescue
- raise e
- end
- if FEATURE_NOT_SUPPORTED == code
- @statements.delete sql_key(sql)
- retry
- else
- raise e
- end
+ def exec_cache(sql, name, binds)
+ stmt_key = prepare_statement(sql)
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+
+ log(sql, name, type_casted_binds, stmt_key) do
+ @connection.exec_prepared(stmt_key, type_casted_binds.map { |_, val| val })
+ end
+ rescue ActiveRecord::StatementInvalid => e
+ pgerror = e.original_exception
+
+ # Get the PG code for the failure. Annoyingly, the code for
+ # prepared statements whose return value may have changed is
+ # FEATURE_NOT_SUPPORTED. Check here for more details:
+ # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
+ begin
+ code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
+ rescue
+ raise e
+ end
+ if FEATURE_NOT_SUPPORTED == code
+ @statements.delete sql_key(sql)
+ retry
+ else
+ raise e
end
end
@@ -681,17 +648,18 @@ module ActiveRecord
sql_key = sql_key(sql)
unless @statements.key? sql_key
nextkey = @statements.next_key
- @connection.prepare nextkey, sql
+ begin
+ @connection.prepare nextkey, sql
+ rescue => e
+ raise translate_exception_class(e, sql)
+ end
+ # Clear the queue
+ @connection.get_last_result
@statements[sql_key] = nextkey
end
@statements[sql_key]
end
- # The internal PostgreSQL identifier of the money data type.
- MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
- # The internal PostgreSQL identifier of the BYTEA data type.
- BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
-
# Connects to a PostgreSQL server and sets up the adapter depending on the
# connected server's characteristics.
def connect
@@ -700,9 +668,15 @@ module ActiveRecord
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
# should know about this but can't detect it there, so deal with it here.
- PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
+ OID::Money.precision = (postgresql_version >= 80300) ? 19 : 10
configure_connection
+ rescue ::PG::Error => error
+ if error.message.include?("does not exist")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -732,9 +706,9 @@ module ActiveRecord
variables.map do |k, v|
if v == ':default' || v == :default
# Sets the value to the global or compile default
- execute("SET SESSION #{k.to_s} TO DEFAULT", 'SCHEMA')
+ execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA')
elsif !v.nil?
- execute("SET SESSION #{k.to_s} TO #{quote(v)}", 'SCHEMA')
+ execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA')
end
end
end
@@ -752,20 +726,6 @@ module ActiveRecord
exec_query("SELECT currval('#{sequence_name}')", 'SQL')
end
- # Executes a SELECT query and returns the results, performing any data type
- # conversions that are required to be performed here instead of in PostgreSQLColumn.
- def select(sql, name = nil, binds = [])
- exec_query(sql, name, binds)
- end
-
- def select_raw(sql, name = nil)
- res = execute(sql, name)
- results = result_as_array(res)
- fields = res.fields
- res.clear
- return fields, results
- end
-
# Returns the list of a table's column names, data types, and default values.
#
# The underlying query is roughly:
@@ -784,7 +744,7 @@ module ActiveRecord
# Query implementation notes:
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
- def column_definitions(table_name) #:nodoc:
+ def column_definitions(table_name) # :nodoc:
exec_query(<<-end_sql, 'SCHEMA').rows
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
@@ -796,23 +756,13 @@ module ActiveRecord
end_sql
end
- def extract_pg_identifier_from_name(name)
- match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
-
- if match_data
- rest = name[match_data[0].length, name.length]
- rest = rest[1, rest.length] if rest.start_with? "."
- [match_data[1], (rest.length > 0 ? rest : nil)]
- end
- end
-
- def extract_table_ref_from_insert_sql(sql)
- sql[/into\s+([^\(]*).*values\s*\(/i]
+ def extract_table_ref_from_insert_sql(sql) # :nodoc:
+ sql[/into\s+([^\(]*).*values\s*\(/im]
$1.strip if $1
end
- def table_definition
- TableDefinition.new(self)
+ def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
+ PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 5839d1d3b4..37ff4e4613 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module ConnectionAdapters
class SchemaCache
- attr_reader :primary_keys, :tables, :version
+ attr_reader :version
attr_accessor :connection
def initialize(conn)
@@ -11,11 +11,15 @@ module ActiveRecord
@columns_hash = {}
@primary_keys = {}
@tables = {}
- prepare_default_proc
+ end
+
+ def primary_keys(table_name)
+ @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil
end
# A cached lookup for table existence.
def table_exists?(name)
+ prepare_tables if @tables.empty?
return @tables[name] if @tables.key? name
@tables[name] = connection.table_exists?(name)
@@ -24,29 +28,27 @@ module ActiveRecord
# Add internal cache for table with +table_name+.
def add(table_name)
if table_exists?(table_name)
- @primary_keys[table_name]
- @columns[table_name]
- @columns_hash[table_name]
+ primary_keys(table_name)
+ columns(table_name)
+ columns_hash(table_name)
end
end
+ def tables(name)
+ @tables[name]
+ end
+
# Get the columns for a table
- def columns(table = nil)
- if table
- @columns[table]
- else
- @columns
- end
+ def columns(table_name)
+ @columns[table_name] ||= connection.columns(table_name)
end
# Get the columns for a table as a hash, key is the column name
# value is the column object.
- def columns_hash(table = nil)
- if table
- @columns_hash[table]
- else
- @columns_hash
- end
+ def columns_hash(table_name)
+ @columns_hash[table_name] ||= Hash[columns(table_name).map { |col|
+ [col.name, col]
+ }]
end
# Clears out internal caches
@@ -58,6 +60,10 @@ module ActiveRecord
@version = nil
end
+ def size
+ [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+
+ end
+
# Clear out internal caches for table with +table_name+.
def clear_table_cache!(table_name)
@columns.delete table_name
@@ -69,33 +75,18 @@ module ActiveRecord
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = ActiveRecord::Migrator.current_version
- [@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val|
- self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h }
- end
+ [@version, @columns, @columns_hash, @primary_keys, @tables]
end
def marshal_load(array)
@version, @columns, @columns_hash, @primary_keys, @tables = array
- prepare_default_proc
end
private
- def prepare_default_proc
- @columns.default_proc = Proc.new do |h, table_name|
- h[table_name] = connection.columns(table_name)
- end
-
- @columns_hash.default_proc = Proc.new do |h, table_name|
- h[table_name] = Hash[columns[table_name].map { |col|
- [col.name, col]
- }]
+ def prepare_tables
+ connection.tables.each { |table| @tables[table] = true }
end
-
- @primary_keys.default_proc = Proc.new do |h, table_name|
- h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 11e8197293..03dfd29a0a 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -6,40 +6,56 @@ gem 'sqlite3', '~> 1.3.6'
require 'sqlite3'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# sqlite3 adapter reuses sqlite_connection.
- def sqlite3_connection(config) # :nodoc:
+ def sqlite3_connection(config)
# Require database.
unless config[:database]
raise ArgumentError, "No database file specified. Missing argument: database"
end
- # Allow database path relative to Rails.root, but only if
- # the database path is not the special path that tells
- # Sqlite to build a database only in memory.
- if defined?(Rails.root) && ':memory:' != config[:database]
- config[:database] = File.expand_path(config[:database], Rails.root)
+ # Allow database path relative to Rails.root, but only if the database
+ # path is not the special path that tells sqlite to build a database only
+ # in memory.
+ if ':memory:' != config[:database]
+ config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
+ dirname = File.dirname(config[:database])
+ Dir.mkdir(dirname) unless File.directory?(dirname)
end
db = SQLite3::Database.new(
- config[:database],
+ config[:database].to_s,
:results_as_hash => true
)
- db.busy_timeout(config[:timeout]) if config[:timeout]
+ db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
- ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
+ ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
+ rescue Errno::ENOENT => error
+ if error.message.include?("No such file or directory")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
end
module ConnectionAdapters #:nodoc:
- class SQLite3Column < Column #:nodoc:
- class << self
- def binary_to_string(value)
- if value.encoding != Encoding::ASCII_8BIT
- value = value.force_encoding(Encoding::ASCII_8BIT)
- end
- value
+ class SQLite3Binary < Type::Binary # :nodoc:
+ def cast_value(value)
+ if value.encoding != Encoding::ASCII_8BIT
+ value = value.force_encoding(Encoding::ASCII_8BIT)
+ end
+ value
+ end
+ end
+
+ class SQLite3String < Type::String # :nodoc:
+ def type_cast_for_database(value)
+ if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT
+ value.encode(Encoding::UTF_8)
+ else
+ super
end
end
end
@@ -51,15 +67,32 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
+ ADAPTER_NAME = 'SQLite'.freeze
+ include Savepoints
+
+ NATIVE_DATABASE_TYPES = {
+ primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
+ string: { name: "varchar" },
+ text: { name: "text" },
+ integer: { name: "integer" },
+ float: { name: "float" },
+ decimal: { name: "decimal" },
+ datetime: { name: "datetime" },
+ time: { name: "time" },
+ date: { name: "date" },
+ binary: { name: "blob" },
+ boolean: { name: "boolean" }
+ }
+
class Version
include Comparable
def initialize(version_string)
- @version = version_string.split('.').map { |v| v.to_i }
+ @version = version_string.split('.').map(&:to_i)
end
def <=>(version_string)
- @version <=> version_string.split('.').map { |v| v.to_i }
+ @version <=> version_string.split('.').map(&:to_i)
end
end
@@ -82,7 +115,7 @@ module ActiveRecord
end
def clear
- cache.values.each do |hash|
+ cache.each_value do |hash|
dealloc hash[:stmt]
end
cache.clear
@@ -98,37 +131,33 @@ module ActiveRecord
end
end
- class BindSubstitution < Arel::Visitors::SQLite # :nodoc:
- include Arel::Visitors::BindVisitor
- end
-
- def initialize(connection, logger, config)
+ def initialize(connection, logger, connection_options, config)
super(connection, logger)
@active = nil
@statements = StatementPool.new(@connection,
- config.fetch(:statement_limit) { 1000 })
+ self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@config = config
- if config.fetch(:prepared_statements) { true }
- @visitor = Arel::Visitors::SQLite.new self
+ @visitor = Arel::Visitors::SQLite.new self
+
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
else
- @visitor = BindSubstitution.new self
+ @prepared_statements = false
end
end
- def adapter_name #:nodoc:
- 'SQLite'
- end
-
- # Returns true
def supports_ddl_transactions?
true
end
- # Returns true if SQLite version is '3.6.8' or greater, false otherwise.
def supports_savepoints?
- sqlite_version >= '3.6.8'
+ true
+ end
+
+ def supports_partial_index?
+ sqlite_version >= '3.8.0'
end
# Returns true, since this connection adapter supports prepared statement
@@ -142,7 +171,6 @@ module ActiveRecord
true
end
- # Returns true.
def supports_primary_key? #:nodoc:
true
end
@@ -151,8 +179,7 @@ module ActiveRecord
true
end
- # Returns true
- def supports_add_column?
+ def supports_views?
true
end
@@ -173,35 +200,19 @@ module ActiveRecord
@statements.clear
end
- # Returns true
- def supports_count_distinct? #:nodoc:
- true
- end
-
- # Returns true
- def supports_autoincrement? #:nodoc:
+ def supports_index_sort_order?
true
end
- def supports_index_sort_order?
- true
+ # Returns 62. SQLite supports index names up to 64
+ # characters. The rest is used by rails internally to perform
+ # temporary rename operations
+ def allowed_index_name_length
+ index_name_length - 2
end
def native_database_types #:nodoc:
- {
- :primary_key => default_primary_key_type,
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "integer" },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "boolean" }
- }
+ NATIVE_DATABASE_TYPES
end
# Returns the current database encoding format as a string, eg: 'UTF-8'
@@ -209,58 +220,49 @@ module ActiveRecord
@connection.encoding.to_s
end
- # Returns true.
def supports_explain?
true
end
# QUOTING ==================================================
- def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
- "x'#{s}'"
+ def _quote(value) # :nodoc:
+ case value
+ when Type::Binary::Data
+ "x'#{value.hex}'"
else
super
end
end
- def quote_string(s) #:nodoc:
- @connection.class.quote(s)
- end
-
- def quote_column_name(name) #:nodoc:
- %Q("#{name.to_s.gsub('"', '""')}")
- end
-
- # Quote date/time values for use in SQL input. Includes microseconds
- # if the value is a Time responding to usec.
- def quoted_date(value) #:nodoc:
- if value.respond_to?(:usec)
- "#{super}.#{sprintf("%06d", value.usec)}"
+ def _type_cast(value) # :nodoc:
+ case value
+ when BigDecimal
+ value.to_f
else
super
end
end
- def type_cast(value, column) # :nodoc:
- return value.to_f if BigDecimal === value
- return super unless String === value
- return super unless column && value
+ def quote_string(s) #:nodoc:
+ @connection.class.quote(s)
+ end
- value = super
- if column.type == :string && value.encoding == Encoding::ASCII_8BIT
- logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger
- value = value.encode Encoding::UTF_8
- end
- value
+ def quote_table_name_for_assignment(table, attr)
+ quote_column_name(attr)
end
+ def quote_column_name(name) #:nodoc:
+ %Q("#{name.to_s.gsub('"', '""')}")
+ end
+
+ #--
# DATABASE STATEMENTS ======================================
+ #++
def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
- ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', []))
end
class ExplainPrettyPrinter
@@ -278,14 +280,20 @@ module ActiveRecord
end
def exec_query(sql, name = nil, binds = [])
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
- # Don't cache statements without bind values
- if binds.empty?
+ log(sql, name, type_casted_binds) do
+ # Don't cache statements if they are not prepared
+ if without_prepared_statement?(binds)
stmt = @connection.prepare(sql)
- cols = stmt.columns
- records = stmt.to_a
- stmt.close
+ begin
+ cols = stmt.columns
+ records = stmt.to_a
+ ensure
+ stmt.close
+ end
stmt = records
else
cache = @statements[sql] ||= {
@@ -294,9 +302,7 @@ module ActiveRecord
stmt = cache[:stmt]
cols = cache[:cols] ||= stmt.columns
stmt.reset!
- stmt.bind_params binds.map { |col, val|
- type_cast(val, col)
- }
+ stmt.bind_params type_casted_binds.map { |_, val| val }
end
ActiveRecord::Result.new(cols, stmt.to_a)
@@ -333,20 +339,8 @@ module ActiveRecord
end
alias :create :insert_sql
- def select_rows(sql, name = nil)
- exec_query(sql, name).rows
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
+ def select_rows(sql, name = nil, binds = [])
+ exec_query(sql, name, binds).rows
end
def begin_db_transaction #:nodoc:
@@ -357,7 +351,7 @@ module ActiveRecord
log('commit transaction',nil) { @connection.commit }
end
- def rollback_db_transaction #:nodoc:
+ def exec_rollback_db_transaction #:nodoc:
log('rollback transaction',nil) { @connection.rollback }
end
@@ -367,7 +361,7 @@ module ActiveRecord
sql = <<-SQL
SELECT name
FROM sqlite_master
- WHERE type = 'table' AND NOT name = 'sqlite_sequence'
+ WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence'
SQL
sql << " AND name = #{quote_table_name(table_name)}" if table_name
@@ -380,7 +374,7 @@ module ActiveRecord
table_name && tables(nil, table_name).any?
end
- # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+.
+ # Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name) #:nodoc:
table_structure(table_name).map do |field|
case field["dflt_value"]
@@ -392,28 +386,41 @@ module ActiveRecord
field["dflt_value"] = $1.gsub('""', '"')
end
- SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0)
+ sql_type = field['type']
+ cast_type = lookup_cast_type(sql_type)
+ new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0)
end
end
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil) #:nodoc:
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row|
+ sql = <<-SQL
+ SELECT sql
+ FROM sqlite_master
+ WHERE name=#{quote(row['name'])} AND type='index'
+ UNION ALL
+ SELECT sql
+ FROM sqlite_temp_master
+ WHERE name=#{quote(row['name'])} AND type='index'
+ SQL
+ index_sql = exec_query(sql).first['sql']
+ match = /\sWHERE\s+(.+)$/i.match(index_sql)
+ where = match[1] if match
IndexDefinition.new(
table_name,
row['name'],
row['unique'] != 0,
exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col|
col['name']
- })
+ }, nil, nil, where)
end
end
def primary_key(table_name) #:nodoc:
- column = table_structure(table_name).find { |field|
- field['pk'] == 1
- }
- column && column['name']
+ pks = table_structure(table_name).select { |f| f['pk'] > 0 }
+ return nil unless pks.count == 1
+ pks[0]['name']
end
def remove_index!(table_name, index_name) #:nodoc:
@@ -424,18 +431,19 @@ module ActiveRecord
#
# Example:
# rename_table('octopuses', 'octopi')
- def rename_table(name, new_name)
- exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
+ def rename_table(table_name, new_name)
+ exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
+ rename_table_indexes(table_name, new_name)
end
# See: http://www.sqlite.org/lang_altertable.html
# SQLite has an additional restriction on the ALTER TABLE statement
- def valid_alter_table_options( type, options)
+ def valid_alter_table_type?(type)
type.to_sym != :primary_key
end
def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if supports_add_column? && valid_alter_table_options( type, options )
+ if valid_alter_table_type?(type)
super(table_name, column_name, type, options)
else
alter_table(table_name) do |definition|
@@ -446,7 +454,7 @@ module ActiveRecord
def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc:
alter_table(table_name) do |definition|
- definition.columns.delete(definition[column_name])
+ definition.remove_column column_name
end
end
@@ -480,15 +488,17 @@ module ActiveRecord
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
- unless columns(table_name).detect{|c| c.name == column_name.to_s }
- raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
- end
- alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
+ column = column_for(table_name, column_name)
+ alter_table(table_name, rename: {column.name => new_column_name.to_s})
+ rename_column_indexes(table_name, column.name, new_column_name)
end
protected
- def select(sql, name = nil, binds = []) #:nodoc:
- exec_query(sql, name, binds)
+
+ def initialize_type_map(m)
+ super
+ m.register_type(/binary/i, SQLite3Binary.new)
+ register_class_with_limit m, %r(char)i, SQLite3String
end
def table_structure(table_name)
@@ -498,7 +508,7 @@ module ActiveRecord
end
def alter_table(table_name, options = {}) #:nodoc:
- altered_table_name = "altered_#{table_name}"
+ altered_table_name = "a#{table_name}"
caller = lambda {|definition| yield definition if block_given?}
transaction do
@@ -535,27 +545,27 @@ module ActiveRecord
end
copy_table_indexes(from, to, options[:rename] || {})
copy_table_contents(from, to,
- @definition.columns.map {|column| column.name},
+ @definition.columns.map(&:name),
options[:rename] || {})
end
def copy_table_indexes(from, to, rename = {}) #:nodoc:
indexes(from).each do |index|
name = index.name
- if to == "altered_#{from}"
- name = "temp_#{name}"
- elsif from == "altered_#{to}"
- name = name[5..-1]
+ if to == "a#{from}"
+ name = "t#{name}"
+ elsif from == "a#{to}"
+ name = name[1..-1]
end
- to_column_names = columns(to).map { |c| c.name }
+ to_column_names = columns(to).map(&:name)
columns = index.columns.map {|c| rename[c] || c }.select do |column|
to_column_names.include?(column)
end
unless columns.empty?
# index name can't be the same
- opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_") }
+ opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true }
opts[:unique] = true if index.unique
add_index(to, columns, opts)
end
@@ -565,40 +575,32 @@ module ActiveRecord
def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
column_mappings = Hash[columns.map {|name| [name, name]}]
rename.each { |a| column_mappings[a.last] = a.first }
- from_columns = columns(from).collect {|col| col.name}
+ from_columns = columns(from).collect(&:name)
columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
+ from_columns_to_copy = columns.map { |col| column_mappings[col] }
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
+ quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ','
- quoted_to = quote_table_name(to)
- exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row|
- sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
- sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
- sql << ')'
- exec_query sql
- end
+ exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns})
+ SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}")
end
def sqlite_version
@sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)'))
end
- def default_primary_key_type
- if supports_autoincrement?
- 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'
- else
- 'INTEGER PRIMARY KEY NOT NULL'
- end
- end
-
def translate_exception(exception, message)
case exception.message
- when /column(s)? .* (is|are) not unique/
+ # SQLite 3.8.2 returns a newly formatted error message:
+ # UNIQUE constraint failed: *table_name*.*column_name*
+ # Older versions of SQLite return:
+ # column *column_name* is not unique
+ when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/
RecordNotUnique.new(message, exception)
else
super
end
end
-
end
end
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index d6d998c7be..984af79642 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -1,5 +1,8 @@
module ActiveRecord
module ConnectionHandling
+ RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] }
+ DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" }
+
# 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):
@@ -15,15 +18,15 @@ module ActiveRecord
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
- # adapter: "sqlite",
- # database: "path/to/dbfile"
+ # adapter: "sqlite3",
+ # 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"
+ # "adapter" => "sqlite3",
+ # "database" => "path/to/dbfile"
# )
#
# Or a URL:
@@ -32,11 +35,19 @@ module ActiveRecord
# "postgres://myuser:mypass@localhost/somedatabase"
# )
#
+ # In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails
+ # automatically loads the contents of config/database.yml into it),
+ # a symbol can also be given as argument, representing a key in the
+ # configuration hash:
+ #
+ # ActiveRecord::Base.establish_connection(:production)
+ #
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
# may be returned on an error.
- def establish_connection(spec = ENV["DATABASE_URL"])
- resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations
- spec = resolver.spec
+ def establish_connection(spec = nil)
+ spec ||= DEFAULT_ENV.call.to_sym
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
+ spec = resolver.spec(spec)
unless respond_to?(spec.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
@@ -46,6 +57,29 @@ module ActiveRecord
connection_handler.establish_connection self, spec
end
+ class MergeAndResolveDefaultUrlConfig # :nodoc:
+ def initialize(raw_configurations)
+ @raw_config = raw_configurations.dup
+ @env = DEFAULT_ENV.call.to_s
+ end
+
+ # Returns fully resolved connection hashes.
+ # Merges connection information from `ENV['DATABASE_URL']` if available.
+ def resolve
+ ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all
+ end
+
+ private
+ def config
+ @raw_config.dup.tap do |cfg|
+ if url = ENV['DATABASE_URL']
+ cfg[@env] ||= {}
+ cfg[@env]["url"] ||= url
+ end
+ end
+ end
+ end
+
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
@@ -54,11 +88,11 @@ module ActiveRecord
end
def connection_id
- Thread.current['ActiveRecord::Base.connection_id']
+ ActiveRecord::RuntimeRegistry.connection_id
end
def connection_id=(connection_id)
- Thread.current['ActiveRecord::Base.connection_id'] = connection_id
+ ActiveRecord::RuntimeRegistry.connection_id = connection_id
end
# Returns the configuration of the associated connection as a hash:
@@ -79,7 +113,7 @@ module ActiveRecord
connection_handler.retrieve_connection(self)
end
- # Returns true if Active Record is connected.
+ # Returns +true+ if Active Record is connected.
def connected?
connection_handler.connected?(self)
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 94c6684700..38b2d632d2 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -1,6 +1,7 @@
+require 'thread'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/object/duplicable'
-require 'thread'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
module Core
@@ -16,7 +17,6 @@ module ActiveRecord
mattr_accessor :logger, instance_writer: false
##
- # :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
# as a Hash.
#
@@ -42,9 +42,16 @@ module ActiveRecord
# 'database' => 'db/production.sqlite3'
# }
# }
- mattr_accessor :configurations, instance_writer: false
+ def self.configurations=(config)
+ @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
+ end
self.configurations = {}
+ # Returns fully resolved configurations hash
+ def self.configurations
+ @@configurations
+ end
+
##
# :singleton-method:
# Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
@@ -69,32 +76,133 @@ module ActiveRecord
mattr_accessor :timestamped_migrations, instance_writer: false
self.timestamped_migrations = true
- class_attribute :connection_handler, instance_writer: false
- self.connection_handler = ConnectionAdapters::ConnectionHandler.new
+ ##
+ # :singleton-method:
+ # Specify whether schema dump should happen at the end of the
+ # db:migrate rake task. This is true by default, which is useful for the
+ # development environment. This should ideally be false in the production
+ # environment where dumping schema is rarely needed.
+ mattr_accessor :dump_schema_after_migration, instance_writer: false
+ self.dump_schema_after_migration = true
+
+ mattr_accessor :maintain_test_schema, instance_accessor: false
+
+ def self.disable_implicit_join_references=(value)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Implicit join references were removed with Rails 4.1.
+ Make sure to remove this configuration because it does nothing.
+ MSG
+ end
+
+ class_attribute :default_connection_handler, instance_writer: false
+ class_attribute :find_by_statement_cache
+
+ def self.connection_handler
+ ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
+ end
+
+ def self.connection_handler=(handler)
+ ActiveRecord::RuntimeRegistry.connection_handler = handler
+ end
+
+ self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
end
module ClassMethods
- def inherited(child_class) #:nodoc:
- child_class.initialize_generated_modules
+ def allocate
+ define_attribute_methods
super
end
- def initialize_generated_modules
- @attribute_methods_mutex = Mutex.new
+ def initialize_find_by_cache
+ self.find_by_statement_cache = {}.extend(Mutex_m)
+ end
- # force attribute methods to be higher in inheritance hierarchy than other generated methods
- generated_attribute_methods.const_set(:AttrNames, Module.new {
- def self.const_missing(name)
- const_set(name, [name.to_s.sub(/ATTR_/, '')].pack('h*').freeze)
- end
- })
+ def inherited(child_class)
+ child_class.initialize_find_by_cache
+ super
+ end
+
+ def find(*ids)
+ # We don't have cache keys for this stuff yet
+ return super unless ids.length == 1
+ # Allow symbols to super to maintain compatibility for deprecated finders until Rails 5
+ return super if ids.first.kind_of?(Symbol)
+ return super if block_given? ||
+ primary_key.nil? ||
+ default_scopes.any? ||
+ current_scope ||
+ columns_hash.include?(inheritance_column) ||
+ ids.first.kind_of?(Array)
+
+ id = ids.first
+ if ActiveRecord::Base === id
+ id = id.id
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are passing an instance of ActiveRecord::Base to `find`.
+ Please pass the id of the object by calling `.id`
+ MSG
+ end
+ key = primary_key
+
+ s = find_by_statement_cache[key] || find_by_statement_cache.synchronize {
+ find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|
+ where(key => params.bind).limit(1)
+ }
+ }
+ record = s.execute([id], self, connection).first
+ unless record
+ raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}"
+ end
+ record
+ rescue RangeError
+ raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'"
+ end
+
+ def find_by(*args)
+ return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any?
+ return super if default_scopes.any?
+
+ hash = args.first
+
+ return super if hash.values.any? { |v|
+ v.nil? || Array === v || Hash === v
+ }
+
+ # We can't cache Post.find_by(author: david) ...yet
+ return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) }
+
+ key = hash.keys
+
+ klass = self
+ s = find_by_statement_cache[key] || find_by_statement_cache.synchronize {
+ find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|
+ wheres = key.each_with_object({}) { |param,o|
+ o[param] = params.bind
+ }
+ klass.where(wheres).limit(1)
+ }
+ }
+ begin
+ s.execute(hash.values, self, connection).first
+ rescue TypeError => e
+ raise ActiveRecord::StatementInvalid.new(e.message, e)
+ rescue RangeError
+ nil
+ end
+ end
- generated_feature_methods
+ def find_by!(*args)
+ find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}")
end
- def generated_feature_methods
- @generated_feature_methods ||= begin
- mod = const_set(:GeneratedFeatureMethods, Module.new)
+ def initialize_generated_modules
+ generated_association_methods
+ end
+
+ def generated_association_methods
+ @generated_association_methods ||= begin
+ mod = const_set(:GeneratedAssociationMethods, Module.new)
include mod
mod
end
@@ -106,6 +214,8 @@ module ActiveRecord
super
elsif abstract_class?
"#{super}(abstract)"
+ elsif !connected?
+ "#{super} (call '#{super}.connection' to establish a connection)"
elsif table_exists?
attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
"#{super}(#{attr_list})"
@@ -122,27 +232,34 @@ module ActiveRecord
# Returns an instance of <tt>Arel::Table</tt> loaded with the current table name.
#
# class Post < ActiveRecord::Base
- # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0))
+ # 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)
+ def arel_table # :nodoc:
+ @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster)
end
# Returns the Arel engine.
- def arel_engine
- @arel_engine ||= begin
+ def arel_engine # :nodoc:
+ @arel_engine ||=
if Base == self || connection_handler.retrieve_connection_pool(self)
self
else
superclass.arel_engine
end
- end
+ end
+
+ def predicate_builder # :nodoc:
+ @predicate_builder ||= PredicateBuilder.new(table_metadata)
+ end
+
+ def type_caster # :nodoc:
+ TypeCaster::Map.new(self)
end
private
- def relation #:nodoc:
- relation = Relation.new(self, arel_table)
+ def relation # :nodoc:
+ relation = Relation.create(self, arel_table, predicate_builder)
if finder_needs_type_condition?
relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
@@ -150,6 +267,10 @@ module ActiveRecord
relation
end
end
+
+ def table_metadata # :nodoc:
+ TableMetadata.new(self, arel_table)
+ end
end
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
@@ -160,21 +281,19 @@ module ActiveRecord
# ==== Example:
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
- def initialize(attributes = nil)
- defaults = self.class.column_defaults.dup
- defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? }
-
- @attributes = self.class.initialize_attributes(defaults)
- @columns_hash = self.class.column_types.dup
+ def initialize(attributes = nil, options = {})
+ @attributes = self.class._default_attributes.dup
init_internals
- ensure_proper_type
- populate_with_current_scope_attributes
+ initialize_internals_callback
- assign_attributes(attributes) if attributes
+ self.class.define_attribute_methods
+ # +options+ argument is only needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ init_attributes(attributes, options) if attributes
yield self if block_given?
- run_callbacks :initialize unless _initialize_callbacks.empty?
+ _run_initialize_callbacks
end
# Initialize an empty model object from +coder+. +coder+ must contain
@@ -188,15 +307,16 @@ module ActiveRecord
# post.init_with('attributes' => { 'title' => 'hello world' })
# post.title # => 'hello world'
def init_with(coder)
- @attributes = self.class.initialize_attributes(coder['attributes'])
- @columns_hash = self.class.column_types.merge(coder['column_types'] || {})
+ @attributes = coder['attributes']
init_internals
- @new_record = false
+ @new_record = coder['new_record']
+
+ self.class.define_attribute_methods
- run_callbacks :find
- run_callbacks :initialize
+ _run_find_callbacks
+ _run_initialize_callbacks
self
end
@@ -229,27 +349,17 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
- cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
- self.class.initialize_attributes(cloned_attributes, :serialized => false)
-
- @attributes = cloned_attributes
- @attributes[self.class.primary_key] = nil
+ @attributes = @attributes.dup
+ @attributes.reset(self.class.primary_key)
- run_callbacks(:initialize) unless _initialize_callbacks.empty?
-
- @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
+ _run_initialize_callbacks
@aggregation_cache = {}
@association_cache = {}
- @attributes_cache = {}
@new_record = true
+ @destroyed = false
- ensure_proper_type
- populate_with_current_scope_attributes
super
end
@@ -266,7 +376,10 @@ module ActiveRecord
# Post.new.encode_with(coder)
# coder # => {"attributes" => {"id" => nil, ... }}
def encode_with(coder)
- coder['attributes'] = attributes
+ # FIXME: Remove this when we better serialize attributes
+ coder['raw_attributes'] = attributes_before_type_cast
+ coder['attributes'] = @attributes
+ coder['new_record'] = new_record?
end
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
@@ -281,7 +394,7 @@ module ActiveRecord
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
- id.present? &&
+ !id.nil? &&
comparison_object.id == id
end
alias :eql? :==
@@ -289,12 +402,18 @@ module ActiveRecord
# 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
+ if id
+ id.hash
+ else
+ super
+ end
end
- # Freeze the attributes hash such that associations are still accessible, even on destroyed records.
+ # Clone and freeze the attributes hash such that associations are still
+ # accessible, even on destroyed records, but cloned models will not be
+ # frozen.
def freeze
- @attributes.freeze
+ @attributes = @attributes.clone.freeze
self
end
@@ -307,6 +426,8 @@ module ActiveRecord
def <=>(other_object)
if other_object.is_a?(self.class)
self.to_key <=> other_object.to_key
+ else
+ super
end
end
@@ -321,16 +442,15 @@ module ActiveRecord
@readonly = true
end
- # Returns the connection currently associated with the class. This can
- # also be used to "borrow" the connection to do database work that isn't
- # easily done without going straight to SQL.
- def connection
- self.class.connection
+ def connection_handler
+ self.class.connection_handler
end
# Returns the contents of the record as a nicely formatted string.
def inspect
- inspection = if @attributes
+ # We check defined?(@attributes) not to issue warnings if the object is
+ # allocated but not initialized.
+ inspection = if defined?(@attributes) && @attributes
self.class.column_names.collect { |name|
if has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
@@ -342,13 +462,79 @@ module ActiveRecord
"#<#{self.class} #{inspection}>"
end
+ # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record`
+ # when pp is required.
+ def pretty_print(pp)
+ pp.object_address_group(self) do
+ if defined?(@attributes) && @attributes
+ column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? }
+ pp.seplist(column_names, proc { pp.text ',' }) do |column_name|
+ column_value = read_attribute(column_name)
+ pp.breakable ' '
+ pp.group(1) do
+ pp.text column_name
+ pp.text ':'
+ pp.breakable
+ pp.pp column_value
+ end
+ end
+ else
+ pp.breakable ' '
+ pp.text 'not initialized'
+ end
+ end
+ end
+
# Returns a hash of the given methods with their names as keys and returned values as values.
def slice(*methods)
- Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access
+ Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access
+ end
+
+ def set_transaction_state(state) # :nodoc:
+ @transaction_state = state
+ end
+
+ def has_transactional_callbacks? # :nodoc:
+ !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty?
end
private
+ # Updates the attributes on this particular ActiveRecord object so that
+ # if it is associated with a transaction, then the state of the AR object
+ # will be updated to reflect the current state of the transaction
+ #
+ # The @transaction_state variable stores the states of the associated
+ # transaction. This relies on the fact that a transaction can only be in
+ # one rollback or commit (otherwise a list of states would be required)
+ # Each AR object inside of a transaction carries that transaction's
+ # TransactionState.
+ #
+ # This method checks to see if the ActiveRecord object's state reflects
+ # the TransactionState, and rolls back or commits the ActiveRecord object
+ # as appropriate.
+ #
+ # Since ActiveRecord objects can be inside multiple transactions, this
+ # method recursively goes through the parent of the TransactionState and
+ # checks if the ActiveRecord object reflects the state of the object.
+ def sync_with_transaction_state
+ update_attributes_from_transaction_state(@transaction_state, 0)
+ end
+
+ def update_attributes_from_transaction_state(transaction_state, depth)
+ if transaction_state && transaction_state.finalized? && !has_transactional_callbacks?
+ unless @reflects_state[depth]
+ restore_transaction_record_state if transaction_state.rolledback?
+ clear_transaction_record_state
+ @reflects_state[depth] = true
+ end
+
+ if transaction_state.parent && !@reflects_state[depth+1]
+ update_attributes_from_transaction_state(transaction_state.parent, depth+1)
+ end
+ end
+ end
+
# 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,
@@ -362,20 +548,32 @@ module ActiveRecord
end
def init_internals
- pk = self.class.primary_key
- @attributes[pk] = nil unless @attributes.key?(pk)
-
- @aggregation_cache = {}
- @association_cache = {}
- @attributes_cache = {}
- @previously_changed = {}
- @changed_attributes = {}
- @readonly = false
- @destroyed = false
- @marked_for_destruction = false
- @new_record = true
- @txn = nil
+ @aggregation_cache = {}
+ @association_cache = {}
+ @readonly = false
+ @destroyed = false
+ @marked_for_destruction = false
+ @destroyed_by_association = nil
+ @new_record = true
+ @txn = nil
@_start_transaction_state = {}
+ @transaction_state = nil
+ @reflects_state = [false]
+ end
+
+ def initialize_internals_callback
+ end
+
+ # This method is needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ def init_attributes(attributes, options)
+ assign_attributes(attributes)
+ end
+
+ def thaw
+ if frozen?
+ @attributes = @attributes.dup
+ end
end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 81f92db271..7d8e0a2063 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -11,7 +11,7 @@ module ActiveRecord
# ==== Parameters
#
# * +id+ - The id of the object you wish to reset a counter on.
- # * +counters+ - One or more counter names to reset
+ # * +counters+ - One or more association counters to reset. Association name or counter name can be given.
#
# ==== Examples
#
@@ -19,8 +19,14 @@ module ActiveRecord
# Post.reset_counters(1, :comments)
def reset_counters(id, *counters)
object = find(id)
- counters.each do |association|
- has_many_association = reflect_on_association(association.to_sym)
+ counters.each do |counter_association|
+ has_many_association = _reflect_on_association(counter_association)
+ unless has_many_association
+ has_many = reflect_on_all_associations(:has_many)
+ has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym }
+ counter_association = has_many_association.plural_name if has_many_association
+ end
+ raise ArgumentError, "'#{self.name}' has no association called '#{counter_association}'" unless has_many_association
if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
has_many_association = has_many_association.through_reflection
@@ -28,14 +34,12 @@ module ActiveRecord
foreign_key = has_many_association.foreign_key.to_s
child_class = has_many_association.klass
- belongs_to = child_class.reflect_on_all_associations(:belongs_to)
- reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
+ reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
counter_name = reflection.counter_cache_column
- stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
- arel_table[counter_name] => object.send(association).count
- })
- connection.update stmt
+ unscoped.where(primary_key => object.id).update_all(
+ counter_name => object.send(counter_association).count(:all)
+ )
end
return true
end
@@ -49,7 +53,7 @@ module ActiveRecord
# ==== Parameters
#
# * +id+ - The id of the object you wish to update a counter on or an Array of ids.
- # * +counters+ - An Array of Hashes containing the names of the fields
+ # * +counters+ - A Hash containing the names of the fields
# to update as keys and the amount to update the field by as values.
#
# ==== Examples
@@ -76,15 +80,15 @@ module ActiveRecord
"#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
end
- where(primary_key => id).update_all updates.join(', ')
+ unscoped.where(primary_key => id).update_all updates.join(', ')
end
# Increment a numeric field by one, via a direct SQL update.
#
- # This method is used primarily for maintaining counter_cache columns used to
- # store aggregate values. For example, a DiscussionBoard may cache posts_count
- # and comments_count to avoid running an SQL query to calculate the number of
- # posts and comments there are each time it is displayed.
+ # This method is used primarily for maintaining counter_cache columns that are
+ # used to store aggregate values. For example, a DiscussionBoard may cache
+ # posts_count and comments_count to avoid running an SQL query to calculate the
+ # number of posts and comments there are, each time it is displayed.
#
# ==== Parameters
#
@@ -117,5 +121,44 @@ module ActiveRecord
update_counters(id, counter_name => -1)
end
end
+
+ private
+
+ def _create_record(*)
+ id = super
+
+ each_counter_cached_associations do |association|
+ if send(association.reflection.name)
+ association.increment_counters
+ @_after_create_counter_called = true
+ end
+ end
+
+ id
+ end
+
+ def destroy_row
+ affected_rows = super
+
+ if affected_rows > 0
+ each_counter_cached_associations do |association|
+ foreign_key = association.reflection.foreign_key.to_sym
+ unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
+ if send(association.reflection.name)
+ association.decrement_counters
+ end
+ end
+ end
+ end
+
+ affected_rows
+ end
+
+ def each_counter_cached_associations
+ _reflections.each do |name, reflection|
+ yield association(name) if reflection.belongs_to? && reflection.counter_cache_column
+ end
+ end
+
end
end
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index 3bac31c6aa..b6dd6814db 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -1,13 +1,12 @@
module ActiveRecord
module DynamicMatchers #:nodoc:
- # This code in this file seems to have a lot of indirection, but the indirection
- # is there to provide extension points for the activerecord-deprecated_finders
- # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5),
- # then we can remove the indirection.
-
def respond_to?(name, include_private = false)
- match = Method.match(self, name)
- match && match.valid? || super
+ if self == Base
+ super
+ else
+ match = Method.match(self, name)
+ match && match.valid? || super
+ end
end
private
@@ -35,7 +34,7 @@ module ActiveRecord
end
def pattern
- /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
+ @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
end
def prefix
@@ -68,29 +67,22 @@ module ActiveRecord
CODE
end
- def body
- raise NotImplementedError
- end
- end
+ private
- module Finder
- # Extended in activerecord-deprecated_finders
def body
- result
- end
-
- # Extended in activerecord-deprecated_finders
- def result
"#{finder}(#{attributes_hash})"
end
- # Extended in activerecord-deprecated_finders
+ # The parameters in the signature may have reserved Ruby words, in order
+ # to prevent errors, we start each param name with `_`.
def signature
- attribute_names.join(', ')
+ attribute_names.map { |name| "_#{name}" }.join(', ')
end
+ # Given that the parameters starts with `_`, the finder needs to use the
+ # same parameter name.
def attributes_hash
- "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}"
+ "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}"
end
def finder
@@ -100,7 +92,6 @@ module ActiveRecord
class FindBy < Method
Method.matchers << self
- include Finder
def self.prefix
"find_by"
@@ -113,7 +104,6 @@ module ActiveRecord
class FindByBang < Method
Method.matchers << self
- include Finder
def self.prefix
"find_by"
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
new file mode 100644
index 0000000000..f053372cfb
--- /dev/null
+++ b/activerecord/lib/active_record/enum.rb
@@ -0,0 +1,198 @@
+require 'active_support/core_ext/object/deep_dup'
+
+module ActiveRecord
+ # Declare an enum attribute where the values map to integers in the database,
+ # but can be queried by name. Example:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: [ :active, :archived ]
+ # end
+ #
+ # # conversation.update! status: 0
+ # conversation.active!
+ # conversation.active? # => true
+ # conversation.status # => "active"
+ #
+ # # conversation.update! status: 1
+ # conversation.archived!
+ # conversation.archived? # => true
+ # conversation.status # => "archived"
+ #
+ # # conversation.update! status: 1
+ # conversation.status = "archived"
+ #
+ # # conversation.update! status: nil
+ # conversation.status = nil
+ # conversation.status.nil? # => true
+ # conversation.status # => nil
+ #
+ # Scopes based on the allowed values of the enum field will be provided
+ # as well. With the above example:
+ #
+ # Conversation.active
+ # Conversation.archived
+ #
+ # You can set the default value from the database declaration, like:
+ #
+ # create_table :conversations do |t|
+ # t.column :status, :integer, default: 0
+ # end
+ #
+ # Good practice is to let the first declared status be the default.
+ #
+ # Finally, it's also possible to explicitly map the relation between attribute and
+ # database integer with a +Hash+:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: { active: 0, archived: 1 }
+ # end
+ #
+ # Note that when an +Array+ is used, the implicit mapping from the values to database
+ # integers is derived from the order the values appear in the array. In the example,
+ # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
+ # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
+ # database.
+ #
+ # Therefore, once a value is added to the enum array, its position in the array must
+ # be maintained, and new values should only be added to the end of the array. To
+ # remove unused values, the explicit +Hash+ syntax should be used.
+ #
+ # In rare circumstances you might need to access the mapping directly.
+ # The mappings are exposed through a class method with the pluralized attribute
+ # name:
+ #
+ # Conversation.statuses # => { "active" => 0, "archived" => 1 }
+ #
+ # Use that class method when you need to know the ordinal value of an enum:
+ #
+ # Conversation.where("status <> ?", Conversation.statuses[:archived])
+ #
+ # Where conditions on an enum attribute must use the ordinal value of an enum.
+ module Enum
+ def self.extended(base) # :nodoc:
+ base.class_attribute(:defined_enums)
+ base.defined_enums = {}
+ end
+
+ def inherited(base) # :nodoc:
+ base.defined_enums = defined_enums.deep_dup
+ super
+ end
+
+ def enum(definitions)
+ klass = self
+ definitions.each do |name, values|
+ # statuses = { }
+ enum_values = ActiveSupport::HashWithIndifferentAccess.new
+ name = name.to_sym
+
+ # def self.statuses statuses end
+ detect_enum_conflict!(name, name.to_s.pluralize, true)
+ klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
+
+ _enum_methods_module.module_eval do
+ # def status=(value) self[:status] = statuses[value] end
+ klass.send(:detect_enum_conflict!, name, "#{name}=")
+ define_method("#{name}=") { |value|
+ if enum_values.has_key?(value) || value.blank?
+ self[name] = enum_values[value]
+ elsif enum_values.has_value?(value)
+ # Assigning a value directly is not a end-user feature, hence it's not documented.
+ # This is used internally to make building objects from the generated scopes work
+ # as expected, i.e. +Conversation.archived.build.archived?+ should be true.
+ self[name] = value
+ else
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
+ end
+ }
+
+ # def status() statuses.key self[:status] end
+ klass.send(:detect_enum_conflict!, name, name)
+ define_method(name) { enum_values.key self[name] }
+
+ # def status_before_type_cast() statuses.key self[:status] end
+ klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast")
+ define_method("#{name}_before_type_cast") { enum_values.key self[name] }
+
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
+ pairs.each do |value, i|
+ enum_values[value] = i
+
+ # def active?() status == 0 end
+ klass.send(:detect_enum_conflict!, name, "#{value}?")
+ define_method("#{value}?") { self[name] == i }
+
+ # def active!() update! status: :active end
+ klass.send(:detect_enum_conflict!, name, "#{value}!")
+ define_method("#{value}!") { update! name => value }
+
+ # scope :active, -> { where status: 0 }
+ klass.send(:detect_enum_conflict!, name, value, true)
+ klass.scope value, -> { klass.where name => i }
+ end
+ end
+ defined_enums[name.to_s] = enum_values
+ end
+ end
+
+ private
+ def _enum_methods_module
+ @_enum_methods_module ||= begin
+ mod = Module.new do
+ private
+ def save_changed_attribute(attr_name, old)
+ if (mapping = self.class.defined_enums[attr_name.to_s])
+ value = _read_attribute(attr_name)
+ if attribute_changed?(attr_name)
+ if mapping[old] == value
+ clear_attribute_changes([attr_name])
+ end
+ else
+ if old != value
+ set_attribute_was(attr_name, mapping.key(old))
+ end
+ end
+ else
+ super
+ end
+ end
+ end
+ include mod
+ mod
+ end
+ end
+
+ ENUM_CONFLICT_MESSAGE = \
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
+ "by %{source}."
+
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
+ if klass_method && dangerous_class_method?(method_name)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'class',
+ method: method_name,
+ source: 'Active Record'
+ }
+ elsif !klass_method && dangerous_attribute_method?(method_name)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'instance',
+ method: method_name,
+ source: 'Active Record'
+ }
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'instance',
+ method: method_name,
+ source: 'another enum'
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index c615d59725..fc28ab585f 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -30,17 +30,18 @@ module ActiveRecord
class SerializationTypeMismatch < ActiveRecordError
end
- # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt>
- # misses adapter field).
+ # Raised when adapter not specified on connection (or configuration file
+ # +config/database.yml+ misses adapter field).
class AdapterNotSpecified < ActiveRecordError
end
- # Raised when Active Record cannot find database adapter specified in <tt>config/database.yml</tt> or programmatically.
+ # Raised when Active Record cannot find database adapter specified in
+ # +config/database.yml+ or programmatically.
class AdapterNotFound < ActiveRecordError
end
- # Raised when connection to the database could not been established (for example when <tt>connection=</tt>
- # is given a nil object).
+ # Raised when connection to the database could not been established (for
+ # example when +connection=+ is given a nil object).
class ConnectionNotEstablished < ActiveRecordError
end
@@ -51,32 +52,48 @@ module ActiveRecord
# Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be
# saved because record is invalid.
class RecordNotSaved < ActiveRecordError
+ attr_reader :record
+
+ def initialize(message, record = nil)
+ @record = record
+ super(message)
+ end
end
# Raised by ActiveRecord::Base.destroy! when a call to destroy would return false.
+ #
+ # begin
+ # complex_operation_that_internally_calls_destroy!
+ # rescue ActiveRecord::RecordNotDestroyed => invalid
+ # puts invalid.record.errors
+ # end
+ #
class RecordNotDestroyed < ActiveRecordError
- end
+ attr_reader :record
- # Raised when SQL statement cannot be executed by the database (for example, it's often the case for
- # MySQL when Ruby driver used is too old).
- class StatementInvalid < ActiveRecordError
- end
-
- # Raised when SQL statement is invalid and the application gets a blank result.
- class ThrowResult < ActiveRecordError
+ def initialize(record)
+ @record = record
+ super()
+ end
end
- # Parent class for all specific exceptions which wrap database driver exceptions
- # provides access to the original exception also.
- class WrappedDatabaseException < StatementInvalid
+ # Superclass for all database execution errors.
+ #
+ # Wraps the underlying database error as +original_exception+.
+ class StatementInvalid < ActiveRecordError
attr_reader :original_exception
- def initialize(message, original_exception)
+ def initialize(message, original_exception = nil)
super(message)
@original_exception = original_exception
end
end
+ # Defunct wrapper class kept for compatibility.
+ # +StatementInvalid+ wraps the original exception now.
+ class WrappedDatabaseException < StatementInvalid
+ end
+
# Raised when a record cannot be inserted because it would violate a uniqueness constraint.
class RecordNotUnique < WrappedDatabaseException
end
@@ -85,23 +102,26 @@ module ActiveRecord
class InvalidForeignKey < WrappedDatabaseException
end
- # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example,
- # when using +find+ method)
- # does not match number of expected variables.
+ # Raised when number of bind variables in statement given to +:condition+ key
+ # (for example, when using +find+ method) does not match number of expected
+ # values supplied.
#
- # For example, in
+ # For example, when there are two placeholders with only one value supplied:
#
# Location.where("lat = ? AND lng = ?", 53.7362)
- #
- # two placeholders are given but only one variable to fill them.
class PreparedStatementInvalid < ActiveRecordError
end
+ # Raised when a given database does not exist.
+ class NoDatabaseError < StatementInvalid
+ end
+
# Raised on attempt to save stale record. Record is stale when it's being saved in another query after
# instantiation, for example, when two users edit the same wiki page and one starts editing and saves
# the page before the other.
#
- # Read more about optimistic locking in ActiveRecord::Locking module RDoc.
+ # Read more about optimistic locking in ActiveRecord::Locking module
+ # documentation.
class StaleObjectError < ActiveRecordError
attr_reader :record, :attempted_action
@@ -113,8 +133,9 @@ module ActiveRecord
end
- # Raised when association is being configured improperly or
- # user tries to use offset and limit together with has_many or has_and_belongs_to_many associations.
+ # Raised when association is being configured improperly or user tries to use
+ # offset and limit together with +has_many+ or +has_and_belongs_to_many+
+ # associations.
class ConfigurationError < ActiveRecordError
end
@@ -152,19 +173,30 @@ module ActiveRecord
class Rollback < ActiveRecordError
end
- # Raised when attribute has a name reserved by Active Record (when attribute has name of one of Active Record instance methods).
+ # Raised when attribute has a name reserved by Active Record (when attribute
+ # has name of one of Active Record instance methods).
class DangerousAttributeError < ActiveRecordError
end
# Raised when unknown attributes are supplied via mass assignment.
class UnknownAttributeError < NoMethodError
+
+ attr_reader :record, :attribute
+
+ def initialize(record, attribute)
+ @record = record
+ @attribute = attribute.to_s
+ super("unknown attribute '#{attribute}' for #{@record.class}.")
+ end
+
end
# Raised when an error occurred while doing a mass assignment to an attribute through the
- # <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the
+ # +attributes=+ method. The exception has an +attribute+ property that is the name of the
# offending attribute.
class AttributeAssignmentError < ActiveRecordError
attr_reader :exception, :attribute
+
def initialize(message, exception, attribute)
super(message)
@exception = exception
@@ -177,12 +209,13 @@ module ActiveRecord
# objects, each corresponding to the error while assigning to an attribute.
class MultiparameterAssignmentErrors < ActiveRecordError
attr_reader :errors
+
def initialize(errors)
@errors = errors
end
end
- # Raised when a primary key is needed, but there is not one specified in the schema or model.
+ # Raised when a primary key is needed, but not specified in the schema or model.
class UnknownPrimaryKey < ActiveRecordError
attr_reader :model
@@ -207,6 +240,13 @@ module ActiveRecord
class ImmutableRelation < ActiveRecordError
end
+ # TransactionIsolationError will be raised under the following conditions:
+ #
+ # * The adapter does not support setting the isolation level
+ # * You are joining an existing open transaction
+ # * You are creating a nested (savepoint) transaction
+ #
+ # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level.
class TransactionIsolationError < ActiveRecordError
end
end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 70683eb731..727a9befc1 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -1,57 +1,22 @@
require 'active_support/lazy_load_hooks'
+require 'active_record/explain_registry'
module ActiveRecord
module Explain
- def self.extended(base)
- base.mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false
- end
-
- # If the database adapter supports explain and auto explain is enabled,
- # this method triggers EXPLAIN logging for the queries triggered by the
- # block if it takes more than the threshold as a whole. That is, the
- # threshold is not checked against each individual query, but against the
- # duration of the entire block. This approach is convenient for relations.
-
- #
- # The available_queries_for_explain thread variable collects the queries
- # to be explained. If the value is nil, it means queries are not being
- # currently collected. A false value indicates collecting is turned
- # off. Otherwise it is an array of queries.
- def logging_query_plan # :nodoc:
- return yield unless logger
-
- threshold = auto_explain_threshold_in_seconds
- current = Thread.current
- if connection.supports_explain? && threshold && current[:available_queries_for_explain].nil?
- begin
- queries = current[:available_queries_for_explain] = []
- start = Time.now
- result = yield
- logger.warn(exec_explain(queries)) if Time.now - start > threshold
- result
- ensure
- current[:available_queries_for_explain] = nil
- end
- else
- yield
- end
- end
-
- # Relation#explain needs to be able to collect the queries regardless of
- # whether auto explain is enabled. This method serves that purpose.
+ # Executes the block with the collect flag enabled. Queries are collected
+ # asynchronously by the subscriber and returned.
def collecting_queries_for_explain # :nodoc:
- current = Thread.current
- original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
- return yield, current[:available_queries_for_explain]
+ ExplainRegistry.collect = true
+ yield
+ ExplainRegistry.queries
ensure
- # Note that the return value above does not depend on this assigment.
- current[:available_queries_for_explain] = original
+ ExplainRegistry.reset
end
# Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
# Returns a formatted string ready to be logged.
def exec_explain(queries) # :nodoc:
- str = queries && queries.map do |sql, bind|
+ str = queries.map do |sql, bind|
[].tap do |msg|
msg << "EXPLAIN for: #{sql}"
unless bind.empty?
@@ -62,26 +27,12 @@ module ActiveRecord
end.join("\n")
end.join("\n")
- # Overriding inspect to be more human readable, specially in the console.
+ # Overriding inspect to be more human readable, especially in the console.
def str.inspect
self
end
- str
- end
- # Silences automatic EXPLAIN logging for the duration of the block.
- #
- # This has high priority, no EXPLAINs will be run even if downwards
- # the threshold is set to 0.
- #
- # As the name of the method suggests this only applies to automatic
- # EXPLAINs, manual calls to <tt>ActiveRecord::Relation#explain</tt> run.
- def silence_auto_explain
- current = Thread.current
- original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false
- yield
- ensure
- current[:available_queries_for_explain] = original
+ str
end
end
end
diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb
new file mode 100644
index 0000000000..f5cd57e075
--- /dev/null
+++ b/activerecord/lib/active_record/explain_registry.rb
@@ -0,0 +1,30 @@
+require 'active_support/per_thread_registry'
+
+module ActiveRecord
+ # This is a thread locals registry for EXPLAIN. For example
+ #
+ # ActiveRecord::ExplainRegistry.queries
+ #
+ # returns the collected queries local to the current thread.
+ #
+ # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # for further details.
+ class ExplainRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ attr_accessor :queries, :collect
+
+ def initialize
+ reset
+ end
+
+ def collect?
+ @collect
+ end
+
+ def reset
+ @collect = false
+ @queries = []
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 0f927496fb..6a49936644 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -1,4 +1,5 @@
require 'active_support/notifications'
+require 'active_record/explain_registry'
module ActiveRecord
class ExplainSubscriber # :nodoc:
@@ -7,8 +8,8 @@ module ActiveRecord
end
def finish(name, id, payload)
- if queries = Thread.current[:available_queries_for_explain]
- queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload)
+ if ExplainRegistry.collect? && !ignore_payload?(payload)
+ ExplainRegistry.queries << payload.values_at(:sql, :binds)
end
end
@@ -18,7 +19,7 @@ module ActiveRecord
# On the other hand, we want to monitor the performance of our real database
# queries, not the performance of the access to the query cache.
IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
- EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)/i
+ EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i
def ignore_payload?(payload)
payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
end
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index 11b53275e1..8132310c91 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -24,7 +24,6 @@ module ActiveRecord
rows.each(&block)
end
- RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] # :nodoc:
private
def rows
@@ -32,14 +31,15 @@ module ActiveRecord
begin
data = YAML.load(render(IO.read(@file)))
- rescue *RESCUE_ERRORS => error
+ rescue ArgumentError, Psych::SyntaxError => error
raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
end
@rows = data ? validate(data).to_a : []
end
def render(content)
- ERB.new(content).result
+ context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new
+ ERB.new(content).result(context.get_binding)
end
# Validate our unmarshalled data.
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index ea3bb8f33f..4732462b05 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -2,11 +2,10 @@ require 'erb'
require 'yaml'
require 'zlib'
require 'active_support/dependencies'
+require 'active_support/core_ext/digest/uuid'
require 'active_record/fixture_set/file'
require 'active_record/errors'
-require 'active_support/deprecation' # temporary
-
module ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
end
@@ -16,9 +15,10 @@ module ActiveRecord
# They are stored in YAML files, one file per model, which are placed in the directory
# appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
- # The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
- # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks
- # like this:
+ # The fixture file ends with the +.yml+ file extension, for example:
+ # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>).
+ #
+ # The format of a fixture file looks like this:
#
# rubyonrails:
# id: 1
@@ -34,7 +34,7 @@ module ActiveRecord
# is followed by an indented list of key/value pairs in the "key: value" format. Records are
# separated by a blank line for your viewing pleasure.
#
- # Note that fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
+ # Note: Fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
# See http://yaml.org/type/omap.html
# for the specification. You will need ordered fixtures when you have foreign key constraints
# on keys in the same table. This is commonly needed for tree structures. Example:
@@ -62,8 +62,8 @@ module ActiveRecord
# end
# end
#
- # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database,
- # so this test will succeed.
+ # By default, +test_helper.rb+ will load all of your fixtures into your test
+ # database, so this test will succeed.
#
# The testing environment will automatically load the all fixtures into the database before each
# test. To ensure consistent data, the environment deletes the fixtures before running the load.
@@ -121,6 +121,23 @@ module ActiveRecord
# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
# in fixtures are to be considered a code smell.
#
+ # Helper methods defined in a fixture will not be available in other fixtures, to prevent against
+ # unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module
+ # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>.
+ #
+ # - define a helper method in `test_helper.rb`
+ # module FixtureFileHelpers
+ # def file_sha(path)
+ # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
+ # end
+ # end
+ # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers
+ #
+ # - use the helper method in a fixture
+ # photo:
+ # name: kitten.png
+ # sha: <%= file_sha 'files/kitten.png' %>
+ #
# = Transactional Fixtures
#
# Test cases can use begin+rollback to isolate their changes to the database instead of having to
@@ -164,6 +181,9 @@ module ActiveRecord
# * Stable, autogenerated IDs
# * Label references for associations (belongs_to, has_one, has_many)
# * HABTM associations as inline lists
+ #
+ # There are some more advanced features available even if the id is specified:
+ #
# * Autofilled timestamp columns
# * Fixture label interpolation
# * Support for YAML defaults
@@ -346,6 +366,7 @@ module ActiveRecord
# geeksomnia:
# name: Geeksomnia's Account
# subdomain: $LABEL
+ # email: $LABEL@email.com
#
# Also, sometimes (like when porting older join table fixtures) you'll need
# to be able to get a hold of the identifier for a given label. ERB
@@ -357,46 +378,42 @@ module ActiveRecord
#
# == Support for YAML defaults
#
- # You probably already know how to use YAML to set and reuse defaults in
- # your <tt>database.yml</tt> file. You can use the same technique in your fixtures:
+ # You can set and reuse defaults in your fixtures YAML file.
+ # This is the same technique used in the +database.yml+ file to specify
+ # defaults:
#
# DEFAULTS: &DEFAULTS
# created_on: <%= 3.weeks.ago.to_s(:db) %>
#
# first:
# name: Smurf
- # *DEFAULTS
+ # <<: *DEFAULTS
#
# second:
# name: Fraggle
- # *DEFAULTS
+ # <<: *DEFAULTS
#
# Any fixture labeled "DEFAULTS" is safely ignored.
class FixtureSet
#--
- # An instance of FixtureSet is normally stored in a single YAML file and possibly in a folder with the same name.
+ # An instance of FixtureSet 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.find_table_name(fixture_set_name) # :nodoc:
- ActiveSupport::Deprecation.warn(
- "ActiveRecord::Fixtures.find_table_name is deprecated and shall be removed from future releases. Use ActiveRecord::Fixtures.default_fixture_model_name instead.")
- default_fixture_model_name(fixture_set_name)
- end
-
- def self.default_fixture_model_name(fixture_set_name) # :nodoc:
- ActiveRecord::Base.pluralize_table_names ?
+ def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ config.pluralize_table_names ?
fixture_set_name.singularize.camelize :
fixture_set_name.camelize
end
- def self.default_fixture_table_name(fixture_set_name) # :nodoc:
- "#{ ActiveRecord::Base.table_name_prefix }"\
+ def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ "#{ config.table_name_prefix }"\
"#{ fixture_set_name.tr('/', '_') }"\
- "#{ ActiveRecord::Base.table_name_suffix }".to_sym
+ "#{ config.table_name_suffix }".to_sym
end
def self.reset_cache
@@ -444,9 +461,41 @@ module ActiveRecord
cattr_accessor :all_loaded_fixtures
self.all_loaded_fixtures = {}
- def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {})
+ class ClassCache
+ def initialize(class_names, config)
+ @class_names = class_names.stringify_keys
+ @config = config
+
+ # Remove string values that aren't constants or subclasses of AR
+ @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) }
+ end
+
+ def [](fs_name)
+ @class_names.fetch(fs_name) {
+ klass = default_fixture_model(fs_name, @config).safe_constantize
+ insert_class(@class_names, fs_name, klass)
+ }
+ end
+
+ private
+
+ def insert_class(class_names, name, klass)
+ # We only want to deal with AR objects.
+ if klass && klass < ActiveRecord::Base
+ class_names[name] = klass
+ else
+ class_names[name] = nil
+ end
+ end
+
+ def default_fixture_model(fs_name, config)
+ ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
+ end
+ end
+
+ def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
fixture_set_names = Array(fixture_set_names).map(&:to_s)
- class_names = class_names.stringify_keys
+ class_names = ClassCache.new class_names, config
# FIXME: Apparently JK uses this.
connection = block_given? ? yield : ActiveRecord::Base.connection
@@ -460,21 +509,23 @@ module ActiveRecord
fixtures_map = {}
fixture_sets = files_to_read.map do |fs_name|
+ klass = class_names[fs_name]
+ conn = klass ? klass.connection : connection
fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
- connection,
+ conn,
fs_name,
- class_names[fs_name] || default_fixture_model_name(fs_name),
+ klass,
::File.join(fixtures_directory, fs_name))
end
- all_loaded_fixtures.update(fixtures_map)
+ update_all_loaded_fixtures fixtures_map
connection.transaction(:requires_new => true) do
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|
+ table_rows.each_key do |table|
conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
end
@@ -500,32 +551,45 @@ module ActiveRecord
end
# Returns a consistent, platform-independent identifier for +label+.
- # Identifiers are positive integers less than 2^32.
- def self.identify(label)
- Zlib.crc32(label.to_s) % MAX_ID
+ # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
+ def self.identify(label, column_type = :integer)
+ if column_type == :uuid
+ Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
+ else
+ Zlib.crc32(label.to_s) % MAX_ID
+ end
+ end
+
+ # Superclass for the evaluation contexts used by ERB fixtures.
+ def self.context_class
+ @context_class ||= Class.new
+ end
+
+ def self.update_all_loaded_fixtures(fixtures_map) # :nodoc:
+ all_loaded_fixtures.update(fixtures_map)
end
- attr_reader :table_name, :name, :fixtures, :model_class
+ attr_reader :table_name, :name, :fixtures, :model_class, :config
- def initialize(connection, name, class_name, path)
- @fixtures = {} # Ordered hash
+ def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
@name = name
@path = path
+ @config = config
+ @model_class = nil
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
+ @model_class = class_name.safe_constantize if class_name
end
- @connection = ( model_class.respond_to?(:connection) ?
- model_class.connection : connection )
+ @connection = connection
@table_name = ( model_class.respond_to?(:table_name) ?
model_class.table_name :
- self.class.default_fixture_table_name(name) )
+ self.class.default_fixture_table_name(name, config) )
- read_fixture_files
+ @fixtures = read_fixture_files path, @model_class
end
def [](x)
@@ -544,10 +608,10 @@ module ActiveRecord
fixtures.size
end
- # Return a hash of rows to be inserted. The key is the table, the value is
+ # Returns a hash of rows to be inserted. The key is the table, the value is
# a list of rows to insert to that table.
def table_rows
- now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
+ now = config.default_timezone == :utc ? Time.now.utc : Time.now
now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
@@ -559,7 +623,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
# 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 |c_name|
@@ -569,12 +633,12 @@ module ActiveRecord
# interpolate the fixture label
row.each do |key, value|
- row[key] = label if value == "$LABEL"
+ row[key] = value.gsub("$LABEL", label) if value.is_a?(String)
end
# generate a primary key if necessary
if has_primary_key_column? && !row.include?(primary_key_name)
- row[primary_key_name] = ActiveRecord::FixtureSet.identify(label)
+ row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type)
end
# If STI is used, find the correct subclass for association reflection
@@ -585,28 +649,24 @@ module ActiveRecord
model_class
end
- reflection_class.reflect_on_all_associations.each do |association|
+ reflection_class._reflections.each_value do |association|
case association.macro
when :belongs_to
# Do not replace association name with association foreign key if they are named the same
fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
- if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
+ if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
# support polymorphic belongs_to as "label (Type)"
row[association.foreign_type] = $1
end
- row[fk_name] = ActiveRecord::FixtureSet.identify(value)
+ fk_type = association.active_record.columns_hash[fk_name].type
+ row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
end
- when :has_and_belongs_to_many
- if (targets = row.delete(association.name.to_s))
- targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
- table_name = association.join_table
- rows[table_name].concat targets.map { |target|
- { association.foreign_key => row[primary_key_name],
- association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) }
- }
+ when :has_many
+ if association.options[:through]
+ add_join_records(rows, row, HasManyThroughProxy.new(association))
end
end
end
@@ -617,11 +677,59 @@ module ActiveRecord
rows
end
+ class ReflectionProxy # :nodoc:
+ def initialize(association)
+ @association = association
+ end
+
+ def join_table
+ @association.join_table
+ end
+
+ def name
+ @association.name
+ end
+
+ def primary_key_type
+ @association.klass.column_types[@association.klass.primary_key].type
+ end
+ end
+
+ class HasManyThroughProxy < ReflectionProxy # :nodoc:
+ def rhs_key
+ @association.foreign_key
+ end
+
+ def lhs_key
+ @association.through_reflection.foreign_key
+ end
+ end
+
private
def primary_key_name
@primary_key_name ||= model_class && model_class.primary_key
end
+ def primary_key_type
+ @primary_key_type ||= model_class && model_class.column_types[model_class.primary_key].type
+ end
+
+ def add_join_records(rows, row, association)
+ # This is the case when the join table has no fixtures file
+ if (targets = row.delete(association.name.to_s))
+ table_name = association.join_table
+ column_type = association.primary_key_type
+ lhs_key = association.lhs_key
+ rhs_key = association.rhs_key
+
+ targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
+ rows[table_name].concat targets.map { |target|
+ { lhs_key => row[primary_key_name],
+ rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
+ }
+ end
+ end
+
def has_primary_key_column?
@has_primary_key_column ||= primary_key_name &&
model_class.columns.any? { |c| c.name == primary_key_name }
@@ -637,15 +745,15 @@ module ActiveRecord
end
def column_names
- @column_names ||= @connection.columns(@table_name).collect { |c| c.name }
+ @column_names ||= @connection.columns(@table_name).collect(&:name)
end
- def read_fixture_files
- yaml_files = Dir["#{@path}/**/*.yml"].select { |f|
+ def read_fixture_files(path, model_class)
+ yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
::File.file?(f)
- } + [yaml_file_path]
+ } + [yaml_file_path(path)]
- yaml_files.each do |file|
+ yaml_files.each_with_object({}) do |file, fixtures|
FixtureSet::File.open(file) do |fh|
fh.each do |fixture_name, row|
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
@@ -654,8 +762,8 @@ module ActiveRecord
end
end
- def yaml_file_path
- "#{@path}.yml"
+ def yaml_file_path(path)
+ "#{path}.yml"
end
end
@@ -698,7 +806,9 @@ module ActiveRecord
def find
if model_class
- model_class.find(fixture[model_class.primary_key])
+ model_class.unscoped do
+ model_class.find(fixture[model_class.primary_key])
+ end
else
raise FixtureClassNotFound, "No class attached to find."
end
@@ -710,24 +820,33 @@ module ActiveRecord
module TestFixtures
extend ActiveSupport::Concern
- included do
- setup :setup_fixtures
- teardown :teardown_fixtures
+ def before_setup
+ setup_fixtures
+ super
+ end
- class_attribute :fixture_path
+ def after_teardown
+ super
+ teardown_fixtures
+ end
+
+ included do
+ class_attribute :fixture_path, :instance_writer => false
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 :pre_loaded_fixtures
+ class_attribute :config
self.fixture_table_names = []
self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = false
+ self.config = ActiveRecord::Base
self.fixture_class_names = Hash.new do |h, fixture_set_name|
- h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name)
+ h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config)
end
end
@@ -740,55 +859,22 @@ module ActiveRecord
# 'namespaced/fixture' => Another::Model
#
# The keys must be the fixture names, that coincide with the short paths to the fixture files.
- #--
- # It is also possible to pass the class name instead of the class:
- # set_fixture_class 'some_fixture' => 'SomeModel'
- # I think this option is redundant, i propose to deprecate it.
- # Isn't it easier to always pass the class itself?
- # (2011-12-20 alexeymuranov)
- #++
def set_fixture_class(class_names = {})
self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys)
end
def fixtures(*fixture_set_names)
if fixture_set_names.first == :all
- fixture_set_names = Dir["#{fixture_path}/**/*.{yml}"]
- fixture_set_names.map! { |f| f[(fixture_path.size + 1)..-5] }
+ fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"]
+ fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
else
- fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s }
+ fixture_set_names = fixture_set_names.flatten.map(&:to_s)
end
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)
- require_dependency file_name
- rescue LoadError => e
- # Let's hope the developer has included it himself
-
- # Let's warn in case this is a subdependency, otherwise
- # subdependency error messages are totally cryptic
- if ActiveRecord::Base.logger
- ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
- end
- end
-
- 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_set_names = nil)
fixture_set_names = Array(fixture_set_names || fixture_table_names)
methods = Module.new do
@@ -822,7 +908,7 @@ module ActiveRecord
def uses_transaction(*methods)
@uses_transaction = [] unless defined?(@uses_transaction)
- @uses_transaction.concat methods.map { |m| m.to_s }
+ @uses_transaction.concat methods.map(&:to_s)
end
def uses_transaction?(method)
@@ -836,9 +922,7 @@ module ActiveRecord
!self.class.uses_transaction?(method_name)
end
- def setup_fixtures
- return if ActiveRecord::Base.configurations.blank?
-
+ def setup_fixtures(config = ActiveRecord::Base)
if pre_loaded_fixtures && !use_transactional_fixtures
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
end
@@ -852,7 +936,7 @@ module ActiveRecord
if @@already_loaded_fixtures[self.class]
@loaded_fixtures = @@already_loaded_fixtures[self.class]
else
- @loaded_fixtures = load_fixtures
+ @loaded_fixtures = load_fixtures(config)
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
@fixture_connections = enlist_fixture_connections
@@ -863,7 +947,7 @@ module ActiveRecord
else
ActiveRecord::FixtureSet.reset_cache
@@already_loaded_fixtures[self.class] = nil
- @loaded_fixtures = load_fixtures
+ @loaded_fixtures = load_fixtures(config)
end
# Instantiate fixtures for every test if requested.
@@ -871,8 +955,6 @@ module ActiveRecord
end
def teardown_fixtures
- return if ActiveRecord::Base.configurations.blank?
-
# Rollback changes if a transaction is active.
if run_in_transaction?
@fixture_connections.each do |connection|
@@ -891,21 +973,14 @@ module ActiveRecord
end
private
- def load_fixtures
- fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
+ def load_fixtures(config)
+ fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config)
Hash[fixtures.map { |f| [f.name, f] }]
end
- # for pre_loaded_fixtures, only require the classes once. huge speed improvement
- @@required_fixture_classes = false
-
def instantiate_fixtures
if pre_loaded_fixtures
raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty?
- unless @@required_fixture_classes
- self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys
- @@required_fixture_classes = true
- end
ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
else
raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
@@ -920,3 +995,13 @@ module ActiveRecord
end
end
end
+
+class ActiveRecord::FixtureSet::RenderContext # :nodoc:
+ def self.create_subclass
+ Class.new ActiveRecord::FixtureSet.context_class do
+ def get_binding
+ binding()
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
new file mode 100644
index 0000000000..a388b529c9
--- /dev/null
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ # Returns the version of the currently loaded Active Record as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 6ab67fdece..b91e9ac137 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -1,9 +1,42 @@
+require 'active_support/core_ext/hash/indifferent_access'
+
module ActiveRecord
+ # == Single table inheritance
+ #
+ # Active Record allows inheritance by storing the name of the class in a column that by
+ # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
+ # This means that an inheritance looking like this:
+ #
+ # class Company < ActiveRecord::Base; end
+ # class Firm < Company; end
+ # class Client < Company; end
+ # class PriorityClient < Client; end
+ #
+ # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
+ # the companies table with type = "Firm". You can then fetch this row again using
+ # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
+ #
+ # Be aware that because the type column is an attribute on the record every new
+ # subclass will instantly be marked as dirty and the type column will be included
+ # in the list of changed attributes on the record. This is different from non
+ # STI classes:
+ #
+ # Company.new.changed? # => false
+ # Firm.new.changed? # => true
+ # Firm.new.changes # => {"type"=>["","Firm"]}
+ #
+ # If you don't have a type column defined in your table, single-table inheritance won't
+ # be triggered. In that case, it'll work just like normal subclasses with no special magic
+ # for differentiating between them or reloading the right type with find.
+ #
+ # Note, all the attributes for all the cases are kept in the same table. Read more:
+ # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ #
module Inheritance
extend ActiveSupport::Concern
included do
- # Determine whether to store the full constant name including namespace when using STI
+ # Determines whether to store the full constant name including namespace when using STI.
class_attribute :store_full_sti_class, instance_writer: false
self.store_full_sti_class = true
end
@@ -11,18 +44,26 @@ module ActiveRecord
module ClassMethods
# Determines if one of the attributes passed in is the inheritance column,
# and if the inheritance column is attr accessible, it initializes an
- # instance of the given subclass instead of the base class
+ # instance of the given subclass instead of the base class.
def new(*args, &block)
- if (attrs = args.first).is_a?(Hash)
- if subclass = subclass_from_attrs(attrs)
- return subclass.new(*args, &block)
- end
+ if abstract_class? || self == Base
+ raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
+ end
+
+ attrs = args.first
+ if subclass_from_attributes?(attrs)
+ subclass = subclass_from_attributes(attrs)
+ end
+
+ if subclass
+ subclass.new(*args, &block)
+ else
+ super
end
- # Delegate to the original .new
- super
end
- # True if this isn't a concrete subclass needing a STI type condition.
+ # Returns +true+ if this does not need STI type condition. Returns
+ # +false+ if STI type condition needs to be applied.
def descends_from_active_record?
if self == Base
false
@@ -39,10 +80,12 @@ module ActiveRecord
end
def symbolized_base_class
+ ActiveSupport::Deprecation.warn('`ActiveRecord::Base.symbolized_base_class` is deprecated and will be removed without replacement.')
@symbolized_base_class ||= base_class.to_s.to_sym
end
def symbolized_sti_name
+ ActiveSupport::Deprecation.warn('`ActiveRecord::Base.symbolized_sti_name` is deprecated and will be removed without replacement.')
@symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
end
@@ -108,16 +151,11 @@ module ActiveRecord
candidates << type_name
candidates.each do |candidate|
- begin
- constant = ActiveSupport::Dependencies.constantize(candidate)
- return constant if candidate == constant.to_s
- rescue NameError => e
- # We don't want to swallow NoMethodError < NameError errors
- raise e unless e.instance_of?(NameError)
- end
+ constant = ActiveSupport::Dependencies.safe_constantize(candidate)
+ return constant if candidate == constant.to_s
end
- raise NameError, "uninitialized constant #{candidates.first}"
+ raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
end
end
@@ -153,8 +191,8 @@ module ActiveRecord
end
def type_condition(table = arel_table)
- sti_column = table[inheritance_column.to_sym]
- sti_names = ([self] + descendants).map { |model| model.sti_name }
+ sti_column = table[inheritance_column]
+ sti_names = ([self] + descendants).map(&:sti_name)
sti_column.in(sti_names)
end
@@ -163,18 +201,37 @@ module ActiveRecord
# is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
# If this is a StrongParameters hash, and access to inheritance_column is not permitted,
# this will ignore the inheritance column and return nil
- def subclass_from_attrs(attrs)
+ def subclass_from_attributes?(attrs)
+ columns_hash.include?(inheritance_column) && attrs.is_a?(Hash)
+ end
+
+ def subclass_from_attributes(attrs)
subclass_name = attrs.with_indifferent_access[inheritance_column]
- return nil if subclass_name.blank? || subclass_name == self.name
- unless subclass = subclasses.detect { |sub| sub.name == subclass_name }
- raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
+
+ if subclass_name.present? && subclass_name != self.name
+ subclass = subclass_name.safe_constantize
+
+ unless descendants.include?(subclass)
+ raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
+ end
+
+ subclass
end
- subclass
end
end
+ def initialize_dup(other)
+ super
+ ensure_proper_type
+ end
+
private
+ def initialize_internals_callback
+ super
+ ensure_proper_type
+ end
+
# Sets the attribute used for single table inheritance to this class name if this is not the
# ActiveRecord::Base descendant.
# Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 7f877a6471..15b2f65dcb 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module Integration
extend ActiveSupport::Concern
@@ -5,8 +7,10 @@ module ActiveRecord
included do
##
# :singleton-method:
- # Indicates the format used to generate the timestamp format in the cache key.
- # This is +:number+, by default.
+ # Indicates the format used to generate the timestamp in the cache key.
+ # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>.
+ #
+ # This is +:nsec+, by default.
class_attribute :cache_timestamp_format, :instance_writer => false
self.cache_timestamp_format = :nsec
end
@@ -19,7 +23,7 @@ module ActiveRecord
# <tt>resources :users</tt> route. Normally, +user_path+ will
# construct a path with the user object's 'id' in it:
#
- # user = User.find_by_name('Phusion')
+ # user = User.find_by(name: 'Phusion')
# user_path(user) # => "/users/1"
#
# You can override +to_param+ in your model to make +user_path+ construct
@@ -31,7 +35,7 @@ module ActiveRecord
# end
# end
#
- # user = User.find_by_name('Phusion')
+ # user = User.find_by(name: 'Phusion')
# user_path(user) # => "/users/Phusion"
def to_param
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
@@ -43,15 +47,66 @@ module ActiveRecord
# Product.new.cache_key # => "products/new"
# Product.find(5).cache_key # => "products/5" (updated_at not available)
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
- def cache_key
+ #
+ # You can also pass a list of named timestamps, and the newest in the list will be
+ # used to generate the key:
+ #
+ # Person.find(5).cache_key(:updated_at, :last_reviewed_at)
+ def cache_key(*timestamp_names)
case
when new_record?
- "#{self.class.model_name.cache_key}/new"
- when timestamp = self[:updated_at]
+ "#{model_name.cache_key}/new"
+ when timestamp_names.any?
+ timestamp = max_updated_column_timestamp(timestamp_names)
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
+ "#{model_name.cache_key}/#{id}-#{timestamp}"
+ when timestamp = max_updated_column_timestamp
timestamp = timestamp.utc.to_s(cache_timestamp_format)
- "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
+ "#{model_name.cache_key}/#{id}-#{timestamp}"
else
- "#{self.class.model_name.cache_key}/#{id}"
+ "#{model_name.cache_key}/#{id}"
+ end
+ end
+
+ module ClassMethods
+ # Defines your model's +to_param+ method to generate "pretty" URLs
+ # using +method_name+, which can be any attribute or method that
+ # responds to +to_s+.
+ #
+ # class User < ActiveRecord::Base
+ # to_param :name
+ # end
+ #
+ # user = User.find_by(name: 'Fancy Pants')
+ # user.id # => 123
+ # user_path(user) # => "/users/123-fancy-pants"
+ #
+ # Values longer than 20 characters will be truncated. The value
+ # is truncated word by word.
+ #
+ # user = User.find_by(name: 'David HeinemeierHansson')
+ # user.id # => 125
+ # user_path(user) # => "/users/125-david"
+ #
+ # Because the generated param begins with the record's +id+, it is
+ # suitable for passing to +find+. In a controller, for example:
+ #
+ # params[:id] # => "123-fancy-pants"
+ # User.find(params[:id]).id # => 123
+ def to_param(method_name = nil)
+ if method_name.nil?
+ super()
+ else
+ define_method :to_param do
+ if (default = super()) &&
+ (result = send(method_name).to_s).present? &&
+ (param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present?
+ "#{default}-#{param}"
+ else
+ default
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 035c77c424..9f053453bd 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -11,7 +11,7 @@ module ActiveRecord
#
# == Usage
#
- # Active Records support optimistic locking if the field +lock_version+ is present. Each update to the
+ # Active Record supports optimistic locking if the +lock_version+ field is present. Each update to the
# record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice
# will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example:
#
@@ -66,7 +66,7 @@ module ActiveRecord
send(lock_col + '=', previous_lock_value + 1)
end
- def update(attribute_names = @attributes.keys) #:nodoc:
+ def _update_record(attribute_names = self.attribute_names) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
@@ -80,13 +80,14 @@ module ActiveRecord
begin
relation = self.class.unscoped
- stmt = relation.where(
- relation.table[self.class.primary_key].eq(id).and(
- relation.table[lock_col].eq(self.class.quote_value(previous_lock_value))
- )
- ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names))
-
- affected_rows = connection.update stmt
+ affected_rows = relation.where(
+ self.class.primary_key => id,
+ lock_col => previous_lock_value,
+ ).update_all(
+ attribute_names.map do |name|
+ [name, _read_attribute(name)]
+ end.to_h
+ )
unless affected_rows == 1
raise ActiveRecord::StaleObjectError.new(self, "update")
@@ -117,7 +118,7 @@ module ActiveRecord
if locking_enabled?
column_name = self.class.locking_column
column = self.class.columns_hash[column_name]
- substitute = connection.substitute_at(column, relation.bind_values.length)
+ substitute = self.class.connection.substitute_at(column)
relation = relation.where(self.class.arel_table[column_name].eq(substitute))
relation.bind_values << [column, self[column_name].to_i]
@@ -138,6 +139,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
+ clear_caches_calculated_from_columns
@locking_column = value.to_s
end
@@ -147,11 +149,6 @@ module ActiveRecord
@locking_column
end
- # Quote the column name used for optimistic locking.
- def quoted_locking_column
- connection.quote_column_name(locking_column)
- end
-
# Reset the column used for optimistic locking back to the +lock_version+ default.
def reset_locking_column
self.locking_column = DEFAULT_LOCKING_COLUMN
@@ -164,18 +161,42 @@ module ActiveRecord
super
end
- def column_defaults
- @column_defaults ||= begin
- defaults = super
-
- if defaults.key?(locking_column) && lock_optimistically
- defaults[locking_column] ||= 0
+ private
+
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `lock_optimistically`, or
+ # `locking_column` would not be picked up.
+ def inherited(subclass)
+ subclass.class_eval do
+ is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
+ decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type|
+ LockingType.new(type)
end
-
- defaults
end
+ super
end
end
end
+
+ class LockingType < SimpleDelegator # :nodoc:
+ def type_cast_from_database(value)
+ # `nil` *should* be changed to 0
+ super.to_i
+ end
+
+ def changed?(old_value, *)
+ # Ensure we save if the default was `nil`
+ super || old_value == 0
+ end
+
+ def init_with(coder)
+ __setobj__(coder['subtype'])
+ end
+
+ def encode_with(coder)
+ coder['subtype'] = __getobj__
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index b4bb95a392..ff7102d35b 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -3,12 +3,12 @@ module ActiveRecord
# Locking::Pessimistic provides support for row-level locking using
# SELECT ... FOR UPDATE and other lock types.
#
- # Pass <tt>lock: true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive
+ # Chain <tt>ActiveRecord::Base#find</tt> to <tt>ActiveRecord::QueryMethods#lock</tt> to obtain an exclusive
# lock on the selected rows:
# # select * from accounts where id=1 for update
- # Account.find(1, lock: true)
+ # Account.lock.find(1)
#
- # Pass <tt>lock: 'some locking clause'</tt> to give a database-specific locking clause
+ # Call <tt>lock('some locking clause')</tt> to use a database-specific locking clause
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
#
# Account.transaction do
@@ -26,7 +26,7 @@ module ActiveRecord
#
# Account.transaction do
# # select * from accounts where ...
- # accounts = Account.where(...).all
+ # accounts = Account.where(...)
# account1 = accounts.detect { |account| ... }
# account2 = accounts.detect { |account| ... }
# # select * from accounts where id=? for update
@@ -64,7 +64,7 @@ module ActiveRecord
end
# Wraps the passed block in a transaction, locking the object
- # before yielding. You pass can the SQL locking clause
+ # before yielding. You can pass the SQL locking clause
# as argument (see <tt>lock!</tt>).
def with_lock(lock = true)
transaction do
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 2366a91bb5..eb64d197f0 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -3,11 +3,11 @@ module ActiveRecord
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
def self.runtime=(value)
- Thread.current[:active_record_sql_runtime] = value
+ ActiveRecord::RuntimeRegistry.sql_runtime = value
end
def self.runtime
- Thread.current[:active_record_sql_runtime] ||= 0
+ ActiveRecord::RuntimeRegistry.sql_runtime ||= 0
end
def self.reset_runtime
@@ -17,17 +17,21 @@ module ActiveRecord
def initialize
super
- @odd_or_even = false
+ @odd = false
end
def render_bind(column, value)
- if column.type == :binary
- rendered_value = "<#{value.bytesize} bytes of binary data>"
+ if column
+ if column.binary?
+ # This specifically deals with the PG adapter that casts bytea columns into a Hash.
+ value = value[:value] if value.is_a?(Hash)
+ value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>"
+ end
+
+ [column.name, value]
else
- rendered_value = value
+ [nil, value]
end
-
- [column.name, rendered_value]
end
def sql(event)
@@ -39,7 +43,7 @@ module ActiveRecord
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
- sql = payload[:sql].squeeze(' ')
+ sql = payload[:sql]
binds = nil
unless (payload[:binds] || []).empty?
@@ -58,17 +62,8 @@ module ActiveRecord
debug " #{name} #{sql}#{binds}"
end
- def identity(event)
- return unless logger.debug?
-
- name = color(event.payload[:name], odd? ? CYAN : MAGENTA, true)
- line = odd? ? color(event.payload[:line], nil, true) : event.payload[:line]
-
- debug " #{name} #{line}"
- end
-
def odd?
- @odd_or_even = !@odd_or_even
+ @odd = !@odd
end
def logger
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 67339c05e5..46f4794010 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,38 +1,49 @@
-require "active_support/core_ext/class/attribute_accessors"
+require "active_support/core_ext/module/attribute_accessors"
require 'set'
module ActiveRecord
+ class MigrationError < ActiveRecordError#:nodoc:
+ def initialize(message = nil)
+ message = "\n\n#{message}\n\n" if message
+ super
+ end
+ end
+
# Exception that can be raised to stop migrations from going backwards.
- class IrreversibleMigration < ActiveRecordError
+ class IrreversibleMigration < MigrationError
end
- class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
+ class DuplicateMigrationVersionError < MigrationError#:nodoc:
def initialize(version)
super("Multiple migrations have the version number #{version}")
end
end
- class DuplicateMigrationNameError < ActiveRecordError#:nodoc:
+ class DuplicateMigrationNameError < MigrationError#:nodoc:
def initialize(name)
super("Multiple migrations have the name #{name}")
end
end
- class UnknownMigrationVersionError < ActiveRecordError #:nodoc:
+ class UnknownMigrationVersionError < MigrationError #:nodoc:
def initialize(version)
super("No migration with version number #{version}")
end
end
- class IllegalMigrationNameError < ActiveRecordError#:nodoc:
+ class IllegalMigrationNameError < MigrationError#:nodoc:
def initialize(name)
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
end
end
- class PendingMigrationError < ActiveRecordError#:nodoc:
+ class PendingMigrationError < MigrationError#:nodoc:
def initialize
- super("Migrations are pending; run 'rake db:migrate RAILS_ENV=#{Rails.env}' to resolve this issue.")
+ if defined?(Rails.env)
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}")
+ else
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate")
+ end
end
end
@@ -102,7 +113,7 @@ module ActiveRecord
# table definition.
# * <tt>drop_table(name)</tt>: Drops the table called +name+.
# * <tt>change_table(name, options)</tt>: Allows to make column alterations to
- # the table called +name+. It makes the table object availabe to a block that
+ # the table called +name+. It makes the table object available to a block that
# can then add/remove columns, indexes or foreign keys to it.
# * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+
# to +new_name+.
@@ -120,8 +131,8 @@ module ActiveRecord
# a column but keeps the type and content.
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
# the column to a different type using the same parameters as add_column.
- # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in
- # +column_names+ from the table called +table_name+.
+ # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column
+ # named +column_name+ from the table called +table_name+.
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
# with the name of the column. Other options include
# <tt>:name</tt>, <tt>:unique</tt> (e.g.
@@ -150,21 +161,14 @@ module ActiveRecord
# in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the
# UTC formatted date and time that the migration was generated.
#
- # You may then edit the <tt>up</tt> and <tt>down</tt> methods of
- # MyNewMigration.
- #
# There is a special syntactic shortcut to generate migrations that add fields to a table.
#
# rails generate migration add_fieldname_to_tablename fieldname:string
#
# This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this:
# class AddFieldnameToTablename < ActiveRecord::Migration
- # def up
- # add_column :tablenames, :fieldname, :string
- # end
- #
- # def down
- # remove_column :tablenames, :fieldname
+ # def change
+ # add_column :tablenames, :field, :string
# end
# end
#
@@ -177,14 +181,17 @@ module ActiveRecord
#
# To roll the database back to a previous migration version, use
# <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
- # you wish to downgrade. If any of the migrations throw an
- # <tt>ActiveRecord::IrreversibleMigration</tt> exception, that step will fail and you'll
- # have some manual work to do.
+ # you wish to downgrade. Alternatively, you can also use the STEP option if you
+ # wish to rollback last few migrations. <tt>rake db:migrate STEP=2</tt> will rollback
+ # the latest two migrations.
+ #
+ # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception,
+ # that step will fail and you'll have some manual work to do.
#
# == Database support
#
# Migrations are currently supported in MySQL, PostgreSQL, SQLite,
- # SQL Server, Sybase, and Oracle (all supported databases except DB2).
+ # SQL Server, and Oracle (all supported databases except DB2).
#
# == More examples
#
@@ -330,6 +337,24 @@ module ActiveRecord
#
# For a list of commands that are reversible, please see
# <tt>ActiveRecord::Migration::CommandRecorder</tt>.
+ #
+ # == Transactional Migrations
+ #
+ # If the database adapter supports DDL transactions, all migrations will
+ # automatically be wrapped in a transaction. There are queries that you
+ # can't execute inside a transaction though, and for these situations
+ # you can turn the automatic transactions off.
+ #
+ # class ChangeEnum < ActiveRecord::Migration
+ # disable_ddl_transaction!
+ #
+ # def up
+ # execute "ALTER TYPE model_size ADD VALUE 'new_value'"
+ # end
+ # end
+ #
+ # Remember that you can still open your own transactions, even if you
+ # are in a Migration with <tt>self.disable_ddl_transaction!</tt>.
class Migration
autoload :CommandRecorder, 'active_record/migration/command_recorder'
@@ -339,34 +364,74 @@ module ActiveRecord
class CheckPending
def initialize(app)
@app = app
+ @last_check = 0
end
def call(env)
- ActiveRecord::Base.logger.quietly do
- ActiveRecord::Migration.check_pending!
+ if connection.supports_migrations?
+ mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ if @last_check < mtime
+ ActiveRecord::Migration.check_pending!(connection)
+ @last_check = mtime
+ end
end
@app.call(env)
end
+
+ private
+
+ def connection
+ ActiveRecord::Base.connection
+ end
end
class << self
attr_accessor :delegate # :nodoc:
- end
+ attr_accessor :disable_ddl_transaction # :nodoc:
- def self.check_pending!
- raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?
- end
+ def check_pending!(connection = Base.connection)
+ raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection)
+ end
- def self.method_missing(name, *args, &block) # :nodoc:
- (delegate || superclass.delegate).send(name, *args, &block)
+ def load_schema_if_pending!
+ if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations?
+ # Roundrip to Rake to allow plugins to hook into database initialization.
+ FileUtils.cd Rails.root do
+ current_config = Base.connection_config
+ Base.clear_all_connections!
+ system("bin/rake db:test:prepare")
+ # Establish a new connection, the old database may be gone (db:test:prepare uses purge)
+ Base.establish_connection(current_config)
+ end
+ check_pending!
+ end
+ end
+
+ def maintain_test_schema! # :nodoc:
+ if ActiveRecord::Base.maintain_test_schema
+ suppress_messages { load_schema_if_pending! }
+ end
+ end
+
+ def method_missing(name, *args, &block) # :nodoc:
+ (delegate || superclass.delegate).send(name, *args, &block)
+ end
+
+ def migrate(direction)
+ new.migrate direction
+ end
+
+ # Disable DDL transactions for this migration.
+ def disable_ddl_transaction!
+ @disable_ddl_transaction = true
+ end
end
- def self.migrate(direction)
- new.migrate direction
+ def disable_ddl_transaction # :nodoc:
+ self.class.disable_ddl_transaction
end
cattr_accessor :verbose
-
attr_accessor :name, :version
def initialize(name = self.class.name, version = nil)
@@ -375,8 +440,8 @@ module ActiveRecord
@connection = nil
end
+ self.verbose = true
# instantiate the delegate object after initialize is defined
- self.verbose = true
self.delegate = new
# Reverses the migration commands for the given block and
@@ -439,7 +504,7 @@ module ActiveRecord
@connection.respond_to?(:reverting) && @connection.reverting
end
- class ReversibleBlockHelper < Struct.new(:reverting)
+ class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc:
def up
yield unless reverting
end
@@ -582,13 +647,15 @@ module ActiveRecord
end
def method_missing(method, *arguments, &block)
- arg_list = arguments.map{ |a| a.inspect } * ', '
+ arg_list = arguments.map(&:inspect) * ', '
say_with_time "#{method}(#{arg_list})" do
unless @connection.respond_to? :revert
- unless arguments.empty? || method == :execute
- arguments[0] = Migrator.proper_table_name(arguments.first)
- arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table
+ unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
+ arguments[0] = proper_table_name(arguments.first, table_name_options)
+ if [:rename_table, :add_foreign_key].include?(method)
+ arguments[1] = proper_table_name(arguments.second, table_name_options)
+ end
end
end
return super unless connection.respond_to?(method)
@@ -599,7 +666,7 @@ module ActiveRecord
def copy(destination, sources, options = {})
copied = []
- FileUtils.mkdir_p(destination) unless File.exists?(destination)
+ FileUtils.mkdir_p(destination) unless File.exist?(destination)
destination_migrations = ActiveRecord::Migrator.migrations(destination)
last = destination_migrations.last
@@ -607,8 +674,17 @@ module ActiveRecord
source_migrations = ActiveRecord::Migrator.migrations(path)
source_migrations.each do |migration|
- source = File.read(migration.filename)
- source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}"
+ source = File.binread(migration.filename)
+ inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n"
+ if /\A#.*\b(?:en)?coding:\s*\S+/ =~ source
+ # If we have a magic comment in the original migration,
+ # insert our comment after the first newline(end of the magic comment line)
+ # so the magic keep working.
+ # Note that magic comments must be at the first line(except sh-bang).
+ source[/\n/] = "\n#{inserted_comment}"
+ else
+ source = "#{inserted_comment}#{source}"
+ end
if duplicate = destination_migrations.detect { |m| m.name == migration.name }
if options[:on_skip] && duplicate.scope != scope.to_s
@@ -622,7 +698,7 @@ module ActiveRecord
old_path, migration.filename = migration.filename, new_path
last = migration
- File.open(migration.filename, "w") { |f| f.write source }
+ File.binwrite(migration.filename, source)
copied << migration
options[:on_copy].call(scope, migration, old_path) if options[:on_copy]
destination_migrations << migration
@@ -632,14 +708,33 @@ module ActiveRecord
copied
end
+ # Finds the correct table name given an Active Record object.
+ # Uses the Active Record object's own table_name, or pre/suffix from the
+ # options passed in.
+ def proper_table_name(name, options = {})
+ if name.respond_to? :table_name
+ name.table_name
+ else
+ "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}"
+ end
+ end
+
+ # Determines the version number of the next migration.
def next_migration_number(number)
if ActiveRecord::Base.timestamped_migrations
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max
else
- "%.3d" % number
+ SchemaMigration.normalize_migration_number(number)
end
end
+ def table_name_options(config = ActiveRecord::Base)
+ {
+ table_name_prefix: config.table_name_prefix,
+ table_name_suffix: config.table_name_suffix
+ }
+ end
+
private
def execute_block
if connection.respond_to? :execute_block
@@ -663,7 +758,11 @@ module ActiveRecord
File.basename(filename)
end
- delegate :migrate, :announce, :write, :to => :migration
+ def mtime
+ File.mtime filename
+ end
+
+ delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
private
@@ -673,11 +772,21 @@ module ActiveRecord
def load_migration
require(File.expand_path(filename))
- name.constantize.new
+ name.constantize.new(name, version)
end
end
+ class NullMigration < MigrationProxy #:nodoc:
+ def initialize
+ super(nil, 0, nil, nil)
+ end
+
+ def mtime
+ 0
+ end
+ end
+
class Migrator#:nodoc:
class << self
attr_writer :migrations_paths
@@ -708,56 +817,54 @@ module ActiveRecord
migrations = migrations(migrations_paths)
migrations.select! { |m| yield m } if block_given?
- self.new(:up, migrations, target_version).migrate
+ new(:up, migrations, target_version).migrate
end
def down(migrations_paths, target_version = nil, &block)
migrations = migrations(migrations_paths)
migrations.select! { |m| yield m } if block_given?
- self.new(:down, migrations, target_version).migrate
+ new(:down, migrations, target_version).migrate
end
def run(direction, migrations_paths, target_version)
- self.new(direction, migrations(migrations_paths), target_version).run
+ new(direction, migrations(migrations_paths), target_version).run
end
def open(migrations_paths)
- self.new(:up, migrations(migrations_paths), nil)
+ new(:up, migrations(migrations_paths), nil)
end
def schema_migrations_table_name
SchemaMigration.table_name
end
- def get_all_versions
- SchemaMigration.all.map { |x| x.version.to_i }.sort
- end
-
- def current_version
- sm_table = schema_migrations_table_name
- if Base.connection.table_exists?(sm_table)
- get_all_versions.max || 0
+ def get_all_versions(connection = Base.connection)
+ if connection.table_exists?(schema_migrations_table_name)
+ SchemaMigration.all.map { |x| x.version.to_i }.sort
else
- 0
+ []
end
end
- def needs_migration?
- current_version < last_version
+ def current_version(connection = Base.connection)
+ get_all_versions(connection).max || 0
+ end
+
+ def needs_migration?(connection = Base.connection)
+ (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0
+ end
+
+ def any_migrations?
+ migrations(migrations_paths).any?
end
def last_version
- migrations(migrations_paths).last.try(:version)||0
+ last_migration.version
end
- def proper_table_name(name)
- # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
- if name.respond_to? :table_name
- name.table_name
- else
- "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
- end
+ def last_migration #:nodoc:
+ migrations(migrations_paths).last || NullMigration.new
end
def migrations_paths
@@ -791,7 +898,7 @@ module ActiveRecord
private
def move(direction, migrations_paths, steps)
- migrator = self.new(direction, migrations(migrations_paths))
+ migrator = new(direction, migrations(migrations_paths))
start_index = migrator.migrations.index(migrator.current_migration)
if start_index
@@ -808,21 +915,15 @@ module ActiveRecord
@direction = direction
@target_version = target_version
@migrated_versions = nil
-
- if Array(migrations).grep(String).empty?
- @migrations = migrations
- else
- ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations"
- @migrations = self.class.migrations(migrations)
- end
+ @migrations = migrations
validate(@migrations)
- ActiveRecord::SchemaMigration.create_table
+ Base.connection.initialize_schema_migrations_table
end
def current_version
- migrated.sort.last || 0
+ migrated.max || 0
end
def current_migration
@@ -831,11 +932,15 @@ module ActiveRecord
alias :current :current_migration
def run
- target = migrations.detect { |m| m.version == @target_version }
- raise UnknownMigrationVersionError.new(@target_version) if target.nil?
- unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
- target.migrate(@direction)
- record_version_state_after_migrating(target.version)
+ migration = migrations.detect { |m| m.version == @target_version }
+ raise UnknownMigrationVersionError.new(@target_version) if migration.nil?
+ unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i))
+ begin
+ execute_migration_in_transaction(migration, @direction)
+ rescue => e
+ canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : ""
+ raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace
+ end
end
end
@@ -844,24 +949,13 @@ module ActiveRecord
raise UnknownMigrationVersionError.new(@target_version)
end
- running = runnable
-
- if block_given?
- message = "block argument to migrate is deprecated, please filter migrations before constructing the migrator"
- ActiveSupport::Deprecation.warn message
- running.select! { |m| yield m }
- end
-
- running.each do |migration|
+ runnable.each do |migration|
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
begin
- ddl_transaction do
- migration.migrate(@direction)
- record_version_state_after_migrating(migration.version)
- end
+ execute_migration_in_transaction(migration, @direction)
rescue => e
- canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
+ canceled_msg = use_transaction?(migration) ? "this and " : ""
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
end
end
@@ -896,6 +990,13 @@ module ActiveRecord
migrated.include?(migration.version.to_i)
end
+ def execute_migration_in_transaction(migration, direction)
+ ddl_transaction(migration) do
+ migration.migrate(direction)
+ record_version_state_after_migrating(migration.version)
+ end
+ end
+
def target
migrations.detect { |m| m.version == @target_version }
end
@@ -935,12 +1036,16 @@ module ActiveRecord
end
# Wrap the migration in a transaction only if supported by the adapter.
- def ddl_transaction
- if Base.connection.supports_ddl_transactions?
+ def ddl_transaction(migration)
+ if use_transaction?(migration)
Base.transaction { yield }
else
yield
end
end
+
+ def use_transaction?(migration)
+ !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions?
+ end
end
end
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 79c55045ba..36256415df 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -73,8 +73,10 @@ module ActiveRecord
[:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
:rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
:change_column_default, :add_reference, :remove_reference, :transaction,
- :drop_join_table, :drop_table, :execute_block,
- :change_column, :execute, :remove_columns, # irreversible methods need to be here too
+ :drop_join_table, :drop_table, :execute_block, :enable_extension,
+ :change_column, :execute, :remove_columns, :change_column_null,
+ :add_foreign_key, :remove_foreign_key
+ # irreversible methods need to be here too
].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def create_table(*args, &block)
@@ -85,8 +87,8 @@ module ActiveRecord
alias :add_belongs_to :add_reference
alias :remove_belongs_to :remove_reference
- def change_table(table_name, options = {})
- yield ConnectionAdapters::Table.new(table_name, self)
+ def change_table(table_name, options = {}) # :nodoc:
+ yield delegate.update_table_definition(table_name, self)
end
private
@@ -100,6 +102,7 @@ module ActiveRecord
add_column: :remove_column,
add_timestamps: :remove_timestamps,
add_reference: :remove_reference,
+ enable_extension: :disable_extension
}.each do |cmd, inv|
[[inv, cmd], [cmd, inv]].uniq.each do |method, inverse|
class_eval <<-EOV, __FILE__, __LINE__ + 1
@@ -139,12 +142,20 @@ module ActiveRecord
def invert_add_index(args)
table, columns, options = *args
- [:remove_index, [table, (options || {}).merge(column: columns)]]
+ options ||= {}
+
+ index_name = options[:name]
+ options_hash = index_name ? { name: index_name } : { column: columns }
+
+ [:remove_index, [table, options_hash]]
end
def invert_remove_index(args)
table, options = *args
- raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." unless options && options[:column]
+
+ unless options && options.is_a?(Hash) && options[:column]
+ raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option."
+ end
options = options.dup
[:add_index, [table, options.delete(:column), options]]
@@ -153,11 +164,33 @@ module ActiveRecord
alias :invert_add_belongs_to :invert_add_reference
alias :invert_remove_belongs_to :invert_remove_reference
+ def invert_change_column_null(args)
+ args[2] = !args[2]
+ [:change_column_null, args]
+ end
+
+ def invert_add_foreign_key(args)
+ from_table, to_table, add_options = args
+ add_options ||= {}
+
+ if add_options[:name]
+ options = { name: add_options[:name] }
+ elsif add_options[:column]
+ options = { column: add_options[:column] }
+ else
+ options = to_table
+ end
+
+ [:remove_foreign_key, [from_table, options]]
+ end
+
# Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
- @delegate.send(method, *args, &block)
- rescue NoMethodError => e
- raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}")
+ if @delegate.respond_to?(method)
+ @delegate.send(method, *args, &block)
+ else
+ super
+ end
end
end
end
diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb
index ebf64cbcdc..05569fadbd 100644
--- a/activerecord/lib/active_record/migration/join_table.rb
+++ b/activerecord/lib/active_record/migration/join_table.rb
@@ -8,7 +8,7 @@ module ActiveRecord
end
def join_table_name(table_1, table_2)
- [table_1.to_s, table_2.to_s].sort.join("_").to_sym
+ ModelSchema.derive_join_table_name(table_1, table_2).to_sym
end
end
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 85fb4be992..641512d323 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -29,11 +29,21 @@ module ActiveRecord
# :singleton-method:
# Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
# "people_basecamp"). By default, the suffix is the empty string.
+ #
+ # If you are organising your models within modules, you can add a suffix to the models within
+ # a namespace by defining a singleton method in the parent module called table_name_suffix which
+ # returns your chosen suffix.
class_attribute :table_name_suffix, instance_writer: false
self.table_name_suffix = ""
##
# :singleton-method:
+ # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations"
+ class_attribute :schema_migrations_table_name, instance_accessor: false
+ self.schema_migrations_table_name = "schema_migrations"
+
+ ##
+ # :singleton-method:
# Indicates whether table names should be the pluralized versions of the corresponding class names.
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
# See table_name for the full rules on table/class naming. This is true, by default.
@@ -41,6 +51,19 @@ module ActiveRecord
self.pluralize_table_names = true
self.inheritance_column = 'type'
+
+ delegate :type_for_attribute, to: :class
+ end
+
+ # Derives the join table name for +first_table+ and +second_table+. The
+ # table names appear in alphabetical order. A common prefix is removed
+ # (useful for namespaced models like Music::Artist and Music::Record):
+ #
+ # artists, records => artists_records
+ # records, artists => artists_records
+ # music_artists, music_records => music_artists_records
+ def self.derive_join_table_name(first_table, second_table) # :nodoc:
+ [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
end
module ClassMethods
@@ -124,7 +147,7 @@ module ActiveRecord
@quoted_table_name = nil
@arel_table = nil
@sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name
- @relation = Relation.new(self, arel_table)
+ @predicate_builder = nil
end
# Returns a quoted version of the table name, used to construct SQL statements.
@@ -147,6 +170,10 @@ module ActiveRecord
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
end
+ def full_table_name_suffix #:nodoc:
+ (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix
+ end
+
# Defines the name of the table column which will store the class name on single-table
# inheritance situations.
#
@@ -184,7 +211,7 @@ module ActiveRecord
# given block. This is required for Oracle and is useful for any
# database which relies on sequences for primary key generation.
#
- # If a sequence name is not explicitly set when using Oracle or Firebird,
+ # If a sequence name is not explicitly set when using Oracle,
# it will default to the commonly used pattern of: #{table_name}_seq
#
# If a sequence name is not explicitly set when using PostgreSQL, it
@@ -203,67 +230,40 @@ module ActiveRecord
connection.schema_cache.table_exists?(table_name)
end
- # Returns an array of column objects for the table associated with this class.
- def columns
- @columns ||= connection.schema_cache.columns[table_name].map do |col|
- col = col.dup
- col.primary = (col.name == primary_key)
- col
- end
- end
-
- # Returns a hash of column objects for the table associated with this class.
- def columns_hash
- @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ def attributes_builder # :nodoc:
+ @attributes_builder ||= AttributeSet::Builder.new(column_types, primary_key)
end
def column_types # :nodoc:
- @column_types ||= decorate_columns(columns_hash.dup)
- end
-
- def decorate_columns(columns_hash) # :nodoc:
- return if columns_hash.empty?
-
- columns_hash.each do |name, col|
- if serialized_attributes.key?(name)
- columns_hash[name] = AttributeMethods::Serialization::Type.new(col)
- end
- if create_time_zone_conversion_attribute?(name, col)
- columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(col)
- end
+ @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h|
+ h.default = Type::Value.new
end
+ end
- columns_hash
+ def type_for_attribute(attr_name) # :nodoc:
+ column_types[attr_name]
end
# Returns a hash where the keys are column names and the values are
# default values when instantiating the AR object for this table.
def column_defaults
- @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }]
+ _default_attributes.to_hash
+ end
+
+ def _default_attributes # :nodoc:
+ @default_attributes ||= attributes_builder.build_from_database(
+ raw_default_values)
end
# Returns an array of column names as strings.
def column_names
- @column_names ||= columns.map { |column| column.name }
+ @column_names ||= columns.map(&:name)
end
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
# and columns used for single table inheritance have been removed.
def content_columns
- @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
- end
-
- # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
- # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
- # is available.
- def column_methods_hash #:nodoc:
- @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods|
- attr_name = attr.to_s
- methods[attr.to_sym] = attr_name
- methods["#{attr}=".to_sym] = attr_name
- methods["#{attr}?".to_sym] = attr_name
- methods["#{attr}_before_type_cast".to_sym] = attr_name
- end
+ @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
end
# Resets all the cached information about columns, which will cause them
@@ -297,23 +297,13 @@ module ActiveRecord
undefine_attribute_methods
connection.schema_cache.clear_table_cache!(table_name) if table_exists?
- @arel_engine = nil
- @column_defaults = nil
- @column_names = nil
- @columns = nil
- @columns_hash = nil
- @column_types = nil
- @content_columns = nil
- @dynamic_methods_hash = nil
- @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
- @relation = nil
- end
-
- # This is a hook for use by modules that need to do extra stuff to
- # attributes when they are initialized. (e.g. attribute
- # serialization)
- def initialize_attributes(attributes, options = {}) #:nodoc:
- attributes
+ @arel_engine = nil
+ @arel_table = nil
+ @column_names = nil
+ @column_types = nil
+ @content_columns = nil
+ @default_attributes = nil
+ @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
end
private
@@ -334,12 +324,17 @@ module ActiveRecord
contained = contained.singularize if parent.pluralize_table_names
contained += '_'
end
- "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
+
+ "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
else
# STI subclasses always use their superclass' table.
base.table_name
end
end
+
+ def raw_default_values
+ columns_hash.transform_values(&:default)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 4c9bd76d7c..846e1162a9 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -58,7 +58,7 @@ module ActiveRecord
# It also allows you to update the avatar through the member:
#
# params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } }
- # member.update_attributes params[:member]
+ # member.update params[:member]
# member.avatar.icon # => 'sad'
#
# By default you will only be able to set and update attributes on the
@@ -81,6 +81,9 @@ module ActiveRecord
#
# Note that the model will _not_ be destroyed until the parent is saved.
#
+ # Also note that the model will not be destroyed unless you also specify
+ # its id in the updated hash.
+ #
# === One-to-many
#
# Consider a member that has a number of posts:
@@ -90,8 +93,9 @@ module ActiveRecord
# accepts_nested_attributes_for :posts
# end
#
- # You can now set or update attributes on an associated post model through
- # the attribute hash.
+ # You can now set or update attributes on the associated posts through
+ # an attribute hash for a member: include the key +:posts_attributes+
+ # with an array of hashes of post attributes as a value.
#
# For each hash that does _not_ have an <tt>id</tt> key a new record will
# be instantiated, unless the hash also contains a <tt>_destroy</tt> key
@@ -110,14 +114,14 @@ module ActiveRecord
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
#
- # You may also set a :reject_if proc to silently ignore any new record
+ # You may also set a +:reject_if+ proc to silently ignore any new record
# hashes if they fail to pass your criteria. For example, the previous
# example could be rewritten as:
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
- # end
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
+ # end
#
# params = { member: {
# name: 'joe', posts_attributes: [
@@ -132,21 +136,21 @@ module ActiveRecord
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
#
- # Alternatively, :reject_if also accepts a symbol for using methods:
+ # Alternatively, +:reject_if+ also accepts a symbol for using methods:
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, reject_if: :new_record?
- # end
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: :new_record?
+ # end
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, reject_if: :reject_posts
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: :reject_posts
#
- # def reject_posts(attributed)
- # attributed['title'].blank?
- # end
- # end
+ # def reject_posts(attributed)
+ # attributed['title'].blank?
+ # end
+ # end
#
# If the hash contains an <tt>id</tt> key that matches an already
# associated record, the matching record will be modified:
@@ -183,18 +187,41 @@ module ActiveRecord
# member.save
# member.reload.posts.length # => 1
#
+ # Nested attributes for an associated collection can also be passed in
+ # the form of a hash of hashes instead of an array of hashes:
+ #
+ # Member.create(name: 'joe',
+ # posts_attributes: { first: { title: 'Foo' },
+ # second: { title: 'Bar' } })
+ #
+ # has the same effect as
+ #
+ # Member.create(name: 'joe',
+ # posts_attributes: [ { title: 'Foo' },
+ # { title: 'Bar' } ])
+ #
+ # The keys of the hash which is the value for +:posts_attributes+ are
+ # ignored in this case.
+ # However, it is not allowed to use +'id'+ or +:id+ for one of
+ # such keys, otherwise the hash will be wrapped in an array and
+ # interpreted as an attribute hash for a single post.
+ #
+ # Passing attributes for an associated collection in the form of a hash
+ # of hashes can be used with hashes generated from HTTP/HTML parameters,
+ # where there maybe no natural way to submit an array of hashes.
+ #
# === Saving
#
# All changes to models, including the destruction of those marked for
# destruction, are saved and destroyed automatically and atomically when
# the parent model is saved. This happens inside the transaction initiated
- # by the parents save method. See ActiveRecord::AutosaveAssociation.
+ # by the parent's save method. See ActiveRecord::AutosaveAssociation.
#
# === Validating the presence of a parent model
#
# If you want to validate that a child record is associated with a parent
- # record, you can use <tt>validates_presence_of</tt> and
- # <tt>inverse_of</tt> as this example illustrates:
+ # record, you can use the +validates_presence_of+ method and the +:inverse_of+
+ # key as this example illustrates:
#
# class Member < ActiveRecord::Base
# has_many :posts, inverse_of: :member
@@ -205,6 +232,27 @@ module ActiveRecord
# belongs_to :member, inverse_of: :posts
# validates_presence_of :member
# end
+ #
+ # Note that if you do not specify the +:inverse_of+ option, then
+ # Active Record will try to automatically guess the inverse association
+ # based on heuristics.
+ #
+ # For one-to-one nested associations, if you build the new (in-memory)
+ # child object yourself before assignment, then this module will not
+ # overwrite it, e.g.:
+ #
+ # class Member < ActiveRecord::Base
+ # has_one :avatar
+ # accepts_nested_attributes_for :avatar
+ #
+ # def avatar
+ # super || build_avatar(width: 200)
+ # end
+ # end
+ #
+ # member = Member.new
+ # member.avatar_attributes = {icon: 'sad'}
+ # member.avatar.width # => 200
module ClassMethods
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
@@ -219,29 +267,31 @@ module ActiveRecord
# Allows you to specify a Proc or a Symbol pointing to a method
# that checks whether a record should be built for a certain attribute
# hash. The hash is passed to the supplied Proc or the method
- # and it should return either +true+ or +false+. When no :reject_if
+ # and it should return either +true+ or +false+. When no +:reject_if+
# is specified, a record will be built for all attribute hashes that
# do not have a <tt>_destroy</tt> value that evaluates to true.
# Passing <tt>:all_blank</tt> instead of a Proc will create a proc
# that will reject a record where all the attributes are blank excluding
- # any value for _destroy.
+ # any value for +_destroy+.
# [:limit]
- # Allows you to specify the maximum number of the associated records that
- # can be processed with the nested attributes. Limit also can be specified as a
- # Proc or a Symbol pointing to a method that should return number. If the size of the
- # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
- # exception is raised. If omitted, any number associations can be processed.
- # Note that the :limit option is only applicable to one-to-many associations.
+ # Allows you to specify the maximum number of associated records that
+ # can be processed with the nested attributes. Limit also can be specified
+ # as a Proc or a Symbol pointing to a method that should return a number.
+ # If the size of the nested attributes array exceeds the specified limit,
+ # NestedAttributes::TooManyRecords exception is raised. If omitted, any
+ # number of associations can be processed.
+ # Note that the +:limit+ option is only applicable to one-to-many
+ # associations.
# [:update_only]
# For a one-to-one association, this option allows you to specify how
- # nested attributes are to be used when an associated record already
+ # nested attributes are going to be used when an associated record already
# exists. In general, an existing record may either be updated with the
# new set of attribute values or be replaced by a wholly new record
- # containing those values. By default the :update_only option is +false+
+ # containing those values. By default the +:update_only+ option is +false+
# and the nested attributes are used to update the existing record only
# if they include the record's <tt>:id</tt> value. Otherwise a new
# record will be instantiated and used to replace the existing one.
- # However if the :update_only option is +true+, the nested attributes
+ # However if the +:update_only+ option is +true+, the nested attributes
# are used to update the record's attributes always, regardless of
# whether the <tt>:id</tt> is present. The option is ignored for collection
# associations.
@@ -260,8 +310,8 @@ module ActiveRecord
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
attr_names.each do |association_name|
- if reflection = reflect_on_association(association_name)
- reflection.options[:autosave] = true
+ if reflection = _reflect_on_association(association_name)
+ reflection.autosave = true
add_autosave_association_callbacks(reflection)
nested_attributes_options = self.nested_attributes_options.dup
@@ -269,23 +319,36 @@ module ActiveRecord
self.nested_attributes_options = nested_attributes_options
type = (reflection.collection? ? :collection : :one_to_one)
-
- # def pirate_attributes=(attributes)
- # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options)
- # end
- generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
- if method_defined?(:#{association_name}_attributes=)
- remove_method(:#{association_name}_attributes=)
- end
- def #{association_name}_attributes=(attributes)
- assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
- end
- eoruby
+ generate_association_writer(association_name, type)
else
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
end
end
end
+
+ private
+
+ # Generates a writer method for this association. Serves as a point for
+ # accessing the objects in the association. For example, this method
+ # could generate the following:
+ #
+ # def pirate_attributes=(attributes)
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
+ # end
+ #
+ # This redirects the attempts to write objects in an association through
+ # the helper methods defined below. Makes it seem like the nested
+ # associations are just regular associations.
+ def generate_association_writer(association_name, type)
+ generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
+ if method_defined?(:#{association_name}_attributes=)
+ remove_method(:#{association_name}_attributes=)
+ end
+ def #{association_name}_attributes=(attributes)
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
+ end
+ eoruby
+ end
end
# Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
@@ -319,20 +382,28 @@ module ActiveRecord
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
+ existing_record = send(association_name)
- if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
- (options[:update_only] || record.id.to_s == attributes['id'].to_s)
- assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
+ if (options[:update_only] || !attributes['id'].blank?) && existing_record &&
+ (options[:update_only] || existing_record.id.to_s == attributes['id'].to_s)
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
elsif attributes['id'].present?
- raise_nested_attributes_record_not_found(association_name, attributes['id'])
+ raise_nested_attributes_record_not_found!(association_name, attributes['id'])
elsif !reject_new_record?(association_name, attributes)
- method = "build_#{association_name}"
- if respond_to?(method)
- send(method, attributes.except(*UNASSIGNABLE_KEYS))
+ assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
+
+ if existing_record && existing_record.new_record?
+ existing_record.assign_attributes(assignable_attributes)
+ association(association_name).initialize_attributes(existing_record)
else
- raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
+ method = "build_#{association_name}"
+ if respond_to?(method)
+ send(method, assignable_attributes)
+ else
+ raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
+ end
end
end
end
@@ -371,20 +442,7 @@ module ActiveRecord
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
end
- if limit = options[:limit]
- limit = case limit
- when Symbol
- send(limit)
- when Proc
- limit.call
- else
- limit
- end
-
- if limit && attributes_collection.size > limit
- raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
- end
- end
+ check_record_limit!(options[:limit], attributes_collection)
if attributes_collection.is_a? Hash
keys = attributes_collection.keys
@@ -412,23 +470,44 @@ module ActiveRecord
association.build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
- unless association.loaded? || call_reject_if(association_name, attributes)
+ unless call_reject_if(association_name, attributes)
# Make sure we are operating on the actual object which is in the association's
# proxy_target array (either by finding it, or adding it if not found)
- target_record = association.target.detect { |record| record == existing_record }
-
+ # Take into account that the proxy_target may have changed due to callbacks
+ target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s }
if target_record
existing_record = target_record
else
- association.add_to_target(existing_record)
+ association.add_to_target(existing_record, :skip_callbacks)
end
- end
- if !call_reject_if(association_name, attributes)
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
else
- raise_nested_attributes_record_not_found(association_name, attributes['id'])
+ raise_nested_attributes_record_not_found!(association_name, attributes['id'])
+ end
+ end
+ end
+
+ # Takes in a limit and checks if the attributes_collection has too many
+ # records. It accepts limit in the form of symbol, proc, or
+ # number-like object (anything that can be compared with an integer).
+ #
+ # Raises TooManyRecords error if the attributes_collection is
+ # larger than the limit.
+ def check_record_limit!(limit, attributes_collection)
+ if limit
+ limit = case limit
+ when Symbol
+ send(limit)
+ when Proc
+ limit.call
+ else
+ limit
+ end
+
+ if limit && attributes_collection.size > limit
+ raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
end
end
end
@@ -442,16 +521,21 @@ module ActiveRecord
# Determines if a hash contains a truthy _destroy key.
def has_destroy_flag?(hash)
- ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
+ Type::Boolean.new.type_cast_from_user(hash['_destroy'])
end
- # Determines if a new record should be build by checking for
+ # Determines if a new record should be rejected by checking
# has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
# association and evaluates to +true+.
def reject_new_record?(association_name, attributes)
has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
end
+ # Determines if a record with the particular +attributes+ should be
+ # rejected by calling the reject_if Symbol or Proc (if defined).
+ # The reject_if option is defined by +accepts_nested_attributes_for+.
+ #
+ # Returns false if there is a +destroy_flag+ on the attributes.
def call_reject_if(association_name, attributes)
return false if has_destroy_flag?(attributes)
case callback = self.nested_attributes_options[association_name][:reject_if]
@@ -462,8 +546,8 @@ module ActiveRecord
end
end
- def raise_nested_attributes_record_not_found(association_name, record_id)
- raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
+ def raise_nested_attributes_record_not_found!(association_name, record_id)
+ raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
end
end
end
diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb
new file mode 100644
index 0000000000..edb5066fa0
--- /dev/null
+++ b/activerecord/lib/active_record/no_touching.rb
@@ -0,0 +1,52 @@
+module ActiveRecord
+ # = Active Record No Touching
+ module NoTouching
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Lets you selectively disable calls to `touch` for the
+ # duration of a block.
+ #
+ # ==== Examples
+ # ActiveRecord::Base.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # does nothing
+ # end
+ #
+ # Project.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # works, but does not touch the associated project
+ # end
+ #
+ def no_touching(&block)
+ NoTouching.apply_to(self, &block)
+ end
+ end
+
+ class << self
+ def apply_to(klass) #:nodoc:
+ klasses.push(klass)
+ yield
+ ensure
+ klasses.pop
+ end
+
+ def applied_to?(klass) #:nodoc:
+ klasses.any? { |k| k >= klass }
+ end
+
+ private
+ def klasses
+ Thread.current[:no_touching_classes] ||= []
+ end
+ end
+
+ def no_touching?
+ NoTouching.applied_to?(self.class)
+ end
+
+ def touch(*) # :nodoc:
+ super unless no_touching?
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 711fc8b883..b406da14dc 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -6,7 +6,7 @@ module ActiveRecord
@records = []
end
- def pluck(_column_name)
+ def pluck(*column_names)
[]
end
@@ -23,7 +23,7 @@ module ActiveRecord
end
def size
- 0
+ calculate :size, nil
end
def empty?
@@ -39,23 +39,37 @@ module ActiveRecord
end
def to_sql
- @to_sql ||= ""
- end
-
- def where_values_hash
- {}
+ ""
end
def count(*)
- 0
+ calculate :count, nil
end
def sum(*)
- 0
+ calculate :sum, nil
+ end
+
+ def average(*)
+ calculate :average, nil
+ end
+
+ def minimum(*)
+ calculate :minimum, nil
+ end
+
+ def maximum(*)
+ calculate :maximum, nil
end
- def calculate(_operation, _column_name, _options = {})
- nil
+ def calculate(operation, _column_name)
+ if [:count, :sum, :size].include? operation
+ group_values.any? ? Hash.new : 0
+ elsif [:average, :minimum, :maximum].include?(operation) && group_values.any?
+ Hash.new
+ else
+ nil
+ end
end
def exists?(_id = false)
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 4d1a9c94b7..f53c5f17ef 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -10,9 +10,6 @@ module ActiveRecord
# The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the
# attributes on the objects that are to be created.
#
- # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
- # in the +options+ parameter.
- #
# ==== Examples
# # Create a single new object
# User.create(first_name: 'Jamie')
@@ -39,8 +36,25 @@ module ActiveRecord
end
end
+ # Creates an object (or multiple objects) and saves it to the database,
+ # if validations pass. Raises a RecordInvalid error if validations fail,
+ # unlike Base#create.
+ #
+ # The +attributes+ parameter can be either a Hash or an Array of Hashes.
+ # These describe which attributes to be created on the object, or
+ # multiple objects when given an Array of Hashes.
+ def create!(attributes = nil, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create!(attr, &block) }
+ else
+ object = new(attributes, &block)
+ object.save!
+ object
+ end
+ end
+
# Given an attributes hash, +instantiate+ returns a new instance of
- # the appropriate class.
+ # the appropriate class. Accepts only keys as strings.
#
# For example, +Post.all+ may return Comments, Messages, and Emails
# by storing the record's subclass in a +type+ attribute. By calling
@@ -49,10 +63,10 @@ module ActiveRecord
#
# See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see
# how this "single-table" inheritance mapping is implemented.
- def instantiate(record, column_types = {})
- klass = discriminate_class_for_record(record)
- column_types = klass.decorate_columns(column_types)
- klass.allocate.init_with('attributes' => record, 'column_types' => column_types)
+ def instantiate(attributes, column_types = {})
+ klass = discriminate_class_for_record(attributes)
+ attributes = klass.attributes_builder.build_from_database(attributes, column_types)
+ klass.allocate.init_with('attributes' => attributes, 'new_record' => false)
end
private
@@ -67,13 +81,15 @@ module ActiveRecord
end
# Returns true if this object hasn't been saved yet -- that is, a record
- # for the object doesn't exist in the data store yet; otherwise, returns false.
+ # for the object doesn't exist in the database yet; otherwise, returns false.
def new_record?
+ sync_with_transaction_state
@new_record
end
# Returns true if this object has been destroyed, otherwise returns false.
def destroyed?
+ sync_with_transaction_state
@destroyed
end
@@ -93,31 +109,45 @@ module ActiveRecord
# validate: false, validations are bypassed altogether. See
# ActiveRecord::Validations for more information.
#
+ # By default, #save also sets the +updated_at+/+updated_on+ attributes to
+ # the current time. However, if you supply <tt>touch: false</tt>, these
+ # timestamps will not be updated.
+ #
# There's a series of callbacks associated with +save+. If any of the
# <tt>before_*</tt> callbacks return +false+ the action is cancelled and
# +save+ returns +false+. See ActiveRecord::Callbacks for further
# details.
- def save(*)
- create_or_update
+ #
+ # Attributes marked as readonly are silently ignored if the record is
+ # being updated.
+ def save(*args)
+ create_or_update(*args)
rescue ActiveRecord::RecordInvalid
false
end
# Saves the model.
#
- # If the model is new a record gets created in the database, otherwise
+ # If the model is new, a record gets created in the database, otherwise
# the existing record gets updated.
#
# With <tt>save!</tt> validations always run. If any of them fail
# ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations
# for more information.
#
+ # By default, #save! also sets the +updated_at+/+updated_on+ attributes to
+ # the current time. However, if you supply <tt>touch: false</tt>, these
+ # timestamps will not be updated.
+ #
# There's a series of callbacks associated with <tt>save!</tt>. If any of
# the <tt>before_*</tt> callbacks return +false+ the action is cancelled
# and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See
# ActiveRecord::Callbacks for further details.
- def save!(*)
- create_or_update || raise(RecordNotSaved)
+ #
+ # Attributes marked as readonly are silently ignored if the record is
+ # being updated.
+ def save!(*args)
+ create_or_update(*args) || raise(RecordNotSaved.new(nil, self))
end
# Deletes the record in the database and freezes this instance to
@@ -127,6 +157,8 @@ module ActiveRecord
# The row is simply removed with an SQL +DELETE+ statement on the
# record's primary key, and no callbacks are executed.
#
+ # Note that this will also delete records marked as <tt>readonly?</tt>.
+ #
# To enforce the object's +before_destroy+ and +after_destroy+
# callbacks or any <tt>:dependent</tt> association
# options, use <tt>#destroy</tt>.
@@ -144,7 +176,7 @@ module ActiveRecord
# and <tt>destroy</tt> returns +false+. See
# ActiveRecord::Callbacks for further details.
def destroy
- raise ReadOnlyRecord if readonly?
+ raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
destroy_associations
destroy_row if persisted?
@destroyed = true
@@ -159,7 +191,7 @@ module ActiveRecord
# and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See
# ActiveRecord::Callbacks for further details.
def destroy!
- destroy || raise(ActiveRecord::RecordNotDestroyed)
+ destroy || raise(ActiveRecord::RecordNotDestroyed, self)
end
# Returns an instance of the specified +klass+ with the attributes of the
@@ -175,7 +207,7 @@ module ActiveRecord
def becomes(klass)
became = klass.new
became.instance_variable_set("@attributes", @attributes)
- became.instance_variable_set("@attributes_cache", @attributes_cache)
+ became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.instance_variable_set("@errors", errors)
@@ -190,7 +222,11 @@ module ActiveRecord
# share the same set of attributes.
def becomes!(klass)
became = becomes(klass)
- became.public_send("#{klass.inheritance_column}=", klass.sti_name) unless self.class.descends_from_active_record?
+ sti_type = nil
+ if !klass.descends_from_active_record?
+ sti_type = klass.sti_name
+ end
+ became.public_send("#{klass.inheritance_column}=", sti_type)
became
end
@@ -202,6 +238,10 @@ module ActiveRecord
# * updated_at/updated_on column is updated if that column is available.
# * Updates all the attributes that are dirty in this object.
#
+ # This method raises an +ActiveRecord::ActiveRecordError+ if the
+ # attribute is marked as readonly.
+ #
+ # See also +update_column+.
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
@@ -212,7 +252,7 @@ module ActiveRecord
# Updates the attributes of the model from the passed-in hash and saves the
# record, all wrapped in a transaction. If the object is invalid, the saving
# will fail and false will be returned.
- def update_attributes(attributes)
+ def update(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
@@ -221,9 +261,11 @@ module ActiveRecord
end
end
- # Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead
+ alias update_attributes update
+
+ # Updates its receiver just like +update+ but calls <tt>save!</tt> instead
# of +save+, so an exception is raised if the record is invalid.
- def update_attributes!(attributes)
+ def update!(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
@@ -232,28 +274,31 @@ module ActiveRecord
end
end
- # Updates a single attribute of an object, without having to explicitly call save on that object.
- #
- # * Validation is skipped.
- # * Callbacks are skipped.
- # * updated_at/updated_on column is not updated if that column is available.
- #
- # Raises an +ActiveRecordError+ when called on new objects, or when the +name+
- # attribute is marked as readonly.
+ alias update_attributes! update!
+
+ # Equivalent to <code>update_columns(name => value)</code>.
def update_column(name, value)
update_columns(name => value)
end
- # Updates the attributes from the passed-in hash, without having to explicitly call save on that object.
+ # Updates the attributes directly in the database issuing an UPDATE SQL
+ # statement and sets them in the receiver:
#
- # * Validation is skipped.
+ # user.update_columns(last_request_at: Time.current)
+ #
+ # This is the fastest way to update attributes because it goes straight to
+ # the database, but take into account that in consequence the regular update
+ # procedures are totally bypassed. In particular:
+ #
+ # * Validations are skipped.
# * Callbacks are skipped.
- # * updated_at/updated_on column is not updated if that column is available.
+ # * +updated_at+/+updated_on+ are not updated.
#
- # Raises an +ActiveRecordError+ when called on new objects, or when at least
- # one of the attributes is marked as readonly.
+ # This method raises an +ActiveRecord::ActiveRecordError+ when called on new
+ # objects, or when at least one of the attributes is marked as readonly.
def update_columns(attributes)
- raise ActiveRecordError, "can not update on a new record object" unless persisted?
+ raise ActiveRecordError, "cannot update a new record" if new_record?
+ raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
attributes.each_key do |key|
verify_readonly_attribute(key.to_s)
@@ -319,37 +364,83 @@ module ActiveRecord
toggle(attribute).update_attribute(attribute, self[attribute])
end
- # Reloads the attributes of this object from the database.
- # The optional options argument is passed to find when reloading so you
- # may do e.g. record.reload(lock: true) to reload the same record with
- # an exclusive row lock.
+ # Reloads the record from the database.
+ #
+ # This method finds record by its primary key (which could be assigned manually) and
+ # modifies the receiver in-place:
+ #
+ # account = Account.new
+ # # => #<Account id: nil, email: nil>
+ # account.id = 1
+ # account.reload
+ # # Account Load (1.2ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT 1 [["id", 1]]
+ # # => #<Account id: 1, email: 'account@example.com'>
+ #
+ # Attributes are reloaded from the database, and caches busted, in
+ # particular the associations cache.
+ #
+ # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt>
+ # is raised. Otherwise, in addition to the in-place modification the method
+ # returns +self+ for convenience.
+ #
+ # The optional <tt>:lock</tt> flag option allows you to lock the reloaded record:
+ #
+ # reload(lock: true) # reload with pessimistic locking
+ #
+ # Reloading is commonly used in test suites to test something is actually
+ # written to the database, or when some action modifies the corresponding
+ # row in the database but not the object in memory:
+ #
+ # assert account.deposit!(25)
+ # assert_equal 25, account.credit # check it is updated in memory
+ # assert_equal 25, account.reload.credit # check it is also persisted
+ #
+ # Another common use case is optimistic locking handling:
+ #
+ # def with_optimistic_retry
+ # begin
+ # yield
+ # rescue ActiveRecord::StaleObjectError
+ # begin
+ # # Reload lock_version in particular.
+ # reload
+ # rescue ActiveRecord::RecordNotFound
+ # # If the record is gone there is nothing to do.
+ # else
+ # retry
+ # end
+ # end
+ # end
+ #
def reload(options = nil)
clear_aggregation_cache
clear_association_cache
fresh_object =
if options && options[:lock]
- self.class.unscoped { self.class.lock.find(id) }
+ self.class.unscoped { self.class.lock(options[:lock]).find(id) }
else
self.class.unscoped { self.class.find(id) }
end
- @attributes.update(fresh_object.instance_variable_get('@attributes'))
- @columns_hash = fresh_object.instance_variable_get('@columns_hash')
-
- @attributes_cache = {}
+ @attributes = fresh_object.instance_variable_get('@attributes')
+ @new_record = false
self
end
# Saves the record with the updated_at/on attributes set to the current time.
- # Please note that no validation is performed and no callbacks are executed.
- # If an attribute name is passed, that attribute is updated along with
- # updated_at/on attributes.
+ # Please note that no validation is performed and only the +after_touch+,
+ # +after_commit+ and +after_rollback+ callbacks are executed.
+ #
+ # If attribute names are passed, they are updated along with updated_at/on
+ # attributes.
#
- # product.touch # updates updated_at/on
- # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on
+ # product.touch # updates updated_at/on
+ # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on
+ # product.touch(:started_at, :ended_at) # updates started_at, ended_at and updated_at/on attributes
#
- # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on associated object.
+ # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on
+ # associated object.
#
# class Brake < ActiveRecord::Base
# belongs_to :car, touch: true
@@ -361,9 +452,18 @@ module ActiveRecord
#
# # triggers @brake.car.touch and @brake.car.corporation.touch
# @brake.touch
- def touch(name = nil)
+ #
+ # Note that +touch+ must be used on a persisted object, or else an
+ # ActiveRecordError will be thrown. For example:
+ #
+ # ball = Ball.new
+ # ball.touch(:updated_at) # => raises ActiveRecordError
+ #
+ def touch(*names)
+ raise ActiveRecordError, "cannot touch on a new record object" unless persisted?
+
attributes = timestamp_attributes_for_update_in_model
- attributes << name if name
+ attributes.concat(names)
unless attributes.empty?
current_time = current_time_from_proper_timezone
@@ -376,9 +476,11 @@ module ActiveRecord
changes[self.class.locking_column] = increment_lock if locking_enabled?
- @changed_attributes.except!(*changes.keys)
+ clear_attribute_changes(changes.keys)
primary_key = self.class.primary_key
self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
+ else
+ true
end
end
@@ -395,7 +497,7 @@ module ActiveRecord
def relation_for_destroy
pk = self.class.primary_key
column = self.class.columns_hash[pk]
- substitute = connection.substitute_at(column, 0)
+ substitute = self.class.connection.substitute_at(column)
relation = self.class.unscoped.where(
self.class.arel_table[pk].eq(substitute))
@@ -404,29 +506,26 @@ module ActiveRecord
relation
end
- def create_or_update
- raise ReadOnlyRecord if readonly?
- result = new_record? ? create : update
+ def create_or_update(*args)
+ raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
+ result = new_record? ? _create_record : _update_record(*args)
result != false
end
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
- def update(attribute_names = @attributes.keys)
- attributes_with_values = arel_attributes_with_values_for_update(attribute_names)
-
- if attributes_with_values.empty?
+ def _update_record(attribute_names = self.attribute_names)
+ attributes_values = arel_attributes_with_values_for_update(attribute_names)
+ if attributes_values.empty?
0
else
- klass = self.class
- stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values)
- klass.connection.update stmt
+ self.class.unscoped._update_record attributes_values, id, id_was
end
end
# Creates a record with values matching those of the instance attributes
# and returns its id.
- def create(attribute_names = @attributes.keys)
+ def _create_record(attribute_names = self.attribute_names)
attributes_values = arel_attributes_with_values_for_create(attribute_names)
new_id = self.class.unscoped.insert attributes_values
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index df8654e5c1..dcb2bd3d84 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Query Cache
class QueryCache
@@ -29,9 +28,10 @@ module ActiveRecord
end
def call(env)
- enabled = ActiveRecord::Base.connection.query_cache_enabled
+ connection = ActiveRecord::Base.connection
+ enabled = connection.query_cache_enabled
connection_id = ActiveRecord::Base.connection_id
- ActiveRecord::Base.connection.enable_query_cache!
+ connection.enable_query_cache!
response = @app.call(env)
response[2] = Rack::BodyProxy.new(response[2]) do
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 5ddcaee6be..e8de4db3a7 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -1,21 +1,22 @@
-
module ActiveRecord
module Querying
- delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all
- delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all
- delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, :to => :all
- delegate :find_by, :find_by!, :to => :all
- delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :all
- delegate :find_each, :find_in_batches, :to => :all
+ delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all
+ delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all
+ delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
+ delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
+ delegate :find_by, :find_by!, to: :all
+ delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
+ delegate :find_each, :find_in_batches, to: :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
- :where, :preload, :eager_load, :includes, :from, :lock, :readonly,
- :having, :create_with, :uniq, :references, :none, :to => :all
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all
+ :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
+ :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
+ delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
+ delegate :pluck, :ids, to: :all
# Executes a custom SQL query against your database and returns all the results. The results will
# be returned as an array with columns requested encapsulated as attributes of the model you call
# this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
- # a Product object with the attributes you specified in the SQL query.
+ # a +Product+ object with the attributes you specified in the SQL query.
#
# If you call a complicated SQL query which spans multiple tables the columns specified by the
# SELECT will be attributes of the model, whether or not they are columns of the corresponding
@@ -30,20 +31,22 @@ module ActiveRecord
# Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id"
# # => [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
#
- # # You can use the same string replacement techniques as you can with ActiveRecord#find
+ # You can use the same string replacement techniques as you can with <tt>ActiveRecord::QueryMethods#where</tt>:
+ #
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
- # # => [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...]
+ # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
def find_by_sql(sql, binds = [])
- logging_query_plan do
- result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
- column_types = {}
+ result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
+ column_types = result_set.column_types.dup
+ columns_hash.each_key { |k| column_types.delete k }
+ message_bus = ActiveSupport::Notifications.instrumenter
- if result_set.respond_to? :column_types
- column_types = result_set.column_types
- else
- ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`"
- end
+ payload = {
+ record_count: result_set.length,
+ class_name: name
+ }
+ message_bus.instrument('instantiation.active_record', payload) do
result_set.map { |record| instantiate(record, column_types) }
end
end
@@ -58,10 +61,8 @@ module ActiveRecord
#
# Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
def count_by_sql(sql)
- logging_query_plan do
- sql = sanitize_conditions(sql)
- connection.select_value(sql, "#{name} Count").to_i
- end
+ sql = sanitize_conditions(sql)
+ connection.select_value(sql, "#{name} Count").to_i
end
end
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index aceb70bc45..f1bdbc845c 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -31,11 +31,23 @@ module ActiveRecord
config.active_record.use_schema_cache_dump = true
+ config.active_record.maintain_test_schema = true
config.eager_load_namespaces << ActiveRecord
rake_tasks do
- require "active_record/base"
+ namespace :db do
+ task :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration
+
+ if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH)
+ if engine.paths['db/migrate'].existent
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a
+ end
+ end
+ end
+ end
+
load "active_record/railties/databases.rake"
end
@@ -49,7 +61,7 @@ module ActiveRecord
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
- runner do |app|
+ runner do
require "active_record/base"
end
@@ -64,7 +76,7 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger }
end
- initializer "active_record.migration_error" do |app|
+ initializer "active_record.migration_error" do
if config.active_record.delete(:migration_error) == :page_load
config.app_middleware.insert_after "::ActionDispatch::Callbacks",
"ActiveRecord::Migration::CheckPending"
@@ -92,45 +104,6 @@ module ActiveRecord
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
- begin
- old_behavior, ActiveSupport::Deprecation.behavior = ActiveSupport::Deprecation.behavior, :stderr
- whitelist_attributes = app.config.active_record.delete(:whitelist_attributes)
-
- if respond_to?(:mass_assignment_sanitizer=)
- mass_assignment_sanitizer = nil
- else
- mass_assignment_sanitizer = app.config.active_record.delete(:mass_assignment_sanitizer)
- end
-
- unless whitelist_attributes.nil? && mass_assignment_sanitizer.nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- Model based mass assignment security has been extracted
- out of Rails into a gem. Please use the new recommended protection model for
- params or add `protected_attributes` to your Gemfile to use the old one.
-
- To disable this message remove the `whitelist_attributes` option from your
- `config/application.rb` file and any `mass_assignment_sanitizer` options
- from your `config/environments/*.rb` files.
-
- See http://guides.rubyonrails.org/security.html#mass-assignment for more information
- EOF
- end
-
- unless app.config.active_record.delete(:observers).nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- Active Record Observers has been extracted out of Rails into a gem.
- Please use callbacks or add `rails-observers` to your Gemfile to use observers.
-
- To disable this message remove the `observers` option from your
- `config/application.rb` or from your initializers.
-
- See http://guides.rubyonrails.org/4_0_release_notes.html for more information
- EOF
- end
- ensure
- ActiveSupport::Deprecation.behavior = old_behavior
- end
-
app.config.active_record.each do |k,v|
send "#{k}=", v
end
@@ -141,22 +114,27 @@ module ActiveRecord
# and then establishes the connection.
initializer "active_record.initialize_database" do |app|
ActiveSupport.on_load(:active_record) do
- unless ENV['DATABASE_URL']
- self.configurations = app.config.database_configuration
- end
- establish_connection
- end
- end
+ self.configurations = Rails.application.config.database_configuration
- initializer "active_record.validate_explain_support" do |app|
- if app.config.active_record[:auto_explain_threshold_in_seconds] &&
- !ActiveRecord::Base.connection.supports_explain?
- warn "auto_explain_threshold_in_seconds is set but will be ignored because your adapter does not support this feature. Please unset the configuration to avoid this warning."
+ begin
+ establish_connection
+ rescue ActiveRecord::NoDatabaseError
+ warn <<-end_warning
+Oops - You have a database configured, but it doesn't exist yet!
+
+Here's how to get started:
+
+ 1. Configure your database in config/database.yml.
+ 2. Run `bin/rake db:create` to create the database.
+ 3. Run `bin/rake db:setup` to load your database schema.
+end_warning
+ raise
+ end
end
end
# Expose database runtime to controller for logging.
- initializer "active_record.log_runtime" do |app|
+ initializer "active_record.log_runtime" do
require "active_record/railties/controller_runtime"
ActiveSupport.on_load(:action_controller) do
include ActiveRecord::Railties::ControllerRuntime
diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb
index 90b462fad6..604a220303 100644
--- a/activerecord/lib/active_record/railties/console_sandbox.rb
+++ b/activerecord/lib/active_record/railties/console_sandbox.rb
@@ -1,4 +1,5 @@
-ActiveRecord::Base.connection.begin_db_transaction
+ActiveRecord::Base.connection.begin_transaction(joinable: false)
+
at_exit do
- ActiveRecord::Base.connection.rollback_db_transaction
+ ActiveRecord::Base.connection.rollback_transaction
end
diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb
index 7695eacbff..af4840476c 100644
--- a/activerecord/lib/active_record/railties/controller_runtime.rb
+++ b/activerecord/lib/active_record/railties/controller_runtime.rb
@@ -21,9 +21,10 @@ module ActiveRecord
def cleanup_view_runtime
if ActiveRecord::Base.connected?
db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
+ self.db_runtime = (db_runtime || 0) + db_rt_before_render
runtime = super
db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
- self.db_runtime = db_rt_before_render + db_rt_after_render
+ self.db_runtime += db_rt_after_render
runtime - db_rt_after_render
else
super
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 259d0ff12b..04c2be045d 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -2,14 +2,8 @@ require 'active_record'
db_namespace = namespace :db do
task :load_config do
- ActiveRecord::Base.configurations = Rails.application.config.database_configuration
- ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
-
- if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH)
- if engine.paths['db/migrate'].existent
- ActiveRecord::Migrator.migrations_paths += engine.paths['db/migrate'].to_a
- end
- end
+ ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
+ ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
end
namespace :create do
@@ -18,13 +12,9 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
+ desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV it defaults to creating the development and test databases.'
task :create => [:load_config] do
- if ENV['DATABASE_URL']
- ActiveRecord::Tasks::DatabaseTasks.create_database_url
- else
- ActiveRecord::Tasks::DatabaseTasks.create_current
- end
+ ActiveRecord::Tasks::DatabaseTasks.create_current
end
namespace :drop do
@@ -33,22 +23,26 @@ db_namespace = namespace :db do
end
end
- desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)'
+ desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to dropping the development and test databases.'
task :drop => [:load_config] do
- if ENV['DATABASE_URL']
- ActiveRecord::Tasks::DatabaseTasks.drop_database_url
- else
- ActiveRecord::Tasks::DatabaseTasks.drop_current
+ ActiveRecord::Tasks::DatabaseTasks.drop_current
+ end
+
+ namespace :purge do
+ task :all => :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
end
end
+ # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases."
+ task :purge => [:load_config] do
+ ActiveRecord::Tasks::DatabaseTasks.purge_current
+ end
+
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task :migrate => [:environment, :load_config] do
- ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
- ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
- ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
- end
- db_namespace['_dump'].invoke
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration
end
task :_dump do
@@ -89,29 +83,28 @@ db_namespace = namespace :db do
# desc 'Runs the "down" for a given migration VERSION.'
task :down => [:environment, :load_config] do
version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
- raise 'VERSION is required' unless version
+ raise 'VERSION is required - To go down one migration, run db:rollback' unless version
ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version)
db_namespace['_dump'].invoke
end
desc 'Display status of migrations'
task :status => [:environment, :load_config] do
- unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
- puts 'Schema migrations table does not exist yet.'
- next # means "return" for rake task
+ unless ActiveRecord::SchemaMigration.table_exists?
+ abort 'Schema migrations table does not exist yet.'
end
- db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
- db_list.map! { |version| "%.3d" % version }
- file_list = []
- ActiveRecord::Migrator.migrations_paths.each do |path|
- Dir.foreach(path) do |file|
- # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
- if match_data = /^(\d{3,})_(.+)\.rb$/.match(file)
- status = db_list.delete(match_data[1]) ? 'up' : 'down'
- file_list << [status, match_data[1], match_data[2].humanize]
+ db_list = ActiveRecord::SchemaMigration.normalized_versions
+
+ file_list =
+ ActiveRecord::Migrator.migrations_paths.flat_map do |path|
+ # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
+ Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do
+ version = ActiveRecord::SchemaMigration.normalize_migration_number($1)
+ status = db_list.delete(version) ? 'up' : 'down'
+ [status, version, $2.humanize]
+ end
end
- end
- end
+
db_list.map! do |version|
['up', version, '********** NO FILE **********']
end
@@ -119,8 +112,8 @@ db_namespace = namespace :db do
puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
puts "-" * 50
- (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration|
- puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}"
+ (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name|
+ puts "#{status.center(8)} #{version.ljust(14)} #{name}"
end
puts
end
@@ -156,7 +149,7 @@ db_namespace = namespace :db do
begin
puts ActiveRecord::Tasks::DatabaseTasks.collation_current
rescue NoMethodError
- $stderr.puts 'Sorry, your database adapter is not supported yet, feel free to submit a patch'
+ $stderr.puts 'Sorry, your database adapter is not supported yet. Feel free to submit a patch.'
end
end
@@ -166,11 +159,11 @@ db_namespace = namespace :db do
end
# desc "Raises an error if there are pending migrations"
- task :abort_if_pending_migrations => [:environment, :load_config] do
+ task :abort_if_pending_migrations => :environment do
pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations
if pending_migrations.any?
- puts "You have #{pending_migrations.size} pending migrations:"
+ puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
pending_migrations.each do |pending_migration|
puts ' %4d %s' % [pending_migration.version, pending_migration.name]
end
@@ -178,13 +171,13 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)'
+ desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)'
task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed]
desc 'Load the seed data from db/seeds.rb'
task :seed do
db_namespace['abort_if_pending_migrations'].invoke
- Rails.application.load_seed
+ ActiveRecord::Tasks::DatabaseTasks.load_seed
end
namespace :fixtures do
@@ -192,12 +185,22 @@ db_namespace = namespace :db do
task :load => [:environment, :load_config] do
require 'active_record/fixtures'
- base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
- fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
+ base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
- ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file)
- end
+ fixtures_dir = if ENV['FIXTURES_DIR']
+ File.join base_dir, ENV['FIXTURES_DIR']
+ else
+ base_dir
+ end
+
+ fixture_files = if ENV['FIXTURES']
+ ENV['FIXTURES'].split(',')
+ else
+ # The use of String#[] here is to support namespaced fixtures
+ Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }
+ end
+
+ ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)
end
# desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
@@ -209,10 +212,11 @@ db_namespace = namespace :db do
puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label
- base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures')
+ base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
+
Dir["#{base_dir}/**/*.yml"].each do |file|
if data = YAML::load(ERB.new(IO.read(file)).result)
- data.keys.each do |key|
+ data.each_key do |key|
key_id = ActiveRecord::FixtureSet.identify(key)
if key == label || key_id == id.to_i
@@ -225,10 +229,10 @@ db_namespace = namespace :db do
end
namespace :schema do
- desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR'
+ desc 'Create a db/schema.rb file that is portable against any DB supported by AR'
task :dump => [:environment, :load_config] do
require 'active_record/schema_dumper'
- filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
+ filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
File.open(filename, "w:utf-8") do |file|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
@@ -236,13 +240,8 @@ db_namespace = namespace :db do
end
desc 'Load a schema.rb file into the database'
- task :load => [:environment, :load_config] do
- file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
- if File.exists?(file)
- load(file)
- else
- abort %{#{file} doesn't exist yet. Run `rake db:migrate` to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded}
- end
+ task :load => [:load_config] do
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA'])
end
task :load_if_ruby => ['db:create', :environment] do
@@ -253,7 +252,7 @@ db_namespace = namespace :db do
desc 'Create a db/schema_cache.dump file.'
task :dump => [:environment, :load_config] do
con = ActiveRecord::Base.connection
- filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump")
+ filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
con.schema_cache.clear!
con.tables.each { |table| con.schema_cache.add(table) }
@@ -262,72 +261,33 @@ db_namespace = namespace :db do
desc 'Clear a db/schema_cache.dump file.'
task :clear => [:environment, :load_config] do
- filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump")
- FileUtils.rm(filename) if File.exists?(filename)
+ filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
+ FileUtils.rm(filename) if File.exist?(filename)
end
end
end
namespace :structure do
- def set_firebird_env(config)
- ENV['ISC_USER'] = config['username'].to_s if config['username']
- ENV['ISC_PASSWORD'] = config['password'].to_s if config['password']
- end
-
- def firebird_db_string(config)
- FireRuby::Database.db_string_for(config.symbolize_keys)
- end
-
desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql'
task :dump => [:environment, :load_config] do
- filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
+ filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- case current_config['adapter']
- when /mysql/, /postgresql/, /sqlite/
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
- when 'oci', 'oracle'
- ActiveRecord::Base.establish_connection(current_config)
- File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump }
- when 'sqlserver'
- `smoscript -s #{current_config['host']} -d #{current_config['database']} -u #{current_config['username']} -p #{current_config['password']} -f #{filename} -A -U`
- when "firebird"
- set_firebird_env(current_config)
- db_string = firebird_db_string(current_config)
- sh "isql -a #{db_string} > #{filename}"
- else
- raise "Task not supported by '#{current_config["adapter"]}'"
- end
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
- if ActiveRecord::Base.connection.supports_migrations?
+ if ActiveRecord::Base.connection.supports_migrations? &&
+ ActiveRecord::SchemaMigration.table_exists?
File.open(filename, "a") do |f|
f.puts ActiveRecord::Base.connection.dump_schema_information
+ f.print "\n"
end
end
db_namespace['structure:dump'].reenable
end
- # desc "Recreate the databases from the structure.sql file"
+ desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
- case current_config['adapter']
- when /mysql/, /postgresql/, /sqlite/
- ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
- when 'sqlserver'
- `sqlcmd -S #{current_config['host']} -d #{current_config['database']} -U #{current_config['username']} -P #{current_config['password']} -i #{filename}`
- when 'oci', 'oracle'
- ActiveRecord::Base.establish_connection(current_config)
- IO.read(filename).split(";\n\n").each do |ddl|
- ActiveRecord::Base.connection.execute(ddl)
- end
- when 'firebird'
- set_firebird_env(current_config)
- db_string = firebird_db_string(current_config)
- sh "isql -i #{filename} #{db_string}"
- else
- raise "Task not supported by '#{current_config['adapter']}'"
- end
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['DB_STRUCTURE'])
end
task :load_if_sql => ['db:create', :environment] do
@@ -337,8 +297,15 @@ db_namespace = namespace :db do
namespace :test do
+ task :deprecated do
+ Rake.application.top_level_tasks.grep(/^db:test:/).each do |task|
+ $stderr.puts "WARNING: #{task} is deprecated. The Rails test helper now maintains " \
+ "your test schema automatically, see the release notes for details."
+ end
+ end
+
# desc "Recreate the test database from the current schema"
- task :load => 'db:test:purge' do
+ task :load => %w(db:test:purge) do
case ActiveRecord::Base.schema_format
when :ruby
db_namespace["test:load_schema"].invoke
@@ -348,24 +315,25 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from an existent schema.rb file"
- task :load_schema => 'db:test:purge' do
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
- ActiveRecord::Schema.verbose = false
- db_namespace["schema:load"].invoke
- end
-
- # desc "Recreate the test database from an existent structure.sql file"
- task :load_structure => 'db:test:purge' do
+ task :load_schema => %w(db:test:purge) do
begin
- ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test'])
- db_namespace["structure:load"].invoke
+ should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
+ ActiveRecord::Schema.verbose = false
+ ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA']
ensure
- ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil)
+ if should_reconnect
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
+ end
end
end
+ # desc "Recreate the test database from an existent structure.sql file"
+ task :load_structure => %w(db:test:purge) do
+ ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA']
+ end
+
# desc "Recreate the test database from a fresh schema"
- task :clone do
+ task :clone => %w(db:test:deprecated environment) do
case ActiveRecord::Base.schema_format
when :ruby
db_namespace["test:clone_schema"].invoke
@@ -375,38 +343,18 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from a fresh schema.rb file"
- task :clone_schema => ["db:schema:dump", "db:test:load_schema"]
+ task :clone_schema => %w(db:test:deprecated db:schema:dump db:test:load_schema)
# desc "Recreate the test database from a fresh structure.sql file"
- task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ]
+ task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure)
# desc "Empty the test database"
- task :purge => [:environment, :load_config] do
- abcs = ActiveRecord::Base.configurations
- case abcs['test']['adapter']
- when /mysql/, /postgresql/, /sqlite/
- ActiveRecord::Tasks::DatabaseTasks.purge abcs['test']
- when 'sqlserver'
- test = abcs.deep_dup['test']
- test_database = test['database']
- test['database'] = 'master'
- ActiveRecord::Base.establish_connection(test)
- ActiveRecord::Base.connection.recreate_database!(test_database)
- when "oci", "oracle"
- ActiveRecord::Base.establish_connection(:test)
- ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl|
- ActiveRecord::Base.connection.execute(ddl)
- end
- when 'firebird'
- ActiveRecord::Base.establish_connection(:test)
- ActiveRecord::Base.connection.recreate_database!
- else
- raise "Task not supported by '#{abcs['test']['adapter']}'"
- end
+ task :purge => %w(environment load_config) do
+ ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
# desc 'Check for pending migrations and load the test schema'
- task :prepare => 'db:abort_if_pending_migrations' do
+ task :prepare => %w(environment load_config) do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
end
@@ -418,9 +366,9 @@ namespace :railties do
namespace :install do
# desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2"
task :migrations => :'db:load_config' do
- to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip }
+ to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map(&:strip)
railties = {}
- Rails.application.railties.each do |railtie|
+ Rails.application.migration_railties.each do |railtie|
next unless to_load == :all || to_load.include?(railtie.railtie_name)
if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first)
@@ -432,7 +380,7 @@ namespace :railties do
puts "NOTE: Migration #{migration.basename} from #{name} has been skipped. Migration with the same name already exists."
end
- on_copy = Proc.new do |name, migration, old_path|
+ on_copy = Proc.new do |name, migration|
puts "Copied migration #{migration.basename} from #{name}"
end
@@ -441,6 +389,3 @@ namespace :railties do
end
end
end
-
-task 'test:prepare' => 'db:test:prepare'
-
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
index 8499bb16e7..ce78f1756d 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
module ReadonlyAttributes
extend ActiveSupport::Concern
@@ -12,7 +11,7 @@ module ActiveRecord
# Attributes listed as readonly will be used to create a new record but update operations will
# ignore these fields.
def attr_readonly(*attributes)
- self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
+ self._attr_readonly = Set.new(attributes.map(&:to_s)) + (self._attr_readonly || [])
end
# Returns an array of all the attributes that have been specified as readonly.
@@ -20,11 +19,5 @@ module ActiveRecord
self._attr_readonly
end
end
-
- def _attr_readonly
- message = "Instance level _attr_readonly method is deprecated, please use class level method."
- ActiveSupport::Deprecation.warn message
- defined?(@_attr_readonly) ? @_attr_readonly : self.class._attr_readonly
- end
end
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index bcfcb061f2..dd746a4e10 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,3 +1,5 @@
+require 'thread'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
# = Active Record Reflection
@@ -5,35 +7,50 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :reflections
- self.reflections = {}
+ class_attribute :_reflections
+ class_attribute :aggregate_reflections
+ self._reflections = {}
+ self.aggregate_reflections = {}
end
- # Reflection enables to interrogate Active Record classes and objects
- # about their associations and aggregations. This information can,
- # for example, be used in a form builder that takes an Active Record object
+ def self.create(macro, name, scope, options, ar)
+ klass = case macro
+ when :composed_of
+ AggregateReflection
+ when :has_many
+ HasManyReflection
+ when :has_one
+ HasOneReflection
+ when :belongs_to
+ BelongsToReflection
+ else
+ raise "Unsupported Macro: #{macro}"
+ end
+
+ reflection = klass.new(name, scope, options, ar)
+ options[:through] ? ThroughReflection.new(reflection) : reflection
+ end
+
+ def self.add_reflection(ar, name, reflection)
+ ar._reflections = ar._reflections.merge(name.to_s => reflection)
+ end
+
+ def self.add_aggregate_reflection(ar, name, reflection)
+ ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
+ end
+
+ # \Reflection enables the ability to examine the associations and aggregations of
+ # Active Record classes and objects. This information, for example,
+ # can be used in a form builder that takes an Active Record object
# and creates input fields for all of the attributes depending on their type
# and displays the associations to other objects.
#
# MacroReflection class has info for AggregateReflection and AssociationReflection
# classes.
module ClassMethods
- def create_reflection(macro, name, scope, options, active_record)
- case macro
- when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
- klass = options[:through] ? ThroughReflection : AssociationReflection
- reflection = klass.new(macro, name, scope, options, active_record)
- when :composed_of
- reflection = AggregateReflection.new(macro, name, scope, options, active_record)
- end
-
- self.reflections = self.reflections.merge(name => reflection)
- reflection
- end
-
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
- reflections.values.grep(AggregateReflection)
+ aggregate_reflections.values
end
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
@@ -41,8 +58,25 @@ module ActiveRecord
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
#
def reflect_on_aggregation(aggregation)
- reflection = reflections[aggregation]
- reflection if reflection.is_a?(AggregateReflection)
+ aggregate_reflections[aggregation.to_s]
+ end
+
+ # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value.
+ #
+ # Account.reflections # => {"balance" => AggregateReflection}
+ #
+ # @api public
+ def reflections
+ ref = {}
+ _reflections.each do |name, reflection|
+ parent_name, parent_reflection = reflection.parent_reflection
+ if parent_name
+ ref[parent_name] = parent_reflection
+ else
+ ref[name] = reflection
+ end
+ end
+ ref
end
# Returns an array of AssociationReflection objects for all the
@@ -55,8 +89,9 @@ module ActiveRecord
# Account.reflect_on_all_associations # returns an array of all associations
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
+ # @api public
def reflect_on_all_associations(macro = nil)
- association_reflections = reflections.values.grep(AssociationReflection)
+ association_reflections = reflections.values
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
end
@@ -65,68 +100,126 @@ module ActiveRecord
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
+ # @api public
def reflect_on_association(association)
- reflection = reflections[association]
- reflection if reflection.is_a?(AssociationReflection)
+ reflections[association.to_s]
+ end
+
+ # @api private
+ def _reflect_on_association(association) #:nodoc:
+ _reflections[association.to_s]
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
+ #
+ # @api public
def reflect_on_all_autosave_associations
reflections.values.select { |reflection| reflection.options[:autosave] }
end
end
- # Abstract base class for AggregateReflection and AssociationReflection. Objects of
+ # Holds all the methods that are shared between MacroReflection, AssociationReflection
+ # and ThroughReflection
+ class AbstractReflection # :nodoc:
+ def table_name
+ klass.table_name
+ end
+
+ # Returns a new, unsaved instance of the associated class. +attributes+ will
+ # be passed to the class's constructor.
+ def build_association(attributes, &block)
+ klass.new(attributes, &block)
+ end
+
+ def quoted_table_name
+ klass.quoted_table_name
+ end
+
+ def primary_key_type
+ klass.type_for_attribute(klass.primary_key)
+ end
+
+ # Returns the class name for the macro.
+ #
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
+ def class_name
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
+ end
+
+ JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
+
+ def join_keys(assoc_klass)
+ JoinKeys.new(foreign_key, active_record_primary_key)
+ end
+
+ def source_macro
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveRecord::Base.source_macro is deprecated and will be removed
+ without replacement.
+ MSG
+
+ macro
+ end
+ end
+ # Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
- class MacroReflection
+ #
+ # MacroReflection
+ # AssociationReflection
+ # AggregateReflection
+ # HasManyReflection
+ # HasOneReflection
+ # BelongsToReflection
+ # ThroughReflection
+ class MacroReflection < AbstractReflection
# Returns the name of the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
attr_reader :name
- # Returns the macro type.
- #
- # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt>
- # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
- attr_reader :macro
-
attr_reader :scope
# Returns the hash of options used for the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
- # <tt>has_many :clients</tt> returns +{}+
+ # <tt>has_many :clients</tt> returns <tt>{}</tt>
attr_reader :options
attr_reader :active_record
attr_reader :plural_name # :nodoc:
- def initialize(macro, name, scope, options, active_record)
- @macro = macro
+ def initialize(name, scope, options, active_record)
@name = name
@scope = scope
@options = options
@active_record = active_record
+ @klass = options[:class]
@plural_name = active_record.pluralize_table_names ?
name.to_s.pluralize : name.to_s
end
+ def autosave=(autosave)
+ @automatic_inverse_of = false
+ @options[:autosave] = autosave
+ _, parent_reflection = self.parent_reflection
+ if parent_reflection
+ parent_reflection.autosave = autosave
+ end
+ end
+
# Returns the class for the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
# <tt>has_many :clients</tt> returns the Client class
def klass
- @klass ||= class_name.constantize
+ @klass ||= compute_class(class_name)
end
- # Returns the class name for the macro.
- #
- # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
- # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
- def class_name
- @class_name ||= (options[:class_name] || derive_class_name).to_s
+ def compute_class(name)
+ name.constantize
end
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
@@ -135,7 +228,7 @@ module ActiveRecord
super ||
other_aggregation.kind_of?(self.class) &&
name == other_aggregation.name &&
- other_aggregation.options &&
+ !other_aggregation.options.nil? &&
active_record == other_aggregation.active_record
end
@@ -171,26 +264,38 @@ module ActiveRecord
# a new association object. Use +build_association+ or +create_association+
# instead. This allows plugins to hook into association object creation.
def klass
- @klass ||= active_record.send(:compute_type, class_name)
+ @klass ||= compute_class(class_name)
end
- def initialize(*args)
- super
- @collection = [:has_many, :has_and_belongs_to_many].include?(macro)
+ def compute_class(name)
+ active_record.send(:compute_type, name)
end
- # Returns a new, unsaved instance of the associated class. +attributes+ will
- # be passed to the class's constructor.
- def build_association(attributes, &block)
- klass.new(attributes, &block)
- end
+ attr_reader :type, :foreign_type
+ attr_accessor :parent_reflection # [:name, Reflection]
- def table_name
- @table_name ||= klass.table_name
+ def initialize(name, scope, options, active_record)
+ super
+ @automatic_inverse_of = nil
+ @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
+ @foreign_type = options[:foreign_type] || "#{name}_type"
+ @constructable = calculate_constructable(macro, options)
+ @association_scope_cache = {}
+ @scope_lock = Mutex.new
+ end
+
+ def association_scope_cache(conn, owner)
+ key = conn.prepared_statements
+ if polymorphic?
+ key = [key, owner._read_attribute(@foreign_type)]
+ end
+ @association_scope_cache[key] ||= @scope_lock.synchronize {
+ @association_scope_cache[key] ||= yield
+ }
end
- def quoted_table_name
- @quoted_table_name ||= klass.quoted_table_name
+ def constructable? # :nodoc:
+ @constructable
end
def join_table
@@ -201,18 +306,6 @@ module ActiveRecord
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
- def foreign_type
- @foreign_type ||= options[:foreign_type] || "#{name}_type"
- end
-
- def type
- @type ||= options[:as] && "#{options[:as]}_type"
- end
-
- def primary_key_column
- @primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key }
- end
-
def association_foreign_key
@association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
end
@@ -234,36 +327,44 @@ module ActiveRecord
end
end
- def columns(tbl_name)
- @columns ||= klass.connection.columns(tbl_name)
- end
-
- def reset_column_information
- @columns = nil
- end
-
def check_validity!
check_validity_of_inverse!
-
- if has_and_belongs_to_many? && association_foreign_key == foreign_key
- raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self)
- end
end
def check_validity_of_inverse!
- unless options[:polymorphic]
+ unless polymorphic?
if has_inverse? && inverse_of.nil?
raise InverseOfAssociationNotFoundError.new(self)
end
end
end
+ def check_preloadable!
+ return unless scope
+
+ if scope.arity > 0
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The association scope '#{name}' is instance dependent (the scope
+ block takes an argument). Preloading happens before the individual
+ instances are created. This means that there is no instance being
+ passed to the association scope. This will most likely result in
+ broken or incorrect behavior. Joining, Preloading and eager loading
+ of these associations is deprecated and will be removed in the future.
+ MSG
+ end
+ end
+ alias :check_eager_loadable! :check_preloadable!
+
+ def join_id_for(owner) # :nodoc:
+ owner[active_record_primary_key]
+ end
+
def through_reflection
nil
end
def source_reflection
- nil
+ self
end
# A chain of reflections from this one back to the owner. For more see the explanation in
@@ -282,21 +383,19 @@ module ActiveRecord
scope ? [[scope]] : [[]]
end
- alias :source_macro :macro
-
def has_inverse?
- @options[:inverse_of]
+ inverse_name
end
def inverse_of
- if has_inverse?
- @inverse_of ||= klass.reflect_on_association(options[:inverse_of])
- end
+ return unless inverse_name
+
+ @inverse_of ||= klass._reflect_on_association inverse_name
end
def polymorphic_inverse_of(associated_class)
if has_inverse?
- if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
+ if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of])
inverse_relationship
else
raise InverseOfAssociationNotFoundError.new(self, associated_class)
@@ -304,11 +403,16 @@ module ActiveRecord
end
end
+ # Returns the macro type.
+ #
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
+ def macro; raise NotImplementedError; end
+
# Returns whether or not this association reflection is for a collection
# association. Returns +true+ if the +macro+ is either +has_many+ or
# +has_and_belongs_to_many+, +false+ otherwise.
def collection?
- @collection
+ false
end
# Returns whether or not the association should be validated as part of
@@ -321,28 +425,23 @@ module ActiveRecord
# * you use autosave; <tt>autosave: true</tt>
# * the association is a +has_many+ association
def validate?
- !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
+ !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?)
end
# Returns +true+ if +self+ is a +belongs_to+ reflection.
- def belongs_to?
- macro == :belongs_to
- end
+ def belongs_to?; false; end
- def has_and_belongs_to_many?
- macro == :has_and_belongs_to_many
- end
+ # Returns +true+ if +self+ is a +has_one+ reflection.
+ def has_one?; false; end
def association_class
case macro
when :belongs_to
- if options[:polymorphic]
+ if polymorphic?
Associations::BelongsToPolymorphicAssociation
else
Associations::BelongsToAssociation
end
- when :has_and_belongs_to_many
- Associations::HasAndBelongsToManyAssociation
when :has_many
if options[:through]
Associations::HasManyThroughAssociation
@@ -359,14 +458,99 @@ module ActiveRecord
end
def polymorphic?
- options.key? :polymorphic
+ options[:polymorphic]
end
+ VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
+ INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key]
+
+ protected
+
+ def actual_source_reflection # FIXME: this is a horrible name
+ self
+ end
+
private
+
+ def calculate_constructable(macro, options)
+ case macro
+ when :belongs_to
+ !polymorphic?
+ when :has_one
+ !options[:through]
+ else
+ true
+ end
+ end
+
+ # Attempts to find the inverse association name automatically.
+ # If it cannot find a suitable inverse association name, it returns
+ # nil.
+ def inverse_name
+ options.fetch(:inverse_of) do
+ if @automatic_inverse_of == false
+ nil
+ else
+ @automatic_inverse_of ||= automatic_inverse_of
+ end
+ end
+ end
+
+ # returns either nil or the inverse association name that it finds.
+ def automatic_inverse_of
+ if can_find_inverse_of_automatically?(self)
+ inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
+
+ begin
+ reflection = klass._reflect_on_association(inverse_name)
+ rescue NameError
+ # Give up: we couldn't compute the klass type so we won't be able
+ # to find any associations either.
+ reflection = false
+ end
+
+ if valid_inverse_reflection?(reflection)
+ return inverse_name
+ end
+ end
+
+ false
+ end
+
+ # Checks if the inverse reflection that is returned from the
+ # +automatic_inverse_of+ method is a valid reflection. We must
+ # make sure that the reflection's active_record name matches up
+ # with the current reflection's klass name.
+ #
+ # Note: klass will always be valid because when there's a NameError
+ # from calling +klass+, +reflection+ will already be set to false.
+ def valid_inverse_reflection?(reflection)
+ reflection &&
+ klass.name == reflection.active_record.name &&
+ can_find_inverse_of_automatically?(reflection)
+ end
+
+ # Checks to see if the reflection doesn't have any options that prevent
+ # us from being able to guess the inverse automatically. First, the
+ # <tt>inverse_of</tt> option cannot be set to false. Second, we must
+ # have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
+ # Third, we must not have options such as <tt>:polymorphic</tt> or
+ # <tt>:foreign_key</tt> which prevent us from correctly guessing the
+ # inverse association.
+ #
+ # Anything with a scope can additionally ruin our attempt at finding an
+ # inverse, so we exclude reflections with scopes.
+ def can_find_inverse_of_automatically?(reflection)
+ reflection.options[:inverse_of] != false &&
+ VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) &&
+ !INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } &&
+ !reflection.scope
+ end
+
def derive_class_name
- class_name = name.to_s.camelize
+ class_name = name.to_s
class_name = class_name.singularize if collection?
- class_name
+ class_name.camelize
end
def derive_foreign_key
@@ -380,7 +564,7 @@ module ActiveRecord
end
def derive_join_table
- [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ ModelSchema.derive_join_table_name active_record.table_name, klass.table_name
end
def primary_key(klass)
@@ -388,13 +572,75 @@ module ActiveRecord
end
end
+ class HasManyReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :has_many; end
+
+ def collection?; true; end
+ end
+
+ class HasOneReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :has_one; end
+
+ def has_one?; true; end
+ end
+
+ class BelongsToReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :belongs_to; end
+
+ def belongs_to?; true; end
+
+ def join_keys(assoc_klass)
+ key = polymorphic? ? association_primary_key(assoc_klass) : association_primary_key
+ JoinKeys.new(key, foreign_key)
+ end
+
+ def join_id_for(owner) # :nodoc:
+ owner[foreign_key]
+ end
+ end
+
+ class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super
+ end
+
+ def macro; :has_and_belongs_to_many; end
+
+ def collection?
+ true
+ end
+ end
+
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
- class ThroughReflection < AssociationReflection #:nodoc:
+ class ThroughReflection < AbstractReflection #:nodoc:
+ attr_reader :delegate_reflection
delegate :foreign_key, :foreign_type, :association_foreign_key,
:active_record_primary_key, :type, :to => :source_reflection
- # Gets the source of the through reflection. It checks both a singularized
+ def initialize(delegate_reflection)
+ @delegate_reflection = delegate_reflection
+ @klass = delegate_reflection.options[:class]
+ @source_reflection_name = delegate_reflection.options[:source]
+ end
+
+ def klass
+ @klass ||= delegate_reflection.compute_class(class_name)
+ end
+
+ # Returns the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
# class Post < ActiveRecord::Base
@@ -402,8 +648,17 @@ module ActiveRecord
# has_many :tags, through: :taggings
# end
#
+ # class Tagging < ActiveRecord::Base
+ # belongs_to :post
+ # belongs_to :tag
+ # end
+ #
+ # tags_reflection = Post.reflect_on_association(:tags)
+ # tags_reflection.source_reflection
+ # # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags">
+ #
def source_reflection
- @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
+ through_reflection.klass._reflect_on_association(source_reflection_name)
end
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
@@ -415,10 +670,11 @@ module ActiveRecord
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
- # taggings_reflection = tags_reflection.through_reflection
+ # tags_reflection.through_reflection
+ # # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings">
#
def through_reflection
- @through_reflection ||= active_record.reflect_on_association(options[:through])
+ active_record._reflect_on_association(options[:through])
end
# Returns an array of reflections which are involved in this association. Each item in the
@@ -427,9 +683,22 @@ module ActiveRecord
# The chain is built by recursively calling #chain on the source reflection and the through
# reflection. The base case for the recursion is a normal association, which just returns
# [self] as its #chain.
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :taggings
+ # has_many :tags, through: :taggings
+ # end
+ #
+ # tags_reflection = Post.reflect_on_association(:tags)
+ # tags_reflection.chain
+ # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>,
+ # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>]
+ #
def chain
@chain ||= begin
- chain = source_reflection.chain + through_reflection.chain
+ a = source_reflection.chain
+ b = through_reflection.chain
+ chain = a + b
chain[0] = self # Use self so we don't lose the information from :source_type
chain
end
@@ -461,11 +730,14 @@ module ActiveRecord
# Add to it the scope from this reflection (if any)
scope_chain.first << scope if scope
- through_scope_chain = through_reflection.scope_chain
+ through_scope_chain = through_reflection.scope_chain.map(&:dup)
if options[:source_type]
- through_scope_chain.first <<
- through_reflection.klass.where(foreign_type => options[:source_type])
+ type = foreign_type
+ source_type = options[:source_type]
+ through_scope_chain.first << lambda { |object|
+ where(type => source_type)
+ }
end
# Recursively fill out the rest of the array from the through reflection
@@ -473,14 +745,23 @@ module ActiveRecord
end
end
+ def join_keys(assoc_klass)
+ source_reflection.join_keys(assoc_klass)
+ end
+
# The macro used by the source association
def source_macro
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveRecord::Base.source_macro is deprecated and will be removed
+ without replacement.
+ MSG
+
source_reflection.source_macro
end
# A through association is nested if there would be more than one join table
def nested?
- chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
+ chain.length > 2
end
# We want to use the klass from this reflection, rather than just delegate straight to
@@ -489,20 +770,45 @@ module ActiveRecord
def association_primary_key(klass = nil)
# Get the "actual" source reflection if the immediate source reflection has a
# source reflection itself
- source_reflection = self.source_reflection
- while source_reflection.source_reflection
- source_reflection = source_reflection.source_reflection
- end
-
- source_reflection.options[:primary_key] || primary_key(klass || self.klass)
+ actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass)
end
- # Gets an array of possible <tt>:through</tt> source reflection names:
+ # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :taggings
+ # has_many :tags, through: :taggings
+ # end
#
- # [:singularized, :pluralized]
+ # tags_reflection = Post.reflect_on_association(:tags)
+ # tags_reflection.source_reflection_names
+ # # => [:tag, :tags]
#
def source_reflection_names
- @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
+ options[:source] ? [options[:source]] : [name.to_s.singularize, name].uniq
+ end
+
+ def source_reflection_name # :nodoc:
+ return @source_reflection_name if @source_reflection_name
+
+ names = [name.to_s.singularize, name].collect(&:to_sym).uniq
+ names = names.find_all { |n|
+ through_reflection.klass._reflect_on_association(n)
+ }
+
+ if names.length > 1
+ example_options = options.dup
+ example_options[:source] = source_reflection_names.first
+ ActiveSupport::Deprecation.warn \
+ "Ambiguous source reflection for through association. Please " \
+ "specify a :source directive on your declaration like:\n" \
+ "\n" \
+ " class #{active_record.name} < ActiveRecord::Base\n" \
+ " #{macro} :#{name}, #{example_options}\n" \
+ " end"
+ end
+
+ @source_reflection_name = names.first
end
def source_options
@@ -513,39 +819,63 @@ module ActiveRecord
through_reflection.options
end
+ def join_id_for(owner) # :nodoc:
+ source_reflection.join_id_for(owner)
+ end
+
def check_validity!
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
end
- if through_reflection.options[:polymorphic]
- raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
+ if through_reflection.polymorphic?
+ if has_one?
+ raise HasOneAssociationPolymorphicThroughError.new(active_record.name, self)
+ else
+ raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
+ end
end
if source_reflection.nil?
raise HasManyThroughSourceAssociationNotFoundError.new(self)
end
- if options[:source_type] && source_reflection.options[:polymorphic].nil?
+ if options[:source_type] && !source_reflection.polymorphic?
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
end
- if source_reflection.options[:polymorphic] && options[:source_type].nil?
+ if source_reflection.polymorphic? && options[:source_type].nil?
raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
end
- if macro == :has_one && through_reflection.collection?
+ if has_one? && through_reflection.collection?
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
end
check_validity_of_inverse!
end
+ protected
+
+ def actual_source_reflection # FIXME: this is a horrible name
+ source_reflection.send(:actual_source_reflection)
+ end
+
+ def primary_key(klass)
+ klass.primary_key || raise(UnknownPrimaryKey.new(klass))
+ end
+
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
options[:source_type] || source_reflection.class_name
end
+
+ delegate_methods = AssociationReflection.public_instance_methods -
+ public_instance_methods
+
+ delegate(*delegate_methods, to: :delegate_reflection)
+
end
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 0df895eb67..ab3debc03b 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,34 +1,32 @@
# -*- coding: utf-8 -*-
+require 'arel/collectors/bind'
module ActiveRecord
# = Active Record Relation
class Relation
- JoinOperation = Struct.new(:relation, :join_class, :on)
-
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
:order, :joins, :where, :having, :bind, :references,
- :extending]
+ :extending, :unscope]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
- :reverse_order, :uniq, :create_with]
+ :reverse_order, :distinct, :create_with, :uniq]
+ INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
- attr_reader :table, :klass, :loaded
- attr_accessor :default_scoped
+ attr_reader :table, :klass, :loaded, :predicate_builder
alias :model :klass
alias :loaded? :loaded
- alias :default_scoped? :default_scoped
- def initialize(klass, table, values = {})
- @klass = klass
- @table = table
- @values = values
- @implicit_readonly = nil
- @loaded = false
- @default_scoped = false
+ def initialize(klass, table, predicate_builder, values = {})
+ @klass = klass
+ @table = table
+ @values = values
+ @offsets = {}
+ @loaded = false
+ @predicate_builder = predicate_builder
end
def initialize_copy(other)
@@ -39,7 +37,7 @@ module ActiveRecord
reset
end
- def insert(values)
+ def insert(values) # :nodoc:
primary_key_value = nil
if primary_key && Hash === values
@@ -56,16 +54,7 @@ module ActiveRecord
im = arel.create_insert
im.into @table
- conn = @klass.connection
-
- substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
- binds = substitutes.map do |arel_attr, value|
- [@klass.columns_hash[arel_attr.name], value]
- end
-
- substitutes.each_with_index do |tuple, i|
- tuple[1] = conn.substitute_at(binds[i][0], i)
- end
+ substitutes, binds = substitute_values values
if values.empty? # empty insert
im.values = Arel.sql(connection.empty_insert_statement_value)
@@ -73,7 +62,7 @@ module ActiveRecord
im.insert substitutes
end
- conn.insert(
+ @klass.connection.insert(
im,
'SQL',
primary_key,
@@ -82,6 +71,40 @@ module ActiveRecord
binds)
end
+ def _update_record(values, id, id_was) # :nodoc:
+ substitutes, binds = substitute_values values
+
+ scope = @klass.unscoped
+
+ if @klass.finder_needs_type_condition?
+ scope.unscope!(where: @klass.inheritance_column)
+ end
+
+ relation = scope.where(@klass.primary_key => (id_was || id))
+ bvs = binds + relation.bind_values
+ um = relation
+ .arel
+ .compile_update(substitutes, @klass.primary_key)
+
+ @klass.connection.update(
+ um,
+ 'SQL',
+ bvs,
+ )
+ end
+
+ def substitute_values(values) # :nodoc:
+ binds = values.map do |arel_attr, value|
+ [@klass.columns_hash[arel_attr.name], value]
+ end
+
+ substitutes = values.each_with_index.map do |(arel_attr, _), i|
+ [arel_attr, @klass.connection.substitute_at(binds[i][0])]
+ end
+
+ [substitutes, binds]
+ end
+
# Initializes new record from relation while maintaining the current
# scope.
#
@@ -141,34 +164,58 @@ module ActiveRecord
first || new(attributes, &block)
end
- # Finds the first record with the given attributes, or creates a record with the attributes
- # if one is not found.
+ # Finds the first record with the given attributes, or creates a record
+ # with the attributes if one is not found:
#
- # ==== Examples
- # # Find the first user named Penélope or create a new one.
+ # # Find the first user named "Penélope" or create a new one.
# User.find_or_create_by(first_name: 'Penélope')
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
#
- # # Find the first user named Penélope or create a new one.
+ # # Find the first user named "Penélope" or create a new one.
# # We already have one so the existing record will be returned.
# User.find_or_create_by(first_name: 'Penélope')
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
#
- # # Find the first user named Scarlett or create a new one with a particular last name.
+ # # Find the first user named "Scarlett" or create a new one with
+ # # a particular last name.
# User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
- # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
#
- # # Find the first user named Scarlett or create a new one with a different last name.
- # # We already have one so the existing record will be returned.
+ # This method accepts a block, which is passed down to +create+. The last example
+ # above can be alternatively written this way:
+ #
+ # # Find the first user named "Scarlett" or create a new one with a
+ # # different last name.
# User.find_or_create_by(first_name: 'Scarlett') do |user|
- # user.last_name = "O'Hara"
+ # user.last_name = 'Johansson'
# end
- # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
+ #
+ # This method always returns a record, but if creation was attempted and
+ # failed due to validation errors it won't be persisted, you get what
+ # +create+ returns in such situation.
+ #
+ # Please note *this method is not atomic*, it runs first a SELECT, and if
+ # there are no results an INSERT is attempted. If there are other threads
+ # or processes there is a race condition between both calls and it could
+ # be the case that you end up with two similar records.
+ #
+ # Whether that is a problem or not depends on the logic of the
+ # application, but in the particular case in which rows have a UNIQUE
+ # constraint an exception may be raised, just retry:
+ #
+ # begin
+ # CreditAccount.find_or_create_by(user_id: user.id)
+ # rescue ActiveRecord::RecordNotUnique
+ # retry
+ # end
+ #
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
- # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
+ # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception
+ # is raised if the created record is invalid.
def find_or_create_by!(attributes, &block)
find_by(attributes) || create!(attributes, &block)
end
@@ -188,8 +235,8 @@ module ActiveRecord
# Please see further details in the
# {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
def explain
- _, queries = collecting_queries_for_explain { exec_queries }
- exec_explain(queries)
+ #TODO: Fix for binds.
+ exec_explain(collecting_queries_for_explain { exec_queries })
end
# Converts relation objects to Array.
@@ -198,21 +245,30 @@ module ActiveRecord
@records
end
+ # Serializes the relation objects Array.
+ def encode_with(coder)
+ coder.represent_seq(nil, to_a)
+ end
+
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
end
# Returns size of the records.
def size
- loaded? ? @records.length : count
+ loaded? ? @records.length : count(:all)
end
# Returns true if there are no records.
def empty?
return @records.empty? if loaded?
- c = count
- c.respond_to?(:zero?) ? c.zero? : c.empty?
+ if limit_value == 0
+ true
+ else
+ c = count(:all)
+ c.respond_to?(:zero?) ? c.zero? : c.empty?
+ end
end
# Returns true if there are any records.
@@ -236,8 +292,9 @@ module ActiveRecord
# Scope all queries to the current scope.
#
# Comment.where(post_id: 1).scoping do
- # Comment.first # SELECT * FROM comments WHERE post_id = 1
+ # Comment.first
# end
+ # # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
#
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
@@ -248,10 +305,11 @@ module ActiveRecord
klass.current_scope = previous
end
- # Updates all records with details given if they match a set of conditions supplied, limits and order can
- # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
- # database. It does not instantiate the involved models and it does not trigger Active Record callbacks
- # or validations.
+ # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE
+ # statement and sends it straight to the database. It does not instantiate the involved models and it does not
+ # trigger Active Record callbacks or validations. Values passed to `update_all` will not go through
+ # ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL
+ # database.
#
# ==== Parameters
#
@@ -270,7 +328,7 @@ module ActiveRecord
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
- stmt = Arel::UpdateManager.new(arel.engine)
+ stmt = Arel::UpdateManager.new
stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
stmt.table(table)
@@ -284,7 +342,8 @@ module ActiveRecord
stmt.wheres = arel.constraints
end
- @klass.connection.update stmt, 'SQL', bind_values
+ bvs = arel.bind_values + bind_values
+ @klass.connection.update stmt, 'SQL', bvs
end
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -303,12 +362,24 @@ module ActiveRecord
# # Updates multiple records
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
# Person.update(people.keys, people.values)
- def update(id, attributes)
+ #
+ # # Updates multiple records from the result of a relation
+ # people = Person.where(group: 'expert')
+ # people.update(group: 'masters')
+ #
+ # Note: Updating a large number of records will run a
+ # UPDATE query for each record, which may cause a performance
+ # issue. So if it is not needed to run callbacks for each update, it is
+ # preferred to use <tt>update_all</tt> for updating all records using
+ # a single query.
+ def update(id = :all, attributes)
if id.is_a?(Array)
id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
+ elsif id == :all
+ to_a.each { |record| record.update(attributes) }
else
object = find(id)
- object.update_attributes(attributes)
+ object.update(attributes)
object
end
end
@@ -342,7 +413,7 @@ module ActiveRecord
if conditions
where(conditions).destroy_all
else
- to_a.each {|object| object.destroy }.tap { reset }
+ to_a.each(&:destroy).tap { reset }
end
end
@@ -388,17 +459,26 @@ module ActiveRecord
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
#
- # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error:
+ # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error:
#
# Post.limit(100).delete_all
- # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
def delete_all(conditions = nil)
- raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value
+ invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method|
+ if MULTI_VALUE_METHODS.include?(method)
+ send("#{method}_values").any?
+ else
+ send("#{method}_value")
+ end
+ }
+ if invalid_methods.any?
+ raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
+ end
if conditions
where(conditions).delete_all
else
- stmt = Arel::DeleteManager.new(arel.engine)
+ stmt = Arel::DeleteManager.new
stmt.from(table)
if joins_values.any?
@@ -444,17 +524,7 @@ module ActiveRecord
#
# Post.where(published: true).load # => #<ActiveRecord::Relation>
def load
- unless loaded?
- # We monitor here the entire execution rather than individual SELECTs
- # because from the point of view of the user fetching the records of a
- # relation is a single unit of work. You want to know if this call takes
- # too long, not if the individual queries take too long.
- #
- # It could be the case that none of the queries involved surpass the
- # threshold, and at the same time the sum of them all does. The user
- # should get a query plan logged in that case.
- logging_query_plan { exec_queries }
- end
+ exec_queries unless loaded?
self
end
@@ -466,34 +536,55 @@ module ActiveRecord
end
def reset
- @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
+ @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
@should_eager_load = @join_dependency = nil
@records = []
+ @offsets = {}
self
end
# Returns sql statement for the relation.
#
- # Users.where(name: 'Oscar').to_sql
+ # User.where(name: 'Oscar').to_sql
# # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
def to_sql
- @to_sql ||= klass.connection.to_sql(arel, bind_values.dup)
- end
-
- # Returns a hash of where conditions
- #
- # Users.where(name: 'Oscar').where_values_hash
- # # => {name: "oscar"}
- def where_values_hash
- equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
- node.left.relation.name == table_name
+ @to_sql ||= begin
+ relation = self
+ connection = klass.connection
+ visitor = connection.visitor
+
+ if eager_loading?
+ find_with_associations { |rel| relation = rel }
+ end
+
+ arel = relation.arel
+ binds = (arel.bind_values + relation.bind_values).dup
+ binds.map! { |bv| connection.quote(*bv.reverse) }
+ collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new)
+ collect.substitute_binds(binds).join
+ end
+ end
+
+ # Returns a hash of where conditions.
+ #
+ # User.where(name: 'Oscar').where_values_hash
+ # # => {name: "Oscar"}
+ def where_values_hash(relation_table_name = table_name)
+ equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
+ node.left.relation.name == relation_table_name
}
binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
Hash[equalities.map { |where|
name = where.left.name
- [name, binds.fetch(name.to_s) { where.right }]
+ [name, binds.fetch(name.to_s) {
+ case where.right
+ when Array then where.right.map(&:val)
+ when Arel::Nodes::Casted, Arel::Nodes::Quoted
+ where.right.val
+ end
+ }]
}]
end
@@ -516,9 +607,17 @@ module ActiveRecord
includes_values & joins_values
end
+ # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+
+ # to maintain backwards compatibility. Use +distinct_value+ instead.
+ def uniq_value
+ distinct_value
+ end
+
# Compares two relations for equality.
def ==(other)
case other
+ when Associations::CollectionProxy, AssociationRelation
+ self == other.to_a
when Relation
other.to_sql == to_sql
when Array
@@ -530,16 +629,6 @@ module ActiveRecord
q.pp(self.to_a)
end
- def with_default_scope #:nodoc:
- if default_scoped? && default_scope = klass.send(:build_default_scope)
- default_scope = default_scope.merge(self)
- default_scope.default_scoped = false
- default_scope
- else
- self
- end
- end
-
# Returns true if relation is blank.
def blank?
to_a.blank?
@@ -559,29 +648,25 @@ module ActiveRecord
private
def exec_queries
- default_scoped = with_default_scope
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values)
- if default_scoped.equal?(self)
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
-
- preload = preload_values
- preload += includes_values unless eager_loading?
- preload.each do |associations|
- ActiveRecord::Associations::Preloader.new(@records, associations).run
- end
-
- # @readonly_value is true only if set explicitly. @implicit_readonly is true if there
- # are JOINS and no explicit SELECT.
- readonly = readonly_value.nil? ? @implicit_readonly : readonly_value
- @records.each { |record| record.readonly! } if readonly
- else
- @records = default_scoped.to_a
+ preload = preload_values
+ preload += includes_values unless eager_loading?
+ preloader = build_preloader
+ preload.each do |associations|
+ preloader.preload @records, associations
end
+ @records.each(&:readonly!) if readonly_value
+
@loaded = true
@records
end
+ def build_preloader
+ ActiveRecord::Associations::Preloader.new
+ end
+
def references_eager_loaded_tables?
joined_tables = arel.join_sources.map do |join|
if join.is_a?(Arel::Nodes::StringJoin)
@@ -594,38 +679,16 @@ module ActiveRecord
joined_tables += [table.name, table.table_alias]
# always convert table names to downcase as in Oracle quoted table names are in uppercase
- joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
- string_tables = tables_in_string(to_sql)
+ joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq
- if (references_values - joined_tables).any?
- true
- elsif (string_tables - joined_tables).any?
- ActiveSupport::Deprecation.warn(
- "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \
- "that are referenced in a string SQL snippet. For example: \n" \
- "\n" \
- " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \
- "\n" \
- "Currently, Active Record recognises the table in the string, and knows to JOIN the " \
- "comments table to the query, rather than loading comments in a separate query. " \
- "However, doing this without writing a full-blown SQL parser is inherently flawed. " \
- "Since we don't want to write an SQL parser, we are removing this functionality. " \
- "From now on, you must explicitly tell Active Record when you are referencing a table " \
- "from a string:\n" \
- "\n" \
- " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n\n"
- )
- true
- else
- false
- end
+ (references_values - joined_tables).any?
end
def tables_in_string(string)
return [] if string.blank?
# always convert table names to downcase as in Oracle quoted table names are in uppercase
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
- string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
+ string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_']
end
end
end
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index b921f2eddb..4f0502ae75 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
module Batches
# Looping through a collection of records from the database
@@ -11,7 +10,7 @@ module ActiveRecord
# The #find_each method uses #find_in_batches with a batch size of 1000 (or as
# specified by the +:batch_size+ option).
#
- # Person.all.find_each do |person|
+ # Person.find_each do |person|
# person.do_awesome_stuff
# end
#
@@ -19,51 +18,100 @@ module ActiveRecord
# person.party_all_night!
# end
#
- # You can also pass the +:start+ option to specify
- # an offset to control the starting point.
+ # If you do not provide a block to #find_each, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # Person.find_each.with_index do |person, index|
+ # person.award_trophy(index + 1)
+ # end
+ #
+ # ==== Options
+ # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
+ # * <tt>:start</tt> - Specifies the primary key value to start from.
+ # This is especially useful if you want multiple workers dealing with
+ # the same processing queue. You can make worker 1 handle all the records
+ # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
+ # (by setting the +:start+ option on that worker).
+ #
+ # # Let's process for a batch of 2000 records, skipping the first 2000 rows
+ # Person.find_each(start: 2000, batch_size: 2000) do |person|
+ # person.party_all_night!
+ # end
+ #
+ # NOTE: It's not possible to set the order. That is automatically set to
+ # ascending on the primary key ("id ASC") to make the batch ordering
+ # work. This also means that this method only works when the primary key is
+ # orderable (e.g. an integer or string).
+ #
+ # NOTE: You can't set the limit either, that's used to control
+ # the batch sizes.
def find_each(options = {})
- find_in_batches(options) do |records|
- records.each { |record| yield record }
+ if block_given?
+ find_in_batches(options) do |records|
+ records.each { |record| yield record }
+ end
+ else
+ enum_for :find_each, options do
+ options[:start] ? where(table[primary_key].gteq(options[:start])).size : size
+ end
end
end
# Yields each batch of records that was found by the find +options+ as
- # an array. The size of each batch is set by the +:batch_size+
- # option; the default is 1000.
- #
- # You can control the starting point for the batch processing by
- # supplying the +:start+ option. This is especially useful if you
- # want multiple workers dealing with the same processing queue. You can
- # make worker 1 handle all the records between id 0 and 10,000 and
- # worker 2 handle from 10,000 and beyond (by setting the +:start+
- # option on that worker).
- #
- # It's not possible to set the order. That is automatically set to
- # ascending on the primary key ("id ASC") to make the batch ordering
- # work. This also means that this method only works with integer-based
- # primary keys. You can't set the limit either, that's used to control
- # the batch sizes.
+ # an array.
#
# Person.where("age > 21").find_in_batches do |group|
# sleep(50) # Make sure it doesn't get too crowded in there!
# group.each { |person| person.party_all_night! }
# end
#
+ # If you do not provide a block to #find_in_batches, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # Person.find_in_batches.with_index do |group, batch|
+ # puts "Processing group ##{batch}"
+ # group.each(&:recover_from_last_night!)
+ # end
+ #
+ # To be yielded each record one by one, use #find_each instead.
+ #
+ # ==== Options
+ # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
+ # * <tt>:start</tt> - Specifies the primary key value to start from.
+ # This is especially useful if you want multiple workers dealing with
+ # the same processing queue. You can make worker 1 handle all the records
+ # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
+ # (by setting the +:start+ option on that worker).
+ #
# # Let's process the next 2000 records
- # Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group|
+ # Person.find_in_batches(start: 2000, batch_size: 2000) do |group|
# group.each { |person| person.party_all_night! }
# end
+ #
+ # NOTE: It's not possible to set the order. That is automatically set to
+ # ascending on the primary key ("id ASC") to make the batch ordering
+ # work. This also means that this method only works when the primary key is
+ # orderable (e.g. an integer or string).
+ #
+ # NOTE: You can't set the limit either, that's used to control
+ # the batch sizes.
def find_in_batches(options = {})
options.assert_valid_keys(:start, :batch_size)
relation = self
+ start = options[:start]
+ batch_size = options[:batch_size] || 1000
- unless arel.orders.blank? && arel.taken.blank?
- ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
+ unless block_given?
+ return to_enum(:find_in_batches, options) do
+ total = start ? where(table[primary_key].gteq(start)).size : size
+ (total - 1).div(batch_size) + 1
+ end
end
- start = options.delete(:start)
- batch_size = options.delete(:batch_size) || 1000
+ if logger && (arel.orders.present? || arel.taken.present?)
+ logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
+ end
relation = relation.reorder(batch_order).limit(batch_size)
records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a
@@ -71,16 +119,13 @@ module ActiveRecord
while records.any?
records_size = records.size
primary_key_offset = records.last.id
+ raise "Primary key not included in the custom select clause" unless primary_key_offset
yield records
break if records_size < batch_size
- if primary_key_offset
- records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
- else
- raise "Primary key not included in the custom select clause"
- end
+ records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
end
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 0b27ea730b..1d4cb1a83b 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -1,5 +1,3 @@
-require 'active_support/deprecation'
-
module ActiveRecord
module Calculations
# Count the records.
@@ -13,54 +11,67 @@ module ActiveRecord
# Person.count(:all)
# # => performs a COUNT(*) (:all is an alias for '*')
#
- # Person.count(:age, distinct: true)
+ # Person.distinct.count(:age)
# # => counts the number of different age values
- def count(column_name = nil, options = {})
- column_name, options = nil, column_name if column_name.is_a?(Hash)
- calculate(:count, column_name, options)
+ #
+ # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column,
+ # and the values are the respective amounts:
+ #
+ # Person.group(:city).count
+ # # => { 'Rome' => 5, 'Paris' => 3 }
+ #
+ # If +count+ is used with +group+ for multiple columns, it returns a Hash whose
+ # keys are an array containing the individual values of each column and the value
+ # of each key would be the +count+.
+ #
+ # Article.group(:status, :category).count
+ # # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
+ # ["published", "business"]=>0, ["published", "technology"]=>2}
+ #
+ # If +count+ is used with +select+, it will count the selected columns:
+ #
+ # Person.select(:age).count
+ # # => counts the number of different age values
+ #
+ # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ
+ # between databases. In invalid cases, an error from the database is thrown.
+ def count(column_name = nil)
+ calculate(:count, column_name)
end
# Calculates the average value on a given column. Returns +nil+ if there's
# no row. See +calculate+ for examples with options.
#
- # Person.average('age') # => 35.8
- def average(column_name, options = {})
- calculate(:average, column_name, options)
+ # Person.average(:age) # => 35.8
+ def average(column_name)
+ calculate(:average, column_name)
end
# Calculates the minimum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
#
- # Person.minimum('age') # => 7
- def minimum(column_name, options = {})
- calculate(:minimum, column_name, options)
+ # Person.minimum(:age) # => 7
+ def minimum(column_name)
+ calculate(:minimum, column_name)
end
# Calculates the maximum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
#
- # Person.maximum('age') # => 93
- def maximum(column_name, options = {})
- calculate(:maximum, column_name, options)
+ # Person.maximum(:age) # => 93
+ def maximum(column_name)
+ calculate(:maximum, column_name)
end
# Calculates the sum of values on a given column. The value is returned
# with the same data type of the column, 0 if there's no row. See
# +calculate+ for examples with options.
#
- # Person.sum('age') # => 4562
+ # Person.sum(:age) # => 4562
def sum(*args)
- if block_given?
- ActiveSupport::Deprecation.warn(
- "Calling #sum with a block is deprecated and will be removed in Rails 4.1. " \
- "If you want to perform sum calculation over the array of elements, use `to_a.sum(&block)`."
- )
- self.to_a.sum(*args) {|*block_args| yield(*block_args)}
- else
- calculate(:sum, *args)
- end
+ calculate(:sum, *args)
end
# This calculates aggregate values in the given column. Methods for count, sum, average,
@@ -78,7 +89,7 @@ module ActiveRecord
# puts values["Drake"]
# # => 43
#
- # drake = Family.find_by_last_name('Drake')
+ # drake = Family.find_by(last_name: 'Drake')
# values = Person.group(:family).maximum(:age) # Person belongs_to :family
# puts values[drake]
# # => 43
@@ -94,20 +105,16 @@ module ActiveRecord
# Person.group(:last_name).having("min(age) > 17").minimum(:age)
#
# Person.sum("2 * age")
- def calculate(operation, column_name, options = {})
- relation = with_default_scope
+ def calculate(operation, column_name)
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ column_name = attribute_alias(column_name)
+ end
- if relation.equal?(self)
- if has_include?(column_name)
- construct_relation_for_association_calculations.calculate(operation, column_name, options)
- else
- perform_calculation(operation, column_name, options)
- end
+ if has_include?(column_name)
+ construct_relation_for_association_calculations.calculate(operation, column_name)
else
- relation.calculate(operation, column_name, options)
+ perform_calculation(operation, column_name)
end
- rescue ThrowResult
- 0
end
# Use <tt>pluck</tt> as a shortcut to select one or more attributes without
@@ -131,7 +138,7 @@ module ActiveRecord
# # SELECT people.id, people.name FROM people
# # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
#
- # Person.uniq.pluck(:role)
+ # Person.pluck('DISTINCT role')
# # SELECT DISTINCT role FROM people
# # => ['admin', 'member', 'guest']
#
@@ -145,10 +152,10 @@ module ActiveRecord
#
def pluck(*column_names)
column_names.map! do |column_name|
- if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s)
- "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ attribute_alias(column_name)
else
- column_name
+ column_name.to_s
end
end
@@ -156,24 +163,11 @@ module ActiveRecord
construct_relation_for_association_calculations.pluck(*column_names)
else
relation = spawn
- relation.select_values = column_names
- result = klass.connection.select_all(relation.arel, nil, bind_values)
- columns = result.columns.map do |key|
- klass.column_types.fetch(key) {
- result.column_types.fetch(key) {
- Class.new { def type_cast(v); v; end }.new
- }
- }
- end
-
- result = result.map do |attributes|
- values = klass.initialize_attributes(attributes).values
-
- columns.zip(values).map do |column, value|
- column.type_cast(value)
- end
- end
- columns.one? ? result.map!(&:first) : result
+ relation.select_values = column_names.map { |cn|
+ columns_hash.key?(cn) ? arel_table[cn] : cn
+ }
+ result = klass.connection.select_all(relation.arel, nil, relation.arel.bind_values + bind_values)
+ result.cast_values(klass.column_types)
end
end
@@ -188,24 +182,24 @@ module ActiveRecord
private
def has_include?(column_name)
- eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?))
+ eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?))
end
- def perform_calculation(operation, column_name, options = {})
+ def perform_calculation(operation, column_name)
operation = operation.to_s.downcase
- distinct = options[:distinct]
+ # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
+ distinct = self.distinct_value
if operation == "count"
- column_name ||= (select_for_count || :all)
+ column_name ||= select_for_count
unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
distinct = true
end
column_name = primary_key if column_name == :all && distinct
-
- distinct = nil if column_name =~ /\s*DISTINCT\s+/i
+ distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i
end
if group_values.any?
@@ -229,31 +223,36 @@ module ActiveRecord
def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
# Postgresql doesn't like ORDER BY when there are no GROUP BY
- relation = reorder(nil)
+ relation = unscope(:order)
column_alias = column_name
+ bind_values = nil
+
if operation == "count" && (relation.limit_value || relation.offset_value)
# Shortcut when limit is zero.
return 0 if relation.limit_value == 0
query_builder = build_count_subquery(relation, column_name, distinct)
+ bind_values = query_builder.bind_values + relation.bind_values
else
column = aggregate_column(column_name)
select_value = operation_over_aggregate_column(column, operation, distinct)
column_alias = select_value.alias
+ column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
relation.select_values = [select_value]
query_builder = relation.arel
+ bind_values = query_builder.bind_values + relation.bind_values
end
- result = @klass.connection.select_all(query_builder, nil, relation.bind_values)
+ result = @klass.connection.select_all(query_builder, nil, bind_values)
row = result.first
value = row && row.values.first
column = result.column_types.fetch(column_alias) do
- column_for(column_name)
+ type_for(column_name)
end
type_cast_calculated_value(value, column, operation)
@@ -263,8 +262,8 @@ module ActiveRecord
group_attrs = group_values
if group_attrs.first.respond_to?(:to_sym)
- association = @klass.reflect_on_association(group_attrs.first.to_sym)
- associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
+ association = @klass._reflect_on_association(group_attrs.first)
+ associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
group_fields = Array(associated ? association.foreign_key : group_attrs)
else
group_fields = group_attrs
@@ -305,7 +304,7 @@ module ActiveRecord
relation.group_values = group
relation.select_values = select_values
- calculated_data = @klass.connection.select_all(relation, nil, bind_values)
+ calculated_data = @klass.connection.select_all(relation, nil, relation.arel.bind_values + bind_values)
if association
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
@@ -316,13 +315,15 @@ module ActiveRecord
Hash[calculated_data.map do |row|
key = group_columns.map { |aliaz, col_name|
column = calculated_data.column_types.fetch(aliaz) do
- column_for(col_name)
+ type_for(col_name)
end
type_cast_calculated_value(row[aliaz], column)
}
key = key.first if key.size == 1
key = key_records[key] if associated
- [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
+
+ column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
+ [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
end]
end
@@ -348,28 +349,26 @@ module ActiveRecord
@klass.connection.table_alias_for(table_name)
end
- def column_for(field)
+ def type_for(field)
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
- @klass.columns_hash[field_name]
+ @klass.type_for_attribute(field_name)
end
- def type_cast_calculated_value(value, column, operation = nil)
+ def type_cast_calculated_value(value, type, operation = nil)
case operation
when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || 0, column)
+ when 'sum' then type.type_cast_from_database(value || 0)
when 'average' then value.respond_to?(:to_d) ? value.to_d : value
- else type_cast_using_column(value, column)
+ else type.type_cast_from_database(value)
end
end
- def type_cast_using_column(value, column)
- column ? column.type_cast(value) : value
- end
-
+ # TODO: refactor to allow non-string `select_values` (eg. Arel nodes).
def select_for_count
if select_values.present?
- select = select_values.join(", ")
- select if select !~ /[,*]/
+ select_values.join(", ")
+ else
+ :all
end
end
@@ -379,9 +378,11 @@ module ActiveRecord
aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
relation.select_values = [aliased_column]
- subquery = relation.arel.as(subquery_alias)
+ arel = relation.arel
+ subquery = arel.as(subquery_alias)
sm = Arel::SelectManager.new relation.engine
+ sm.bind_values = arel.bind_values
select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
sm.project(select_value).from(subquery)
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 431d083f21..50f4d5c7ab 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,9 +1,35 @@
+require 'set'
require 'active_support/concern'
-require 'thread'
-require 'thread_safe'
+require 'active_support/deprecation'
module ActiveRecord
module Delegation # :nodoc:
+ module DelegateCache
+ def relation_delegate_class(klass) # :nodoc:
+ @relation_delegate_cache[klass]
+ end
+
+ def initialize_relation_delegate_cache # :nodoc:
+ @relation_delegate_cache = cache = {}
+ [
+ ActiveRecord::Relation,
+ ActiveRecord::Associations::CollectionProxy,
+ ActiveRecord::AssociationRelation
+ ].each do |klass|
+ delegate = Class.new(klass) {
+ include ClassSpecificRelation
+ }
+ const_set klass.name.gsub('::', '_'), delegate
+ cache[klass] = delegate
+ end
+ end
+
+ def inherited(child_class)
+ child_class.initialize_relation_delegate_cache
+ super
+ end
+ end
+
extend ActiveSupport::Concern
# This module creates compiled delegation methods dynamically at runtime, which makes
@@ -11,18 +37,25 @@ module ActiveRecord
# may vary depending on the klass of a relation, so we create a subclass of Relation
# for each different klass, and the delegations are compiled into that subclass only.
- delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
+ BLACKLISTED_ARRAY_METHODS = [
+ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
+ :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
+ :keep_if, :pop, :shift, :delete_at, :compact, :select!
+ ].to_set # :nodoc:
+
+ delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a
+
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
- :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass
+ :connection, :columns_hash, :to => :klass
- module ClassSpecificRelation
+ module ClassSpecificRelation # :nodoc:
extend ActiveSupport::Concern
included do
@delegation_mutex = Mutex.new
end
- module ClassMethods
+ module ClassMethods # :nodoc:
def name
superclass.name
end
@@ -38,11 +71,9 @@ module ActiveRecord
end
RUBY
else
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{method}(*args, &block)
- scoping { @klass.send(#{method.inspect}, *args, &block) }
- end
- RUBY
+ define_method method do |*args, &block|
+ scoping { @klass.public_send(method, *args, &block) }
+ end
end
end
end
@@ -60,66 +91,47 @@ module ActiveRecord
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
self.class.delegate_to_scoped_klass(method)
- scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- self.class.delegate method, :to => :to_a
- to_a.send(method, *args, &block)
+ scoping { @klass.public_send(method, *args, &block) }
elsif arel.respond_to?(method)
self.class.delegate method, :to => :arel
- arel.send(method, *args, &block)
+ arel.public_send(method, *args, &block)
else
super
end
end
end
- module ClassMethods
- @@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2)
-
- def new(klass, *args)
- relation = relation_class_for(klass).allocate
- relation.__send__(:initialize, klass, *args)
- relation
- end
-
- # This doesn't have to be thread-safe. relation_class_for guarantees that this will only be
- # called exactly once for a given const name.
- def const_missing(name)
- const_set(name, Class.new(self) { include ClassSpecificRelation })
+ module ClassMethods # :nodoc:
+ def create(klass, *args)
+ relation_class_for(klass).new(klass, *args)
end
private
- # Cache the constants in @@subclasses because looking them up via const_get
- # make instantiation significantly slower.
+
def relation_class_for(klass)
- if klass && (klass_name = klass.name)
- my_cache = @@subclasses.compute_if_absent(self) { ThreadSafe::Cache.new }
- # This hash is keyed by klass.name to avoid memory leaks in development mode
- my_cache.compute_if_absent(klass_name) do
- # Cache#compute_if_absent guarantees that the block will only executed once for the given klass_name
- const_get("#{name.gsub('::', '_')}_#{klass_name.gsub('::', '_')}", false)
- end
- else
- ActiveRecord::Relation
- end
+ klass.relation_delegate_class(self)
end
end
def respond_to?(method, include_private = false)
- super || Array.method_defined?(method) ||
- @klass.respond_to?(method, include_private) ||
+ super || @klass.respond_to?(method, include_private) ||
+ array_delegable?(method) ||
arel.respond_to?(method, include_private)
end
protected
+ def array_delegable?(method)
+ Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method)
+ end
+
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
- scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- to_a.send(method, *args, &block)
+ scoping { @klass.public_send(method, *args, &block) }
+ elsif array_delegable?(method)
+ to_a.public_send(method, *args, &block)
elsif arel.respond_to?(method)
- arel.send(method, *args, &block)
+ arel.public_send(method, *args, &block)
else
super
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 7ddaea1bb0..088bc203b7 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,19 +1,27 @@
+require 'active_support/deprecation'
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module FinderMethods
+ ONE_AS_ONE = '1 AS one'
+
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
# If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
# is an integer, find by id coerces its arguments using +to_i+.
#
- # Person.find(1) # returns the object for ID = 1
- # Person.find("1") # returns the object for ID = 1
- # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
- # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
- # Person.find([1]) # returns an array for the object with ID = 1
+ # Person.find(1) # returns the object for ID = 1
+ # Person.find("1") # returns the object for ID = 1
+ # Person.find("31-sarah") # returns the object for ID = 31
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
+ # Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
- # Note that returned records may not be in the same order as the ids you
- # provide since database rows are unordered. Give an explicit <tt>order</tt>
- # to ensure the results are sorted.
+ # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
+ #
+ # NOTE: The returned records may not be in the same order as the ids you
+ # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt>
+ # option if you want the results are sorted.
#
# ==== Find with lock
#
@@ -28,16 +36,44 @@ module ActiveRecord
# person.visits += 1
# person.save!
# end
+ #
+ # ==== Variations of +find+
+ #
+ # Person.where(name: 'Spartacus', rating: 4)
+ # # returns a chainable list (which can be empty).
+ #
+ # Person.find_by(name: 'Spartacus', rating: 4)
+ # # returns the first item or nil.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).first_or_initialize
+ # # returns the first item or returns a new instance (requires you call .save to persist against the database).
+ #
+ # Person.where(name: 'Spartacus', rating: 4).first_or_create
+ # # returns the first item or creates it and returns it, available since Rails 3.2.1.
+ #
+ # ==== Alternatives for +find+
+ #
+ # Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
+ # # returns a boolean indicating if any record with the given conditions exist.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
+ # # returns a chainable list of instances with only the mentioned fields.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).ids
+ # # returns an Array of ids, available since Rails 3.2.1.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
+ # # returns an Array of the required fields, available since Rails 3.1.
def find(*args)
if block_given?
- to_a.find { |*block_args| yield(*block_args) }
+ to_a.find(*args) { |*block_args| yield(*block_args) }
else
find_with_ids(*args)
end
end
# Finds the first record matching the specified conditions. There
- # is no implied ording so if order matters, you should specify it
+ # is no implied ordering so if order matters, you should specify it
# yourself.
#
# If no record is found, returns <tt>nil</tt>.
@@ -46,19 +82,23 @@ module ActiveRecord
# Post.find_by "published_at < ?", 2.weeks.ago
def find_by(*args)
where(*args).take
+ rescue RangeError
+ nil
end
# Like <tt>find_by</tt>, except that if no record is found, raises
# an <tt>ActiveRecord::RecordNotFound</tt> error.
def find_by!(*args)
where(*args).take!
+ rescue RangeError
+ raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value"
end
# Gives a record (or N records if a parameter is supplied) without any implied
# order. The order will depend on the database implementation.
# If an order is supplied it will be respected.
#
- # Person.take # returns an object fetched by SELECT * FROM people
+ # Person.take # returns an object fetched by SELECT * FROM people LIMIT 1
# Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
# Person.where(["name LIKE '%?'", name]).take
def take(limit = nil)
@@ -68,7 +108,7 @@ module ActiveRecord
# Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
# is found. Note that <tt>take!</tt> accepts no arguments.
def take!
- take or raise RecordNotFound
+ take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
end
# Find the first record (or first N records if a parameter is supplied).
@@ -79,22 +119,31 @@ module ActiveRecord
# Person.where(["user_name = :u", { u: user_name }]).first
# Person.order("created_on DESC").offset(5).first
# Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
+ #
+ # ==== Rails 3
+ #
+ # Person.first # SELECT "people".* FROM "people" LIMIT 1
+ #
+ # NOTE: Rails 3 may not order this query by the primary key and the order
+ # will depend on the database implementation. In order to ensure that behavior,
+ # use <tt>User.order(:id).first</tt> instead.
+ #
+ # ==== Rails 4
+ #
+ # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1
+ #
def first(limit = nil)
if limit
- if order_values.empty? && primary_key
- order(arel_table[primary_key].asc).limit(limit).to_a
- else
- limit(limit).to_a
- end
+ find_nth_with_limit(offset_index, limit)
else
- find_first
+ find_nth(0, offset_index)
end
end
# Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
# is found. Note that <tt>first!</tt> accepts no arguments.
def first!
- first or raise RecordNotFound
+ find_nth! 0
end
# Find the last record (or last N records if a parameter is supplied).
@@ -127,7 +176,87 @@ module ActiveRecord
# Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
# is found. Note that <tt>last!</tt> accepts no arguments.
def last!
- last or raise RecordNotFound
+ last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ end
+
+ # Find the second record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.second # returns the second object fetched by SELECT * FROM people
+ # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4)
+ # Person.where(["user_name = :u", { u: user_name }]).second
+ def second
+ find_nth(1, offset_index)
+ end
+
+ # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def second!
+ find_nth! 1
+ end
+
+ # Find the third record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.third # returns the third object fetched by SELECT * FROM people
+ # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5)
+ # Person.where(["user_name = :u", { u: user_name }]).third
+ def third
+ find_nth(2, offset_index)
+ end
+
+ # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def third!
+ find_nth! 2
+ end
+
+ # Find the fourth record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.fourth # returns the fourth object fetched by SELECT * FROM people
+ # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6)
+ # Person.where(["user_name = :u", { u: user_name }]).fourth
+ def fourth
+ find_nth(3, offset_index)
+ end
+
+ # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def fourth!
+ find_nth! 3
+ end
+
+ # Find the fifth record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.fifth # returns the fifth object fetched by SELECT * FROM people
+ # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7)
+ # Person.where(["user_name = :u", { u: user_name }]).fifth
+ def fifth
+ find_nth(4, offset_index)
+ end
+
+ # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def fifth!
+ find_nth! 4
+ end
+
+ # Find the forty-second record. Also known as accessing "the reddit".
+ # If no order is defined it will order by primary key.
+ #
+ # Person.forty_two # returns the forty-second object fetched by SELECT * FROM people
+ # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44)
+ # Person.where(["user_name = :u", { u: user_name }]).forty_two
+ def forty_two
+ find_nth(41, offset_index)
+ end
+
+ # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def forty_two!
+ find_nth! 41
end
# Returns +true+ if a record exists in the table that matches the +id+ or
@@ -137,14 +266,14 @@ module ActiveRecord
# * String - Finds the record with a primary key corresponding to this
# string (such as <tt>'5'</tt>).
# * Array - Finds the record that matches these +find+-style conditions
- # (such as <tt>['color = ?', 'red']</tt>).
+ # (such as <tt>['name LIKE ?', "%#{query}%"]</tt>).
# * Hash - Finds the record that matches these +find+-style conditions
- # (such as <tt>{color: 'red'}</tt>).
+ # (such as <tt>{name: 'David'}</tt>).
# * +false+ - Returns always +false+.
# * No args - Returns +false+ if the table is empty, +true+ otherwise.
#
- # For more information about specifying conditions as a Hash or Array,
- # see the Conditions section in the introduction to ActiveRecord::Base.
+ # For more information about specifying conditions as a hash or array,
+ # see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>.
#
# Note: You can't pass in a condition as a string (like <tt>name =
# 'Jamie'</tt>), since it would be sanitized and then queried against
@@ -153,87 +282,147 @@ module ActiveRecord
# Person.exists?(5)
# Person.exists?('5')
# Person.exists?(['name LIKE ?', "%#{query}%"])
+ # Person.exists?(id: [1, 4, 8])
# Person.exists?(name: 'David')
# Person.exists?(false)
# Person.exists?
def exists?(conditions = :none)
- conditions = conditions.id if Base === conditions
+ if Base === conditions
+ conditions = conditions.id
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are passing an instance of ActiveRecord::Base to `exists?`.
+ Please pass the id of the object by calling `.id`
+ MSG
+ end
+
return false if !conditions
- join_dependency = construct_join_dependency_for_association_find
- relation = construct_relation_for_association_find(join_dependency)
- relation = relation.except(:select, :order).select("1 AS one").limit(1)
+ relation = apply_join_dependency(self, construct_join_dependency)
+ return false if ActiveRecord::NullRelation === relation
+
+ relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
case conditions
when Array, Hash
relation = relation.where(conditions)
else
- relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
+ unless conditions == :none
+ relation = where(primary_key => conditions)
+ end
end
- connection.select_value(relation, "#{name} Exists", relation.bind_values)
- rescue ThrowResult
- false
+ connection.select_value(relation, "#{name} Exists", relation.arel.bind_values + relation.bind_values) ? true : false
end
- protected
+ # This method is called whenever no records are found with either a single
+ # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception.
+ #
+ # The error message is different depending on whether a single id or
+ # multiple ids are provided. If multiple ids are provided, then the number
+ # of results obtained should be provided in the +result_size+ argument and
+ # the expected number of results should be provided in the +expected_size+
+ # argument.
+ def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc:
+ conditions = arel.where_sql(@klass.arel_engine)
+ conditions = " [#{conditions}]" if conditions
+
+ if Array(ids).size == 1
+ error = "Couldn't find #{@klass.name} with '#{primary_key}'=#{ids}#{conditions}"
+ else
+ error = "Couldn't find all #{@klass.name.pluralize} with '#{primary_key}': "
+ error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
+ end
- def find_with_associations
- join_dependency = construct_join_dependency_for_association_find
- relation = construct_relation_for_association_find(join_dependency)
- rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
- join_dependency.instantiate(rows)
- rescue ThrowResult
- []
+ raise RecordNotFound, error
end
- def construct_join_dependency_for_association_find
- including = (eager_load_values + includes_values).uniq
- ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
- end
+ private
- def construct_relation_for_association_calculations
- including = (eager_load_values + includes_values).uniq
- join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first)
- relation = except(:includes, :eager_load, :preload)
- apply_join_dependency(relation, join_dependency)
+ def offset_index
+ offset_value || 0
end
- def construct_relation_for_association_find(join_dependency)
- relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns)
- apply_join_dependency(relation, join_dependency)
- end
+ def find_with_associations
+ # NOTE: the JoinDependency constructed here needs to know about
+ # any joins already present in `self`, so pass them in
+ #
+ # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
+ # incorrect SQL is generated. In that case, the join dependency for
+ # SpecialCategorizations is constructed without knowledge of the
+ # preexisting join in joins_values to categorizations (by way of
+ # the `has_many :through` for categories).
+ #
+ join_dependency = construct_join_dependency(joins_values)
+
+ aliases = join_dependency.aliases
+ relation = select aliases.columns
+ relation = apply_join_dependency(relation, join_dependency)
- def apply_join_dependency(relation, join_dependency)
- join_dependency.join_associations.each do |association|
- relation = association.join_relation(relation)
+ if block_given?
+ yield relation
+ else
+ if ActiveRecord::NullRelation === relation
+ []
+ else
+ arel = relation.arel
+ rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
+ join_dependency.instantiate(rows, aliases)
+ end
end
+ end
- limitable_reflections = using_limitable_reflections?(join_dependency.reflections)
+ def construct_join_dependency(joins = [])
+ including = eager_load_values + includes_values
+ ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
+ end
- if !limitable_reflections && relation.limit_value
- limited_id_condition = construct_limited_ids_condition(relation.except(:select))
- relation = relation.where(limited_id_condition)
+ def construct_relation_for_association_calculations
+ from = arel.froms.first
+ if Arel::Table === from
+ apply_join_dependency(self, construct_join_dependency)
+ else
+ # FIXME: as far as I can tell, `from` will always be an Arel::Table.
+ # There are no tests that test this branch, but presumably it's
+ # possible for `from` to be a list?
+ apply_join_dependency(self, construct_join_dependency(from))
end
+ end
- relation = relation.except(:limit, :offset) unless limitable_reflections
+ def apply_join_dependency(relation, join_dependency)
+ relation = relation.except(:includes, :eager_load, :preload)
+ relation = relation.joins join_dependency
- relation
+ if using_limitable_reflections?(join_dependency.reflections)
+ relation
+ else
+ if relation.limit_value
+ limited_ids = limited_ids_for(relation)
+ limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids)
+ end
+ relation.except(:limit, :offset)
+ end
end
- def construct_limited_ids_condition(relation)
- orders = relation.order_values.map { |val| val.presence }.compact
- values = @klass.connection.distinct("#{quoted_table_name}.#{primary_key}", orders)
+ def limited_ids_for(relation)
+ values = @klass.connection.columns_for_distinct(
+ "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
- relation = relation.dup.select(values)
+ relation = relation.except(:select).select(values).distinct!
+ arel = relation.arel
- id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values)
- ids_array = id_rows.map {|row| row[primary_key]}
+ id_rows = @klass.connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
+ id_rows.map {|row| row[primary_key]}
+ end
- ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
+ def using_limitable_reflections?(reflections)
+ reflections.none?(&:collection?)
end
+ protected
+
def find_with_ids(*ids)
+ raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
+
expects_array = ids.first.kind_of?(Array)
return ids.first if expects_array && ids.first.empty?
@@ -248,28 +437,29 @@ module ActiveRecord
else
find_some(ids)
end
+ rescue RangeError
+ raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID"
end
def find_one(id)
- id = id.id if ActiveRecord::Base === id
+ if ActiveRecord::Base === id
+ id = id.id
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are passing an instance of ActiveRecord::Base to `find`.
+ Please pass the id of the object by calling `.id`
+ MSG
+ end
- column = columns_hash[primary_key]
- substitute = connection.substitute_at(column, bind_values.length)
- relation = where(table[primary_key].eq(substitute))
- relation.bind_values += [[column, id]]
+ relation = where(primary_key => id)
record = relation.take
- unless record
- conditions = arel.where_sql
- conditions = " [#{conditions}]" if conditions
- raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}"
- end
+ raise_record_not_found_exception!(id, 0, 1) unless record
record
end
def find_some(ids)
- result = where(table[primary_key].in(ids)).to_a
+ result = where(primary_key => ids).to_a
expected_size =
if limit_value && ids.size > limit_value
@@ -286,12 +476,7 @@ module ActiveRecord
if result.size == expected_size
result
else
- conditions = arel.where_sql
- conditions = " [#{conditions}]" if conditions
-
- error = "Couldn't find all #{@klass.name.pluralize} with IDs "
- error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
- raise RecordNotFound, error
+ raise_record_not_found_exception!(ids, result.size, expected_size)
end
end
@@ -303,34 +488,41 @@ module ActiveRecord
end
end
- def find_first
+ def find_nth(index, offset)
if loaded?
- @records.first
+ @records[index]
else
- @first ||=
- if with_default_scope.order_values.empty? && primary_key
- order(arel_table[primary_key].asc).limit(1).to_a.first
- else
- limit(1).to_a.first
- end
+ offset += index
+ @offsets[offset] ||= find_nth_with_limit(offset, 1).first
end
end
+ def find_nth!(index)
+ find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ end
+
+ def find_nth_with_limit(offset, limit)
+ relation = if order_values.empty? && primary_key
+ order(arel_table[primary_key].asc)
+ else
+ self
+ end
+
+ relation = relation.offset(offset) unless offset.zero?
+ relation.limit(limit).to_a
+ end
+
def find_last
if loaded?
@records.last
else
@last ||=
- if offset_value || limit_value
+ if limit_value
to_a.last
else
reverse_order.limit(1).to_a.first
end
end
end
-
- def using_limitable_reflections?(reflections)
- reflections.none? { |r| r.collection? }
- end
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 59226d316e..afb0b208c3 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/hash/keys'
+require "set"
module ActiveRecord
class Relation
@@ -12,7 +13,7 @@ module ActiveRecord
@hash = hash
end
- def merge
+ def merge #:nodoc:
Merger.new(relation, other).merge
end
@@ -21,7 +22,7 @@ module ActiveRecord
# build a relation to merge in rather than directly merging
# the values.
def other
- other = Relation.new(relation.klass, relation.table)
+ other = Relation.create(relation.klass, relation.table, relation.predicate_builder)
hash.each { |k, v|
if k == :joins
if Hash === v
@@ -29,6 +30,8 @@ module ActiveRecord
else
other.joins!(*v)
end
+ elsif k == :select
+ other._select!(v)
else
other.send("#{k}!", v)
end
@@ -38,20 +41,17 @@ module ActiveRecord
end
class Merger # :nodoc:
- attr_reader :relation, :values
+ attr_reader :relation, :values, :other
def initialize(relation, other)
- if other.default_scoped? && other.klass != relation.klass
- other = other.with_default_scope
- end
-
@relation = relation
@values = other.values
+ @other = other
end
NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS +
Relation::MULTI_VALUE_METHODS -
- [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
+ [:joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
def normal_values
NORMAL_VALUES
@@ -60,70 +60,109 @@ module ActiveRecord
def merge
normal_values.each do |name|
value = values[name]
- relation.send("#{name}!", *value) unless value.blank?
+ # The unless clause is here mostly for performance reasons (since the `send` call might be moderately
+ # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
+ # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
+ # don't fall through the cracks.
+ unless value.nil? || (value.blank? && false != value)
+ if name == :select
+ relation._select!(*value)
+ else
+ relation.send("#{name}!", *value)
+ end
+ end
end
merge_multi_values
merge_single_values
+ merge_joins
relation
end
private
+ def merge_joins
+ return if other.joins_values.blank?
+
+ if other.klass == relation.klass
+ relation.joins!(*other.joins_values)
+ else
+ joins_dependency, rest = other.joins_values.partition do |join|
+ case join
+ when Hash, Symbol, Array
+ true
+ else
+ false
+ end
+ end
+
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
+ joins_dependency,
+ [])
+ relation.joins! rest
+
+ @relation = relation.joins join_dependency
+ end
+ end
+
def merge_multi_values
- relation.where_values = merged_wheres
- relation.bind_values = merged_binds
+ lhs_wheres = relation.where_values
+ rhs_wheres = other.where_values
+
+ lhs_binds = relation.bind_values
+ rhs_binds = other.bind_values
+
+ removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
- if values[:reordering]
+ where_values = kept + rhs_wheres
+ bind_values = filter_binds(lhs_binds, removed) + rhs_binds
+
+ relation.where_values = where_values
+ relation.bind_values = bind_values
+
+ if other.reordering_value
# override any order specified in the original relation
- relation.reorder! values[:order]
- elsif values[:order]
- # merge in order_values from r
- relation.order! values[:order]
+ relation.reorder! other.order_values
+ elsif other.order_values
+ # merge in order_values from relation
+ relation.order! other.order_values
end
- relation.extend(*values[:extending]) unless values[:extending].blank?
+ relation.extend(*other.extending_values) unless other.extending_values.blank?
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]
+ relation.from_value = other.from_value unless relation.from_value
+ relation.lock_value = other.lock_value unless relation.lock_value
- unless values[:create_with].blank?
- relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with])
+ unless other.create_with_value.blank?
+ relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
end
end
- def merged_binds
- if values[:bind]
- (relation.bind_values + values[:bind]).uniq(&:first)
- else
- relation.bind_values
- end
+ def filter_binds(lhs_binds, removed_wheres)
+ return lhs_binds if removed_wheres.empty?
+
+ set = Set.new removed_wheres.map { |x| x.left.name.to_s }
+ lhs_binds.dup.delete_if { |col,_| set.include? col.name }
end
- def merged_wheres
- if values[:where]
- merged_wheres = relation.where_values + values[:where]
-
- unless relation.where_values.empty?
- # Remove equalities with duplicated left-hand. Last one wins.
- seen = {}
- merged_wheres = merged_wheres.reverse.reject { |w|
- nuke = false
- if w.respond_to?(:operator) && w.operator == :==
- nuke = seen[w.left]
- seen[w.left] = true
- end
- nuke
- }.reverse
- end
+ # Remove equalities from the existing relation with a LHS which is
+ # present in the relation being merged in.
+ # returns [things_to_remove, things_to_keep]
+ def partition_overwrites(lhs_wheres, rhs_wheres)
+ if lhs_wheres.empty? || rhs_wheres.empty?
+ return [[], lhs_wheres]
+ end
- merged_wheres
- else
- relation.where_values
+ nodes = rhs_wheres.find_all do |w|
+ w.respond_to?(:operator) && w.operator == :==
+ end
+ seen = Set.new(nodes) { |node| node.left }
+
+ lhs_wheres.partition do |w|
+ w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left)
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 83074e72c1..567efce8ae 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,55 +1,44 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
- def self.build_from_hash(klass, attributes, default_table)
- queries = []
+ require 'active_record/relation/predicate_builder/array_handler'
+ require 'active_record/relation/predicate_builder/association_query_handler'
+ require 'active_record/relation/predicate_builder/base_handler'
+ require 'active_record/relation/predicate_builder/basic_object_handler'
+ require 'active_record/relation/predicate_builder/class_handler'
+ require 'active_record/relation/predicate_builder/range_handler'
+ require 'active_record/relation/predicate_builder/relation_handler'
- attributes.each do |column, value|
- table = default_table
+ delegate :resolve_column_aliases, to: :table
- if value.is_a?(Hash)
- table = Arel::Table.new(column, default_table.engine)
- association = klass.reflect_on_association(column.to_sym)
-
- if value.empty?
- queries.concat ['1 = 2']
- else
- value.each do |k, v|
- queries.concat expand(association && association.klass, table, k, v)
- end
- end
- else
- column = column.to_s
-
- if column.include?('.')
- table_name, column = column.split('.', 2)
- table = Arel::Table.new(table_name, default_table.engine)
- end
-
- queries.concat expand(klass, table, column, value)
- end
- end
+ def initialize(table)
+ @table = table
+ @handlers = []
- queries
+ register_handler(BasicObject, BasicObjectHandler.new(self))
+ register_handler(Class, ClassHandler.new(self))
+ register_handler(Base, BaseHandler.new(self))
+ register_handler(Range, RangeHandler.new(self))
+ register_handler(Relation, RelationHandler.new)
+ register_handler(Array, ArrayHandler.new(self))
+ register_handler(AssociationQueryValue, AssociationQueryHandler.new(self))
end
- def self.expand(klass, table, column, value)
- queries = []
+ def build_from_hash(attributes)
+ attributes = convert_dot_notation_to_hash(attributes.stringify_keys)
+ expand_from_hash(attributes)
+ end
+ def expand(column, value)
# Find the foreign key when using queries such as:
# Post.where(author: author)
#
# For polymorphic relationships, find the foreign key and type:
# PriceEstimate.where(estimate_of: treasure)
- if klass && value.class < Base && reflection = klass.reflect_on_association(column.to_sym)
- if reflection.polymorphic?
- queries << build(table[reflection.foreign_type], value.class.base_class)
- end
-
- column = reflection.foreign_key
+ if table.associated_with?(column)
+ value = AssociationQueryValue.new(table.associated_table(column), value)
end
- queries << build(table[column.to_sym], value)
- queries
+ build(table.arel_attribute(column), value)
end
def self.references(attributes)
@@ -58,49 +47,65 @@ module ActiveRecord
key
else
key = key.to_s
- key.split('.').first.to_sym if key.include?('.')
+ key.split('.').first if key.include?('.')
end
end.compact
end
- private
- def self.build(attribute, value)
- case value
- when Array, ActiveRecord::Associations::CollectionProxy
- values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x}
- ranges, values = values.partition {|v| v.is_a?(Range)}
-
- values_predicate = if values.include?(nil)
- values = values.compact
-
- case values.length
- when 0
- attribute.eq(nil)
- when 1
- attribute.eq(values.first).or(attribute.eq(nil))
- else
- attribute.in(values).or(attribute.eq(nil))
- end
- else
- attribute.in(values)
- end
-
- 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::Base
- attribute.eq(value.id)
- when Class
- # FIXME: I think we need to deprecate this behavior
- attribute.eq(value.name)
+ # Define how a class is converted to Arel nodes when passed to +where+.
+ # The handler can be any object that responds to +call+, and will be used
+ # for any value that +===+ the class given. For example:
+ #
+ # MyCustomDateRange = Struct.new(:start, :end)
+ # handler = proc do |column, range|
+ # Arel::Nodes::Between.new(column,
+ # Arel::Nodes::And.new([range.start, range.end])
+ # )
+ # end
+ # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
+ def register_handler(klass, handler)
+ @handlers.unshift([klass, handler])
+ end
+
+ def build(attribute, value)
+ handler_for(value).call(attribute, value)
+ end
+
+ protected
+
+ attr_reader :table
+
+ def expand_from_hash(attributes)
+ return ["1=0"] if attributes.empty?
+
+ attributes.flat_map do |key, value|
+ if value.is_a?(Hash)
+ builder = self.class.new(table.associated_table(key))
+ builder.expand_from_hash(value)
else
- attribute.eq(value)
+ expand(key, value)
end
end
+ end
+
+ private
+
+ def convert_dot_notation_to_hash(attributes)
+ dot_notation = attributes.keys.select { |s| s.include?(".") }
+
+ dot_notation.each do |key|
+ table_name, column_name = key.split(".")
+ value = attributes.delete(key)
+ attributes[table_name] ||= {}
+
+ attributes[table_name] = attributes[table_name].merge(column_name => value)
+ end
+
+ attributes
+ end
+
+ def handler_for(object)
+ @handlers.detect { |klass, _| klass === object }.last
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
new file mode 100644
index 0000000000..4b5f5773a0
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -0,0 +1,45 @@
+require 'active_support/core_ext/string/filters'
+
+module ActiveRecord
+ class PredicateBuilder
+ class ArrayHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ values = value.map { |x| x.is_a?(Base) ? x.id : x }
+ nils, values = values.partition(&:nil?)
+
+ return attribute.in([]) if values.empty? && nils.empty?
+
+ ranges, values = values.partition { |v| v.is_a?(Range) }
+
+ values_predicate =
+ case values.length
+ when 0 then NullPredicate
+ when 1 then predicate_builder.build(attribute, values.first)
+ else attribute.in(values)
+ end
+
+ unless nils.empty?
+ values_predicate = values_predicate.or(predicate_builder.build(attribute, nil))
+ end
+
+ array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) }
+ array_predicates.unshift(values_predicate)
+ array_predicates.inject { |composite, predicate| composite.or(predicate) }
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+
+ module NullPredicate # :nodoc:
+ def self.or(other)
+ other
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
new file mode 100644
index 0000000000..aabcf20c1d
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
@@ -0,0 +1,58 @@
+module ActiveRecord
+ class PredicateBuilder
+ class AssociationQueryHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ queries = {}
+
+ table = value.associated_table
+ if value.base_class
+ queries[table.association_foreign_type] = value.base_class.name
+ end
+
+ queries[table.association_foreign_key] = value.ids
+ predicate_builder.build_from_hash(queries)
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+ end
+
+ class AssociationQueryValue # :nodoc:
+ attr_reader :associated_table, :value
+
+ def initialize(associated_table, value)
+ @associated_table = associated_table
+ @value = value
+ end
+
+ def ids
+ value
+ end
+
+ def base_class
+ if associated_table.polymorphic_association?
+ @base_class ||= polymorphic_base_class_from_value
+ end
+ end
+
+ private
+
+ def polymorphic_base_class_from_value
+ case value
+ when Relation
+ value.klass.base_class
+ when Array
+ val = value.compact.first
+ val.class.base_class if val.is_a?(Base)
+ when Base
+ value.class.base_class
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
new file mode 100644
index 0000000000..6fa5b16f73
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ class PredicateBuilder
+ class BaseHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ predicate_builder.build(attribute, value.id)
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
new file mode 100644
index 0000000000..6cec75dc0a
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ class PredicateBuilder
+ class BasicObjectHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ attribute.eq(value)
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb
new file mode 100644
index 0000000000..ed313fc9d4
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb
@@ -0,0 +1,27 @@
+module ActiveRecord
+ class PredicateBuilder
+ class ClassHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ print_deprecation_warning
+ predicate_builder.build(attribute, value.name)
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+
+ private
+
+ def print_deprecation_warning
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing a class as a value in an Active Record query is deprecated and
+ will be removed. Pass a string instead.
+ MSG
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
new file mode 100644
index 0000000000..1b3849e3ad
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ class PredicateBuilder
+ class RangeHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ attribute.between(value)
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
new file mode 100644
index 0000000000..063150958a
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ class PredicateBuilder
+ class RelationHandler # :nodoc:
+ def call(attribute, value)
+ if value.select_values.empty?
+ value = value.select(value.klass.arel_table[value.klass.primary_key])
+ end
+
+ attribute.in(value.arel)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 46c0d6206f..ef380abfe8 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -1,21 +1,25 @@
require 'active_support/core_ext/array/wrap'
+require 'active_support/core_ext/string/filters'
+require 'active_model/forbidden_attributes_protection'
module ActiveRecord
module QueryMethods
extend ActiveSupport::Concern
+ include ActiveModel::ForbiddenAttributesProtection
+
# WhereChain objects act as placeholder for queries in which #where does not have any parameter.
- # In this case, #where must be chained with either #not, #like, or #not_like to return a new relation.
+ # In this case, #where must be chained with #not to return a new relation.
class WhereChain
def initialize(scope)
@scope = scope
end
- # Returns a new relation expressing WHERE + NOT condition
- # according to the conditions in the arguments.
+ # Returns a new relation expressing WHERE + NOT condition according to
+ # the conditions in the arguments.
#
- # #not accepts conditions in one of these formats: String, Array, Hash.
- # See #where for more details on each format.
+ # +not+ accepts conditions as a string, array, or hash. See #where for
+ # more details on each format.
#
# User.where.not("name = 'Jon'")
# # SELECT * FROM users WHERE NOT (name = 'Jon')
@@ -31,9 +35,14 @@ module ActiveRecord
#
# User.where.not(name: %w(Ko1 Nobu))
# # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
+ #
+ # User.where.not(name: "Jon", role: "admin")
+ # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
def not(opts, *rest)
where_value = @scope.send(:build_where, opts, rest).map do |rel|
case rel
+ when NilClass
+ raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
when Arel::Nodes::In
Arel::Nodes::NotIn.new(rel.left, rel.right)
when Arel::Nodes::Equality
@@ -44,6 +53,8 @@ module ActiveRecord
Arel::Nodes::Not.new(rel)
end
end
+
+ @scope.references!(PredicateBuilder.references(opts)) if Hash === opts
@scope.where_values += where_value
@scope
end
@@ -57,6 +68,7 @@ module ActiveRecord
#
def #{name}_values=(values) # def select_values=(values)
raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
+ check_cached_relation
@values[:#{name}] = values # @values[:select] = values
end # end
CODE
@@ -74,11 +86,22 @@ module ActiveRecord
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_value=(value) # def readonly_value=(value)
raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
+ check_cached_relation
@values[:#{name}] = value # @values[:readonly] = value
end # end
CODE
end
+ def check_cached_relation # :nodoc:
+ if defined?(@arel) && @arel
+ @arel = nil
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Modifying already cached Relation. The cache will be reset. Use a
+ cloned Relation to prevent this warning.
+ MSG
+ end
+ end
+
def create_with_value # :nodoc:
@values[:create_with] || {}
end
@@ -97,6 +120,14 @@ module ActiveRecord
# firing an additional query. This will often result in a
# performance improvement over a simple +join+.
#
+ # You can also specify multiple relationships, like this:
+ #
+ # users = User.includes(:address, :friends)
+ #
+ # Loading nested relationships is possible using a Hash:
+ #
+ # users = User.includes(:address, friends: [:address, :followers])
+ #
# === conditions
#
# If you want to add conditions to your included models you'll have
@@ -107,14 +138,19 @@ module ActiveRecord
# Will throw an error, but this will work:
#
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
+ #
+ # Note that +includes+ works with association names while +references+ needs
+ # the actual table name.
def includes(*args)
- args.empty? ? self : spawn.includes!(*args)
+ check_if_method_has_arguments!(:includes, args)
+ spawn.includes!(*args)
end
def includes!(*args) # :nodoc:
- args.reject! {|a| a.blank? }
+ args.reject!(&:blank?)
+ args.flatten!
- self.includes_values = (includes_values + args).flatten.uniq
+ self.includes_values |= args
self
end
@@ -125,7 +161,8 @@ module ActiveRecord
# FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
# "users"."id"
def eager_load(*args)
- args.blank? ? self : spawn.eager_load!(*args)
+ check_if_method_has_arguments!(:eager_load, args)
+ spawn.eager_load!(*args)
end
def eager_load!(*args) # :nodoc:
@@ -138,7 +175,8 @@ module ActiveRecord
# User.preload(:posts)
# => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
def preload(*args)
- args.blank? ? self : spawn.preload!(*args)
+ check_if_method_has_arguments!(:preload, args)
+ spawn.preload!(*args)
end
def preload!(*args) # :nodoc:
@@ -146,22 +184,26 @@ module ActiveRecord
self
end
- # Used to indicate that an association is referenced by an SQL string, and should
- # therefore be JOINed in any query rather than loaded separately.
+ # Use to indicate that the given +table_names+ are referenced by an SQL string,
+ # and should therefore be JOINed in any query rather than loaded separately.
+ # This method only works in conjunction with +includes+.
+ # See #includes for more details.
#
# User.includes(:posts).where("posts.name = 'foo'")
# # => Doesn't JOIN the posts table, resulting in an error.
#
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
# # => Query now knows the string references posts, so adds a JOIN
- def references(*args)
- args.blank? ? self : spawn.references!(*args)
+ def references(*table_names)
+ check_if_method_has_arguments!(:references, table_names)
+ spawn.references!(*table_names)
end
- def references!(*args) # :nodoc:
- args.flatten!
+ def references!(*table_names) # :nodoc:
+ table_names.flatten!
+ table_names.map!(&:to_s)
- self.references_values = (references_values + args.map!(&:to_s)).uniq
+ self.references_values |= table_names
self
end
@@ -178,7 +220,7 @@ module ActiveRecord
# fields are retrieved:
#
# Model.select(:field)
- # # => [#<Model field:value>]
+ # # => [#<Model id: nil, field: "value">]
#
# Although in the above example it looks as though this method returns an
# array, it actually returns a relation object and can have other query
@@ -187,10 +229,20 @@ module ActiveRecord
# The argument to the method can also be an array of fields.
#
# Model.select(:field, :other_field, :and_one_more)
- # # => [#<Model field: "value", other_field: "value", and_one_more: "value">]
+ # # => [#<Model id: nil, field: "value", other_field: "value", and_one_more: "value">]
+ #
+ # You can also use one or more strings, which will be used unchanged as SELECT fields.
+ #
+ # Model.select('field AS field_one', 'other_field AS field_two')
+ # # => [#<Model id: nil, field: "value", other_field: "value">]
+ #
+ # If an alias was specified, it will be accessible from the resulting objects:
+ #
+ # Model.select('field AS field_one').first.field_one
+ # # => "value"
#
# Accessing attributes of an object that do not have fields retrieved by a select
- # will throw <tt>ActiveModel::MissingAttributeError</tt>:
+ # except +id+ will throw <tt>ActiveModel::MissingAttributeError</tt>:
#
# Model.select(:field).first.other_field
# # => ActiveModel::MissingAttributeError: missing attribute: other_field
@@ -199,12 +251,16 @@ module ActiveRecord
to_a.select { |*block_args| yield(*block_args) }
else
raise ArgumentError, 'Call this with at least one field' if fields.empty?
- spawn.select!(*fields)
+ spawn._select!(*fields)
end
end
- def select!(*fields) # :nodoc:
- self.select_values += fields.flatten
+ def _select!(*fields) # :nodoc:
+ fields.flatten!
+ fields.map! do |field|
+ klass.attribute_alias?(field) ? klass.attribute_alias(field) : field
+ end
+ self.select_values += fields
self
end
@@ -220,8 +276,16 @@ module ActiveRecord
#
# User.group(:name)
# => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
+ #
+ # User.group('name AS grouped_name, age')
+ # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>]
+ #
+ # Passing in an array of attributes to group by is also supported.
+ # User.select([:id, :first_name]).group(:id, :first_name).first(3)
+ # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">]
def group(*args)
- args.blank? ? self : spawn.group!(*args)
+ check_if_method_has_arguments!(:group, args)
+ spawn.group!(*args)
end
def group!(*args) # :nodoc:
@@ -233,15 +297,6 @@ module ActiveRecord
# Allows to specify an order attribute:
#
- # User.order('name')
- # => SELECT "users".* FROM "users" ORDER BY name
- #
- # User.order('name DESC')
- # => SELECT "users".* FROM "users" ORDER BY name DESC
- #
- # User.order('name DESC, email')
- # => SELECT "users".* FROM "users" ORDER BY name DESC, email
- #
# User.order(:name)
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
#
@@ -250,19 +305,24 @@ module ActiveRecord
#
# User.order(:name, email: :desc)
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
+ #
+ # User.order('name')
+ # => SELECT "users".* FROM "users" ORDER BY name
+ #
+ # User.order('name DESC')
+ # => SELECT "users".* FROM "users" ORDER BY name DESC
+ #
+ # User.order('name DESC, email')
+ # => SELECT "users".* FROM "users" ORDER BY name DESC, email
def order(*args)
- args.blank? ? self : spawn.order!(*args)
+ check_if_method_has_arguments!(:order, args)
+ spawn.order!(*args)
end
def order!(*args) # :nodoc:
- args.flatten!
- validate_order_args args
+ preprocess_order_args(args)
- references = args.reject { |arg| Arel::Node === arg }
- references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
- references!(references) if references.any?
-
- self.order_values = args + self.order_values
+ self.order_values += args
self
end
@@ -274,34 +334,110 @@ module ActiveRecord
#
# User.order('email DESC').reorder('id ASC').order('name ASC')
#
- # generates a query with 'ORDER BY name ASC, id ASC'.
+ # generates a query with 'ORDER BY id ASC, name ASC'.
def reorder(*args)
- args.blank? ? self : spawn.reorder!(*args)
+ check_if_method_has_arguments!(:reorder, args)
+ spawn.reorder!(*args)
end
def reorder!(*args) # :nodoc:
- args.flatten!
- validate_order_args args
+ preprocess_order_args(args)
self.reordering_value = true
self.order_values = args
self
end
+ VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
+ :limit, :offset, :joins, :includes, :from,
+ :readonly, :having])
+
+ # Removes an unwanted relation that is already defined on a chain of relations.
+ # This is useful when passing around chains of relations and would like to
+ # modify the relations without reconstructing the entire chain.
+ #
+ # User.order('email DESC').unscope(:order) == User.all
+ #
+ # The method arguments are symbols which correspond to the names of the methods
+ # which should be unscoped. The valid arguments are given in VALID_UNSCOPING_VALUES.
+ # The method can also be called with multiple arguments. For example:
+ #
+ # User.order('email DESC').select('id').where(name: "John")
+ # .unscope(:order, :select, :where) == User.all
+ #
+ # One can additionally pass a hash as an argument to unscope specific :where values.
+ # This is done by passing a hash with a single key-value pair. The key should be
+ # :where and the value should be the where value to unscope. For example:
+ #
+ # User.where(name: "John", active: true).unscope(where: :name)
+ # == User.where(active: true)
+ #
+ # This method is similar to <tt>except</tt>, but unlike
+ # <tt>except</tt>, it persists across merges:
+ #
+ # User.order('email').merge(User.except(:order))
+ # == User.order('email')
+ #
+ # User.order('email').merge(User.unscope(:order))
+ # == User.all
+ #
+ # This means it can be used in association definitions:
+ #
+ # has_many :comments, -> { unscope where: :trashed }
+ #
+ def unscope(*args)
+ check_if_method_has_arguments!(:unscope, args)
+ spawn.unscope!(*args)
+ end
+
+ def unscope!(*args) # :nodoc:
+ args.flatten!
+ self.unscope_values += args
+
+ args.each do |scope|
+ case scope
+ when Symbol
+ symbol_unscoping(scope)
+ when Hash
+ scope.each do |key, target_value|
+ if key != :where
+ raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key."
+ end
+
+ Array(target_value).each do |val|
+ where_unscoping(val)
+ end
+ end
+ else
+ raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example."
+ end
+ end
+
+ self
+ end
+
# Performs a joins on +args+:
#
# User.joins(:posts)
# => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
+ #
+ # You can use strings in order to customize your joins:
+ #
+ # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
+ # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
def joins(*args)
- args.compact.blank? ? self : spawn.joins!(*args.flatten)
+ check_if_method_has_arguments!(:joins, args)
+ spawn.joins!(*args)
end
def joins!(*args) # :nodoc:
+ args.compact!
+ args.flatten!
self.joins_values += args
self
end
- def bind(value)
+ def bind(value) # :nodoc:
spawn.bind!(value)
end
@@ -320,7 +456,7 @@ module ActiveRecord
# === string
#
# A single string, without additional arguments, is passed to the query
- # constructor as a SQL fragment, and used in the where clause of the query.
+ # constructor as an SQL fragment, and used in the where clause of the query.
#
# Client.where("orders_count = '2'")
# # SELECT * from clients where orders_count = '2';
@@ -439,17 +575,26 @@ module ActiveRecord
end
end
- # #where! is identical to #where, except that instead of returning a new relation, it adds
- # the condition to the existing relation.
- def where!(opts = :chain, *rest) # :nodoc:
- if opts == :chain
- WhereChain.new(self)
- else
- references!(PredicateBuilder.references(opts)) if Hash === opts
-
- self.where_values += build_where(opts, rest)
- self
+ def where!(opts, *rest) # :nodoc:
+ if Hash === opts
+ opts = sanitize_forbidden_attributes(opts)
+ references!(PredicateBuilder.references(opts))
end
+
+ self.where_values += build_where(opts, rest)
+ self
+ end
+
+ # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
+ #
+ # Post.where(trashed: true).where(trashed: false) # => WHERE `trashed` = 1 AND `trashed` = 0
+ # Post.where(trashed: true).rewhere(trashed: false) # => WHERE `trashed` = 0
+ # Post.where(active: true).where(trashed: true).rewhere(trashed: false) # => WHERE `active` = 1 AND `trashed` = 0
+ #
+ # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping
+ # the named conditions -- not the entire where statement.
+ def rewhere(conditions)
+ unscope(where: conditions.keys).where(conditions)
end
# Allows to specify a HAVING clause. Note that you can't use HAVING
@@ -514,12 +659,11 @@ module ActiveRecord
self
end
- # Returns a chainable relation with zero records, specifically an
- # instance of the <tt>ActiveRecord::NullRelation</tt> class.
+ # Returns a chainable relation with zero records.
#
- # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the
- # Null Object pattern. It is an object with defined null behavior and always returns an empty
- # array of records without quering the database.
+ # The returned relation implements the Null Object pattern. It is an
+ # object with defined null behavior and always returns an empty array of
+ # records without querying the database.
#
# Any subsequent condition chained to the returned relation will continue
# generating an empty relation and will not fire any query to the database.
@@ -539,16 +683,16 @@ module ActiveRecord
# when 'Reviewer'
# Post.published
# when 'Bad User'
- # Post.none # => returning [] instead breaks the previous code
+ # Post.none # It can't be chained if [] is returned.
# end
# end
#
def none
- extending(NullRelation)
+ where("1=0").extending!(NullRelation)
end
def none! # :nodoc:
- extending!(NullRelation)
+ where!("1=0").extending!(NullRelation)
end
# Sets readonly attributes for the returned relation. If value is
@@ -584,14 +728,20 @@ module ActiveRecord
end
def create_with!(value) # :nodoc:
- self.create_with_value = value ? create_with_value.merge(value) : {}
+ if value
+ value = sanitize_forbidden_attributes(value)
+ self.create_with_value = create_with_value.merge(value)
+ else
+ self.create_with_value = {}
+ end
+
self
end
# Specifies table from which the records will be fetched. For example:
#
# Topic.select('title').from('posts')
- # #=> SELECT title FROM posts
+ # # => SELECT title FROM posts
#
# Can accept other relation objects. For example:
#
@@ -605,7 +755,6 @@ module ActiveRecord
spawn.from!(value, subquery_name)
end
- # Like #from, but modifies relation in place.
def from!(value, subquery_name = nil) # :nodoc:
self.from_value = [value, subquery_name]
self
@@ -616,20 +765,22 @@ module ActiveRecord
# User.select(:name)
# # => Might return two records with the same name
#
- # User.select(:name).uniq
- # # => Returns 1 record per unique name
+ # User.select(:name).distinct
+ # # => Returns 1 record per distinct name
#
- # User.select(:name).uniq.uniq(false)
+ # User.select(:name).distinct.distinct(false)
# # => You can also remove the uniqueness
- def uniq(value = true)
- spawn.uniq!(value)
+ def distinct(value = true)
+ spawn.distinct!(value)
end
+ alias uniq distinct
- # Like #uniq, but modifies relation in place.
- def uniq!(value = true) # :nodoc:
- self.uniq_value = value
+ # Like #distinct, but modifies relation in place.
+ def distinct!(value = true) # :nodoc:
+ self.distinct_value = value
self
end
+ alias uniq! distinct!
# Used to extend a scope with additional methods, either through
# a module or through a block provided.
@@ -676,9 +827,10 @@ module ActiveRecord
end
def extending!(*modules, &block) # :nodoc:
- modules << Module.new(&block) if block_given?
+ modules << Module.new(&block) if block
+ modules.flatten!
- self.extending_values += modules.flatten
+ self.extending_values += modules
extend(*extending_values) if extending_values.any?
self
@@ -692,51 +844,84 @@ module ActiveRecord
end
def reverse_order! # :nodoc:
- self.reverse_order_value = !reverse_order_value
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
+ self.order_values = reverse_sql_order(orders)
self
end
# Returns the Arel object associated with the relation.
- def arel
- @arel ||= with_default_scope.build_arel
+ def arel # :nodoc:
+ @arel ||= build_arel
end
- # Like #arel, but ignores the default scope of the model.
+ private
+
def build_arel
- arel = Arel::SelectManager.new(table.engine, table)
+ arel = Arel::SelectManager.new(table)
- build_joins(arel, joins_values) unless joins_values.empty?
+ build_joins(arel, joins_values.flatten) unless joins_values.empty?
- collapse_wheres(arel, (where_values - ['']).uniq)
+ collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds
- arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty?
+ arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty?
arel.take(connection.sanitize_limit(limit_value)) if limit_value
arel.skip(offset_value.to_i) if offset_value
- arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty?
+ arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty?
build_order(arel)
build_select(arel, select_values.uniq)
- arel.distinct(uniq_value)
+ arel.distinct(distinct_value)
arel.from(build_from) if from_value
arel.lock(lock_value) if lock_value
arel
end
- private
+ def symbol_unscoping(scope)
+ if !VALID_UNSCOPING_VALUES.include?(scope)
+ raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
+ end
+
+ single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope)
+ unscope_code = "#{scope}_value#{'s' unless single_val_method}="
+
+ case scope
+ when :order
+ result = []
+ when :where
+ self.bind_values = []
+ else
+ result = [] unless single_val_method
+ end
+
+ self.send(unscope_code, result)
+ end
+
+ def where_unscoping(target_value)
+ target_value = target_value.to_s
+
+ where_values.reject! do |rel|
+ case rel
+ when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual
+ subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
+ subrelation.name.to_s == target_value
+ end
+ end
+
+ bind_values.reject! { |col,_| col.name == target_value }
+ end
def custom_join_ast(table, joins)
- joins = joins.reject { |join| join.blank? }
+ joins = joins.reject(&:blank?)
return [] if joins.empty?
- @implicit_readonly = true
-
- joins.map do |join|
+ joins.map! do |join|
case join
when Array
join = Arel.sql(join.join(' ')) if array_of_strings?(join)
@@ -748,14 +933,13 @@ module ActiveRecord
end
def collapse_wheres(arel, wheres)
- equalities = wheres.grep(Arel::Nodes::Equality)
-
- arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty?
-
- (wheres - equalities).each do |where|
+ predicates = wheres.map do |where|
+ next where if ::Arel::Nodes::Equality === where
where = Arel.sql(where) if String === where
- arel.where(Arel::Nodes::Grouping.new(where))
+ Arel::Nodes::Grouping.new(where)
end
+
+ arel.where(Arel::Nodes::And.new(predicates)) if predicates.present?
end
def build_where(opts, other = [])
@@ -763,18 +947,66 @@ module ActiveRecord
when String, Array
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
when Hash
- attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
- PredicateBuilder.build_from_hash(klass, attributes, table)
+ opts = predicate_builder.resolve_column_aliases(opts)
+
+ tmp_opts, bind_values = create_binds(opts)
+ self.bind_values += bind_values
+
+ attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts)
+ add_relations_to_bind_values(attributes)
+
+ predicate_builder.build_from_hash(attributes)
else
[opts]
end
end
+ def create_binds(opts)
+ bindable, non_binds = opts.partition do |column, value|
+ case value
+ when String, Integer, ActiveRecord::StatementCache::Substitute
+ @klass.columns_hash.include? column.to_s
+ else
+ false
+ end
+ end
+
+ association_binds, non_binds = non_binds.partition do |column, value|
+ value.is_a?(Hash) && association_for_table(column)
+ end
+
+ new_opts = {}
+ binds = []
+
+ bindable.each do |(column,value)|
+ binds.push [@klass.columns_hash[column.to_s], value]
+ new_opts[column] = connection.substitute_at(column)
+ end
+
+ association_binds.each do |(column, value)|
+ association_relation = association_for_table(column).klass.send(:relation)
+ association_new_opts, association_bind = association_relation.send(:create_binds, value)
+ new_opts[column] = association_new_opts
+ binds += association_bind
+ end
+
+ non_binds.each { |column,value| new_opts[column] = value }
+
+ [new_opts, binds]
+ end
+
+ def association_for_table(table_name)
+ table_name = table_name.to_s
+ @klass._reflect_on_association(table_name) ||
+ @klass._reflect_on_association(table_name.singularize)
+ end
+
def build_from
opts, name = from_value
case opts
when Relation
name ||= 'subquery'
+ self.bind_values = opts.bind_values + self.bind_values
opts.arel.as(name.to_s)
else
opts
@@ -788,7 +1020,7 @@ module ActiveRecord
:string_join
when Hash, Symbol, Array
:association_join
- when ActiveRecord::Associations::JoinDependency::JoinAssociation
+ when ActiveRecord::Associations::JoinDependency
:stashed_join
when Arel::Nodes::Join
:join_node
@@ -800,9 +1032,7 @@ module ActiveRecord
association_joins = buckets[:association_join] || []
stashed_association_joins = buckets[:stashed_join] || []
join_nodes = (buckets[:join_node] || []).uniq
- string_joins = (buckets[:string_join] || []).map { |x|
- x.strip
- }.uniq
+ string_joins = (buckets[:string_join] || []).map(&:strip).uniq
join_list = join_nodes + custom_join_ast(manager, string_joins)
@@ -812,24 +1042,29 @@ module ActiveRecord
join_list
)
- join_dependency.graft(*stashed_association_joins)
-
- @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
+ join_infos = join_dependency.join_constraints stashed_association_joins
- # FIXME: refactor this to build an AST
- join_dependency.join_associations.each do |association|
- association.join_to(manager)
+ join_infos.each do |info|
+ info.joins.each { |join| manager.from(join) }
+ manager.bind_values.concat info.binds
end
- manager.join_sources.concat join_list
+ manager.join_sources.concat(join_list)
manager
end
def build_select(arel, selects)
- unless selects.empty?
- @implicit_readonly = false
- arel.project(*selects)
+ if !selects.empty?
+ expanded_select = selects.map do |field|
+ if (Symbol === field || String === field) && columns_hash.key?(field.to_s)
+ arel_table[field]
+ else
+ field
+ end
+ end
+
+ arel.project(*expanded_select)
else
arel.project(@klass.arel_table[Arel.star])
end
@@ -843,16 +1078,10 @@ module ActiveRecord
when Arel::Nodes::Ordering
o.reverse
when String
- o.to_s.split(',').collect do |s|
+ o.to_s.split(',').map! do |s|
s.strip!
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
end
- when Symbol
- { o => :desc }
- when Hash
- o.each_with_object({}) do |(field, dir), memo|
- memo[field] = (dir == :asc ? :desc : :asc )
- end
else
o
end
@@ -860,34 +1089,88 @@ module ActiveRecord
end
def array_of_strings?(o)
- o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
+ o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) }
end
def build_order(arel)
- orders = order_values
- orders = reverse_sql_order(orders) if reverse_order_value
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
+
+ arel.order(*orders) unless orders.empty?
+ end
+
+ VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
+ 'asc', 'desc', 'ASC', 'DESC'] # :nodoc:
- orders = orders.uniq.reject(&:blank?).flat_map do |order|
- case order
+ def validate_order_args(args)
+ args.each do |arg|
+ next unless arg.is_a?(Hash)
+ arg.each do |_key, value|
+ raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \
+ "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value)
+ end
+ end
+ end
+
+ def preprocess_order_args(order_args)
+ order_args.flatten!
+ validate_order_args(order_args)
+
+ references = order_args.grep(String)
+ references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
+ references!(references) if references.any?
+
+ # if a symbol is given we prepend the quoted table name
+ order_args.map! do |arg|
+ case arg
when Symbol
- table[order].asc
+ arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg)
+ table[arg].asc
when Hash
- order.map { |field, dir| table[field].send(dir) }
+ arg.map { |field, dir|
+ field = klass.attribute_alias(field) if klass.attribute_alias?(field)
+ table[field].send(dir.downcase)
+ }
else
- order
+ arg
end
- end
+ end.flatten!
+ end
- arel.order(*orders) unless orders.empty?
+ # Checks to make sure that the arguments are not blank. Note that if some
+ # blank-like object were initially passed into the query method, then this
+ # method will not raise an error.
+ #
+ # Example:
+ #
+ # Post.references() # => raises an error
+ # Post.references([]) # => does not raise an error
+ #
+ # This particular method should be called with a method_name and the args
+ # passed into that method as an input. For example:
+ #
+ # def references(*args)
+ # check_if_method_has_arguments!("references", args)
+ # ...
+ # end
+ def check_if_method_has_arguments!(method_name, args)
+ if args.blank?
+ raise ArgumentError, "The method .#{method_name}() must contain arguments."
+ end
end
- def validate_order_args(args)
- args.select { |a| Hash === a }.each do |h|
- unless (h.values - [:asc, :desc]).empty?
- raise ArgumentError, 'Direction should be :asc or :desc'
+ # This function is recursive just for better readablity.
+ # #where argument doesn't support more than one level nested hash in real world.
+ def add_relations_to_bind_values(attributes)
+ if attributes.is_a?(Hash)
+ attributes.each_value do |value|
+ if value.is_a?(ActiveRecord::Relation)
+ self.bind_values += value.bind_values
+ else
+ add_relations_to_bind_values(value)
+ end
end
end
end
-
end
end
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index de784f9f57..01bddea6c9 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -58,14 +58,16 @@ module ActiveRecord
# Post.order('id asc').only(:where) # discards the order condition
# Post.order('id asc').only(:where, :order) # uses the specified order
def only(*onlies)
+ if onlies.any? { |o| o == :where }
+ onlies << :bind
+ end
relation_with values.slice(*onlies)
end
private
def relation_with(values) # :nodoc:
- result = Relation.new(klass, table, values)
- result.default_scoped = default_scoped
+ result = Relation.create(klass, table, predicate_builder, values)
result.extend(*extending_values) if extending_values.any?
result
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index bea195e9b8..3a3e65ef32 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -3,11 +3,36 @@ module ActiveRecord
# This class encapsulates a Result returned from calling +exec_query+ on any
# database connection adapter. For example:
#
- # x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo')
- # x # => #<ActiveRecord::Result:0xdeadbeef>
+ # result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts')
+ # result # => #<ActiveRecord::Result:0xdeadbeef>
+ #
+ # # Get the column names of the result:
+ # result.columns
+ # # => ["id", "title", "body"]
+ #
+ # # Get the record values of the result:
+ # result.rows
+ # # => [[1, "title_1", "body_1"],
+ # [2, "title_2", "body_2"],
+ # ...
+ # ]
+ #
+ # # Get an array of hashes representing the result (column => value):
+ # result.to_hash
+ # # => [{"id" => 1, "title" => "title_1", "body" => "body_1"},
+ # {"id" => 2, "title" => "title_2", "body" => "body_2"},
+ # ...
+ # ]
+ #
+ # # ActiveRecord::Result also includes Enumerable.
+ # result.each do |row|
+ # puts row['title'] + " " + row['body']
+ # end
class Result
include Enumerable
+ IDENTITY_TYPE = Type::Value.new # :nodoc:
+
attr_reader :columns, :rows, :column_types
def initialize(columns, rows, column_types = {})
@@ -17,8 +42,16 @@ module ActiveRecord
@column_types = column_types
end
+ def length
+ @rows.length
+ end
+
def each
- hash_rows.each { |row| yield row }
+ if block_given?
+ hash_rows.each { |row| yield row }
+ else
+ hash_rows.to_enum { @rows.size }
+ end
end
def to_hash
@@ -45,13 +78,30 @@ module ActiveRecord
hash_rows.last
end
+ def cast_values(type_overrides = {}) # :nodoc:
+ types = columns.map { |name| column_type(name, type_overrides) }
+ result = rows.map do |values|
+ types.zip(values).map { |type, value| type.type_cast_from_database(value) }
+ end
+
+ columns.one? ? result.map!(&:first) : result
+ end
+
def initialize_copy(other)
- @columns = columns.dup
- @rows = rows.dup
- @hash_rows = nil
+ @columns = columns.dup
+ @rows = rows.dup
+ @column_types = column_types.dup
+ @hash_rows = nil
end
private
+
+ def column_type(name, type_overrides = {})
+ type_overrides.fetch(name) do
+ column_types.fetch(name, IDENTITY_TYPE)
+ end
+ end
+
def hash_rows
@hash_rows ||=
begin
@@ -59,7 +109,21 @@ module ActiveRecord
# used as keys in ActiveRecord::Base's @attributes hash
columns = @columns.map { |c| c.dup.freeze }
@rows.map { |row|
- Hash[columns.zip(row)]
+ # In the past we used Hash[columns.zip(row)]
+ # though elegant, the verbose way is much more efficient
+ # both time and memory wise cause it avoids a big array allocation
+ # this method is called a lot and needs to be micro optimised
+ hash = {}
+
+ index = 0
+ length = columns.length
+
+ while index < length
+ hash[columns[index]] = row[index]
+ index += 1
+ end
+
+ hash
}
end
end
diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb
new file mode 100644
index 0000000000..9d605b826a
--- /dev/null
+++ b/activerecord/lib/active_record/runtime_registry.rb
@@ -0,0 +1,22 @@
+require 'active_support/per_thread_registry'
+
+module ActiveRecord
+ # This is a thread locals registry for Active Record. For example:
+ #
+ # ActiveRecord::RuntimeRegistry.connection_handler
+ #
+ # returns the connection handler local to the current thread.
+ #
+ # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # for further details.
+ class RuntimeRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ attr_accessor :connection_handler, :sql_runtime, :connection_id
+
+ [:connection_handler, :sql_runtime, :connection_id].each do |val|
+ class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
+ class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 2dad1dc177..f5aa60a69a 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -3,8 +3,8 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- def quote_value(value, column = nil) #:nodoc:
- connection.quote(value,column)
+ def quote_value(value, column) #:nodoc:
+ connection.quote(value, column)
end
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
@@ -29,14 +29,15 @@ module ActiveRecord
end
end
alias_method :sanitize_sql, :sanitize_sql_for_conditions
+ alias_method :sanitize_conditions, :sanitize_sql
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a SET clause.
# { name: nil, group_id: 4 } returns "name = NULL , group_id='4'"
- def sanitize_sql_for_assignment(assignments)
+ def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name)
case assignments
when Array; sanitize_sql_array(assignments)
- when Hash; sanitize_sql_hash_for_assignment(assignments)
+ when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
else assignments
end
end
@@ -86,11 +87,16 @@ module ActiveRecord
# { address: Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'"
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
+ table = Arel::Table.new(table_name).alias(default_table_name)
+ predicate_builder = PredicateBuilder.new(TableMetadata.new(self, table))
+ ActiveSupport::Deprecation.warn(<<-EOWARN)
+sanitize_sql_hash_for_conditions is deprecated, and will be removed in Rails 5.0
+ EOWARN
+ attrs = predicate_builder.resolve_column_aliases(attrs)
attrs = expand_hash_conditions_for_aggregates(attrs)
- table = Arel::Table.new(table_name, arel_engine).alias(default_table_name)
- PredicateBuilder.build_from_hash(self.class, attrs, table).map { |b|
- connection.visitor.accept b
+ predicate_builder.build_from_hash(attrs).map { |b|
+ connection.visitor.compile b
}.join(' AND ')
end
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
@@ -98,12 +104,21 @@ module ActiveRecord
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
# { status: nil, group_id: 1 }
# # => "status = NULL , group_id = 1"
- def sanitize_sql_hash_for_assignment(attrs)
+ def sanitize_sql_hash_for_assignment(attrs, table)
+ c = connection
attrs.map do |attr, value|
- "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}"
+ value = type_for_attribute(attr.to_s).type_cast_for_database(value)
+ "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}"
end.join(', ')
end
+ # Sanitizes a +string+ so that it is safe to use within an SQL
+ # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%"
+ def sanitize_sql_like(string, escape_character = "\\")
+ pattern = Regexp.union(escape_character, "%", "_")
+ string.gsub(pattern) { |x| [escape_character, x].join }
+ end
+
# Accepts an array of conditions. The array has each value
# sanitized and interpolated into the SQL statement.
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
@@ -120,13 +135,21 @@ module ActiveRecord
end
end
- alias_method :sanitize_conditions, :sanitize_sql
-
def replace_bind_variables(statement, values) #:nodoc:
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
bound = values.dup
c = connection
- statement.gsub('?') { quote_bound_value(bound.shift, c) }
+ statement.gsub(/\?/) do
+ replace_bind_variable(bound.shift, c)
+ end
+ end
+
+ def replace_bind_variable(value, c = connection) #:nodoc:
+ if ActiveRecord::Relation === value
+ value.to_sql
+ else
+ quote_bound_value(value, c)
+ end
end
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
@@ -134,7 +157,7 @@ module ActiveRecord
if $1 == ':' # skip postgresql casts
$& # return the whole match
elsif bind_vars.include?(match = $2.to_sym)
- quote_bound_value(bind_vars[match])
+ replace_bind_variable(bind_vars[match])
else
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
end
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index 3259dbbd80..0a5546a760 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Schema
#
@@ -43,7 +42,7 @@ module ActiveRecord
unless info[:version].blank?
initialize_schema_migrations_table
- assume_migrated_upto_version(info[:version], migrations_paths)
+ connection.assume_migrated_upto_version(info[:version], migrations_paths)
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 36bde44e7c..da95920571 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -17,13 +17,24 @@ module ActiveRecord
cattr_accessor :ignore_tables
@@ignore_tables = []
- def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT)
- new(connection).dump(stream)
- stream
+ class << self
+ def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base)
+ new(connection, generate_options(config)).dump(stream)
+ stream
+ end
+
+ private
+ def generate_options(config)
+ {
+ table_name_prefix: config.table_name_prefix,
+ table_name_suffix: config.table_name_suffix
+ }
+ end
end
def dump(stream)
header(stream)
+ extensions(stream)
tables(stream)
trailer(stream)
stream
@@ -31,10 +42,10 @@ module ActiveRecord
private
- def initialize(connection)
+ def initialize(connection, options = {})
@connection = connection
- @types = @connection.native_database_types
@version = Migrator::current_version rescue nil
+ @options = options
end
def header(stream)
@@ -66,17 +77,30 @@ HEADER
stream.puts "end"
end
+ def extensions(stream)
+ return unless @connection.supports_extensions?
+ extensions = @connection.extensions
+ if extensions.any?
+ stream.puts " # These are extensions that must be enabled in order to support this database"
+ extensions.each do |extension|
+ stream.puts " enable_extension #{extension.inspect}"
+ end
+ stream.puts
+ end
+ end
+
def tables(stream)
- @connection.tables.sort.each do |tbl|
- next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
- case ignored
- when String; remove_prefix_and_suffix(tbl) == ignored
- when Regexp; remove_prefix_and_suffix(tbl) =~ ignored
- else
- raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
- end
+ sorted_tables = @connection.tables.sort
+
+ sorted_tables.each do |table_name|
+ table(table_name, stream) unless ignored?(table_name)
+ end
+
+ # dump foreign keys at the end to make sure all dependent tables exist.
+ if @connection.supports_foreign_keys?
+ sorted_tables.each do |tbl|
+ foreign_keys(tbl, stream) unless ignored?(tbl)
end
- table(tbl, stream)
end
end
@@ -86,28 +110,31 @@ HEADER
tbl = StringIO.new
# first dump primary key column
- if @connection.respond_to?(:pk_and_sequence_for)
- pk, _ = @connection.pk_and_sequence_for(table)
- elsif @connection.respond_to?(:primary_key)
- pk = @connection.primary_key(table)
- end
+ pk = @connection.primary_key(table)
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
- if columns.detect { |c| c.name == pk }
+ pkcol = columns.detect { |c| c.name == pk }
+ if pkcol
if pk != 'id'
tbl.print %Q(, primary_key: "#{pk}")
end
+ pkcolspec = @connection.column_spec_for_primary_key(pkcol)
+ if pkcolspec
+ pkcolspec.each do |key, value|
+ tbl.print ", #{key}: #{value}"
+ end
+ end
else
tbl.print ", id: false"
end
- tbl.print ", force: true"
+ tbl.print ", force: :cascade"
tbl.puts " do |t|"
# then dump all non-primary key columns
column_specs = columns.map do |column|
- raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
next if column.name == pk
- @connection.column_spec(column, @types)
+ @connection.column_spec(column)
end.compact
# find all migration keys used in this table
@@ -158,21 +185,22 @@ HEADER
if (indexes = @connection.indexes(table)).any?
add_index_statements = indexes.map do |index|
statement_parts = [
- ('add_index ' + remove_prefix_and_suffix(index.table).inspect),
+ "add_index #{remove_prefix_and_suffix(index.table).inspect}",
index.columns.inspect,
- ('name: ' + index.name.inspect),
+ "name: #{index.name.inspect}",
]
statement_parts << 'unique: true' if index.unique
index_lengths = (index.lengths || []).compact
- statement_parts << ('length: ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
-
- index_orders = (index.orders || {})
- statement_parts << ('order: ' + index.orders.inspect) unless index_orders.empty?
+ statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any?
- statement_parts << ('where: ' + index.where.inspect) if index.where
+ index_orders = index.orders || {}
+ statement_parts << "order: #{index.orders.inspect}" if index_orders.any?
+ statement_parts << "where: #{index.where.inspect}" if index.where
+ statement_parts << "using: #{index.using.inspect}" if index.using
+ statement_parts << "type: #{index.type.inspect}" if index.type
- ' ' + statement_parts.join(', ')
+ " #{statement_parts.join(', ')}"
end
stream.puts add_index_statements.sort.join("\n")
@@ -180,8 +208,44 @@ HEADER
end
end
+ def foreign_keys(table, stream)
+ if (foreign_keys = @connection.foreign_keys(table)).any?
+ add_foreign_key_statements = foreign_keys.map do |foreign_key|
+ parts = [
+ "add_foreign_key #{remove_prefix_and_suffix(foreign_key.from_table).inspect}",
+ remove_prefix_and_suffix(foreign_key.to_table).inspect,
+ ]
+
+ if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table)
+ parts << "column: #{foreign_key.column.inspect}"
+ end
+
+ if foreign_key.custom_primary_key?
+ parts << "primary_key: #{foreign_key.primary_key.inspect}"
+ end
+
+ if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
+ parts << "name: #{foreign_key.name.inspect}"
+ end
+
+ parts << "on_update: #{foreign_key.on_update.inspect}" if foreign_key.on_update
+ parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete
+
+ " #{parts.join(', ')}"
+ end
+
+ stream.puts add_foreign_key_statements.sort.join("\n")
+ end
+ end
+
def remove_prefix_and_suffix(table)
- table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2")
+ table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2")
+ end
+
+ def ignored?(table_name)
+ ['schema_migrations', ignore_tables].flatten.any? do |ignored|
+ ignored === remove_prefix_and_suffix(table_name)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 9830abe7d8..b5038104ac 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -4,28 +4,48 @@ require 'active_record/base'
module ActiveRecord
class SchemaMigration < ActiveRecord::Base
+ class << self
+ def primary_key
+ nil
+ end
- def self.table_name
- "#{Base.table_name_prefix}schema_migrations#{Base.table_name_suffix}"
- end
+ def table_name
+ "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
+ end
- def self.index_name
- "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
- end
+ def index_name
+ "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
+ end
+
+ def table_exists?
+ connection.table_exists?(table_name)
+ end
- def self.create_table
- unless connection.table_exists?(table_name)
- connection.create_table(table_name, :id => false) do |t|
- t.column :version, :string, :null => false
+ def create_table(limit=nil)
+ unless table_exists?
+ version_options = {null: false}
+ version_options[:limit] = limit if limit
+
+ connection.create_table(table_name, id: false) do |t|
+ t.column :version, :string, version_options
+ end
+ connection.add_index table_name, :version, unique: true, name: index_name
end
- connection.add_index table_name, :version, :unique => true, :name => index_name
end
- end
- def self.drop_table
- if connection.table_exists?(table_name)
- connection.remove_index table_name, :name => index_name
- connection.drop_table(table_name)
+ def drop_table
+ if table_exists?
+ connection.remove_index table_name, name: index_name
+ connection.drop_table(table_name)
+ end
+ end
+
+ def normalize_migration_number(number)
+ "%.3d" % number.to_i
+ end
+
+ def normalized_versions
+ pluck(:version).map { |v| normalize_migration_number v }
end
end
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 0c3fd1bd29..3e43591672 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -1,3 +1,4 @@
+require 'active_support/per_thread_registry'
module ActiveRecord
module Scoping
@@ -10,11 +11,11 @@ module ActiveRecord
module ClassMethods
def current_scope #:nodoc:
- Thread.current["#{self}_current_scope"]
+ ScopeRegistry.value_for(:current_scope, base_class.to_s)
end
def current_scope=(scope) #:nodoc:
- Thread.current["#{self}_current_scope"] = scope
+ ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope)
end
end
@@ -26,5 +27,61 @@ module ActiveRecord
end
end
+ def initialize_internals_callback
+ super
+ populate_with_current_scope_attributes
+ end
+
+ # This class stores the +:current_scope+ and +:ignore_default_scope+ values
+ # for different classes. The registry is stored as a thread local, which is
+ # accessed through +ScopeRegistry.current+.
+ #
+ # This class allows you to store and get the scope values on different
+ # classes and different types of scopes. For example, if you are attempting
+ # to get the current_scope for the +Board+ model, then you would use the
+ # following code:
+ #
+ # registry = ActiveRecord::Scoping::ScopeRegistry
+ # registry.set_value_for(:current_scope, "Board", some_new_scope)
+ #
+ # Now when you run:
+ #
+ # registry.value_for(:current_scope, "Board")
+ #
+ # You will obtain whatever was defined in +some_new_scope+. The +value_for+
+ # and +set_value_for+ methods are delegated to the current +ScopeRegistry+
+ # object, so the above example code can also be called as:
+ #
+ # ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope,
+ # "Board", some_new_scope)
+ class ScopeRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ VALID_SCOPE_TYPES = [:current_scope, :ignore_default_scope]
+
+ def initialize
+ @registry = Hash.new { |hash, key| hash[key] = {} }
+ end
+
+ # Obtains the value for a given +scope_name+ and +variable_name+.
+ def value_for(scope_type, variable_name)
+ raise_invalid_scope_type!(scope_type)
+ @registry[scope_type][variable_name]
+ end
+
+ # Sets the +value+ for a given +scope_type+ and +variable_name+.
+ def set_value_for(scope_type, variable_name, value)
+ raise_invalid_scope_type!(scope_type)
+ @registry[scope_type][variable_name] = value
+ end
+
+ private
+
+ def raise_invalid_scope_type!(scope_type)
+ if !VALID_SCOPE_TYPES.include?(scope_type)
+ raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES"
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 6835d0e01b..18190cb535 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
module Scoping
module Default
@@ -6,12 +5,13 @@ module ActiveRecord
included do
# Stores the default scope for the class.
- class_attribute :default_scopes, instance_writer: false
+ class_attribute :default_scopes, instance_writer: false, instance_predicate: false
+
self.default_scopes = []
end
module ClassMethods
- # Returns a scope for the model without the +default_scope+.
+ # Returns a scope for the model without the previously set scopes.
#
# class Post < ActiveRecord::Base
# def self.default_scope
@@ -19,23 +19,16 @@ module ActiveRecord
# end
# end
#
- # Post.all # Fires "SELECT * FROM posts WHERE published = true"
- # Post.unscoped.all # Fires "SELECT * FROM posts"
+ # Post.all # Fires "SELECT * FROM posts WHERE published = true"
+ # Post.unscoped.all # Fires "SELECT * FROM posts"
+ # Post.where(published: false).unscoped.all # Fires "SELECT * FROM posts"
#
# This method also accepts a block. All queries inside the block will
- # not use the +default_scope+:
+ # not use the previously set scopes.
#
# Post.unscoped {
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
- #
- # It is recommended that you use the block form of unscoped because
- # chaining unscoped with +scope+ does not work. Assuming that
- # +published+ is a +scope+, the following two statements
- # are equal: the +default_scope+ is applied on both.
- #
- # Post.unscoped.published
- # Post.published
def unscoped
block_given? ? relation.scoping { yield } : relation
end
@@ -91,40 +84,35 @@ module ActiveRecord
scope = Proc.new if block_given?
if scope.is_a?(Relation) || !scope.respond_to?(:call)
- ActiveSupport::Deprecation.warn(
- "Calling #default_scope without a block is deprecated. For example instead " \
+ raise ArgumentError,
+ "Support for calling #default_scope without a block is removed. For example instead " \
"of `default_scope where(color: 'red')`, please use " \
"`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \
"self.default_scope.)"
- )
end
- self.default_scopes = default_scopes + [scope]
+ self.default_scopes += [scope]
end
- def build_default_scope # :nodoc:
+ def build_default_scope(base_rel = relation) # :nodoc:
if !Base.is_a?(method(:default_scope).owner)
# The user has defined their own default scope method, so call that
evaluate_default_scope { default_scope }
elsif default_scopes.any?
evaluate_default_scope do
- default_scopes.inject(relation) do |default_scope, scope|
- if !scope.is_a?(Relation) && scope.respond_to?(:call)
- default_scope.merge(unscoped { scope.call })
- else
- default_scope.merge(scope)
- end
+ default_scopes.inject(base_rel) do |default_scope, scope|
+ default_scope.merge(base_rel.scoping { scope.call })
end
end
end
end
def ignore_default_scope? # :nodoc:
- Thread.current["#{self}_ignore_default_scope"]
+ ScopeRegistry.value_for(:ignore_default_scope, self)
end
def ignore_default_scope=(ignore) # :nodoc:
- Thread.current["#{self}_ignore_default_scope"] = ignore
+ ScopeRegistry.set_value_for(:ignore_default_scope, self, ignore)
end
# The ignore_default_scope flag is used to prevent an infinite recursion
@@ -140,7 +128,6 @@ module ActiveRecord
self.ignore_default_scope = false
end
end
-
end
end
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 8b7eda6eee..35420e6551 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -25,22 +25,18 @@ module ActiveRecord
if current_scope
current_scope.clone
else
- scope = relation
- scope.default_scoped = true
- scope
+ default_scoped
end
end
+ def default_scoped # :nodoc:
+ relation.merge(build_default_scope)
+ end
+
# Collects attributes from scopes that should be applied when creating
# an AR instance for the particular class this is called on.
def scope_attributes # :nodoc:
- if current_scope
- current_scope.scope_for_create
- else
- scope = relation
- scope.default_scoped = true
- scope.scope_for_create
- end
+ all.scope_for_create
end
# Are there default attributes associated with this scope?
@@ -134,37 +130,32 @@ module ActiveRecord
# end
#
# def self.titles
- # map(&:title)
+ # pluck(:title)
# end
- #
# end
#
# We are able to call the methods like this:
#
# Article.published.featured.latest_article
# Article.featured.titles
-
def scope(name, body, &block)
- extension = Module.new(&block) if block
+ unless body.respond_to?(:call)
+ raise ArgumentError, 'The scope body needs to be callable.'
+ end
- # Check body.is_a?(Relation) to prevent the relation actually being
- # loaded by respond_to?
- if body.is_a?(Relation) || !body.respond_to?(:call)
- ActiveSupport::Deprecation.warn(
- "Using #scope without passing a callable object is deprecated. For " \
- "example `scope :red, where(color: 'red')` should be changed to " \
- "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \
- "in the former usage and it makes the implementation more complicated " \
- "and buggy. (If you prefer, you can just define a class method named " \
- "`self.red`.)"
- )
+ if dangerous_class_method?(name)
+ raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
+ "on the model \"#{self.name}\", but Active Record already defined " \
+ "a class method with the same name."
end
+ extension = Module.new(&block) if block
+
singleton_class.send(:define_method, name) do |*args|
- options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body
- relation = all.merge(options)
+ scope = all.scoping { body.call(*args) }
+ scope = scope.extending(extension) if extension
- extension ? relation.extending(extension) : relation
+ scope || all
end
end
end
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index 6b55af4205..48c12dcf9f 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -5,13 +5,13 @@ module ActiveRecord #:nodoc:
include ActiveModel::Serializers::JSON
included do
- self.include_root_in_json = true
+ self.include_root_in_json = false
end
def serializable_hash(options = nil)
options = options.try(:clone) || {}
- options[:except] = Array(options[:except]).map { |n| n.to_s }
+ options[:except] = Array(options[:except]).map(&:to_s)
options[:except] |= Array(self.class.inheritance_column)
super(options)
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 1a766093d0..c2484d02ed 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -180,13 +180,9 @@ module ActiveRecord #:nodoc:
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
def compute_type
klass = @serializable.class
- type = if klass.serialized_attributes.key?(name)
- super
- elsif klass.columns_hash.key?(name)
- klass.columns_hash[name].type
- else
- NilClass
- end
+ column = klass.columns_hash[name] || Type::Value.new
+
+ type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type
{ :text => :string,
:time => :datetime }[type] || type
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
new file mode 100644
index 0000000000..192a19f05d
--- /dev/null
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -0,0 +1,111 @@
+module ActiveRecord
+
+ # Statement cache is used to cache a single statement in order to avoid creating the AST again.
+ # Initializing the cache is done by passing the statement in the create block:
+ #
+ # cache = StatementCache.create(Book.connection) do |params|
+ # Book.where(name: "my book").where("author_id > 3")
+ # end
+ #
+ # The cached statement is executed by using the +execute+ method:
+ #
+ # cache.execute([], Book, Book.connection)
+ #
+ # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped.
+ # Database is queried when +to_a+ is called on the relation.
+ #
+ # If you want to cache the statement without the values you can use the +bind+ method of the
+ # block parameter.
+ #
+ # cache = StatementCache.create(Book.connection) do |params|
+ # Book.where(name: params.bind)
+ # end
+ #
+ # And pass the bind values as the first argument of +execute+ call.
+ #
+ # cache.execute(["my book"], Book, Book.connection)
+ class StatementCache # :nodoc:
+ class Substitute; end # :nodoc:
+
+ class Query # :nodoc:
+ def initialize(sql)
+ @sql = sql
+ end
+
+ def sql_for(binds, connection)
+ @sql
+ end
+ end
+
+ class PartialQuery < Query # :nodoc:
+ def initialize values
+ @values = values
+ @indexes = values.each_with_index.find_all { |thing,i|
+ Arel::Nodes::BindParam === thing
+ }.map(&:last)
+ end
+
+ def sql_for(binds, connection)
+ val = @values.dup
+ binds = binds.dup
+ @indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) }
+ val.join
+ end
+ end
+
+ def self.query(visitor, ast)
+ Query.new visitor.accept(ast, Arel::Collectors::SQLString.new).value
+ end
+
+ def self.partial_query(visitor, ast, collector)
+ collected = visitor.accept(ast, collector).value
+ PartialQuery.new collected
+ end
+
+ class Params # :nodoc:
+ def bind; Substitute.new; end
+ end
+
+ class BindMap # :nodoc:
+ def initialize(bind_values)
+ @indexes = []
+ @bind_values = bind_values
+
+ bind_values.each_with_index do |(_, value), i|
+ if Substitute === value
+ @indexes << i
+ end
+ end
+ end
+
+ def bind(values)
+ bvs = @bind_values.map(&:dup)
+ @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] }
+ bvs
+ end
+ end
+
+ attr_reader :bind_map, :query_builder
+
+ def self.create(connection, block = Proc.new)
+ relation = block.call Params.new
+ bind_map = BindMap.new relation.bind_values
+ query_builder = connection.cacheable_query relation.arel
+ new query_builder, bind_map
+ end
+
+ def initialize(query_builder, bind_map)
+ @query_builder = query_builder
+ @bind_map = bind_map
+ end
+
+ def execute(params, klass, connection)
+ bind_values = bind_map.bind params
+
+ sql = query_builder.sql_for bind_values, connection
+
+ klass.find_by_sql sql, bind_values
+ end
+ alias :call :execute
+ end
+end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index df7f58c81f..919bc58ba5 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -15,6 +15,15 @@ module ActiveRecord
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
#
+ # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for
+ # the serialization provided by +store+. Simply use +store_accessor+ instead to generate
+ # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
+ # using a symbol.
+ #
+ # NOTE: The default validations with the exception of +uniqueness+ will work.
+ # For example, if you want to check for +uniqueness+ with +hstore+ you will
+ # need to use a custom validation to handle it.
+ #
# Examples:
#
# class User < ActiveRecord::Base
@@ -42,29 +51,28 @@ module ActiveRecord
#
# All stored values are automatically available through accessors on the Active Record
# object, but sometimes you want to specialize this behavior. This can be done by overwriting
- # the default accessors (using the same name as the attribute) and calling
- # <tt>read_store_attribute(store_attribute_name, attr_name)</tt> and
- # <tt>write_store_attribute(store_attribute_name, attr_name, value)</tt> to actually
- # change things.
+ # the default accessors (using the same name as the attribute) and calling <tt>super</tt>
+ # to actually change things.
#
# class Song < ActiveRecord::Base
# # Uses a stored integer to hold the volume adjustment of the song
# store :settings, accessors: [:volume_adjustment]
#
# def volume_adjustment=(decibels)
- # write_store_attribute(:settings, :volume_adjustment, decibels.to_i)
+ # super(decibels.to_i)
# end
#
# def volume_adjustment
- # read_store_attribute(:settings, :volume_adjustment).to_i
+ # super.to_i
# end
# end
module Store
extend ActiveSupport::Concern
included do
- class_attribute :stored_attributes, instance_accessor: false
- self.stored_attributes = {}
+ class << self
+ attr_accessor :local_stored_attributes
+ end
end
module ClassMethods
@@ -75,43 +83,97 @@ module ActiveRecord
def store_accessor(store_attribute, *keys)
keys = keys.flatten
- keys.each do |key|
- define_method("#{key}=") do |value|
- write_store_attribute(store_attribute, key, value)
- end
- define_method(key) do
- read_store_attribute(store_attribute, key)
+ _store_accessors_module.module_eval do
+ keys.each do |key|
+ define_method("#{key}=") do |value|
+ write_store_attribute(store_attribute, key, value)
+ end
+
+ define_method(key) do
+ read_store_attribute(store_attribute, key)
+ end
end
end
- self.stored_attributes[store_attribute] ||= []
- self.stored_attributes[store_attribute] |= keys
+ # assign new store attribute and create new hash to ensure that each class in the hierarchy
+ # has its own hash of stored attributes.
+ self.local_stored_attributes ||= {}
+ self.local_stored_attributes[store_attribute] ||= []
+ self.local_stored_attributes[store_attribute] |= keys
+ end
+
+ def _store_accessors_module # :nodoc:
+ @_store_accessors_module ||= begin
+ mod = Module.new
+ include mod
+ mod
+ end
+ end
+
+ def stored_attributes
+ parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {}
+ if self.local_stored_attributes
+ parent.merge!(self.local_stored_attributes) { |k, a, b| a | b }
+ end
+ parent
end
end
protected
def read_store_attribute(store_attribute, key)
- attribute = initialize_store_attribute(store_attribute)
- attribute[key]
+ accessor = store_accessor_for(store_attribute)
+ accessor.read(self, store_attribute, key)
end
def write_store_attribute(store_attribute, key, value)
- attribute = initialize_store_attribute(store_attribute)
- if value != attribute[key]
- send :"#{store_attribute}_will_change!"
- attribute[key] = value
- end
+ accessor = store_accessor_for(store_attribute)
+ accessor.write(self, store_attribute, key, value)
end
private
- def initialize_store_attribute(store_attribute)
- attribute = send(store_attribute)
- unless attribute.is_a?(HashWithIndifferentAccess)
- attribute = IndifferentCoder.as_indifferent_hash(attribute)
- send :"#{store_attribute}=", attribute
+ def store_accessor_for(store_attribute)
+ type_for_attribute(store_attribute.to_s).accessor
+ end
+
+ class HashAccessor # :nodoc:
+ def self.read(object, attribute, key)
+ prepare(object, attribute)
+ object.public_send(attribute)[key]
+ end
+
+ def self.write(object, attribute, key, value)
+ prepare(object, attribute)
+ if value != read(object, attribute, key)
+ object.public_send :"#{attribute}_will_change!"
+ object.public_send(attribute)[key] = value
+ end
+ end
+
+ def self.prepare(object, attribute)
+ object.public_send :"#{attribute}=", {} unless object.send(attribute)
+ end
+ end
+
+ class StringKeyedHashAccessor < HashAccessor # :nodoc:
+ def self.read(object, attribute, key)
+ super object, attribute, key.to_s
+ end
+
+ def self.write(object, attribute, key, value)
+ super object, attribute, key.to_s, value
+ end
+ end
+
+ class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc:
+ def self.prepare(object, store_attribute)
+ attribute = object.send(store_attribute)
+ unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
+ attribute = IndifferentCoder.as_indifferent_hash(attribute)
+ object.send :"#{store_attribute}=", attribute
+ end
+ attribute
end
- attribute
end
class IndifferentCoder # :nodoc:
@@ -129,17 +191,17 @@ module ActiveRecord
end
def load(yaml)
- self.class.as_indifferent_hash @coder.load(yaml)
+ self.class.as_indifferent_hash(@coder.load(yaml || ''))
end
def self.as_indifferent_hash(obj)
case obj
- when HashWithIndifferentAccess
+ when ActiveSupport::HashWithIndifferentAccess
obj
when Hash
obj.with_indifferent_access
else
- HashWithIndifferentAccess.new
+ ActiveSupport::HashWithIndifferentAccess.new
end
end
end
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
new file mode 100644
index 0000000000..11e33e8dfe
--- /dev/null
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -0,0 +1,53 @@
+module ActiveRecord
+ class TableMetadata # :nodoc:
+ delegate :foreign_type, :foreign_key, to: :association, prefix: true
+
+ def initialize(klass, arel_table, association = nil)
+ @klass = klass
+ @arel_table = arel_table
+ @association = association
+ end
+
+ def resolve_column_aliases(hash)
+ hash = hash.dup
+ hash.keys.grep(Symbol) do |key|
+ if klass.attribute_alias? key
+ hash[klass.attribute_alias(key)] = hash.delete key
+ end
+ end
+ hash
+ end
+
+ def arel_attribute(column_name)
+ arel_table[column_name]
+ end
+
+ def associated_with?(association_name)
+ klass && klass._reflect_on_association(association_name)
+ end
+
+ def associated_table(table_name)
+ return self if table_name == arel_table.name
+
+ association = klass._reflect_on_association(table_name)
+ if association && !association.polymorphic?
+ association_klass = association.klass
+ arel_table = association_klass.arel_table
+ else
+ type_caster = TypeCaster::Connection.new(klass.connection, table_name)
+ association_klass = nil
+ arel_table = Arel::Table.new(table_name, type_caster: type_caster)
+ end
+
+ TableMetadata.new(association_klass, arel_table, association)
+ end
+
+ def polymorphic_association?
+ association && association.polymorphic?
+ end
+
+ protected
+
+ attr_reader :klass, :arel_table, :association
+ end
+end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index fda51b3d76..69aceb66b1 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -1,9 +1,44 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module Tasks # :nodoc:
- module DatabaseTasks # :nodoc:
+ class DatabaseAlreadyExists < StandardError; end # :nodoc:
+ class DatabaseNotSupported < StandardError; end # :nodoc:
+
+ # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates
+ # logic behind common tasks used to manage database and migrations.
+ #
+ # The tasks defined here are used with Rake tasks provided by Active Record.
+ #
+ # In order to use DatabaseTasks, a few config values need to be set. All the needed
+ # config values are set by Rails already, so it's necessary to do it only if you
+ # want to change the defaults or when you want to use Active Record outside of Rails
+ # (in such case after configuring the database tasks, you can also use the rake tasks
+ # defined in Active Record).
+ #
+ # The possible config values are:
+ #
+ # * +env+: current environment (like Rails.env).
+ # * +database_configuration+: configuration of your databases (as in +config/database.yml+).
+ # * +db_dir+: your +db+ directory.
+ # * +fixtures_path+: a path to fixtures directory.
+ # * +migrations_paths+: a list of paths to directories with migrations.
+ # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method.
+ # * +root+: a path to the root of the application.
+ #
+ # Example usage of +DatabaseTasks+ outside Rails could look as such:
+ #
+ # include ActiveRecord::Tasks
+ # DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml')
+ # DatabaseTasks.db_dir = 'db'
+ # # other settings...
+ #
+ # DatabaseTasks.create_current('production')
+ module DatabaseTasks
extend self
- attr_writer :current_config
+ attr_writer :current_config, :db_dir, :migrations_paths, :fixtures_path, :root, :env, :seed_loader
+ attr_accessor :database_configuration
LOCAL_HOSTS = ['127.0.0.1', 'localhost']
@@ -12,26 +47,52 @@ module ActiveRecord
@tasks[pattern] = task
end
- register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks)
- register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
- register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
+ register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks)
+ register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
+ register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
+
+ def db_dir
+ @db_dir ||= Rails.application.config.paths["db"].first
+ end
+
+ def migrations_paths
+ @migrations_paths ||= Rails.application.paths['db/migrate'].to_a
+ end
+
+ def fixtures_path
+ @fixtures_path ||= if ENV['FIXTURES_PATH']
+ File.join(root, ENV['FIXTURES_PATH'])
+ else
+ File.join(root, 'test', 'fixtures')
+ end
+ end
+
+ def root
+ @root ||= Rails.root
+ end
+
+ def env
+ @env ||= Rails.env
+ end
+
+ def seed_loader
+ @seed_loader ||= Rails.application
+ end
def current_config(options = {})
- options.reverse_merge! :env => Rails.env
+ options.reverse_merge! :env => env
if options.has_key?(:config)
@current_config = options[:config]
else
- @current_config ||= if ENV['DATABASE_URL']
- database_url_config
- else
- ActiveRecord::Base.configurations[options[:env]]
- end
+ @current_config ||= ActiveRecord::Base.configurations[options[:env]]
end
end
def create(*arguments)
configuration = arguments.first
class_for_adapter(configuration['adapter']).new(*arguments).create
+ rescue DatabaseAlreadyExists
+ $stderr.puts "#{configuration['database']} already exists"
rescue Exception => error
$stderr.puts error, *(error.backtrace)
$stderr.puts "Couldn't create database for #{configuration.inspect}"
@@ -41,20 +102,18 @@ module ActiveRecord
each_local_configuration { |configuration| create configuration }
end
- def create_current(environment = Rails.env)
+ def create_current(environment = env)
each_current_configuration(environment) { |configuration|
create configuration
}
- ActiveRecord::Base.establish_connection environment
- end
-
- def create_database_url
- create database_url_config
+ ActiveRecord::Base.establish_connection(environment.to_sym)
end
def drop(*arguments)
configuration = arguments.first
class_for_adapter(configuration['adapter']).new(*arguments).drop
+ rescue ActiveRecord::NoDatabaseError
+ $stderr.puts "Database '#{configuration['database']}' does not exist"
rescue Exception => error
$stderr.puts error, *(error.backtrace)
$stderr.puts "Couldn't drop #{configuration['database']}"
@@ -64,17 +123,25 @@ module ActiveRecord
each_local_configuration { |configuration| drop configuration }
end
- def drop_current(environment = Rails.env)
+ def drop_current(environment = env)
each_current_configuration(environment) { |configuration|
drop configuration
}
end
- def drop_database_url
- drop database_url_config
+ def migrate
+ verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
+ scope = ENV['SCOPE']
+ verbose_was, Migration.verbose = Migration.verbose, verbose
+ Migrator.migrate(Migrator.migrations_paths, version) do |migration|
+ scope.blank? || scope == migration.scope
+ end
+ ensure
+ Migration.verbose = verbose_was
end
- def charset_current(environment = Rails.env)
+ def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -83,7 +150,7 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).charset
end
- def collation_current(environment = Rails.env)
+ def collation_current(environment = env)
collation ActiveRecord::Base.configurations[environment]
end
@@ -96,6 +163,19 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(configuration).purge
end
+ def purge_all
+ each_local_configuration { |configuration|
+ purge configuration
+ }
+ end
+
+ def purge_current(environment = env)
+ each_current_configuration(environment) { |configuration|
+ purge configuration
+ }
+ ActiveRecord::Base.establish_connection(environment.to_sym)
+ end
+
def structure_dump(*arguments)
configuration = arguments.first
filename = arguments.delete_at 1
@@ -108,21 +188,84 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename)
end
- private
+ def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc:
+ file ||= schema_file(format)
- def database_url_config
- @database_url_config ||=
- ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys
+ case format
+ when :ruby
+ check_schema_file(file)
+ ActiveRecord::Base.establish_connection(configuration)
+ load(file)
+ when :sql
+ check_schema_file(file)
+ structure_load(configuration, file)
+ else
+ raise ArgumentError, "unknown format #{format.inspect}"
+ end
+ end
+
+ def load_schema_for(*args)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ This method was renamed to `#load_schema` and will be removed in the future.
+ Use `#load_schema` instead.
+ MSG
+ load_schema(*args)
+ end
+
+ def schema_file(format = ActiveSupport::Base.schema_format)
+ case format
+ when :ruby
+ File.join(db_dir, "schema.rb")
+ when :sql
+ File.join(db_dir, "structure.sql")
+ end
+ end
+
+ def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
+ if File.exist?(file || schema_file(format))
+ load_schema_current(format, file, environment)
+ end
+ end
+
+ def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
+ each_current_configuration(environment) { |configuration|
+ load_schema configuration, format, file
+ }
+ ActiveRecord::Base.establish_connection(environment.to_sym)
+ end
+
+ def check_schema_file(filename)
+ unless File.exist?(filename)
+ message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.}
+ message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails)
+ Kernel.abort message
+ end
+ end
+
+ def load_seed
+ if seed_loader
+ seed_loader.load_seed
+ else
+ raise "You tried to load seed data, but no seed loader is specified. Please specify seed " +
+ "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" +
+ "Seed loader should respond to load_seed method"
+ end
end
+ private
+
def class_for_adapter(adapter)
key = @tasks.keys.detect { |pattern| adapter[pattern] }
+ unless key
+ raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter"
+ end
@tasks[key]
end
def each_current_configuration(environment)
environments = [environment]
- environments << 'test' if environment.development?
+ # add test environment only if no RAILS_ENV was specified.
+ environments << 'test' if environment == 'development' && ENV['RAILS_ENV'].nil?
configurations = ActiveRecord::Base.configurations.values_at(*environments)
configurations.compact.each do |configuration|
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 3d27c97254..eafbb2c249 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -1,7 +1,6 @@
module ActiveRecord
module Tasks # :nodoc:
class MySQLDatabaseTasks # :nodoc:
-
DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8'
DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci'
ACCESS_DENIED_ERROR = 1045
@@ -16,18 +15,26 @@ module ActiveRecord
establish_connection configuration_without_database
connection.create_database configuration['database'], creation_options
establish_connection configuration
+ rescue ActiveRecord::StatementInvalid => error
+ if /database exists/ === error.message
+ raise DatabaseAlreadyExists
+ else
+ raise
+ end
rescue error_class => error
- raise error unless error.errno == ACCESS_DENIED_ERROR
-
- $stdout.print error.error
- establish_connection root_configuration_without_database
- connection.create_database configuration['database'], creation_options
- connection.execute grant_statement.gsub(/\s+/, ' ').strip
- establish_connection configuration
- rescue error_class => error
- $stderr.puts error.error
- $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}"
- $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding']
+ if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR
+ $stdout.print error.error
+ establish_connection root_configuration_without_database
+ connection.create_database configuration['database'], creation_options
+ if configuration['username'] != 'root'
+ connection.execute grant_statement.gsub(/\s+/, ' ').strip
+ end
+ establish_connection configuration
+ else
+ $stderr.puts error.inspect
+ $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}"
+ $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding']
+ end
end
def drop
@@ -36,7 +43,7 @@ module ActiveRecord
end
def purge
- establish_connection :test
+ establish_connection configuration
connection.recreate_database configuration['database'], creation_options
end
@@ -53,7 +60,10 @@ module ActiveRecord
args.concat(["--result-file", "#{filename}"])
args.concat(["--no-data"])
args.concat(["#{configuration['database']}"])
- Kernel.system(*args)
+ unless Kernel.system(*args)
+ $stderr.puts "Could not dump the database structure. "\
+ "Make sure `mysqldump` is in your PATH and check the command output for warnings."
+ end
end
def structure_load(filename)
@@ -87,14 +97,15 @@ module ActiveRecord
end
def error_class
- case configuration['adapter']
- when /jdbc/
+ if configuration['adapter'] =~ /jdbc/
require 'active_record/railties/jdbcmysql_error'
ArJdbcMySQL::Error
- when /mysql2/
+ elsif defined?(Mysql2)
Mysql2::Error
- else
+ elsif defined?(Mysql)
Mysql::Error
+ else
+ StandardError
end
end
@@ -114,7 +125,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
end
def root_password
- $stdout.print "Please provide the root password for your mysql installation\n>"
+ $stdout.print "Please provide the root password for your MySQL installation\n>"
$stdin.gets.strip
end
@@ -124,11 +135,11 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
args << "--password=#{configuration['password']}" if configuration['password']
args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding']
configuration.slice('host', 'port', 'socket').each do |k, v|
- args.concat([ "--#{k}", v ]) if v
+ args.concat([ "--#{k}", v.to_s ]) if v
end
+
args
end
-
end
end
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index ea5cb888fb..ce1de4b76e 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -3,7 +3,6 @@ require 'shellwords'
module ActiveRecord
module Tasks # :nodoc:
class PostgreSQLDatabaseTasks # :nodoc:
-
DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8'
delegate :connection, :establish_connection, :clear_active_connections!,
@@ -18,6 +17,12 @@ module ActiveRecord
connection.create_database configuration['database'],
configuration.merge('encoding' => encoding)
establish_connection configuration
+ rescue ActiveRecord::StatementInvalid => error
+ if /database .* already exists/ === error.message
+ raise DatabaseAlreadyExists
+ else
+ raise
+ end
end
def drop
@@ -49,12 +54,12 @@ module ActiveRecord
command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}"
raise 'Error dumping database' unless Kernel.system(command)
- File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" }
+ File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" }
end
def structure_load(filename)
set_psql_env
- Kernel.system("psql -f #{filename} #{configuration['database']}")
+ Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}")
end
private
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index da01058a82..9ab64d0325 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -1,18 +1,14 @@
module ActiveRecord
module Tasks # :nodoc:
class SQLiteDatabaseTasks # :nodoc:
-
delegate :connection, :establish_connection, to: ActiveRecord::Base
- def initialize(configuration, root = Rails.root)
+ def initialize(configuration, root = ActiveRecord::Tasks::DatabaseTasks.root)
@configuration, @root = configuration, root
end
def create
- if File.exist?(configuration['database'])
- $stderr.puts "#{configuration['database']} already exists"
- return
- end
+ raise DatabaseAlreadyExists if File.exist?(configuration['database'])
establish_connection configuration
connection
@@ -25,7 +21,11 @@ module ActiveRecord
FileUtils.rm(file) if File.exist?(file)
end
- alias :purge :drop
+
+ def purge
+ drop
+ create
+ end
def charset
connection.encoding
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
deleted file mode 100644
index c035ad43a2..0000000000
--- a/activerecord/lib/active_record/test_case.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-require 'active_support/test_case'
-
-ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase')
-module ActiveRecord
- # = Active Record Test Case
- #
- # Defines some test assertions to test against SQL queries.
- class TestCase < ActiveSupport::TestCase #:nodoc:
- def teardown
- SQLCounter.clear_log
- 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
- if current_adapter?(:SybaseAdapter)
- assert_equal expected.to_s, actual.to_date.to_s, message
- else
- assert_equal expected.to_s, actual.to_s, message
- end
- end
-
- def assert_sql(*patterns_to_match)
- SQLCounter.clear_log
- yield
- SQLCounter.log_all
- ensure
- failed_patterns = []
- patterns_to_match.each do |pattern|
- failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
- end
- assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
- end
-
- def assert_queries(num = 1, options = {})
- ignore_none = options.fetch(:ignore_none) { num == :any }
- SQLCounter.clear_log
- yield
- ensure
- the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
- if num == :any
- assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
- else
- mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
- assert_equal num, the_log.size, mesg
- end
- end
-
- def assert_no_queries(&block)
- assert_queries(0, :ignore_none => true, &block)
- end
-
- end
-
- class SQLCounter
- class << self
- attr_accessor :ignored_sql, :log, :log_all
- def clear_log; self.log = []; self.log_all = []; end
- end
-
- self.clear_log
-
- self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
-
- # FIXME: this needs to be refactored so specific database can add their own
- # ignored SQL, or better yet, use a different notification for the queries
- # instead examining the SQL content.
- oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
- mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/]
- postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im]
-
- [oracle_ignored, mysql_ignored, postgresql_ignored].each do |db_ignored_sql|
- ignored_sql.concat db_ignored_sql
- end
-
- attr_reader :ignore
-
- def initialize(ignore = Regexp.union(self.class.ignored_sql))
- @ignore = ignore
- end
-
- def call(name, start, finish, message_id, values)
- sql = values[:sql]
-
- # FIXME: this seems bad. we should probably have a better way to indicate
- # the query was cached
- return if 'CACHE' == values[:name]
-
- self.class.log_all << sql
- self.class.log << sql unless ignore =~ sql
- end
- end
-
- ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
-end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index cf17b1d8a4..20e4235788 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Timestamp
#
@@ -10,9 +9,9 @@ module ActiveRecord
#
# config.active_record.record_timestamps = false
#
- # Timestamps are in the local timezone by default but you can use UTC by setting:
+ # Timestamps are in UTC by default but you can use the local timezone by setting:
#
- # config.active_record.default_timezone = :utc
+ # config.active_record.default_timezone = :local
#
# == Time Zone aware attributes
#
@@ -37,19 +36,20 @@ module ActiveRecord
end
def initialize_dup(other) # :nodoc:
- clear_timestamp_attributes
super
+ clear_timestamp_attributes
end
private
- def create
+ def _create_record
if self.record_timestamps
current_time = current_time_from_proper_timezone
all_timestamp_attributes.each do |column|
- if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
- write_attribute(column.to_s, current_time)
+ column = column.to_s
+ if has_attribute?(column) && !attribute_present?(column)
+ write_attribute(column, current_time)
end
end
end
@@ -57,8 +57,8 @@ module ActiveRecord
super
end
- def update(*args)
- if should_record_timestamps?
+ def _update_record(*args, touch: true, **options)
+ if touch && should_record_timestamps?
current_time = current_time_from_proper_timezone
timestamp_attributes_for_update_in_model.each do |column|
@@ -67,11 +67,11 @@ module ActiveRecord
write_attribute(column, current_time)
end
end
- super
+ super(*args)
end
def should_record_timestamps?
- self.record_timestamps && (!partial_writes? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
+ self.record_timestamps && (!partial_writes? || changed?)
end
def timestamp_attributes_for_create_in_model
@@ -98,6 +98,14 @@ module ActiveRecord
timestamp_attributes_for_create + timestamp_attributes_for_update
end
+ def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update)
+ timestamp_names
+ .map { |attr| self[attr] }
+ .compact
+ .map(&:to_time)
+ .max
+ end
+
def current_time_from_proper_timezone
self.class.default_timezone == :utc ? Time.now.utc : Time.now
end
@@ -106,7 +114,7 @@ module ActiveRecord
def clear_timestamp_attributes
all_timestamp_attributes_in_model.each do |attribute_name|
self[attribute_name] = nil
- changed_attributes.delete(attribute_name)
+ clear_attribute_changes([attribute_name])
end
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 4a608e4f7b..de701edca0 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -1,16 +1,27 @@
-require 'thread'
-
module ActiveRecord
# See ActiveRecord::Transactions::ClassMethods for documentation.
module Transactions
extend ActiveSupport::Concern
+ #:nodoc:
ACTIONS = [:create, :destroy, :update]
-
- class TransactionError < ActiveRecordError # :nodoc:
- end
+ #:nodoc:
+ CALLBACK_WARN_MESSAGE = "Currently, Active Record suppresses errors raised " \
+ "within `after_rollback`/`after_commit` callbacks and only print them to " \
+ "the logs. In the next version, these errors will no longer be suppressed. " \
+ "Instead, the errors will propagate normally just like in other Active " \
+ "Record callbacks.\n" \
+ "\n" \
+ "You can opt into the new behavior and remove this warning by setting:\n" \
+ "\n" \
+ " config.active_record.raise_in_transactional_callbacks = true\n\n"
included do
- define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
+ define_callbacks :commit, :rollback,
+ terminator: ->(_, result) { result == false },
+ scope: [:kind, :name]
+
+ mattr_accessor :raise_in_transactional_callbacks, instance_writer: false
+ self.raise_in_transactional_callbacks = false
end
# = Active Record Transactions
@@ -160,7 +171,7 @@ module ActiveRecord
# end
# end
#
- # only "Kotori" is created. (This works on MySQL and PostgreSQL, but not on SQLite3.)
+ # only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it.
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that we're aware of that supports true nested
@@ -218,15 +229,17 @@ module ActiveRecord
# after_commit :do_bar, on: :update
# after_commit :do_baz, on: :destroy
#
- # Also, to have the callback fired on create and update, but not on destroy:
- #
- # after_commit :do_zoo, if: :persisted?
+ # after_commit :do_foo_bar, on: [:create, :update]
+ # after_commit :do_bar_baz, on: [:update, :destroy]
#
# Note that transactional fixtures do not play well with this feature. Please
# use the +test_after_commit+ gem to have these hooks fired in tests.
def after_commit(*args, &block)
set_options_for_callbacks!(args)
set_callback(:commit, :after, *args, &block)
+ unless ActiveRecord::Base.raise_in_transactional_callbacks
+ ActiveSupport::Deprecation.warn(CALLBACK_WARN_MESSAGE)
+ end
end
# This callback is called after a create, update, or destroy are rolled back.
@@ -235,6 +248,9 @@ module ActiveRecord
def after_rollback(*args, &block)
set_options_for_callbacks!(args)
set_callback(:rollback, :after, *args, &block)
+ unless ActiveRecord::Base.raise_in_transactional_callbacks
+ ActiveSupport::Deprecation.warn(CALLBACK_WARN_MESSAGE)
+ end
end
private
@@ -242,15 +258,16 @@ module ActiveRecord
def set_options_for_callbacks!(args)
options = args.last
if options.is_a?(Hash) && options[:on]
- assert_valid_transaction_action(options[:on])
+ fire_on = Array(options[:on])
+ assert_valid_transaction_action(fire_on)
options[:if] = Array(options[:if])
- options[:if] << "transaction_include_action?(:#{options[:on]})"
+ options[:if] << "transaction_include_any_action?(#{fire_on})"
end
end
- def assert_valid_transaction_action(action)
- unless ACTIONS.include?(action.to_sym)
- raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}"
+ def assert_valid_transaction_action(actions)
+ if (actions - ACTIONS).any?
+ raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS}"
end
end
end
@@ -274,6 +291,10 @@ module ActiveRecord
with_transaction_returning_status { super }
end
+ def touch(*) #:nodoc:
+ with_transaction_returning_status { super }
+ end
+
# Reset id and @new_record if the transaction rolls back.
def rollback_active_record_state!
remember_transaction_record_state
@@ -285,22 +306,26 @@ module ActiveRecord
clear_transaction_record_state
end
- # Call the after_commit callbacks
- def committed! #:nodoc:
- run_callbacks :commit
+ # Call the +after_commit+ callbacks.
+ #
+ # Ensure that it is not called if the object was never persisted (failed create),
+ # but call it after the commit of a destroyed object.
+ def committed!(should_run_callbacks = true) #:nodoc:
+ _run_commit_callbacks if should_run_callbacks && destroyed? || persisted?
ensure
- clear_transaction_record_state
+ force_clear_transaction_record_state
end
- # Call the after rollback callbacks. The restore_state argument indicates if the record
+ # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.
- def rolledback!(force_restore_state = false) #:nodoc:
- run_callbacks :rollback
+ def rolledback!(force_restore_state = false, should_run_callbacks = true) #:nodoc:
+ _run_rollback_callbacks if should_run_callbacks
ensure
restore_transaction_record_state(force_restore_state)
+ clear_transaction_record_state
end
- # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks
+ # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks
# can be called.
def add_to_transaction
if self.class.connection.add_transaction_record(self)
@@ -321,7 +346,7 @@ module ActiveRecord
begin
status = yield
rescue ActiveRecord::Rollback
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
+ clear_transaction_record_state
status = nil
end
@@ -334,37 +359,36 @@ module ActiveRecord
# Save the new record state and id of a record so it can be restored later if a transaction fails.
def remember_transaction_record_state #:nodoc:
- @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
- @_start_transaction_state[:new_record] = @new_record
- @_start_transaction_state[:destroyed] = @destroyed
+ @_start_transaction_state[:id] = id
+ @_start_transaction_state.reverse_merge!(
+ new_record: @new_record,
+ destroyed: @destroyed,
+ frozen?: frozen?,
+ )
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
- @_start_transaction_state[:frozen?] = @attributes.frozen?
end
# Clear the new record state and id of a record.
def clear_transaction_record_state #:nodoc:
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- @_start_transaction_state.clear if @_start_transaction_state[:level] < 1
+ force_clear_transaction_record_state if @_start_transaction_state[:level] < 1
+ end
+
+ # Force to clear the transaction record state.
+ def force_clear_transaction_record_state #:nodoc:
+ @_start_transaction_state.clear
end
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
def restore_transaction_record_state(force = false) #:nodoc:
unless @_start_transaction_state.empty?
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- if @_start_transaction_state[:level] < 1 || force
+ transaction_level = (@_start_transaction_state[:level] || 0) - 1
+ if transaction_level < 1 || force
restore_state = @_start_transaction_state
- was_frozen = restore_state[:frozen?]
- @attributes = @attributes.dup if @attributes.frozen?
+ thaw unless restore_state[:frozen?]
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
- if restore_state.has_key?(:id)
- self.id = restore_state[:id]
- else
- @attributes.delete(self.class.primary_key)
- @attributes_cache.delete(self.class.primary_key)
- end
- @attributes.freeze if was_frozen
- @_start_transaction_state.clear
+ write_attribute(self.class.primary_key, restore_state[:id])
end
end
end
@@ -375,14 +399,16 @@ module ActiveRecord
end
# Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
- def transaction_include_action?(action) #:nodoc:
- case action
- when :create
- transaction_record_state(:new_record)
- when :destroy
- destroyed?
- when :update
- !(transaction_record_state(:new_record) || destroyed?)
+ def transaction_include_any_action?(actions) #:nodoc:
+ actions.any? do |action|
+ case action
+ when :create
+ transaction_record_state(:new_record)
+ when :destroy
+ destroyed?
+ when :update
+ !(transaction_record_state(:new_record) || destroyed?)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
new file mode 100644
index 0000000000..250e8d5b23
--- /dev/null
+++ b/activerecord/lib/active_record/type.rb
@@ -0,0 +1,23 @@
+require 'active_record/type/decorator'
+require 'active_record/type/mutable'
+require 'active_record/type/numeric'
+require 'active_record/type/time_value'
+require 'active_record/type/value'
+
+require 'active_record/type/big_integer'
+require 'active_record/type/binary'
+require 'active_record/type/boolean'
+require 'active_record/type/date'
+require 'active_record/type/date_time'
+require 'active_record/type/decimal'
+require 'active_record/type/decimal_without_scale'
+require 'active_record/type/float'
+require 'active_record/type/integer'
+require 'active_record/type/serialized'
+require 'active_record/type/string'
+require 'active_record/type/text'
+require 'active_record/type/time'
+require 'active_record/type/unsigned_integer'
+
+require 'active_record/type/type_map'
+require 'active_record/type/hash_lookup_type_map'
diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activerecord/lib/active_record/type/big_integer.rb
new file mode 100644
index 0000000000..0c72d8914f
--- /dev/null
+++ b/activerecord/lib/active_record/type/big_integer.rb
@@ -0,0 +1,13 @@
+require 'active_record/type/integer'
+
+module ActiveRecord
+ module Type
+ class BigInteger < Integer # :nodoc:
+ private
+
+ def max_value
+ ::Float::INFINITY
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb
new file mode 100644
index 0000000000..005a48ef0d
--- /dev/null
+++ b/activerecord/lib/active_record/type/binary.rb
@@ -0,0 +1,50 @@
+module ActiveRecord
+ module Type
+ class Binary < Value # :nodoc:
+ def type
+ :binary
+ end
+
+ def binary?
+ true
+ end
+
+ def type_cast(value)
+ if value.is_a?(Data)
+ value.to_s
+ else
+ super
+ end
+ end
+
+ def type_cast_for_database(value)
+ return if value.nil?
+ Data.new(super)
+ end
+
+ def changed_in_place?(raw_old_value, value)
+ old_value = type_cast_from_database(raw_old_value)
+ old_value != value
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value.to_s
+ end
+
+ def to_s
+ @value
+ end
+ alias_method :to_str, :to_s
+
+ def hex
+ @value.unpack('H*')[0]
+ end
+
+ def ==(other)
+ other == to_s || super
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb
new file mode 100644
index 0000000000..978d16d524
--- /dev/null
+++ b/activerecord/lib/active_record/type/boolean.rb
@@ -0,0 +1,30 @@
+module ActiveRecord
+ module Type
+ class Boolean < Value # :nodoc:
+ def type
+ :boolean
+ end
+
+ private
+
+ def cast_value(value)
+ if value == ''
+ nil
+ elsif ConnectionAdapters::Column::TRUE_VALUES.include?(value)
+ true
+ else
+ if !ConnectionAdapters::Column::FALSE_VALUES.include?(value)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You attempted to assign a value which is not explicitly `true` or `false`
+ to a boolean column. Currently this value casts to `false`. This will
+ change to match Ruby's semantics, and will cast to `true` in Rails 5.
+ If you would like to maintain the current behavior, you should
+ explicitly handle the values you would like cast to `false`.
+ MSG
+ end
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb
new file mode 100644
index 0000000000..d90a6069b7
--- /dev/null
+++ b/activerecord/lib/active_record/type/date.rb
@@ -0,0 +1,46 @@
+module ActiveRecord
+ module Type
+ class Date < Value # :nodoc:
+ def type
+ :date
+ end
+
+ def klass
+ ::Date
+ end
+
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ private
+
+ def cast_value(value)
+ if value.is_a?(::String)
+ return if value.empty?
+ fast_string_to_date(value) || fallback_string_to_date(value)
+ elsif value.respond_to?(:to_date)
+ value.to_date
+ else
+ value
+ end
+ end
+
+ def fast_string_to_date(string)
+ if string =~ ConnectionAdapters::Column::Format::ISO_DATE
+ new_date $1.to_i, $2.to_i, $3.to_i
+ end
+ end
+
+ def fallback_string_to_date(string)
+ new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
+ end
+
+ def new_date(year, mon, mday)
+ if year && year != 0
+ ::Date.new(year, mon, mday) rescue nil
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
new file mode 100644
index 0000000000..5f19608a33
--- /dev/null
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module Type
+ class DateTime < Value # :nodoc:
+ include TimeValue
+
+ def type
+ :datetime
+ end
+
+ def type_cast_for_database(value)
+ zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
+
+ if value.acts_like?(:time)
+ value.send(zone_conversion_method)
+ else
+ super
+ end
+ end
+
+ private
+
+ def cast_value(string)
+ return string unless string.is_a?(::String)
+ return if string.empty?
+
+ fast_string_to_time(string) || fallback_string_to_time(string)
+ end
+
+ # '0.123456' -> 123456
+ # '1.123456' -> 123456
+ def microseconds(time)
+ time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
+ end
+
+ def fallback_string_to_time(string)
+ time_hash = ::Date._parse(string)
+ time_hash[:sec_fraction] = microseconds(time_hash)
+
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb
new file mode 100644
index 0000000000..7b2bee2c42
--- /dev/null
+++ b/activerecord/lib/active_record/type/decimal.rb
@@ -0,0 +1,48 @@
+module ActiveRecord
+ module Type
+ class Decimal < Value # :nodoc:
+ include Numeric
+
+ def type
+ :decimal
+ end
+
+ def type_cast_for_schema(value)
+ value.to_s
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when ::Float
+ convert_float_to_big_decimal(value)
+ when ::Numeric, ::String
+ BigDecimal(value, precision.to_i)
+ else
+ if value.respond_to?(:to_d)
+ value.to_d
+ else
+ cast_value(value.to_s)
+ end
+ end
+ end
+
+ def convert_float_to_big_decimal(value)
+ if precision
+ BigDecimal(value, float_precision)
+ else
+ value.to_d
+ end
+ end
+
+ def float_precision
+ if precision.to_i > ::Float::DIG + 1
+ ::Float::DIG + 1
+ else
+ precision.to_i
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb
new file mode 100644
index 0000000000..ff5559e300
--- /dev/null
+++ b/activerecord/lib/active_record/type/decimal_without_scale.rb
@@ -0,0 +1,11 @@
+require 'active_record/type/big_integer'
+
+module ActiveRecord
+ module Type
+ class DecimalWithoutScale < BigInteger # :nodoc:
+ def type
+ :decimal
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/decorator.rb b/activerecord/lib/active_record/type/decorator.rb
new file mode 100644
index 0000000000..9fce38ea44
--- /dev/null
+++ b/activerecord/lib/active_record/type/decorator.rb
@@ -0,0 +1,14 @@
+module ActiveRecord
+ module Type
+ module Decorator # :nodoc:
+ def init_with(coder)
+ @subtype = coder['subtype']
+ __setobj__(@subtype)
+ end
+
+ def encode_with(coder)
+ coder['subtype'] = __getobj__
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb
new file mode 100644
index 0000000000..42eb44b9a9
--- /dev/null
+++ b/activerecord/lib/active_record/type/float.rb
@@ -0,0 +1,19 @@
+module ActiveRecord
+ module Type
+ class Float < Value # :nodoc:
+ include Numeric
+
+ def type
+ :float
+ end
+
+ alias type_cast_for_database type_cast
+
+ private
+
+ def cast_value(value)
+ value.to_f
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
new file mode 100644
index 0000000000..82d9327fc0
--- /dev/null
+++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ module Type
+ class HashLookupTypeMap < TypeMap # :nodoc:
+ delegate :key?, to: :@mapping
+
+ def alias_type(type, alias_type)
+ register_type(type) { |_, *args| lookup(alias_type, *args) }
+ end
+
+ private
+
+ def perform_fetch(type, *args, &block)
+ @mapping.fetch(type, block).call(type, *args)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb
new file mode 100644
index 0000000000..fc260a081a
--- /dev/null
+++ b/activerecord/lib/active_record/type/integer.rb
@@ -0,0 +1,55 @@
+module ActiveRecord
+ module Type
+ class Integer < Value # :nodoc:
+ include Numeric
+
+ def initialize(*)
+ super
+ @range = min_value...max_value
+ end
+
+ def type
+ :integer
+ end
+
+ alias type_cast_for_database type_cast
+
+ def type_cast_from_database(value)
+ return if value.nil?
+ value.to_i
+ end
+
+ protected
+
+ attr_reader :range
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then 1
+ when false then 0
+ else
+ result = value.to_i rescue nil
+ ensure_in_range(result) if result
+ result
+ end
+ end
+
+ def ensure_in_range(value)
+ unless range.cover?(value)
+ raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || 4}"
+ end
+ end
+
+ def max_value
+ limit = self.limit || 4
+ 1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign
+ end
+
+ def min_value
+ -max_value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb
new file mode 100644
index 0000000000..066617ea59
--- /dev/null
+++ b/activerecord/lib/active_record/type/mutable.rb
@@ -0,0 +1,16 @@
+module ActiveRecord
+ module Type
+ module Mutable # :nodoc:
+ def type_cast_from_user(value)
+ type_cast_from_database(type_cast_for_database(value))
+ end
+
+ # +raw_old_value+ will be the `_before_type_cast` version of the
+ # value (likely a string). +new_value+ will be the current, type
+ # cast value.
+ def changed_in_place?(raw_old_value, new_value)
+ raw_old_value != type_cast_for_database(new_value)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb
new file mode 100644
index 0000000000..674f996f38
--- /dev/null
+++ b/activerecord/lib/active_record/type/numeric.rb
@@ -0,0 +1,36 @@
+module ActiveRecord
+ module Type
+ module Numeric # :nodoc:
+ def number?
+ true
+ end
+
+ def type_cast(value)
+ value = case value
+ when true then 1
+ when false then 0
+ when ::String then value.presence
+ else value
+ end
+ super(value)
+ end
+
+ def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
+ super || number_to_non_number?(old_value, new_value_before_type_cast)
+ end
+
+ private
+
+ def number_to_non_number?(old_value, new_value_before_type_cast)
+ old_value != nil && non_numeric_string?(new_value_before_type_cast)
+ end
+
+ def non_numeric_string?(value)
+ # 'wibble'.to_i will give zero, we want to make sure
+ # that we aren't marking int zero to string zero as
+ # changed.
+ value.to_s !~ /\A-?\d+\.?\d*\z/
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
new file mode 100644
index 0000000000..3cac03464e
--- /dev/null
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -0,0 +1,56 @@
+module ActiveRecord
+ module Type
+ class Serialized < DelegateClass(Type::Value) # :nodoc:
+ include Mutable
+ include Decorator
+
+ attr_reader :subtype, :coder
+
+ def initialize(subtype, coder)
+ @subtype = subtype
+ @coder = coder
+ super(subtype)
+ end
+
+ def type_cast_from_database(value)
+ if default_value?(value)
+ value
+ else
+ coder.load(super)
+ end
+ end
+
+ def type_cast_for_database(value)
+ return if value.nil?
+ unless default_value?(value)
+ super coder.dump(value)
+ end
+ end
+
+ def changed_in_place?(raw_old_value, value)
+ return false if value.nil?
+ subtype.changed_in_place?(raw_old_value, type_cast_for_database(value))
+ end
+
+ def accessor
+ ActiveRecord::Store::IndifferentHashAccessor
+ end
+
+ def init_with(coder)
+ @coder = coder['coder']
+ super
+ end
+
+ def encode_with(coder)
+ coder['coder'] = @coder
+ super
+ end
+
+ private
+
+ def default_value?(value)
+ value == coder.load(nil)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb
new file mode 100644
index 0000000000..cf95e25be0
--- /dev/null
+++ b/activerecord/lib/active_record/type/string.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ module Type
+ class String < Value # :nodoc:
+ def type
+ :string
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ if new_value.is_a?(::String)
+ raw_old_value != new_value
+ end
+ end
+
+ def type_cast_for_database(value)
+ case value
+ when ::Numeric, ActiveSupport::Duration then value.to_s
+ when ::String then ::String.new(value)
+ when true then "t"
+ when false then "f"
+ else super
+ end
+ end
+
+ def text?
+ true
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then "t"
+ when false then "f"
+ # String.new is slightly faster than dup
+ else ::String.new(value.to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb
new file mode 100644
index 0000000000..26f980f060
--- /dev/null
+++ b/activerecord/lib/active_record/type/text.rb
@@ -0,0 +1,11 @@
+require 'active_record/type/string'
+
+module ActiveRecord
+ module Type
+ class Text < String # :nodoc:
+ def type
+ :text
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb
new file mode 100644
index 0000000000..41f7d97f0c
--- /dev/null
+++ b/activerecord/lib/active_record/type/time.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+ module Type
+ class Time < Value # :nodoc:
+ include TimeValue
+
+ def type
+ :time
+ end
+
+ private
+
+ def cast_value(value)
+ return value unless value.is_a?(::String)
+ return if value.empty?
+
+ dummy_time_value = "2000-01-01 #{value}"
+
+ fast_string_to_time(dummy_time_value) || begin
+ time_hash = ::Date._parse(dummy_time_value)
+ return if time_hash[:hour].nil?
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb
new file mode 100644
index 0000000000..d611d72dd4
--- /dev/null
+++ b/activerecord/lib/active_record/type/time_value.rb
@@ -0,0 +1,38 @@
+module ActiveRecord
+ module Type
+ module TimeValue # :nodoc:
+ def klass
+ ::Time
+ end
+
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ private
+
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
+ # Treat 0000-00-00 00:00:00 as nil.
+ return if year.nil? || (year == 0 && mon == 0 && mday == 0)
+
+ if offset
+ time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
+ return unless time
+
+ time -= offset
+ Base.default_timezone == :utc ? time : time.getlocal
+ else
+ ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
+ end
+
+ # Doesn't handle time zones.
+ def fast_string_to_time(string)
+ if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME
+ microsec = ($7.to_r * 1_000_000).to_i
+ new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
new file mode 100644
index 0000000000..09f5ba6b74
--- /dev/null
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -0,0 +1,64 @@
+require 'thread_safe'
+
+module ActiveRecord
+ module Type
+ class TypeMap # :nodoc:
+ def initialize
+ @mapping = {}
+ @cache = ThreadSafe::Cache.new do |h, key|
+ h.fetch_or_store(key, ThreadSafe::Cache.new)
+ end
+ end
+
+ def lookup(lookup_key, *args)
+ fetch(lookup_key, *args) { default_value }
+ end
+
+ def fetch(lookup_key, *args, &block)
+ @cache[lookup_key].fetch_or_store(args) do
+ perform_fetch(lookup_key, *args, &block)
+ end
+ end
+
+ def register_type(key, value = nil, &block)
+ raise ::ArgumentError unless value || block
+ @cache.clear
+
+ if block
+ @mapping[key] = block
+ else
+ @mapping[key] = proc { value }
+ end
+ end
+
+ def alias_type(key, target_key)
+ register_type(key) do |sql_type, *args|
+ metadata = sql_type[/\(.*\)/, 0]
+ lookup("#{target_key}#{metadata}", *args)
+ end
+ end
+
+ def clear
+ @mapping.clear
+ end
+
+ private
+
+ def perform_fetch(lookup_key, *args)
+ matching_pair = @mapping.reverse_each.detect do |key, _|
+ key === lookup_key
+ end
+
+ if matching_pair
+ matching_pair.last.call(lookup_key, *args)
+ else
+ yield lookup_key, *args
+ end
+ end
+
+ def default_value
+ @default_value ||= Value.new
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb
new file mode 100644
index 0000000000..ed3e527483
--- /dev/null
+++ b/activerecord/lib/active_record/type/unsigned_integer.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module Type
+ class UnsignedInteger < Integer # :nodoc:
+ private
+
+ def max_value
+ super * 2
+ end
+
+ def min_value
+ 0
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
new file mode 100644
index 0000000000..60ae47db3d
--- /dev/null
+++ b/activerecord/lib/active_record/type/value.rb
@@ -0,0 +1,105 @@
+module ActiveRecord
+ module Type
+ class Value # :nodoc:
+ attr_reader :precision, :scale, :limit
+
+ # Valid options are +precision+, +scale+, and +limit+. They are only
+ # used when dumping schema.
+ def initialize(options = {})
+ options.assert_valid_keys(:precision, :scale, :limit)
+ @precision = options[:precision]
+ @scale = options[:scale]
+ @limit = options[:limit]
+ end
+
+ # The simplified type that this object represents. Returns a symbol such
+ # as +:string+ or +:integer+
+ def type; end
+
+ # Type casts a string from the database into the appropriate ruby type.
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +cast_value+ instead.
+ def type_cast_from_database(value)
+ type_cast(value)
+ end
+
+ # Type casts a value from user input (e.g. from a setter). This value may
+ # be a string from the form builder, or an already type cast value
+ # provided manually to a setter.
+ #
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +type_cast+ or +cast_value+
+ # instead.
+ def type_cast_from_user(value)
+ type_cast(value)
+ end
+
+ # Cast a value from the ruby type to a type that the database knows how
+ # to understand. The returned value from this method should be a
+ # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
+ # +nil+
+ def type_cast_for_database(value)
+ value
+ end
+
+ # Type cast a value for schema dumping. This method is private, as we are
+ # hoping to remove it entirely.
+ def type_cast_for_schema(value) # :nodoc:
+ value.inspect
+ end
+
+ # These predicates are not documented, as I need to look further into
+ # their use, and see if they can be removed entirely.
+ def text? # :nodoc:
+ false
+ end
+
+ def number? # :nodoc:
+ false
+ end
+
+ def binary? # :nodoc:
+ false
+ end
+
+ def klass # :nodoc:
+ end
+
+ # Determines whether a value has changed for dirty checking. +old_value+
+ # and +new_value+ will always be type-cast. Types should not need to
+ # override this method.
+ def changed?(old_value, new_value, _new_value_before_type_cast)
+ old_value != new_value
+ end
+
+ # Determines whether the mutable value has been modified since it was
+ # read. Returns +false+ by default. This method should not be overridden
+ # directly. Types which return a mutable value should include
+ # +Type::Mutable+, which will define this method.
+ def changed_in_place?(*)
+ false
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ precision == other.precision &&
+ scale == other.scale &&
+ limit == other.limit
+ end
+
+ private
+
+ def type_cast(value)
+ cast_value(value) unless value.nil?
+ end
+
+ # Convenience method for types which do not need separate type casting
+ # behavior for user and database inputs. Called by
+ # +type_cast_from_database+ and +type_cast_from_user+ for all values
+ # except +nil+.
+ def cast_value(value) # :doc:
+ value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb
new file mode 100644
index 0000000000..63ba10c289
--- /dev/null
+++ b/activerecord/lib/active_record/type_caster.rb
@@ -0,0 +1,7 @@
+require 'active_record/type_caster/map'
+require 'active_record/type_caster/connection'
+
+module ActiveRecord
+ module TypeCaster
+ end
+end
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
new file mode 100644
index 0000000000..9e4a130b40
--- /dev/null
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -0,0 +1,34 @@
+module ActiveRecord
+ module TypeCaster
+ class Connection
+ def initialize(connection, table_name)
+ @connection = connection
+ @table_name = table_name
+ end
+
+ def type_cast_for_database(attribute_name, value)
+ return value if value.is_a?(Arel::Nodes::BindParam)
+ type = type_for(attribute_name)
+ type.type_cast_for_database(value)
+ end
+
+ protected
+
+ attr_reader :connection, :table_name
+
+ private
+
+ def type_for(attribute_name)
+ if connection.schema_cache.table_exists?(table_name)
+ column_for(attribute_name).cast_type
+ else
+ Type::Value.new
+ end
+ end
+
+ def column_for(attribute_name)
+ connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb
new file mode 100644
index 0000000000..03c9e8ff83
--- /dev/null
+++ b/activerecord/lib/active_record/type_caster/map.rb
@@ -0,0 +1,19 @@
+module ActiveRecord
+ module TypeCaster
+ class Map
+ def initialize(types)
+ @types = types
+ end
+
+ def type_cast_for_database(attr_name, value)
+ return value if value.is_a?(Arel::Nodes::BindParam)
+ type = types.type_for_attribute(attr_name.to_s)
+ type.type_cast_for_database(value)
+ end
+
+ protected
+
+ attr_reader :types
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 3706885881..f27adc9c40 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -5,13 +5,14 @@ module ActiveRecord
# +record+ method to retrieve the record which did not validate.
#
# begin
- # complex_operation_that_calls_save!_internally
+ # complex_operation_that_internally_calls_save!
# rescue ActiveRecord::RecordInvalid => invalid
# puts invalid.record.errors
# end
class RecordInvalid < ActiveRecordError
- attr_reader :record # :nodoc:
- def initialize(record) # :nodoc:
+ attr_reader :record
+
+ def initialize(record)
@record = record
errors = @record.errors.full_messages.join(", ")
super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
@@ -29,21 +30,6 @@ module ActiveRecord
extend ActiveSupport::Concern
include ActiveModel::Validations
- module ClassMethods
- # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+
- # so an exception is raised if the record is invalid.
- def create!(attributes = nil, &block)
- if attributes.is_a?(Array)
- attributes.collect { |attr| create!(attr, &block) }
- else
- object = new(attributes)
- yield(object) if block_given?
- object.save!
- object
- end
- end
- end
-
# The validation process on save can be skipped by passing <tt>validate: false</tt>.
# The regular Base#save method is replaced with this when the validations
# module is mixed in, which it is by default.
@@ -54,12 +40,14 @@ module ActiveRecord
# Attempts to save the record just like Base#save but will raise a +RecordInvalid+
# exception instead of returning +false+ if the record is not valid.
def save!(options={})
- perform_validations(options) ? super : raise(RecordInvalid.new(self))
+ perform_validations(options) ? super : raise_record_invalid
end
# Runs all the validations within the specified context. Returns +true+ if
# no errors are found, +false+ otherwise.
#
+ # Aliased as validate.
+ #
# If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
# <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
#
@@ -71,11 +59,28 @@ module ActiveRecord
errors.empty? && output
end
+ alias_method :validate, :valid?
+
+ # Runs all the validations within the specified context. Returns +true+ if
+ # no errors are found, raises +RecordInvalid+ otherwise.
+ #
+ # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
+ # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
+ #
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
+ # some <tt>:on</tt> option will only run in the specified context.
+ def validate!(context = nil)
+ valid?(context) || raise_record_invalid
+ end
+
protected
+ def raise_record_invalid
+ raise(RecordInvalid.new(self))
+ end
+
def perform_validations(options={}) # :nodoc:
- perform_validation = options[:validate] != false
- perform_validation ? valid?(options[:context]) : true
+ options[:validate] == false || valid?(options[:context])
end
end
end
@@ -83,3 +88,4 @@ end
require "active_record/validations/associated"
require "active_record/validations/uniqueness"
require "active_record/validations/presence"
+require "active_record/validations/length"
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 7f1972ccf9..47ccef31a5 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -2,15 +2,15 @@ module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator #:nodoc:
def validate_each(record, attribute, value)
- if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?(record.validation_context) }.any?
+ if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
record.errors.add(attribute, :invalid, options.merge(:value => value))
end
end
end
module ClassMethods
- # Validates whether the associated object or objects are all valid
- # themselves. Works with any kind of association.
+ # Validates whether the associated object or objects are all valid.
+ # Works with any kind of association.
#
# class Book < ActiveRecord::Base
# has_many :pages
@@ -29,9 +29,11 @@ module ActiveRecord
# Configuration options:
#
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
- # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
- # validation contexts by default (+nil+), other options are <tt>:create</tt>
- # and <tt>:update</tt>.
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # Runs in all validation contexts by default (nil). You can pass a symbol
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
+ # <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb
new file mode 100644
index 0000000000..ef5a6cbbe7
--- /dev/null
+++ b/activerecord/lib/active_record/validations/length.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module Validations
+ class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc:
+ def validate_each(record, attribute, association_or_value)
+ if association_or_value.respond_to?(:loaded?) && association_or_value.loaded?
+ association_or_value = association_or_value.target.reject(&:marked_for_destruction?)
+ end
+ super
+ end
+ end
+
+ module ClassMethods
+ # See <tt>ActiveModel::Validation::LengthValidator</tt> for more information.
+ def validates_length_of(*attr_names)
+ validates_with LengthValidator, _merge_attributes(attr_names)
+ end
+
+ alias_method :validates_size_of, :validates_length_of
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 6b14c39686..61b30749d9 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -4,11 +4,11 @@ module ActiveRecord
def validate(record)
super
attributes.each do |attribute|
- next unless record.class.reflect_on_association(attribute)
- associated_records = Array(record.send(attribute))
+ next unless record.class._reflect_on_association(attribute)
+ associated_records = Array.wrap(record.send(attribute))
# Superclass validates presence. Ensure present records aren't about to be destroyed.
- if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? }
+ if associated_records.present? && associated_records.all?(&:marked_for_destruction?)
record.errors.add(attribute, :blank, options)
end
end
@@ -44,9 +44,11 @@ module ActiveRecord
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "can't be blank").
- # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
- # validation contexts by default (+nil+), other options are <tt>:create</tt>
- # and <tt>:update</tt>.
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # Runs in all validation contexts by default (nil). You can pass a symbol
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
+ # <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if
# the validation should occur (e.g. <tt>if: :allow_validation</tt>, or
# <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 1427189851..f52f91e89c 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -2,24 +2,23 @@ module ActiveRecord
module Validations
class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
def initialize(options)
+ if options[:conditions] && !options[:conditions].respond_to?(:call)
+ raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
+ "Pass a callable instead: `conditions: -> { where(approved: true) }`"
+ end
super({ case_sensitive: true }.merge!(options))
- end
-
- # Unfortunately, we have to tie Uniqueness validators to a class.
- def setup(klass)
- @klass = klass
+ @klass = options[:class]
end
def validate_each(record, attribute, value)
finder_class = find_finder_class_for(record)
table = finder_class.arel_table
- value = deserialize_attribute(record, attribute, value)
+ value = map_enum_attribute(finder_class, attribute, value)
relation = build_relation(finder_class, table, attribute, value)
- relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted?
+ relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted?
relation = scope_relation(record, table, relation)
- relation = finder_class.unscoped.where(relation)
- relation.merge!(options[:conditions]) if options[:conditions]
+ relation = relation.merge(options[:conditions]) if options[:conditions]
if relation.exists?
error_options = options.except(:case_sensitive, :scope, :conditions)
@@ -30,7 +29,6 @@ module ActiveRecord
end
protected
-
# The check for an existing value should be run from a class that
# isn't abstract. This means working down from the current class
# (self), to the first non-abstract class. Since classes don't know
@@ -47,41 +45,54 @@ module ActiveRecord
end
def build_relation(klass, table, attribute, value) #:nodoc:
- if reflection = klass.reflect_on_association(attribute)
+ if reflection = klass._reflect_on_association(attribute)
attribute = reflection.foreign_key
- value = value.attributes[reflection.primary_key_column.name]
+ value = value.attributes[reflection.klass.primary_key] unless value.nil?
+ end
+
+ attribute_name = attribute.to_s
+
+ # the attribute may be an aliased attribute
+ if klass.attribute_aliases[attribute_name]
+ attribute = klass.attribute_aliases[attribute_name]
+ attribute_name = attribute.to_s
+ end
+
+ column = klass.columns_hash[attribute_name]
+ value = klass.type_for_attribute(attribute_name).type_cast_for_database(value)
+ value = klass.connection.type_cast(value)
+ if value.is_a?(String) && column.limit
+ value = value.to_s[0, column.limit]
end
- column = klass.columns_hash[attribute.to_s]
- value = klass.connection.type_cast(value, column)
- value = value.to_s[0, column.limit] if value && column.limit && column.text?
+ value = Arel::Nodes::Quoted.new(value)
- if !options[:case_sensitive] && value && column.text?
+ comparison = if !options[:case_sensitive] && value && column.text?
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
- value = klass.connection.case_sensitive_modifier(value) unless value.nil?
- table[attribute].eq(value)
+ klass.connection.case_sensitive_comparison(table, attribute, column, value)
end
+ klass.unscoped.where(comparison)
end
def scope_relation(record, table, relation)
Array(options[:scope]).each do |scope_item|
- if reflection = record.class.reflect_on_association(scope_item)
+ if reflection = record.class._reflect_on_association(scope_item)
scope_value = record.send(reflection.foreign_key)
scope_item = reflection.foreign_key
else
- scope_value = record.read_attribute(scope_item)
+ scope_value = record._read_attribute(scope_item)
end
- relation = relation.and(table[scope_item].eq(scope_value))
+ relation = relation.where(scope_item => scope_value)
end
relation
end
- def deserialize_attribute(record, attribute, value)
- coder = record.class.serialized_attributes[attribute.to_s]
- value = coder.dump value if value && coder
+ def map_enum_attribute(klass, attribute, value)
+ mapping = klass.defined_enums[attribute.to_s]
+ value = mapping[value] if value && mapping
value
end
end
@@ -116,7 +127,7 @@ module ActiveRecord
# of the title attribute:
#
# class Article < ActiveRecord::Base
- # validates_uniqueness_of :title, conditions: where('status != ?', 'archived')
+ # validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
# end
#
# When the record is created, a check is performed to make sure that no
@@ -132,7 +143,7 @@ module ActiveRecord
# the uniqueness constraint.
# * <tt>:conditions</tt> - Specify the conditions to be included as a
# <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
- # (e.g. <tt>conditions: where('status = ?', 'active')</tt>).
+ # (e.g. <tt>conditions: -> { where(status: 'active') }</tt>).
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
# non-text columns (+true+ by default).
# * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
@@ -144,7 +155,7 @@ module ActiveRecord
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
# proc or string should return or evaluate to a +true+ or +false+ value.
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
- # determine if the validation should ot occur (e.g. <tt>unless: :skip_validation</tt>,
+ # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a +true+ or +false+
# value.
@@ -167,11 +178,11 @@ module ActiveRecord
# WHERE title = 'My Post' |
# |
# | # User 2 does the same thing and also
- # | # infers that his title is unique.
+ # | # infers that their title is unique.
# | SELECT * FROM comments
# | WHERE title = 'My Post'
# |
- # # User 1 inserts his comment. |
+ # # User 1 inserts their comment. |
# INSERT INTO comments |
# (title, content) VALUES |
# ('My Post', 'hi!') |
@@ -197,9 +208,9 @@ module ActiveRecord
# exception. You can either choose to let this error propagate (which
# will result in the default Rails exception page being shown), or you
# can catch it and restart the transaction (e.g. by telling the user
- # that the title already exists, and asking him to re-enter the title).
- # This technique is also known as optimistic concurrency control:
- # http://en.wikipedia.org/wiki/Optimistic_concurrency_control.
+ # that the title already exists, and asking them to re-enter the title).
+ # This technique is also known as
+ # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control].
#
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
# constraint errors from other types of database errors by throwing an
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index 0c35adc11d..cf76a13b44 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,10 +1,8 @@
-module ActiveRecord
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+require_relative 'gem_version'
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+module ActiveRecord
+ # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
end
end