aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/aggregations.rb6
-rw-r--r--activerecord/lib/active_record/associations.rb32
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb2
-rw-r--r--activerecord/lib/active_record/associations/association.rb27
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb55
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb5
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb21
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb53
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb29
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb64
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb113
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb34
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb11
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb25
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb4
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb14
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb82
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/autosave_association.rb11
-rw-r--r--activerecord/lib/active_record/base.rb3
-rw-r--r--activerecord/lib/active_record/callbacks.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb36
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb110
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb65
-rw-r--r--activerecord/lib/active_record/core.rb29
-rw-r--r--activerecord/lib/active_record/counter_cache.rb34
-rw-r--r--activerecord/lib/active_record/database_configurations.rb63
-rw-r--r--activerecord/lib/active_record/errors.rb3
-rw-r--r--activerecord/lib/active_record/fixtures.rb4
-rw-r--r--activerecord/lib/active_record/inheritance.rb8
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb2
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb2
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb33
-rw-r--r--activerecord/lib/active_record/migration.rb16
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb19
-rw-r--r--activerecord/lib/active_record/model_schema.rb7
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb2
-rw-r--r--activerecord/lib/active_record/no_touching.rb7
-rw-r--r--activerecord/lib/active_record/persistence.rb20
-rw-r--r--activerecord/lib/active_record/query_cache.rb15
-rw-r--r--activerecord/lib/active_record/querying.rb7
-rw-r--r--activerecord/lib/active_record/railtie.rb8
-rw-r--r--activerecord/lib/active_record/railties/databases.rake63
-rw-r--r--activerecord/lib/active_record/reflection.rb54
-rw-r--r--activerecord/lib/active_record/relation.rb106
-rw-r--r--activerecord/lib/active_record/relation/batches.rb13
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb2
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb5
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb11
-rw-r--r--activerecord/lib/active_record/relation/merger.rb16
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb11
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb6
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb31
-rw-r--r--activerecord/lib/active_record/result.rb26
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb18
-rw-r--r--activerecord/lib/active_record/scoping/named.rb2
-rw-r--r--activerecord/lib/active_record/store.rb43
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb51
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb4
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb4
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb8
-rw-r--r--activerecord/lib/active_record/test_databases.rb13
-rw-r--r--activerecord/lib/active_record/timestamp.rb8
-rw-r--r--activerecord/lib/active_record/transactions.rb56
-rw-r--r--activerecord/lib/active_record/translation.rb2
-rw-r--r--activerecord/lib/active_record/type/serialized.rb4
-rw-r--r--activerecord/lib/arel.rb40
-rw-r--r--activerecord/lib/arel/alias_predication.rb9
-rw-r--r--activerecord/lib/arel/attributes.rb22
-rw-r--r--activerecord/lib/arel/attributes/attribute.rb37
-rw-r--r--activerecord/lib/arel/collectors/bind.rb24
-rw-r--r--activerecord/lib/arel/collectors/composite.rb32
-rw-r--r--activerecord/lib/arel/collectors/plain_string.rb20
-rw-r--r--activerecord/lib/arel/collectors/sql_string.rb24
-rw-r--r--activerecord/lib/arel/collectors/substitute_binds.rb29
-rw-r--r--activerecord/lib/arel/compatibility/wheres.rb35
-rw-r--r--activerecord/lib/arel/crud.rb42
-rw-r--r--activerecord/lib/arel/delete_manager.rb25
-rw-r--r--activerecord/lib/arel/errors.rb9
-rw-r--r--activerecord/lib/arel/expressions.rb29
-rw-r--r--activerecord/lib/arel/factory_methods.rb45
-rw-r--r--activerecord/lib/arel/insert_manager.rb49
-rw-r--r--activerecord/lib/arel/math.rb45
-rw-r--r--activerecord/lib/arel/nodes.rb67
-rw-r--r--activerecord/lib/arel/nodes/and.rb32
-rw-r--r--activerecord/lib/arel/nodes/ascending.rb23
-rw-r--r--activerecord/lib/arel/nodes/binary.rb52
-rw-r--r--activerecord/lib/arel/nodes/bind_param.rb28
-rw-r--r--activerecord/lib/arel/nodes/case.rb55
-rw-r--r--activerecord/lib/arel/nodes/casted.rb46
-rw-r--r--activerecord/lib/arel/nodes/count.rb12
-rw-r--r--activerecord/lib/arel/nodes/delete_statement.rb38
-rw-r--r--activerecord/lib/arel/nodes/descending.rb23
-rw-r--r--activerecord/lib/arel/nodes/equality.rb11
-rw-r--r--activerecord/lib/arel/nodes/extract.rb24
-rw-r--r--activerecord/lib/arel/nodes/false.rb16
-rw-r--r--activerecord/lib/arel/nodes/full_outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/function.rb44
-rw-r--r--activerecord/lib/arel/nodes/grouping.rb8
-rw-r--r--activerecord/lib/arel/nodes/in.rb8
-rw-r--r--activerecord/lib/arel/nodes/infix_operation.rb80
-rw-r--r--activerecord/lib/arel/nodes/inner_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/insert_statement.rb37
-rw-r--r--activerecord/lib/arel/nodes/join_source.rb20
-rw-r--r--activerecord/lib/arel/nodes/matches.rb18
-rw-r--r--activerecord/lib/arel/nodes/named_function.rb23
-rw-r--r--activerecord/lib/arel/nodes/node.rb60
-rw-r--r--activerecord/lib/arel/nodes/node_expression.rb13
-rw-r--r--activerecord/lib/arel/nodes/outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/over.rb15
-rw-r--r--activerecord/lib/arel/nodes/regexp.rb16
-rw-r--r--activerecord/lib/arel/nodes/right_outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/select_core.rb65
-rw-r--r--activerecord/lib/arel/nodes/select_statement.rb41
-rw-r--r--activerecord/lib/arel/nodes/sql_literal.rb16
-rw-r--r--activerecord/lib/arel/nodes/string_join.rb11
-rw-r--r--activerecord/lib/arel/nodes/table_alias.rb27
-rw-r--r--activerecord/lib/arel/nodes/terminal.rb16
-rw-r--r--activerecord/lib/arel/nodes/true.rb16
-rw-r--r--activerecord/lib/arel/nodes/unary.rb45
-rw-r--r--activerecord/lib/arel/nodes/unary_operation.rb20
-rw-r--r--activerecord/lib/arel/nodes/unqualified_column.rb22
-rw-r--r--activerecord/lib/arel/nodes/update_statement.rb40
-rw-r--r--activerecord/lib/arel/nodes/values.rb16
-rw-r--r--activerecord/lib/arel/nodes/values_list.rb24
-rw-r--r--activerecord/lib/arel/nodes/window.rb126
-rw-r--r--activerecord/lib/arel/nodes/with.rb11
-rw-r--r--activerecord/lib/arel/order_predications.rb13
-rw-r--r--activerecord/lib/arel/predications.rb241
-rw-r--r--activerecord/lib/arel/select_manager.rb273
-rw-r--r--activerecord/lib/arel/table.rb111
-rw-r--r--activerecord/lib/arel/tree_manager.rb38
-rw-r--r--activerecord/lib/arel/update_manager.rb59
-rw-r--r--activerecord/lib/arel/visitors.rb20
-rw-r--r--activerecord/lib/arel/visitors/depth_first.rb200
-rw-r--r--activerecord/lib/arel/visitors/dot.rb292
-rw-r--r--activerecord/lib/arel/visitors/ibm_db.rb15
-rw-r--r--activerecord/lib/arel/visitors/informix.rb55
-rw-r--r--activerecord/lib/arel/visitors/mssql.rb125
-rw-r--r--activerecord/lib/arel/visitors/mysql.rb87
-rw-r--r--activerecord/lib/arel/visitors/oracle.rb153
-rw-r--r--activerecord/lib/arel/visitors/oracle12.rb61
-rw-r--r--activerecord/lib/arel/visitors/postgresql.rb104
-rw-r--r--activerecord/lib/arel/visitors/sqlite.rb27
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb847
-rw-r--r--activerecord/lib/arel/visitors/visitor.rb42
-rw-r--r--activerecord/lib/arel/visitors/where_sql.rb23
-rw-r--r--activerecord/lib/arel/window_predications.rb9
176 files changed, 5804 insertions, 710 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index d43378c64f..d198466dbf 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -40,6 +40,7 @@ module ActiveRecord
autoload :Core
autoload :ConnectionHandling
autoload :CounterCache
+ autoload :DatabaseConfigurations
autoload :DynamicMatchers
autoload :Enum
autoload :InternalMetadata
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 27a641f05b..3250e29b82 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -3,8 +3,6 @@
module ActiveRecord
# See ActiveRecord::Aggregations::ClassMethods for documentation
module Aggregations
- extend ActiveSupport::Concern
-
def initialize_dup(*) # :nodoc:
@aggregation_cache = {}
super
@@ -225,6 +223,10 @@ module ActiveRecord
def composed_of(part_id, options = {})
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
+ unless self < Aggregations
+ include Aggregations
+ end
+
name = part_id.id2name
class_name = options[:class_name] || name.camelize
mapping = options[:mapping] || [ name, name ]
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index b1cad0d0a4..1ee52945ea 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -241,7 +241,7 @@ module ActiveRecord
association
end
- def association_cached?(name) # :nodoc
+ def association_cached?(name) # :nodoc:
@association_cache.key?(name)
end
@@ -292,13 +292,13 @@ module ActiveRecord
#
# The project class now has the following methods (and more) to ease the traversal and
# manipulation of its relationships:
- # * <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(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>
+ # * <tt>Project#portfolio</tt>, <tt>Project#portfolio=(portfolio)</tt>, <tt>Project#reload_portfolio</tt>
+ # * <tt>Project#project_manager</tt>, <tt>Project#project_manager=(project_manager)</tt>, <tt>Project#reload_project_manager</tt>
+ # * <tt>Project#milestones.empty?</tt>, <tt>Project#milestones.size</tt>, <tt>Project#milestones</tt>, <tt>Project#milestones<<(milestone)</tt>,
+ # <tt>Project#milestones.delete(milestone)</tt>, <tt>Project#milestones.destroy(milestone)</tt>, <tt>Project#milestones.find(milestone_id)</tt>,
+ # <tt>Project#milestones.build</tt>, <tt>Project#milestones.create</tt>
+ # * <tt>Project#categories.empty?</tt>, <tt>Project#categories.size</tt>, <tt>Project#categories</tt>, <tt>Project#categories<<(category1)</tt>,
+ # <tt>Project#categories.delete(category1)</tt>, <tt>Project#categories.destroy(category1)</tt>
#
# === A word of warning
#
@@ -1232,9 +1232,9 @@ module ActiveRecord
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "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>)
- # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</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>)
+ # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new(firm_id: id); c.save!</tt>)
# * <tt>Firm#clients.reload</tt>
# The declaration can also include an +options+ hash to specialize the behavior of the association.
#
@@ -1405,9 +1405,9 @@ module ActiveRecord
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
# * <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>)
+ # * <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>)
# * <tt>Account#reload_beneficiary</tt>
#
# === Scopes
@@ -1746,8 +1746,8 @@ module ActiveRecord
# * <tt>Developer#projects.size</tt>
# * <tt>Developer#projects.find(id)</tt>
# * <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>)
+ # * <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>)
# * <tt>Developer#projects.reload</tt>
# The declaration may include an +options+ hash to specialize the behavior of the association.
#
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 4f3893588e..272eede824 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -33,7 +33,7 @@ module ActiveRecord
elsif join.is_a?(Arel::Nodes::Join)
join.left.name == name ? 1 : 0
elsif join.is_a?(Hash)
- join.fetch(name, 0)
+ join[name]
else
raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join"
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index ca8c7794e0..44596f4424 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -19,7 +19,6 @@ module ActiveRecord
# HasManyThroughAssociation + ThroughAssociation
class Association #:nodoc:
attr_reader :owner, :target, :reflection
- attr_accessor :inversed
delegate :options, to: :reflection
@@ -67,7 +66,7 @@ module ActiveRecord
#
# Note that if the target has not been loaded, it is not considered stale.
def stale_target?
- !inversed && 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+.
@@ -98,23 +97,24 @@ module ActiveRecord
# Set the inverse association, if possible
def set_inverse_instance(record)
- if invertible_for?(record)
- inverse = record.association(inverse_reflection_for(record).name)
- inverse.target = owner
- inverse.inversed = true
+ if inverse = inverse_association_for(record)
+ inverse.inversed_from(owner)
end
record
end
# Remove the inverse association, if possible
def remove_inverse_instance(record)
- if invertible_for?(record)
- inverse = record.association(inverse_reflection_for(record).name)
- inverse.target = nil
- inverse.inversed = false
+ if inverse = inverse_association_for(record)
+ inverse.inversed_from(nil)
end
end
+ def inversed_from(record)
+ self.target = record
+ @inversed = !!record
+ end
+
# Returns the class of the target. belongs_to polymorphic overrides this to look at the
# polymorphic_type field on the owner.
def klass
@@ -240,6 +240,12 @@ module ActiveRecord
end
end
+ def inverse_association_for(record)
+ if invertible_for?(record)
+ record.association(inverse_reflection_for(record).name)
+ end
+ end
+
# Can be redefined by subclasses, notably polymorphic belongs_to
# The record parameter is necessary to support polymorphic inverses as we must check for
# the association in the specific class of the record.
@@ -269,6 +275,7 @@ module ActiveRecord
def build_record(attributes)
reflection.build_association(attributes) do |record|
initialize_attributes(record, attributes)
+ yield(record) if block_given?
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 1109fee462..3d4ad1dd5b 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -16,20 +16,7 @@ module ActiveRecord
end
end
- def replace(record)
- if record
- raise_on_type_mismatch!(record)
- update_counters_on_replace(record)
- set_inverse_instance(record)
- @updated = true
- else
- decrement_counters
- end
-
- self.target = record
- end
-
- def target=(record)
+ def inversed_from(record)
replace_keys(record)
super
end
@@ -55,14 +42,32 @@ module ActiveRecord
update_counters(1)
end
+ def target_changed?
+ owner.saved_change_to_attribute?(reflection.foreign_key)
+ end
+
private
+ def replace(record)
+ if record
+ raise_on_type_mismatch!(record)
+ update_counters_on_replace(record)
+ set_inverse_instance(record)
+ @updated = true
+ else
+ decrement_counters
+ end
+
+ replace_keys(record)
+
+ self.target = record
+ end
def update_counters(by)
if require_counter_update? && foreign_key_present?
if target && !stale_target?
target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch])
else
- klass.update_counters(target_id, reflection.counter_cache_column => by, touch: reflection.options[:touch])
+ counter_cache_target.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch])
end
end
end
@@ -78,19 +83,22 @@ module ActiveRecord
def update_counters_on_replace(record)
if require_counter_update? && different_target?(record)
owner.instance_variable_set :@_after_replace_counter_called, true
- record.increment!(reflection.counter_cache_column)
+ record.increment!(reflection.counter_cache_column, touch: reflection.options[:touch])
decrement_counters
end
end
# Checks whether record is different to the current target, without loading it
def different_target?(record)
- record.id != owner._read_attribute(reflection.foreign_key)
+ record._read_attribute(primary_key(record)) != owner._read_attribute(reflection.foreign_key)
end
def replace_keys(record)
- owner[reflection.foreign_key] = record ?
- record._read_attribute(reflection.association_primary_key(record.class)) : nil
+ owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record)) : nil
+ end
+
+ def primary_key(record)
+ reflection.association_primary_key(record.class)
end
def foreign_key_present?
@@ -104,12 +112,9 @@ module ActiveRecord
inverse && inverse.has_one?
end
- def target_id
- if options[:primary_key]
- owner.send(reflection.name).try(:id)
- else
- owner._read_attribute(reflection.foreign_key)
- end
+ def counter_cache_target
+ primary_key = reflection.association_primary_key(klass)
+ klass.unscoped.where!(primary_key => owner._read_attribute(reflection.foreign_key))
end
def stale_state
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 75b4c4481a..3fd2fb5f67 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -9,8 +9,11 @@ module ActiveRecord
type.presence && type.constantize
end
- private
+ def target_changed?
+ super || owner.saved_change_to_attribute?(reflection.foreign_type)
+ end
+ private
def replace_keys(record)
super
owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index c161454c1a..0166ed98ca 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -34,9 +34,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
foreign_key = reflection.foreign_key
cache_column = reflection.counter_cache_column
- if (@_after_replace_counter_called ||= false)
+ if @_after_replace_counter_called ||= false
@_after_replace_counter_called = false
- elsif saved_change_to_attribute?(foreign_key) && !new_record?
+ elsif association(reflection.name).target_changed?
if reflection.polymorphic?
model = attribute_in_database(reflection.foreign_type).try(:constantize)
model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize)
@@ -48,15 +48,21 @@ module ActiveRecord::Associations::Builder # :nodoc:
foreign_key_was = attribute_before_last_save foreign_key
foreign_key = attribute_in_database foreign_key
- if foreign_key && model.respond_to?(:increment_counter)
- model.increment_counter(cache_column, foreign_key)
+ if foreign_key && model < ActiveRecord::Base
+ counter_cache_target(reflection, model, foreign_key).update_counters(cache_column => 1)
end
- if foreign_key_was && model_was.respond_to?(:decrement_counter)
- model_was.decrement_counter(cache_column, foreign_key_was)
+ if foreign_key_was && model_was < ActiveRecord::Base
+ counter_cache_target(reflection, model_was, foreign_key_was).update_counters(cache_column => -1)
end
end
end
+
+ private
+ def counter_cache_target(reflection, model, foreign_key)
+ primary_key = reflection.association_primary_key(model)
+ model.unscoped.where!(primary_key => foreign_key)
+ end
end
end
@@ -84,7 +90,8 @@ module ActiveRecord::Associations::Builder # :nodoc:
else
klass = association.klass
end
- old_record = klass.find_by(klass.primary_key => old_foreign_id)
+ primary_key = reflection.association_primary_key(klass)
+ old_record = klass.find_by(primary_key => old_foreign_id)
if old_record
if touch != true
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 1981da11a2..e3070e0472 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
@@ -2,39 +2,6 @@
module ActiveRecord::Associations::Builder # :nodoc:
class HasAndBelongsToMany # :nodoc:
- class JoinTableResolver # :nodoc:
- KnownTable = Struct.new :join_table
-
- class KnownClass # :nodoc:
- 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 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.to_s
- end
- end
- end
-
attr_reader :lhs_model, :association_name, :options
def initialize(association_name, lhs_model, options)
@@ -44,8 +11,6 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
def through_model
- habtm = JoinTableResolver.build lhs_model, association_name, options
-
join_model = Class.new(ActiveRecord::Base) {
class << self
attr_accessor :left_model
@@ -56,7 +21,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
def self.table_name
- table_name_resolver.join_table
+ # Table name needs to be resolved lazily
+ # because RHS class might not have been loaded
+ @table_name ||= table_name_resolver.call
end
def self.compute_type(class_name)
@@ -86,7 +53,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
}
join_model.name = "HABTM_#{association_name.to_s.camelize}"
- join_model.table_name_resolver = habtm
+ join_model.table_name_resolver = -> { table_name }
join_model.left_model = lhs_model
join_model.add_left_association :left_side, anonymous_class: lhs_model
@@ -117,6 +84,18 @@ module ActiveRecord::Associations::Builder # :nodoc:
middle_options
end
+ def table_name
+ if options[:join_table]
+ options[:join_table].to_s
+ else
+ class_name = options.fetch(:class_name) {
+ association_name.to_s.camelize.singularize
+ }
+ klass = lhs_model.send(:compute_type, class_name.to_s)
+ [lhs_model.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
+ end
+ end
+
def belongs_to_options(options)
rhs_options = {}
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 443ccaaa72..840d900bbc 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -45,6 +45,8 @@ module ActiveRecord
def ids_reader
if loaded?
target.pluck(reflection.association_primary_key)
+ elsif !target.empty?
+ load_target.pluck(reflection.association_primary_key)
else
@association_ids ||= scope.pluck(reflection.association_primary_key)
end
@@ -103,9 +105,7 @@ module ActiveRecord
if attributes.is_a?(Array)
attributes.collect { |attr| build(attr, &block) }
else
- add_to_target(build_record(attributes)) do |record|
- yield(record) if block_given?
- end
+ add_to_target(build_record(attributes, &block))
end
end
@@ -212,9 +212,11 @@ module ActiveRecord
def size
if !find_target? || loaded?
target.size
+ elsif @association_ids
+ @association_ids.size
elsif !association_scope.group_values.empty?
load_target.size
- elsif !association_scope.distinct_value && target.is_a?(Array)
+ elsif !association_scope.distinct_value && !target.empty?
unsaved_records = target.select(&:new_record?)
unsaved_records.size + count_records
else
@@ -231,10 +233,10 @@ module ActiveRecord
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded?
+ if loaded? || @association_ids
size.zero?
else
- @target.blank? && !scope.exists?
+ target.empty? && !scope.exists?
end
end
@@ -356,15 +358,18 @@ module ActiveRecord
if attributes.is_a?(Array)
attributes.collect { |attr| _create_record(attr, raise, &block) }
else
+ record = build_record(attributes, &block)
transaction do
- add_to_target(build_record(attributes)) do |record|
- yield(record) if block_given?
- insert_record(record, true, raise) {
+ result = nil
+ add_to_target(record) do
+ result = insert_record(record, true, raise) {
@_was_loaded = loaded?
@association_ids = nil
}
end
+ raise ActiveRecord::Rollback unless result
end
+ record
end
end
@@ -395,7 +400,7 @@ module ActiveRecord
records.each { |record| callback(:before_remove, record) }
delete_records(existing_records, method) if existing_records.any?
- records.each { |record| target.delete(record) }
+ @target -= records
records.each { |record| callback(:after_remove, record) }
end
@@ -442,7 +447,9 @@ module ActiveRecord
end
end
- result && records
+ raise ActiveRecord::Rollback unless result
+
+ records
end
def replace_on_target(record, index, skip_callbacks)
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 59929b8c4e..617956c768 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -90,7 +90,7 @@ module ActiveRecord
def build_record(attributes)
ensure_not_nested
- record = super(attributes)
+ record = super
inverse = source_reflection.inverse_of
if inverse
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 090b082cb0..390bfd8b08 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -23,35 +23,6 @@ module ActiveRecord
end
end
- def replace(record, save = true)
- raise_on_type_mismatch!(record) if record
- load_target
-
- return target unless target || record
-
- assigning_another_record = target != record
- if assigning_another_record || record.has_changes_to_save?
- save &&= owner.persisted?
-
- transaction_if(save) do
- remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
-
- if record
- set_owner_attributes(record)
- set_inverse_instance(record)
-
- if save && !record.save
- nullify_owner_attributes(record)
- set_owner_attributes(target) if target
- raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
- end
- end
- end
- end
-
- self.target = record
- end
-
def delete(method = options[:dependent])
if load_target
case method
@@ -68,6 +39,33 @@ module ActiveRecord
end
private
+ def replace(record, save = true)
+ raise_on_type_mismatch!(record) if record
+
+ return target unless load_target || record
+
+ assigning_another_record = target != record
+ if assigning_another_record || record.has_changes_to_save?
+ save &&= owner.persisted?
+
+ transaction_if(save) do
+ remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
+
+ if record
+ set_owner_attributes(record)
+ set_inverse_instance(record)
+
+ if save && !record.save
+ nullify_owner_attributes(record)
+ set_owner_attributes(target) if target
+ raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
+ end
+ end
+ end
+ end
+
+ self.target = record
+ end
# The reason that the save param for replace is false, if for create (not just build),
# is because the setting of the foreign keys is actually handled by the scoping when
@@ -107,6 +105,14 @@ module ActiveRecord
yield
end
end
+
+ def _create_record(attributes, raise_error = false, &block)
+ unless owner.persisted?
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
+ end
+
+ super
+ end
end
end
end
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 491282adf7..10978b2d93 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -6,12 +6,12 @@ module ActiveRecord
class HasOneThroughAssociation < HasOneAssociation #:nodoc:
include ThroughAssociation
- def replace(record, save = true)
- create_through_record(record, save)
- self.target = record
- end
-
private
+ def replace(record, save = true)
+ create_through_record(record, save)
+ self.target = record
+ end
+
def create_through_record(record, save)
ensure_not_nested
@@ -28,7 +28,11 @@ module ActiveRecord
end
if through_record
- through_record.update(attributes)
+ if through_record.new_record?
+ through_record.assign_attributes(attributes)
+ else
+ through_record.update(attributes)
+ end
elsif owner.new_record? || !save
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 f88e383fe0..b76005b587 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -14,10 +14,8 @@ module ActiveRecord
i[column.name] = column.alias
}
}
- @name_and_alias_cache = tables.each_with_object({}) { |table, h|
- h[table.node] = table.columns.map { |column|
- [column.name, column.alias]
- }
+ @columns_cache = tables.each_with_object({}) { |table, h|
+ h[table.node] = table.columns
}
end
@@ -25,9 +23,8 @@ module ActiveRecord
@tables.flat_map(&:column_aliases)
end
- # An array of [column_name, alias] pairs for the table
def column_aliases(node)
- @name_and_alias_cache[node]
+ @columns_cache[node]
end
def column_alias(node, column)
@@ -67,42 +64,31 @@ module ActiveRecord
end
end
- def initialize(base, table, associations, alias_tracker)
- @alias_tracker = alias_tracker
+ def initialize(base, table, associations)
tree = self.class.make_tree associations
@join_root = JoinBase.new(base, table, 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(joins_to_add, join_type)
- joins = join_root.children.flat_map { |child|
- make_join_constraints(join_root, child, join_type)
- }
+ def join_constraints(joins_to_add, join_type, alias_tracker)
+ @alias_tracker = alias_tracker
+
+ construct_tables!(join_root)
+ joins = make_join_constraints(join_root, join_type)
joins.concat joins_to_add.flat_map { |oj|
+ construct_tables!(oj.join_root)
if join_root.match? oj.join_root
walk join_root, oj.join_root
else
- oj.join_root.children.flat_map { |child|
- make_join_constraints(oj.join_root, child, join_type)
- }
+ make_join_constraints(oj.join_root, join_type)
end
}
end
- def aliases
- @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 instantiate(result_set, &block)
primary_key = aliases.column_alias(join_root, join_root.primary_key)
@@ -127,35 +113,49 @@ module ActiveRecord
result_set.each { |row_hash|
parent_key = primary_key ? row_hash[primary_key] : row_hash
parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block)
- construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
+ construct(parent, join_root, row_hash, seen, model_cache)
}
end
parents.values
end
+ def apply_column_aliases(relation)
+ relation._select!(-> { aliases.columns })
+ end
+
protected
- attr_reader :alias_tracker, :base_klass, :join_root
+ attr_reader :join_root
private
+ attr_reader :alias_tracker
- 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, join_type, tables, chain)
+ def aliases
+ @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 make_outer_joins(parent, child)
- join_type = Arel::Nodes::OuterJoin
- make_join_constraints(parent, child, join_type, true)
+ def construct_tables!(join_root)
+ join_root.each_children do |parent, child|
+ child.tables = table_aliases_for(parent, child)
+ end
end
- def make_join_constraints(parent, child, join_type, aliasing = false)
- tables = aliasing ? table_aliases_for(parent, child) : child.tables
- joins = make_constraints(parent, child, tables, join_type)
+ def make_join_constraints(join_root, join_type)
+ join_root.children.flat_map do |child|
+ make_constraints(join_root, child, join_type)
+ end
+ end
- joins.concat child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) }
+ def make_constraints(parent, child, join_type = Arel::Nodes::OuterJoin)
+ foreign_table = parent.table
+ foreign_klass = parent.base_klass
+ joins = child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker)
+ joins.concat child.children.flat_map { |c| make_constraints(child, c, join_type) }
end
def table_aliases_for(parent, node)
@@ -168,13 +168,8 @@ module ActiveRecord
}
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 = reflection.alias_candidate(parent.table_name)
join ? "#{name}_join" : name
end
@@ -183,8 +178,8 @@ module ActiveRecord
[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
+ joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r) }
+ joins.concat missing.flat_map { |_, n| make_constraints(left, n) }
end
def find_reflection(klass, name)
@@ -202,11 +197,11 @@ module ActiveRecord
raise EagerLoadPolymorphicError.new(reflection)
end
- JoinAssociation.new(reflection, build(right, reflection.klass), alias_tracker)
+ JoinAssociation.new(reflection, build(right, reflection.klass))
end
end
- def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
+ def construct(ar_parent, parent, row, seen, model_cache)
return if ar_parent.nil?
parent.children.each do |node|
@@ -215,7 +210,7 @@ module ActiveRecord
other.loaded!
elsif ar_parent.association_cached?(node.reflection.name)
model = ar_parent.association(node.reflection.name).target
- construct(model, node, row, rs, seen, model_cache, aliases)
+ construct(model, node, row, seen, model_cache)
next
end
@@ -227,25 +222,20 @@ module ActiveRecord
next
end
- model = seen[ar_parent.object_id][node.base_klass][id]
+ model = seen[ar_parent.object_id][node][id]
if model
- construct(model, node, row, rs, seen, model_cache, aliases)
+ construct(model, node, row, seen, model_cache)
else
- model = construct_model(ar_parent, node, row, model_cache, id, aliases)
-
- if node.reflection.scope &&
- node.reflection.scope_for(node.base_klass.unscoped).readonly_value
- model.readonly!
- end
+ model = construct_model(ar_parent, node, row, model_cache, id)
- seen[ar_parent.object_id][node.base_klass][id] = model
- construct(model, node, row, rs, seen, model_cache, aliases)
+ seen[ar_parent.object_id][node][id] = model
+ construct(model, node, row, seen, model_cache)
end
end
end
- def construct_model(record, node, row, model_cache, id, aliases)
+ def construct_model(record, node, row, model_cache, id)
other = record.association(node.reflection.name)
model = model_cache[node][id] ||=
@@ -259,6 +249,7 @@ module ActiveRecord
other.target = model
end
+ model.readonly! if node.readonly?
model
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 c36386ec7e..4583d89cba 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -6,17 +6,14 @@ module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
- # The reflection of the association represented
- attr_reader :reflection
+ attr_reader :reflection, :tables
+ attr_accessor :table
- attr_accessor :tables
-
- def initialize(reflection, children, alias_tracker)
+ def initialize(reflection, children)
super(reflection.klass, children)
- @alias_tracker = alias_tracker
- @reflection = reflection
- @tables = nil
+ @reflection = reflection
+ @tables = nil
end
def match?(other)
@@ -24,14 +21,13 @@ module ActiveRecord
super && reflection == other.reflection
end
- def join_constraints(foreign_table, foreign_klass, join_type, tables, chain)
- joins = []
- tables = tables.reverse
+ def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker)
+ joins = []
# 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 do |reflection|
- table = tables.shift
+ reflection.chain.reverse_each.with_index(1) do |reflection, i|
+ table = tables[-i]
klass = reflection.klass
constraint = reflection.build_join_constraint(table, foreign_table)
@@ -54,12 +50,16 @@ module ActiveRecord
joins
end
- def table
- tables.first
+ def tables=(tables)
+ @tables = tables
+ @table = tables.first
end
- private
- attr_reader :alias_tracker
+ def readonly?
+ return @readonly if defined?(@readonly)
+
+ @readonly = reflection.scope && reflection.scope_for(base_klass.unscoped).readonly_value
+ end
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 2181f308bf..3ad72a3646 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -33,6 +33,13 @@ module ActiveRecord
children.each { |child| child.each(&block) }
end
+ def each_children(&block)
+ children.each do |child|
+ yield self, child
+ child.each_children(&block)
+ end
+ end
+
# An Arel::Table for the active_record
def table
raise NotImplementedError
@@ -47,8 +54,8 @@ module ActiveRecord
length = column_names_with_alias.length
while index < length
- column_name, alias_name = column_names_with_alias[index]
- hash[column_name] = row[alias_name]
+ column = column_names_with_alias[index]
+ hash[column.name] = row[column.alias]
index += 1
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 1ea0aeac3a..5c2ac5b374 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -98,26 +98,30 @@ module ActiveRecord
private
# Loads all the given data into +records+ for the +association+.
- def preloaders_on(association, records, scope)
+ def preloaders_on(association, records, scope, polymorphic_parent = false)
case association
when Hash
- preloaders_for_hash(association, records, scope)
+ preloaders_for_hash(association, records, scope, polymorphic_parent)
when Symbol
- preloaders_for_one(association, records, scope)
+ preloaders_for_one(association, records, scope, polymorphic_parent)
when String
- preloaders_for_one(association.to_sym, records, scope)
+ preloaders_for_one(association.to_sym, records, scope, polymorphic_parent)
else
raise ArgumentError, "#{association.inspect} was not recognized for preload"
end
end
- def preloaders_for_hash(association, records, scope)
+ def preloaders_for_hash(association, records, scope, polymorphic_parent)
association.flat_map { |parent, child|
- loaders = preloaders_for_one parent, records, scope
+ loaders = preloaders_for_one parent, records, scope, polymorphic_parent
recs = loaders.flat_map(&:preloaded_records).uniq
+
+ reflection = records.first.class._reflect_on_association(parent)
+ polymorphic_parent = reflection && reflection.options[:polymorphic]
+
loaders.concat Array.wrap(child).flat_map { |assoc|
- preloaders_on assoc, recs, scope
+ preloaders_on assoc, recs, scope, polymorphic_parent
}
loaders
}
@@ -135,8 +139,8 @@ 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 preloaders_for_one(association, records, scope)
- grouped_records(association, records).flat_map do |reflection, klasses|
+ def preloaders_for_one(association, records, scope, polymorphic_parent)
+ grouped_records(association, records, polymorphic_parent).flat_map do |reflection, klasses|
klasses.map do |rhs_klass, rs|
loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
loader.run self
@@ -145,10 +149,11 @@ module ActiveRecord
end
end
- def grouped_records(association, records)
+ def grouped_records(association, records, polymorphic_parent)
h = {}
records.each do |record|
next unless record
+ next if polymorphic_parent && !record.class._reflect_on_association(association)
assoc = record.association(association)
next unless assoc.klass
klasses = h[assoc.reflection] ||= {}
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 0f38d6bbda..d6f7359055 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -42,11 +42,11 @@ module ActiveRecord
def associate_records_to_owner(owner, records)
association = owner.association(reflection.name)
+ association.loaded!
if reflection.collection?
- association.loaded!
association.target.concat(records)
else
- association.target = records.first
+ association.target = records.first unless records.empty?
end
end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 441bd715e4..cfab16a745 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -17,9 +17,8 @@ module ActiveRecord
replace(record)
end
- def build(attributes = {})
- record = build_record(attributes)
- yield(record) if block_given?
+ def build(attributes = {}, &block)
+ record = build_record(attributes, &block)
set_new_record(record)
record
end
@@ -62,13 +61,8 @@ module ActiveRecord
replace(record)
end
- def _create_record(attributes, raise_error = false)
- unless owner.persisted?
- raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
- end
-
- record = build_record(attributes)
- yield(record) if block_given?
+ def _create_record(attributes, raise_error = false, &block)
+ record = build_record(attributes, &block)
saved = record.save
set_new_record(record)
raise RecordInvalid.new(record) if !saved && raise_error
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index 5afb0bc068..15e6565e69 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -114,7 +114,7 @@ module ActiveRecord
attributes[inverse.foreign_key] = target.id
end
- super(attributes)
+ super
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 7db9bbe46b..e4b8b1a330 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -26,7 +26,7 @@ module ActiveRecord
def self.set_name_cache(name, value)
const_name = "ATTR_#{name}"
unless const_defined? const_name
- const_set const_name, value.dup.freeze
+ const_set const_name, -value
end
end
}
@@ -59,7 +59,7 @@ module ActiveRecord
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
- superclass.define_attribute_methods unless self == base_class
+ superclass.define_attribute_methods unless base_class?
super(attribute_names)
@attribute_methods_generated = true
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 3de6fe566d..233ee29fac 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -39,11 +39,12 @@ module ActiveRecord
end
end
- # Did this attribute change when we last saved? This method can be invoked
- # as +saved_change_to_name?+ instead of <tt>saved_change_to_attribute?("name")</tt>.
- # Behaves similarly to +attribute_changed?+. This method is useful in
- # after callbacks to determine if the call to save changed a certain
- # attribute.
+ # Did this attribute change when we last saved?
+ #
+ # This method is useful in after callbacks to determine if an attribute
+ # was changed during the save that triggered the callbacks to run. It can
+ # be invoked as +saved_change_to_name?+ instead of
+ # <tt>saved_change_to_attribute?("name")</tt>.
#
# ==== Options
#
@@ -60,19 +61,20 @@ module ActiveRecord
# attribute was changed, the result will be an array containing the
# original value and the saved value.
#
- # Behaves similarly to +attribute_change+. This method is useful in after
- # callbacks, to see the change in an attribute that just occurred
- #
- # This method can be invoked as +saved_change_to_name+ in instead of
- # <tt>saved_change_to_attribute("name")</tt>
+ # This method is useful in after callbacks, to see the change in an
+ # attribute during the save that triggered the callbacks to run. It can be
+ # invoked as +saved_change_to_name+ instead of
+ # <tt>saved_change_to_attribute("name")</tt>.
def saved_change_to_attribute(attr_name)
mutations_before_last_save.change_to_attribute(attr_name)
end
# Returns the original value of an attribute before the last save.
- # Behaves similarly to +attribute_was+. This method is useful in after
- # callbacks to get the original value of an attribute before the save that
- # just occurred
+ #
+ # This method is useful in after callbacks to get the original value of an
+ # attribute before the save that triggered the callbacks to run. It can be
+ # invoked as +name_before_last_save+ instead of
+ # <tt>attribute_before_last_save("name")</tt>.
def attribute_before_last_save(attr_name)
mutations_before_last_save.original_value(attr_name)
end
@@ -87,39 +89,75 @@ module ActiveRecord
mutations_before_last_save.changes
end
- # Alias for +attribute_changed?+
+ # Will this attribute change the next time we save?
+ #
+ # This method is useful in validations and before callbacks to determine
+ # if the next call to +save+ will change a particular attribute. It can be
+ # invoked as +will_save_change_to_name?+ instead of
+ # <tt>will_save_change_to_attribute("name")</tt>.
+ #
+ # ==== Options
+ #
+ # +from+ When passed, this method will return false unless the original
+ # value is equal to the given option
+ #
+ # +to+ When passed, this method will return false unless the value will be
+ # changed to the given value
def will_save_change_to_attribute?(attr_name, **options)
mutations_from_database.changed?(attr_name, **options)
end
- # Alias for +attribute_change+
+ # Returns the change to an attribute that will be persisted during the
+ # next save.
+ #
+ # This method is useful in validations and before callbacks, to see the
+ # change to an attribute that will occur when the record is saved. It can
+ # be invoked as +name_change_to_be_saved+ instead of
+ # <tt>attribute_change_to_be_saved("name")</tt>.
+ #
+ # If the attribute will change, the result will be an array containing the
+ # original value and the new value about to be saved.
def attribute_change_to_be_saved(attr_name)
mutations_from_database.change_to_attribute(attr_name)
end
- # Alias for +attribute_was+
+ # Returns the value of an attribute in the database, as opposed to the
+ # in-memory value that will be persisted the next time the record is
+ # saved.
+ #
+ # This method is useful in validations and before callbacks, to see the
+ # original value of an attribute prior to any changes about to be
+ # saved. It can be invoked as +name_in_database+ instead of
+ # <tt>attribute_in_database("name")</tt>.
def attribute_in_database(attr_name)
mutations_from_database.original_value(attr_name)
end
- # Alias for +changed?+
+ # Will the next call to +save+ have any changes to persist?
def has_changes_to_save?
mutations_from_database.any_changes?
end
- # Alias for +changes+
+ # Returns a hash containing all the changes that will be persisted during
+ # the next save.
def changes_to_save
mutations_from_database.changes
end
- # Alias for +changed+
+ # Returns an array of the names of any attributes that will change when
+ # the record is next saved.
def changed_attribute_names_to_save
- changes_to_save.keys
+ mutations_from_database.changed_attribute_names
end
- # Alias for +changed_attributes+
+ # Returns a hash of the attributes that will change when the record is
+ # next saved.
+ #
+ # The hash keys are the attribute names, and the hash values are the
+ # original attribute values in the database (as opposed to the in-memory
+ # values about to be saved).
def attributes_in_database
- changes_to_save.transform_values(&:first)
+ mutations_from_database.changed_values
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 2907547634..9b267bb7c0 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -83,7 +83,7 @@ module ActiveRecord
end
def reset_primary_key #:nodoc:
- if self == base_class
+ if base_class?
self.primary_key = get_primary_key(base_class.name)
else
self.primary_key = base_class.primary_key
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 14f700b6a9..0f7bcba564 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -69,7 +69,7 @@ module ActiveRecord
if defined?(JRUBY_VERSION)
# This form is significantly faster on JRuby, and this is one of our biggest hotspots.
# https://github.com/jruby/jruby/pull/2562
- def _read_attribute(attr_name, &block) # :nodoc
+ def _read_attribute(attr_name, &block) # :nodoc:
@attributes.fetch_value(attr_name.to_s, &block)
end
else
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 d2b7817b45..294a3dc32c 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -73,7 +73,7 @@ module ActiveRecord
# `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|
+ decorate_matching_attribute_types(matcher, "_time_zone_conversion") do |type|
TimeZoneConverter.new(type)
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index a1250c3835..a405f05e0b 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -392,7 +392,7 @@ module ActiveRecord
records -= records_to_destroy
end
- records.each do |record|
+ records.each_with_index do |record, index|
next if record.destroyed?
saved = true
@@ -400,8 +400,13 @@ module ActiveRecord
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?
+ elsif !reflection.nested?
+ if reflection.validate?
+ valid = association_valid?(reflection, record, index)
+ saved = valid ? association.insert_record(record, false) : false
+ else
+ association.insert_record(record)
+ end
end
elsif autosave
saved = record.save(validate: false)
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index cc99401390..5169f312f5 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -288,8 +288,10 @@ module ActiveRecord #:nodoc:
extend Enum
extend Delegation::DelegateCache
extend CollectionCacheKey
+ extend Aggregations::ClassMethods
include Core
+ include DatabaseConfigurations
include Persistence
include ReadonlyAttributes
include ModelSchema
@@ -313,7 +315,6 @@ module ActiveRecord #:nodoc:
include ActiveModel::SecurePassword
include AutosaveAssociation
include NestedAttributes
- include Aggregations
include Transactions
include TouchLater
include NoTouching
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 9dbdf845bd..b6852bfc71 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -75,21 +75,7 @@ module ActiveRecord
# end
#
# Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is
- # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation
- # where the +before_destroy+ method is overridden:
- #
- # class Topic < ActiveRecord::Base
- # def before_destroy() destroy_author end
- # end
- #
- # class Reply < Topic
- # def before_destroy() destroy_readers end
- # end
- #
- # 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 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.
+ # run, both +destroy_author+ and +destroy_readers+ are called.
#
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
# callbacks before specifying the associations. Otherwise, you might trigger the loading of a
@@ -142,7 +128,7 @@ module ActiveRecord
# end
# end
#
- # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
+ # So you specify the object you want to be messaged on a given callback. When that callback is triggered, the object has
# a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other
# initialization data such as the name of the attribute to work with:
#
@@ -332,6 +318,10 @@ module ActiveRecord
_run_touch_callbacks { super }
end
+ def increment!(*, touch: nil) # :nodoc:
+ touch ? _run_touch_callbacks { super } : super
+ end
+
private
def create_or_update(*)
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 be999e7749..e977d36cb9 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1013,8 +1013,8 @@ module ActiveRecord
# Returns true if a connection that's accessible to this class has
# already been opened.
def connected?(spec_name)
- conn = retrieve_connection_pool(spec_name)
- conn && conn.connected?
+ pool = retrieve_connection_pool(spec_name)
+ pool && pool.connected?
end
# Remove the connection for this class. This will close the active
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 08f3e15a4b..1d36c3c8b1 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -259,7 +259,9 @@ module ActiveRecord
attr_reader :transaction_manager #:nodoc:
- delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager
+ delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction,
+ :commit_transaction, :rollback_transaction, :materialize_transactions,
+ :disable_lazy_transactions!, :enable_lazy_transactions!, to: :transaction_manager
def transaction_open?
current_transaction.open?
@@ -385,7 +387,7 @@ module ActiveRecord
end
end
- def empty_insert_statement_value
+ def empty_insert_statement_value(primary_key = nil)
"DEFAULT VALUES"
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 25622e34c8..8aeb934ec2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -110,12 +110,7 @@ module ActiveRecord
if @query_cache[sql].key?(binds)
ActiveSupport::Notifications.instrument(
"sql.active_record",
- sql: sql,
- binds: binds,
- type_casted_binds: -> { type_casted_binds(binds) },
- name: name,
- connection_id: object_id,
- cached: true,
+ cache_notification_info(sql, name, binds)
)
@query_cache[sql][binds]
else
@@ -125,6 +120,19 @@ module ActiveRecord
end
end
+ # Database adapters can override this method to
+ # provide custom cache information.
+ def cache_notification_info(sql, name, binds)
+ {
+ sql: sql,
+ binds: binds,
+ type_casted_binds: -> { type_casted_binds(binds) },
+ name: name,
+ connection_id: object_id,
+ cached: true
+ }
+ end
+
# If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such
# queries should not be cached.
def locked?(arel)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index aec5fa6ba1..98b1348135 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -130,6 +130,7 @@ module ActiveRecord
end
def quoted_time(value) # :nodoc:
+ value = value.change(year: 2000, month: 1, day: 1)
quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "")
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 584a86da21..582ac516c7 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -101,6 +101,10 @@ module ActiveRecord
end
alias validated? validate?
+ def export_name_on_schema_dump?
+ name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern
+ end
+
def defined_for?(to_table_ord = nil, to_table: nil, **options)
if to_table_ord
self.to_table == to_table_ord.to_s
@@ -352,8 +356,12 @@ module ActiveRecord
type = type.to_sym if type
options = options.dup
- 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."
+ if @columns_hash[name]
+ if @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."
+ else
+ raise ArgumentError, "you can't define an already defined column '#{name}'."
+ end
end
index_options = options.delete(:index)
@@ -493,6 +501,9 @@ module ActiveRecord
# t.date
# t.binary
# t.boolean
+ # t.foreign_key
+ # t.json
+ # t.virtual
# t.remove
# t.remove_references
# t.remove_belongs_to
@@ -657,19 +668,19 @@ module ActiveRecord
# Adds a foreign key.
#
- # t.foreign_key(:authors)
+ # t.foreign_key(:authors)
#
# See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key]
- def foreign_key(*args) # :nodoc:
+ def foreign_key(*args)
@base.add_foreign_key(name, *args)
end
# Checks to see if a foreign key exists.
#
- # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors)
+ # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors)
#
# See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?]
- def foreign_key_exists?(*args) # :nodoc:
+ def foreign_key_exists?(*args)
@base.foreign_key_exists?(name, *args)
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 e2147b7fcf..4702de1964 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -305,7 +305,7 @@ module ActiveRecord
yield td if block_given?
if options[:force]
- drop_table(table_name, **options, if_exists: true)
+ drop_table(table_name, options.merge(if_exists: true))
end
result = execute schema_creation.accept td
@@ -741,22 +741,13 @@ module ActiveRecord
# ====== Creating an index with a specific operator class
#
# add_index(:developers, :name, using: 'gist', opclass: :gist_trgm_ops)
- #
- # generates:
- #
- # CREATE INDEX developers_on_name ON developers USING gist (name gist_trgm_ops) -- PostgreSQL
+ # # CREATE INDEX developers_on_name ON developers USING gist (name gist_trgm_ops) -- PostgreSQL
#
# add_index(:developers, [:name, :city], using: 'gist', opclass: { city: :gist_trgm_ops })
- #
- # generates:
- #
- # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL
+ # # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL
#
# add_index(:developers, [:name, :city], using: 'gist', opclass: :gist_trgm_ops)
- #
- # generates:
- #
- # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL
+ # # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL
#
# Note: only supported by PostgreSQL
#
@@ -908,7 +899,7 @@ module ActiveRecord
foreign_key_options = { to_table: reference_name }
end
foreign_key_options[:column] ||= "#{ref_name}_id"
- remove_foreign_key(table_name, **foreign_key_options)
+ remove_foreign_key(table_name, foreign_key_options)
end
remove_column(table_name, "#{ref_name}_id")
@@ -989,11 +980,18 @@ module ActiveRecord
#
# remove_foreign_key :accounts, column: :owner_id
#
+ # Removes the foreign key on +accounts.owner_id+.
+ #
+ # remove_foreign_key :accounts, to_table: :owners
+ #
# Removes the foreign key named +special_fk_name+ on the +accounts+ table.
#
# remove_foreign_key :accounts, name: :special_fk_name
#
- # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
+ # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key
+ # with an addition of
+ # [<tt>:to_table</tt>]
+ # The name of the table that contains the referenced primary key.
def remove_foreign_key(from_table, options_or_to_table = {})
return unless supports_foreign_keys?
@@ -1062,13 +1060,7 @@ module ActiveRecord
if (duplicate = inserting.detect { |v| inserting.count(v) > 1 })
raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict."
end
- if supports_multi_insert?
- execute insert_versions_sql(inserting)
- else
- inserting.each do |v|
- execute insert_versions_sql(v)
- end
- end
+ execute insert_versions_sql(inserting)
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 d9ac8db6a8..564b226b39 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -17,11 +17,19 @@ module ActiveRecord
end
def committed?
- @state == :committed
+ @state == :committed || @state == :fully_committed
+ end
+
+ def fully_committed?
+ @state == :fully_committed
end
def rolledback?
- @state == :rolledback
+ @state == :rolledback || @state == :fully_rolledback
+ end
+
+ def fully_rolledback?
+ @state == :fully_rolledback
end
def fully_completed?
@@ -55,10 +63,19 @@ module ActiveRecord
@state = :rolledback
end
+ def full_rollback!
+ @children.each { |c| c.rollback! }
+ @state = :fully_rolledback
+ end
+
def commit!
@state = :committed
end
+ def full_commit!
+ @state = :fully_committed
+ end
+
def nullify!
@state = nil
end
@@ -74,13 +91,14 @@ module ActiveRecord
end
class Transaction #:nodoc:
- attr_reader :connection, :state, :records, :savepoint_name
- attr_writer :joinable
+ attr_reader :connection, :state, :records, :savepoint_name, :isolation_level
def initialize(connection, options, run_commit_callbacks: false)
@connection = connection
@state = TransactionState.new
@records = []
+ @isolation_level = options[:isolation]
+ @materialized = false
@joinable = options.fetch(:joinable, true)
@run_commit_callbacks = run_commit_callbacks
end
@@ -89,8 +107,12 @@ module ActiveRecord
records << record
end
- def rollback
- @state.rollback!
+ def materialize!
+ @materialized = true
+ end
+
+ def materialized?
+ @materialized
end
def rollback_records
@@ -104,10 +126,6 @@ module ActiveRecord
end
end
- def commit
- @state.commit!
- end
-
def before_commit_records
records.uniq.each(&:before_committed!) if @run_commit_callbacks
end
@@ -133,48 +151,55 @@ module ActiveRecord
end
class SavepointTransaction < Transaction
- def initialize(connection, savepoint_name, parent_transaction, options, *args)
- super(connection, options, *args)
+ def initialize(connection, savepoint_name, parent_transaction, *args)
+ super(connection, *args)
parent_transaction.state.add_child(@state)
- if options[:isolation]
+ if isolation_level
raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
end
- connection.create_savepoint(@savepoint_name = savepoint_name)
+
+ @savepoint_name = savepoint_name
end
- def rollback
- connection.rollback_to_savepoint(savepoint_name)
+ def materialize!
+ connection.create_savepoint(savepoint_name)
super
end
+ def rollback
+ connection.rollback_to_savepoint(savepoint_name) if materialized?
+ @state.rollback!
+ end
+
def commit
- connection.release_savepoint(savepoint_name)
- super
+ connection.release_savepoint(savepoint_name) if materialized?
+ @state.commit!
end
def full_rollback?; false; end
end
class RealTransaction < Transaction
- def initialize(connection, options, *args)
- super
- if options[:isolation]
- connection.begin_isolated_db_transaction(options[:isolation])
+ def materialize!
+ if isolation_level
+ connection.begin_isolated_db_transaction(isolation_level)
else
connection.begin_db_transaction
end
+
+ super
end
def rollback
- connection.rollback_db_transaction
- super
+ connection.rollback_db_transaction if materialized?
+ @state.full_rollback!
end
def commit
- connection.commit_db_transaction
- super
+ connection.commit_db_transaction if materialized?
+ @state.full_commit!
end
end
@@ -182,6 +207,9 @@ module ActiveRecord
def initialize(connection)
@stack = []
@connection = connection
+ @has_unmaterialized_transactions = false
+ @materializing_transactions = false
+ @lazy_transactions_enabled = true
end
def begin_transaction(options = {})
@@ -195,11 +223,41 @@ module ActiveRecord
run_commit_callbacks: run_commit_callbacks)
end
+ transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled?
@stack.push(transaction)
+ @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions?
transaction
end
end
+ def disable_lazy_transactions!
+ materialize_transactions
+ @lazy_transactions_enabled = false
+ end
+
+ def enable_lazy_transactions!
+ @lazy_transactions_enabled = true
+ end
+
+ def lazy_transactions_enabled?
+ @lazy_transactions_enabled
+ end
+
+ def materialize_transactions
+ return if @materializing_transactions
+ return unless @has_unmaterialized_transactions
+
+ @connection.lock.synchronize do
+ begin
+ @materializing_transactions = true
+ @stack.each { |t| t.materialize! unless t.materialized? }
+ ensure
+ @materializing_transactions = false
+ end
+ @has_unmaterialized_transactions = false
+ end
+ end
+
def commit_transaction
@connection.lock.synchronize do
transaction = @stack.last
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 559f068c39..8999d3232a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -80,8 +80,12 @@ module ActiveRecord
attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock
alias :in_use? :owner
+ set_callback :checkin, :after, :enable_lazy_transactions!
+
def self.type_cast_config_to_integer(config)
- if config =~ SIMPLE_INT
+ if config.is_a?(Integer)
+ config
+ elsif SIMPLE_INT.match?(config)
config.to_i
else
config
@@ -117,6 +121,10 @@ module ActiveRecord
else
@prepared_statements = false
end
+
+ @advisory_locks_enabled = self.class.type_cast_config_to_boolean(
+ config.fetch(:advisory_locks, true)
+ )
end
def migrations_paths # :nodoc:
@@ -137,6 +145,10 @@ module ActiveRecord
def <=>(version_string)
@version <=> version_string.split(".").map(&:to_i)
end
+
+ def to_s
+ @version.join(".")
+ end
end
def valid_type?(type) # :nodoc:
@@ -320,6 +332,7 @@ module ActiveRecord
def supports_multi_insert?
true
end
+ deprecate :supports_multi_insert?
# Does this adapter support virtual columns?
def supports_virtual_columns?
@@ -331,6 +344,10 @@ module ActiveRecord
false
end
+ def supports_lazy_transactions?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -442,6 +459,7 @@ module ActiveRecord
# This is useful for when you need to call a proprietary method such as
# PostgreSQL's lo_* methods.
def raw_connection
+ disable_lazy_transactions!
@connection
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 fbedddd7f9..ad045f85ef 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -44,7 +44,7 @@ module ActiveRecord
class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
private def dealloc(stmt)
- stmt[:stmt].close
+ stmt.close
end
end
@@ -111,7 +111,7 @@ module ActiveRecord
end
def supports_advisory_locks?
- true
+ @advisory_locks_enabled
end
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
@@ -180,6 +180,8 @@ module ActiveRecord
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
+ materialize_transactions
+
log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@connection.query(sql)
@@ -223,7 +225,7 @@ module ActiveRecord
end
end
- def empty_insert_statement_value
+ def empty_insert_statement_value(primary_key = nil)
"VALUES ()"
end
@@ -558,17 +560,6 @@ module ActiveRecord
@max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin)
end
- def with_multi_statements
- previous_flags = @config[:flags]
- @config[:flags] = Mysql2::Client::MULTI_STATEMENTS
- reconnect!
-
- yield
- ensure
- @config[:flags] = previous_flags
- reconnect!
- end
-
def initialize_type_map(m = type_map)
super
@@ -630,6 +621,7 @@ module ActiveRecord
ER_DUP_ENTRY = 1062
ER_NOT_NULL_VIOLATION = 1048
ER_DO_NOT_HAVE_DEFAULT = 1364
+ ER_ROW_IS_REFERENCED_2 = 1451
ER_NO_REFERENCED_ROW_2 = 1452
ER_DATA_TOO_LONG = 1406
ER_OUT_OF_RANGE = 1264
@@ -644,7 +636,7 @@ module ActiveRecord
case error_number(exception)
when ER_DUP_ENTRY
RecordNotUnique.new(message)
- when ER_NO_REFERENCED_ROW_2
+ when ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2
InvalidForeignKey.new(message)
when ER_CANNOT_ADD_FOREIGN
mismatched_foreign_key(message)
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 901717ae3d..204691006c 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -57,9 +57,7 @@ module ActiveRecord
private
- def uri
- @uri
- end
+ attr_reader :uri
def uri_parser
@uri_parser ||= URI::Parser.new
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
index 458c9bfd70..684c7042a7 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -29,6 +29,8 @@ module ActiveRecord
end
def exec_query(sql, name = "SQL", binds = [], prepare: false)
+ materialize_transactions
+
if without_prepared_statement?(binds)
execute_and_free(sql, name) do |result|
ActiveRecord::Result.new(result.fields, result.to_a) if result
@@ -41,6 +43,8 @@ module ActiveRecord
end
def exec_delete(sql, name = nil, binds = [])
+ materialize_transactions
+
if without_prepared_statement?(binds)
execute_and_free(sql, name) { @connection.affected_rows }
else
@@ -62,6 +66,42 @@ module ActiveRecord
@connection.abandon_results!
end
+ def supports_set_server_option?
+ @connection.respond_to?(:set_server_option)
+ end
+
+ def multi_statements_enabled?(flags)
+ if flags.is_a?(Array)
+ flags.include?("MULTI_STATEMENTS")
+ else
+ (flags & Mysql2::Client::MULTI_STATEMENTS) != 0
+ end
+ end
+
+ def with_multi_statements
+ previous_flags = @config[:flags]
+
+ unless multi_statements_enabled?(previous_flags)
+ if supports_set_server_option?
+ @connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_ON)
+ else
+ @config[:flags] = Mysql2::Client::MULTI_STATEMENTS
+ reconnect!
+ end
+ end
+
+ yield
+ ensure
+ unless multi_statements_enabled?(previous_flags)
+ if supports_set_server_option?
+ @connection.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF)
+ else
+ @config[:flags] = previous_flags
+ reconnect!
+ end
+ end
+ end
+
def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@@ -71,10 +111,7 @@ module ActiveRecord
log(sql, name, binds, type_casted_binds) do
if cache_stmt
- cache = @statements[sql] ||= {
- stmt: @connection.prepare(sql)
- }
- stmt = cache[:stmt]
+ stmt = @statements[sql] ||= @connection.prepare(sql)
else
stmt = @connection.prepare(sql)
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
index ce50590651..1cf210d85b 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -36,7 +36,7 @@ module ActiveRecord
end
indexes.last[-2] << row[:Column_name]
- indexes.last[-1][:lengths].merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part]
+ indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part]
indexes.last[-1][:orders].merge!(row[:Column_name] => :desc) if row[:Collation] == "D"
end
end
@@ -80,8 +80,8 @@ module ActiveRecord
def new_column_from_field(table_name, field)
type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
- if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\(\))?\z/i.match?(field[:Default])
- default, default_function = nil, "CURRENT_TIMESTAMP"
+ if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(field[:Default])
+ default, default_function = nil, field[:Default]
else
default, default_function = field[:Default], nil
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 4c57bd48ab..92f15de219 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -58,6 +58,10 @@ module ActiveRecord
true
end
+ def supports_lazy_transactions?
+ true
+ end
+
# HELPER METHODS ===========================================
def each_hash(result) # :nodoc:
@@ -117,7 +121,7 @@ module ActiveRecord
end
def configure_connection
- @connection.query_options.merge!(as: :array)
+ @connection.query_options[:as] = :array
super
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 8db2a645af..6bd6b67165 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -58,6 +58,8 @@ module ActiveRecord
# Queries the database and returns the results in an Array-like object
def query(sql, name = nil) #:nodoc:
+ materialize_transactions
+
log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
result_as_array @connection.async_exec(sql)
@@ -70,6 +72,8 @@ module ActiveRecord
# Note: the PG::Result object is manually memory managed; if you don't
# need it specifically, you may want consider the <tt>exec_query</tt> wrapper.
def execute(sql, name = nil)
+ materialize_transactions
+
log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@connection.async_exec(sql)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
index d6852082ac..26abeea7ed 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -66,6 +66,10 @@ module ActiveRecord
deserialize(raw_old_value) != new_value
end
+ def force_equality?(value)
+ value.is_a?(::Array)
+ end
+
private
def type_cast_array(value, method)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
index 6edb7cfd3c..d85f9ab3ef 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -53,6 +53,10 @@ module ActiveRecord
::Range.new(new_begin, new_end, value.exclude_end?)
end
+ def force_equality?(value)
+ value.is_a?(::Range)
+ end
+
private
def type_cast_single(value)
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
index 231278c184..79351bc3a4 100644
--- 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
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/array/extract"
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -16,12 +18,12 @@ module ActiveRecord
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".freeze }
- enums, nodes = nodes.partition { |row| row["typtype"] == "e".freeze }
- domains, nodes = nodes.partition { |row| row["typtype"] == "d".freeze }
- arrays, nodes = nodes.partition { |row| row["typinput"] == "array_in".freeze }
- composites, nodes = nodes.partition { |row| row["typelem"].to_i != 0 }
+ mapped = nodes.extract! { |row| @store.key? row["typname"] }
+ ranges = nodes.extract! { |row| row["typtype"] == "r".freeze }
+ enums = nodes.extract! { |row| row["typtype"] == "e".freeze }
+ domains = nodes.extract! { |row| row["typtype"] == "d".freeze }
+ arrays = nodes.extract! { |row| row["typinput"] == "array_in".freeze }
+ composites = nodes.extract! { |row| row["typelem"].to_i != 0 }
mapped.each { |row| register_mapped_type(row) }
enums.each { |row| register_enum_type(row) }
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
index 6047217fcd..206b855a18 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -13,10 +13,10 @@ module ActiveRecord
# t.timestamps
# end
#
- # By default, this will use the +gen_random_uuid()+ function from the
+ # By default, this will use the <tt>gen_random_uuid()</tt> function from the
# +pgcrypto+ extension. As that extension is only available in
# PostgreSQL 9.4+, for earlier versions an explicit default can be set
- # to use +uuid_generate_v4()+ from the +uuid-ossp+ extension instead:
+ # to use <tt>uuid_generate_v4()</tt> from the +uuid-ossp+ extension instead:
#
# create_table :stuffs, id: false do |t|
# t.primary_key :id, :uuid, default: "uuid_generate_v4()"
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 e20e5f2914..00da7690a2 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -700,6 +700,11 @@ module ActiveRecord
sql
end
+ def add_column_for_alter(table_name, column_name, type, options = {})
+ return super unless options.key?(:comment)
+ [super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }]
+ end
+
def change_column_for_alter(table_name, column_name, type, options = {})
sqls = [change_column_sql(table_name, column_name, type, options)]
sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default)
@@ -708,7 +713,6 @@ module ActiveRecord
sqls
end
-
# Changes the default value of a table column.
def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc:
column = column_for(table_name, column_name)
@@ -751,7 +755,7 @@ module ActiveRecord
def data_source_sql(name = nil, type: nil)
scope = quoted_scope(name, type: type)
- scope[:type] ||= "'r','v','m','f'" # (r)elation/table, (v)iew, (m)aterialized view, (f)oreign table
+ scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table
sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup
sql << " WHERE n.nspname = #{scope[:schema]}"
@@ -765,7 +769,7 @@ module ActiveRecord
type = \
case type
when "BASE TABLE"
- "'r'"
+ "'r','p'"
when "VIEW"
"'v','m'"
when "FOREIGN TABLE"
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
index b252a76caa..ffd3be26b0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
module ActiveRecord
+ # :stopdoc:
module ConnectionAdapters
class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata)
undef to_yaml if method_defined?(:to_yaml)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index fdf6f75108..30e651ee63 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -298,7 +298,7 @@ module ActiveRecord
end
def supports_advisory_locks?
- true
+ @advisory_locks_enabled
end
def supports_explain?
@@ -326,6 +326,10 @@ module ActiveRecord
postgresql_version >= 90400
end
+ def supports_lazy_transactions?
+ true
+ end
+
def get_advisory_lock(lock_id) # :nodoc:
unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
@@ -597,6 +601,8 @@ module ActiveRecord
end
def exec_no_cache(sql, name, binds)
+ materialize_transactions
+
type_casted_binds = type_casted_binds(binds)
log(sql, name, binds, type_casted_binds) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@@ -606,6 +612,8 @@ module ActiveRecord
end
def exec_cache(sql, name, binds)
+ materialize_transactions
+
stmt_key = prepare_statement(sql)
type_casted_binds = type_casted_binds(binds)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
index 70de96326c..abedf01f10 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -17,6 +17,7 @@ module ActiveRecord
end
def quoted_time(value)
+ value = value.change(year: 2000, month: 1, day: 1)
quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ")
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index 58e5138e02..24e7bc65fa 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -7,6 +7,10 @@ module ActiveRecord
# Returns an array of indexes for the given table.
def indexes(table_name)
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row|
+ # Indexes SQLite creates implicitly for internal use start with "sqlite_".
+ # See https://www.sqlite.org/fileformat2.html#intschema
+ next if row["name"].starts_with?("sqlite_")
+
index_sql = query_value(<<-SQL, "SCHEMA")
SELECT sql
FROM sqlite_master
@@ -40,7 +44,7 @@ module ActiveRecord
where: where,
orders: orders
)
- end
+ end.compact
end
def create_schema_dumper(options)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index a958600446..efe454fa7f 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -15,6 +15,8 @@ require "sqlite3"
module ActiveRecord
module ConnectionHandling # :nodoc:
def sqlite3_connection(config)
+ config = config.symbolize_keys
+
# Require database.
unless config[:database]
raise ArgumentError, "No database file specified. Missing argument: database"
@@ -31,7 +33,7 @@ module ActiveRecord
db = SQLite3::Database.new(
config[:database].to_s,
- results_as_hash: true
+ config.merge(results_as_hash: true)
)
db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
@@ -94,7 +96,7 @@ module ActiveRecord
class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
private
def dealloc(stmt)
- stmt[:stmt].close unless stmt[:stmt].closed?
+ stmt.close unless stmt.closed?
end
end
@@ -104,6 +106,10 @@ module ActiveRecord
@active = true
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
+ if sqlite_version < "3.8.0"
+ raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8."
+ end
+
configure_connection
end
@@ -116,7 +122,7 @@ module ActiveRecord
end
def supports_partial_index?
- sqlite_version >= "3.8.0"
+ true
end
def requires_reloading?
@@ -124,7 +130,7 @@ module ActiveRecord
end
def supports_foreign_keys_in_create?
- sqlite_version >= "3.6.19"
+ true
end
def supports_views?
@@ -139,10 +145,6 @@ module ActiveRecord
true
end
- def supports_multi_insert?
- sqlite_version >= "3.7.11"
- end
-
def active?
@active
end
@@ -184,16 +186,23 @@ module ActiveRecord
true
end
+ def supports_lazy_transactions?
+ true
+ end
+
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity # :nodoc:
- old = query_value("PRAGMA foreign_keys")
+ old_foreign_keys = query_value("PRAGMA foreign_keys")
+ old_defer_foreign_keys = query_value("PRAGMA defer_foreign_keys")
begin
+ execute("PRAGMA defer_foreign_keys = ON")
execute("PRAGMA foreign_keys = OFF")
yield
ensure
- execute("PRAGMA foreign_keys = #{old}")
+ execute("PRAGMA defer_foreign_keys = #{old_defer_foreign_keys}")
+ execute("PRAGMA foreign_keys = #{old_foreign_keys}")
end
end
@@ -207,6 +216,8 @@ module ActiveRecord
end
def exec_query(sql, name = nil, binds = [], prepare: false)
+ materialize_transactions
+
type_casted_binds = type_casted_binds(binds)
log(sql, name, binds, type_casted_binds) do
@@ -224,11 +235,8 @@ module ActiveRecord
stmt.close
end
else
- cache = @statements[sql] ||= {
- stmt: @connection.prepare(sql)
- }
- stmt = cache[:stmt]
- cols = cache[:cols] ||= stmt.columns
+ stmt = @statements[sql] ||= @connection.prepare(sql)
+ cols = stmt.columns
stmt.reset!
stmt.bind_params(type_casted_binds)
records = stmt.to_a
@@ -250,6 +258,8 @@ module ActiveRecord
end
def execute(sql, name = nil) #:nodoc:
+ materialize_transactions
+
log(sql, name) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@connection.execute(sql)
@@ -407,12 +417,25 @@ module ActiveRecord
def alter_table(table_name, options = {})
altered_table_name = "a#{table_name}"
- caller = lambda { |definition| yield definition if block_given? }
+ foreign_keys = foreign_keys(table_name)
+
+ caller = lambda do |definition|
+ rename = options[:rename] || {}
+ foreign_keys.each do |fk|
+ if column = rename[fk.options[:column]]
+ fk.options[:column] = column
+ end
+ definition.foreign_key(fk.to_table, fk.options)
+ end
+
+ yield definition if block_given?
+ end
transaction do
- move_table(table_name, altered_table_name,
- options.merge(temporary: true))
- move_table(altered_table_name, table_name, &caller)
+ disable_referential_integrity do
+ move_table(table_name, altered_table_name, options.merge(temporary: true))
+ move_table(altered_table_name, table_name, &caller)
+ end
end
end
@@ -442,6 +465,7 @@ module ActiveRecord
primary_key: column_name == from_primary_key
)
end
+
yield @definition if block_given?
end
copy_table_indexes(from, to, options[:rename] || {})
@@ -453,9 +477,6 @@ module ActiveRecord
def copy_table_indexes(from, to, rename = {})
indexes(from).each do |index|
name = index.name
- # indexes sqlite creates for internal use start with `sqlite_` and
- # don't need to be copied
- next if name.starts_with?("sqlite_")
if to == "a#{from}"
name = "t#{name}"
elsif from == "a#{to}"
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index e1a0b2ecf8..c983bc0d93 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -99,7 +99,7 @@ module ActiveRecord
##
# :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
+ # db:migrate rails command. 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, default: true
@@ -139,11 +139,6 @@ module ActiveRecord
end
module ClassMethods # :nodoc:
- def allocate
- define_attribute_methods
- super
- end
-
def initialize_find_by_cache # :nodoc:
@find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new }
end
@@ -350,6 +345,28 @@ module ActiveRecord
end
##
+ # Initializer used for instantiating objects that have been read from the
+ # database. +attributes+ should be an attributes object, and unlike the
+ # `initialize` method, no assignment calls are made per attribute.
+ #
+ # :nodoc:
+ def init_from_db(attributes)
+ init_internals
+
+ @new_record = false
+ @attributes = attributes
+
+ self.class.define_attribute_methods
+
+ yield self if block_given?
+
+ _run_find_callbacks
+ _run_initialize_callbacks
+
+ self
+ end
+
+ ##
# :method: clone
# Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied.
# That means that modifying attributes of the clone will modify the original, since they will both point to the
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index ee4f818cbf..c7f0077a76 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -47,8 +47,12 @@ module ActiveRecord
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
- updates = { counter_name.to_sym => object.send(counter_association).count(:all) }
- updates.merge!(touch_updates(touch)) if touch
+ updates = { counter_name => object.send(counter_association).count(:all) }
+
+ if touch
+ names = touch if touch != true
+ updates.merge!(touch_attributes_with_time(*names))
+ end
unscoped.where(primary_key => object.id).update_all(updates)
end
@@ -68,8 +72,8 @@ module ActiveRecord
# * +counters+ - A Hash containing the names of the fields
# to update as keys and the amount to update the field by as values.
# * <tt>:touch</tt> option - Touch timestamp columns when updating.
- # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
- # touch that column or an array of symbols to touch just those ones.
+ # If attribute names are passed, they are updated along with updated_at/on
+ # attributes.
#
# ==== Examples
#
@@ -98,20 +102,7 @@ module ActiveRecord
# # `updated_at` = '2016-10-13T09:59:23-05:00'
# # WHERE id IN (10, 15)
def update_counters(id, counters)
- touch = counters.delete(:touch)
-
- updates = counters.map do |counter_name, value|
- operator = value < 0 ? "-" : "+"
- quoted_column = connection.quote_column_name(counter_name)
- "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
- end
-
- if touch
- touch_updates = touch_updates(touch)
- updates << sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty?
- end
-
- unscoped.where(primary_key => id).update_all updates.join(", ")
+ unscoped.where!(primary_key => id).update_counters(counters)
end
# Increment a numeric field by one, via a direct SQL update.
@@ -165,13 +156,6 @@ module ActiveRecord
def decrement_counter(counter_name, id, touch: nil)
update_counters(id, counter_name => -1, touch: touch)
end
-
- private
- def touch_updates(touch)
- touch = timestamp_attributes_for_update_in_model if touch == true
- touch_time = current_time_from_proper_timezone
- Array(touch).map { |column| [ column, touch_time ] }.to_h
- end
end
private
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
new file mode 100644
index 0000000000..ffeed45030
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module DatabaseConfigurations # :nodoc:
+ class DatabaseConfig
+ attr_reader :env_name, :spec_name, :config
+
+ def initialize(env_name, spec_name, config)
+ @env_name = env_name
+ @spec_name = spec_name
+ @config = config
+ end
+ end
+
+ # Selects the config for the specified environment and specification name
+ #
+ # For example if passed :development, and :animals it will select the database
+ # under the :development and :animals configuration level
+ def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc:
+ configs_for(environment, configs).find do |db_config|
+ db_config.spec_name == specification_name
+ end
+ end
+
+ # Collects the configs for the environment passed in.
+ #
+ # If a block is given returns the specification name and configuration
+ # otherwise returns an array of DatabaseConfig structs for the environment.
+ def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc:
+ env_with_configs = db_configs(configs).select do |db_config|
+ db_config.env_name == env
+ end
+
+ if block_given?
+ env_with_configs.each do |env_with_config|
+ yield env_with_config.spec_name, env_with_config.config
+ end
+ else
+ env_with_configs
+ end
+ end
+
+ # Given an env, spec and config creates DatabaseConfig structs with
+ # each attribute set.
+ def self.walk_configs(env_name, spec_name, config) # :nodoc:
+ if config["database"] || config["url"] || config["adapter"]
+ DatabaseConfig.new(env_name, spec_name, config)
+ else
+ config.each_pair.map do |sub_spec_name, sub_config|
+ walk_configs(env_name, sub_spec_name, sub_config)
+ end
+ end
+ end
+
+ # Walks all the configs passed in and returns an array
+ # of DatabaseConfig structs for each configuration.
+ def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc:
+ configs.each_pair.flat_map do |env_name, config|
+ walk_configs(env_name, "primary", config)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index c2a180c939..f61bc7b9e8 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -111,7 +111,8 @@ module ActiveRecord
class RecordNotUnique < WrappedDatabaseException
end
- # Raised when a record cannot be inserted or updated because it references a non-existent record.
+ # Raised when a record cannot be inserted or updated because it references a non-existent record,
+ # or when a record cannot be deleted because a parent record references it.
class InvalidForeignKey < WrappedDatabaseException
end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 8f022ff7a7..6e0f1a0dfb 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -179,8 +179,8 @@ module ActiveRecord
# end
# end
#
- # If you preload your test database with all fixture data (probably in the rake task) and use
- # transactional tests, then you may omit all fixtures declarations in your test cases since
+ # If you preload your test database with all fixture data (probably by running `rails db:fixtures:load`)
+ # and use transactional tests, then you may omit all fixtures declarations in your test cases since
# all the data's already there and every case rolls back its changes.
#
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 208ba95c52..6891c575c7 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -55,7 +55,7 @@ module ActiveRecord
if has_attribute?(inheritance_column)
subclass = subclass_from_attributes(attributes)
- if subclass.nil? && base_class == self
+ if subclass.nil? && base_class?
subclass = subclass_from_attributes(column_defaults)
end
end
@@ -104,6 +104,12 @@ module ActiveRecord
end
end
+ # Returns whether the class is a base class.
+ # See #base_class for more information.
+ def base_class?
+ base_class == self
+ end
+
# Set this to +true+ if this is an abstract class (see
# <tt>abstract_class?</tt>).
# If you are using inheritance with Active Record and don't want a class
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 7f096bb532..1030b2368b 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -165,7 +165,7 @@ module ActiveRecord
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|
+ decorate_matching_attribute_types(is_lock_column, "_optimistic_locking") do |type|
LockingType.new(type)
end
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index bb85c47e06..5d1d15c94d 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -62,7 +62,7 @@ module ActiveRecord
# the locked record.
def lock!(lock = true)
if persisted?
- if changed?
+ if has_changes_to_save?
raise(<<-MSG.squish)
Locking a record with unpersisted changes is not supported. Use
`save` to persist the changes, or `reload` to discard them
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 9234029c22..6b84431343 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -4,6 +4,8 @@ module ActiveRecord
class LogSubscriber < ActiveSupport::LogSubscriber
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
+ class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new
+
def self.runtime=(value)
ActiveRecord::RuntimeRegistry.sql_runtime = value
end
@@ -100,36 +102,15 @@ module ActiveRecord
end
def log_query_source
- source_line, line_number = extract_callstack(caller_locations)
-
- if source_line
- if defined?(::Rails.root)
- app_root = "#{::Rails.root.to_s}/".freeze
- source_line = source_line.sub(app_root, "")
- end
-
- logger.debug(" ↳ #{ source_line }:#{ line_number }")
- end
- end
+ source = extract_query_source_location(caller)
- def extract_callstack(callstack)
- line = callstack.find do |frame|
- frame.absolute_path && !ignored_callstack(frame.absolute_path)
+ if source
+ logger.debug(" ↳ #{source}")
end
-
- offending_line = line || callstack.first
-
- [
- offending_line.path,
- offending_line.lineno
- ]
end
- RAILS_GEM_ROOT = File.expand_path("../../../..", __FILE__) + "/"
-
- def ignored_callstack(path)
- path.start_with?(RAILS_GEM_ROOT) ||
- path.start_with?(RbConfig::CONFIG["rubylibdir"])
+ def extract_query_source_location(locations)
+ backtrace_cleaner.clean(locations).first
end
end
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 663b3c590a..d68ed3cad9 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require "benchmark"
require "set"
require "zlib"
require "active_support/core_ext/module/attribute_accessors"
@@ -129,9 +130,9 @@ module ActiveRecord
class PendingMigrationError < MigrationError#:nodoc:
def initialize(message = nil)
if !message && defined?(Rails.env)
- super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate RAILS_ENV=#{::Rails.env}")
+ super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate RAILS_ENV=#{::Rails.env}")
elsif !message
- super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate")
+ super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate")
else
super
end
@@ -149,7 +150,7 @@ module ActiveRecord
class NoEnvironmentInSchemaError < MigrationError #:nodoc:
def initialize
- msg = "Environment data not found in the schema. To resolve this issue, run: \n\n bin/rails db:environment:set"
+ msg = "Environment data not found in the schema. To resolve this issue, run: \n\n rails db:environment:set"
if defined?(Rails.env)
super("#{msg} RAILS_ENV=#{::Rails.env}")
else
@@ -172,7 +173,7 @@ module ActiveRecord
msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n".dup
msg << "You are running in `#{ current }` environment. "
msg << "If you are sure you want to continue, first set the environment using:\n\n"
- msg << " bin/rails db:environment:set"
+ msg << " rails db:environment:set"
if defined?(Rails.env)
super("#{msg} RAILS_ENV=#{::Rails.env}\n\n")
else
@@ -351,7 +352,7 @@ module ActiveRecord
# <tt>rails db:migrate</tt>. This will update the database by running all of the
# pending migrations, creating the <tt>schema_migrations</tt> table
# (see "About the schema_migrations table" section below) if missing. It will also
- # invoke the db:schema:dump task, which will update your db/schema.rb file
+ # invoke the db:schema:dump command, which will update your db/schema.rb file
# to match the structure of your database.
#
# To roll the database back to a previous migration version, use
@@ -830,10 +831,14 @@ module ActiveRecord
write "== %s %s" % [text, "=" * length]
end
+ # Takes a message argument and outputs it as is.
+ # A second boolean argument can be passed to specify whether to indent or not.
def say(message, subitem = false)
write "#{subitem ? " ->" : "--"} #{message}"
end
+ # Outputs text along with how long it took to run its block.
+ # If the block returns an integer it assumes it is the number of rows affected.
def say_with_time(message)
say(message)
result = nil
@@ -843,6 +848,7 @@ module ActiveRecord
result
end
+ # Takes a block as an argument and suppresses any output generated by the block.
def suppress_messages
save, self.verbose = verbose, false
yield
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 087632b10f..dea6d4ec08 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -214,11 +214,24 @@ module ActiveRecord
end
def invert_remove_foreign_key(args)
- from_table, to_table, remove_options = args
- raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash)
+ from_table, options_or_to_table, options_or_nil = args
+
+ to_table = if options_or_to_table.is_a?(Hash)
+ options_or_to_table[:to_table]
+ else
+ options_or_to_table
+ end
+
+ remove_options = if options_or_to_table.is_a?(Hash)
+ options_or_to_table.except(:to_table)
+ else
+ options_or_nil
+ end
+
+ raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil?
reversed_args = [from_table, to_table]
- reversed_args << remove_options if remove_options
+ reversed_args << remove_options if remove_options.present?
[:add_foreign_key, reversed_args]
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index b04dc04899..694ff85fa1 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -276,7 +276,7 @@ module ActiveRecord
end
def sequence_name
- if base_class == self
+ if base_class?
@sequence_name ||= reset_sequence_name
else
(@sequence_name ||= nil) || base_class.sequence_name
@@ -501,8 +501,7 @@ module ActiveRecord
# Computes and returns a table name according to default conventions.
def compute_table_name
- base = base_class
- if self == base
+ if base_class?
# Nested classes are prefixed with singular parent table name.
if parent < Base && !parent.abstract_class?
contained = parent.table_name
@@ -513,7 +512,7 @@ module ActiveRecord
"#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
else
# STI subclasses always use their superclass' table.
- base.table_name
+ base_class.table_name
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index fa20bce3a9..50767ee93f 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -501,7 +501,7 @@ module ActiveRecord
if attributes["id"].blank?
unless reject_new_record?(association_name, attributes)
- association.build(attributes.except(*UNASSIGNABLE_KEYS))
+ association.reader.build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes["id"].to_s }
unless call_reject_if(association_name, attributes)
diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb
index 754c891884..697076bdae 100644
--- a/activerecord/lib/active_record/no_touching.rb
+++ b/activerecord/lib/active_record/no_touching.rb
@@ -43,6 +43,13 @@ module ActiveRecord
end
end
+ # Returns +true+ if the class has +no_touching+ set, +false+ otherwise.
+ #
+ # Project.no_touching do
+ # Project.first.no_touching? # true
+ # Message.first.no_touching? # false
+ # end
+ #
def no_touching?
NoTouching.applied_to?(self.class)
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 7721e6b691..05963e5546 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -67,8 +67,18 @@ module ActiveRecord
# how this "single-table" inheritance mapping is implemented.
def instantiate(attributes, column_types = {}, &block)
klass = discriminate_class_for_record(attributes)
+ instantiate_instance_of(klass, attributes, column_types, &block)
+ end
+
+ # Given a class, an attributes hash, +instantiate_instance_of+ returns a
+ # new instance of the class. Accepts only keys as strings.
+ #
+ # This is private, don't call it. :)
+ #
+ # :nodoc:
+ def instantiate_instance_of(klass, attributes, column_types = {}, &block)
attributes = klass.attributes_builder.build_from_database(attributes, column_types)
- klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block)
+ klass.allocate.init_from_db(attributes, &block)
end
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -97,13 +107,11 @@ module ActiveRecord
# When running callbacks is not needed for each record update,
# it is preferred to use {update_all}[rdoc-ref:Relation#update_all]
# for updating all records in a single query.
- def update(id = :all, attributes)
+ def update(id, attributes)
if id.is_a?(Array)
id.map { |one_id| find(one_id) }.each_with_index { |object, idx|
object.update(attributes[idx])
}
- elsif id == :all
- all.each { |record| record.update(attributes) }
else
if ActiveRecord::Base === id
raise ArgumentError,
@@ -178,7 +186,7 @@ module ActiveRecord
end
if values.empty?
- im = arel_table.compile_insert(connection.empty_insert_statement_value)
+ im = arel_table.compile_insert(connection.empty_insert_statement_value(primary_key))
im.into arel_table
else
im = arel_table.compile_insert(_substitute_values(values))
@@ -373,7 +381,7 @@ module ActiveRecord
became = klass.allocate
became.send(:initialize)
became.instance_variable_set("@attributes", @attributes)
- became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database)
+ became.instance_variable_set("@mutations_from_database", @mutations_from_database ||= nil)
became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index c8e340712d..28194c7c46 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -26,19 +26,12 @@ module ActiveRecord
end
def self.run
- ActiveRecord::Base.connection_handler.connection_pool_list.map do |pool|
- caching_was_enabled = pool.query_cache_enabled
-
- pool.enable_query_cache!
-
- [pool, caching_was_enabled]
- end
+ ActiveRecord::Base.connection_handler.connection_pool_list.
+ reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! }
end
- def self.complete(caching_pools)
- caching_pools.each do |pool, caching_was_enabled|
- pool.disable_query_cache! unless caching_was_enabled
- end
+ def self.complete(pools)
+ pools.each { |pool| pool.disable_query_cache! }
ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
pool.release_connection if pool.active_connection? && !pool.connection.transaction_open?
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index d33d36ac02..c84f3d0fbb 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -49,7 +49,12 @@ module ActiveRecord
}
message_bus.instrument("instantiation.active_record", payload) do
- result_set.map { |record| instantiate(record, column_types, &block) }
+ if result_set.includes_column?(inheritance_column)
+ result_set.map { |record| instantiate(record, column_types, &block) }
+ else
+ # Instantiate a homogeneous set
+ result_set.map { |record| instantiate_instance_of(self, record, column_types, &block) }
+ end
end
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 6ab80a654d..7ece083fd4 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -77,6 +77,10 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger }
end
+ initializer "active_record.backtrace_cleaner" do
+ ActiveSupport.on_load(:active_record) { LogSubscriber.backtrace_cleaner = ::Rails.backtrace_cleaner }
+ end
+
initializer "active_record.migration_error" do
if config.active_record.delete(:migration_error) == :page_load
config.app_middleware.insert_after ::ActionDispatch::Callbacks,
@@ -141,8 +145,8 @@ 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/rails db:create` to create the database.
- 3. Run `bin/rails db:setup` to load your database schema.
+ 2. Run `rails db:create` to create the database.
+ 3. Run `rails db:setup` to load your database schema.
end_warning
raise
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 662a8bc720..8b7d18fb3d 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -22,6 +22,14 @@ db_namespace = namespace :db do
task all: :load_config do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
+
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Create #{spec_name} database for current environment"
+ task spec_name => :load_config do
+ db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
+ end
+ end
end
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 or when RAILS_ENV is development, it defaults to creating the development and test databases."
@@ -33,6 +41,14 @@ db_namespace = namespace :db do
task all: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
+
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Drop #{spec_name} database for current environment"
+ task spec_name => [:load_config, :check_protected_environments] do
+ db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config)
+ end
+ end
end
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 or when RAILS_ENV is development, it defaults to dropping the development and test databases."
@@ -57,7 +73,10 @@ db_namespace = namespace :db do
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task migrate: :load_config do
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
db_namespace["_dump"].invoke
end
@@ -77,6 +96,15 @@ db_namespace = namespace :db do
end
namespace :migrate do
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Migrate #{spec_name} database for current environment"
+ task spec_name => :load_config do
+ db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+ end
+
# desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
task redo: :load_config do
raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty?
@@ -189,7 +217,7 @@ db_namespace = namespace :db do
task setup: ["db:schema:load_if_ruby", "db:structure:load_if_sql", :seed]
desc "Loads the seed data from db/seeds.rb"
- task :seed do
+ task seed: :load_config do
db_namespace["abort_if_pending_migrations"].invoke
ActiveRecord::Tasks::DatabaseTasks.load_seed
end
@@ -246,10 +274,15 @@ db_namespace = namespace :db do
desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record"
task dump: :load_config do
require "active_record/schema_dumper"
- 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)
+
+ ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby)
+ File.open(filename, "w:utf-8") do |file|
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
+ end
end
+
db_namespace["schema:dump"].reenable
end
@@ -276,22 +309,24 @@ db_namespace = namespace :db do
rm_f filename, verbose: false
end
end
-
end
namespace :structure do
desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql"
task dump: :load_config do
- filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
-
- if ActiveRecord::SchemaMigration.table_exists?
- File.open(filename, "a") do |f|
- f.puts ActiveRecord::Base.connection.dump_schema_information
- f.print "\n"
+ ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
+ ActiveRecord::Base.establish_connection(config)
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql)
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename)
+
+ if ActiveRecord::SchemaMigration.table_exists?
+ File.open(filename, "a") do |f|
+ f.puts ActiveRecord::Base.connection.dump_schema_information
+ f.print "\n"
+ end
end
end
+
db_namespace["structure:dump"].reenable
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 2f43d005f3..6d2f75a3ae 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -13,33 +13,37 @@ module ActiveRecord
class_attribute :aggregate_reflections, instance_writer: false, default: {}
end
- 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
+ class << self
+ def create(macro, name, scope, options, ar)
+ reflection = reflection_class_for(macro).new(name, scope, options, ar)
+ options[:through] ? ThroughReflection.new(reflection) : reflection
+ end
- reflection = klass.new(name, scope, options, ar)
- options[:through] ? ThroughReflection.new(reflection) : reflection
- end
+ def add_reflection(ar, name, reflection)
+ ar.clear_reflections_cache
+ name = name.to_s
+ ar._reflections = ar._reflections.except(name).merge!(name => reflection)
+ end
- def self.add_reflection(ar, name, reflection)
- ar.clear_reflections_cache
- name = name.to_s
- ar._reflections = ar._reflections.except(name).merge!(name => reflection)
- end
+ def add_aggregate_reflection(ar, name, reflection)
+ ar.aggregate_reflections = ar.aggregate_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)
+ private
+ def reflection_class_for(macro)
+ case macro
+ when :composed_of
+ AggregateReflection
+ when :has_many
+ HasManyReflection
+ when :has_one
+ HasOneReflection
+ when :belongs_to
+ BelongsToReflection
+ else
+ raise "Unsupported Macro: #{macro}"
+ end
+ end
end
# \Reflection enables the ability to examine the associations and aggregations of
@@ -417,7 +421,7 @@ module ActiveRecord
class AssociationReflection < MacroReflection #:nodoc:
def compute_class(name)
if polymorphic?
- raise ArgumentError, "Polymorphic association does not support to compute class."
+ raise ArgumentError, "Polymorphic associations do not support computing the class."
end
active_record.send(:compute_type, name)
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 34e643b2de..2d3e1eaa08 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -8,8 +8,8 @@ module ActiveRecord
:extending, :unscope]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
- :reverse_order, :distinct, :create_with, :skip_query_cache,
- :skip_preloading]
+ :reverse_order, :distinct, :create_with, :skip_query_cache]
+
CLAUSE_METHODS = [:where, :having, :from]
INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having]
@@ -19,6 +19,7 @@ module ActiveRecord
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded, :predicate_builder
+ attr_accessor :skip_preloading_value
alias :model :klass
alias :loaded? :loaded
alias :locked? :lock_value
@@ -30,6 +31,7 @@ module ActiveRecord
@offsets = {}
@loaded = false
@predicate_builder = predicate_builder
+ @delegate_to_klass = false
end
def initialize_copy(other)
@@ -41,6 +43,12 @@ module ActiveRecord
klass.arel_attribute(name, table)
end
+ def bind_attribute(name, value) # :nodoc:
+ attr = arel_attribute(name)
+ bind = predicate_builder.build_bind_attribute(attr.name, value)
+ yield attr, bind
+ end
+
# Initializes new record from relation while maintaining the current
# scope.
#
@@ -215,7 +223,7 @@ module ActiveRecord
# are needed by the next ones when eager loading is going on.
#
# Please see further details in the
- # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
+ # {Active Record Query Interface guide}[https://guides.rubyonrails.org/active_record_querying.html#running-explain].
def explain
exec_explain(collecting_queries_for_explain { exec_queries })
end
@@ -307,10 +315,17 @@ module ActiveRecord
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
def scoping
- previous, klass.current_scope = klass.current_scope(true), self
+ previous, klass.current_scope = klass.current_scope(true), self unless @delegate_to_klass
yield
ensure
- klass.current_scope = previous
+ klass.current_scope = previous unless @delegate_to_klass
+ end
+
+ def _exec_scope(*args, &block) # :nodoc:
+ @delegate_to_klass = true
+ instance_exec(*args, &block) || self
+ ensure
+ @delegate_to_klass = false
end
# Updates all records in the current relation with details given. This method constructs a single SQL UPDATE
@@ -360,6 +375,62 @@ module ActiveRecord
@klass.connection.update stmt, "#{@klass} Update All"
end
+ def update(attributes) # :nodoc:
+ each { |record| record.update(attributes) }
+ end
+
+ def update_counters(counters) # :nodoc:
+ touch = counters.delete(:touch)
+
+ updates = counters.map do |counter_name, value|
+ operator = value < 0 ? "-" : "+"
+ quoted_column = connection.quote_column_name(counter_name)
+ "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
+ end
+
+ if touch
+ names = touch if touch != true
+ touch_updates = klass.touch_attributes_with_time(*names)
+ updates << klass.sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty?
+ end
+
+ update_all updates.join(", ")
+ end
+
+ # Touches all records in the current relation without instantiating records first with the updated_at/on attributes
+ # set to the current time or the time specified.
+ # This method can be passed attribute names and an optional time argument.
+ # If attribute names are passed, they are updated along with updated_at/on attributes.
+ # If no time argument is passed, the current time is used as default.
+ #
+ # === Examples
+ #
+ # # Touch all records
+ # Person.all.touch_all
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670'"
+ #
+ # # Touch multiple records with a custom attribute
+ # Person.all.touch_all(:created_at)
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670', \"created_at\" = '2018-01-04 22:55:23.132670'"
+ #
+ # # Touch multiple records with a specified time
+ # Person.all.touch_all(time: Time.new(2020, 5, 16, 0, 0, 0))
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2020-05-16 00:00:00'"
+ #
+ # # Touch records with scope
+ # Person.where(name: 'David').touch_all
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'"
+ def touch_all(*names, time: nil)
+ updates = touch_attributes_with_time(*names, time: time)
+
+ if klass.locking_enabled?
+ quoted_locking_column = connection.quote_column_name(klass.locking_column)
+ updates = sanitize_sql_for_assignment(updates) + ", #{quoted_locking_column} = COALESCE(#{quoted_locking_column}, 0) + 1"
+ end
+
+ update_all(updates)
+ end
+
# Destroys the records by instantiating each
# record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method.
# Each object's callbacks are executed (including <tt>:dependent</tt> association options).
@@ -446,6 +517,7 @@ module ActiveRecord
end
def reset
+ @delegate_to_klass = false
@to_sql = @arel = @loaded = @should_eager_load = nil
@records = [].freeze
@offsets = {}
@@ -458,17 +530,16 @@ module ActiveRecord
# # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
def to_sql
@to_sql ||= begin
- relation = self
-
- if eager_loading?
- apply_join_dependency { |rel, _| relation = rel }
- end
-
- conn = klass.connection
- conn.unprepared_statement {
- conn.to_sql(relation.arel)
- }
- end
+ if eager_loading?
+ apply_join_dependency do |relation, join_dependency|
+ relation = join_dependency.apply_column_aliases(relation)
+ relation.to_sql
+ end
+ else
+ conn = klass.connection
+ conn.unprepared_statement { conn.to_sql(arel) }
+ end
+ end
end
# Returns a hash of where conditions.
@@ -547,7 +618,7 @@ module ActiveRecord
ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins)
end
- def preload_associations(records)
+ def preload_associations(records) # :nodoc:
preload = preload_values
preload += includes_values unless eager_loading?
preloader = nil
@@ -578,6 +649,7 @@ module ActiveRecord
if ActiveRecord::NullRelation === relation
[]
else
+ relation = join_dependency.apply_column_aliases(relation)
rows = connection.select_all(relation.arel, "SQL")
join_dependency.instantiate(rows, &block)
end.freeze
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index ec4bb06c57..9c579843b1 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -251,8 +251,9 @@ module ActiveRecord
end
end
- bind = primary_key_bind(primary_key_offset)
- batch_relation = relation.where(arel_attribute(primary_key).gt(bind))
+ batch_relation = relation.where(
+ bind_attribute(primary_key, primary_key_offset) { |attr, bind| attr.gt(bind) }
+ )
end
end
@@ -265,15 +266,11 @@ module ActiveRecord
end
def apply_start_limit(relation, start)
- relation.where(arel_attribute(primary_key).gteq(primary_key_bind(start)))
+ relation.where(bind_attribute(primary_key, start) { |attr, bind| attr.gteq(bind) })
end
def apply_finish_limit(relation, finish)
- relation.where(arel_attribute(primary_key).lteq(primary_key_bind(finish)))
- end
-
- def primary_key_bind(value)
- predicate_builder.build_bind_attribute(primary_key, value)
+ relation.where(bind_attribute(primary_key, finish) { |attr, bind| attr.lteq(bind) })
end
def batch_order
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index f215c95f51..40fe39fa9d 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -245,7 +245,7 @@ module ActiveRecord
if distinct && (group_values.any? || select_values.empty? && order_values.empty?)
column_name = primary_key
end
- elsif column_name =~ /\s*DISTINCT[\s(]+/i
+ elsif /\s*DISTINCT[\s(]+/i.match?(column_name.to_s)
distinct = nil
end
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 4b16b49cdf..488f71cdde 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -82,6 +82,11 @@ module ActiveRecord
if @klass.respond_to?(method)
self.class.delegate_to_scoped_klass(method)
scoping { @klass.public_send(method, *args, &block) }
+ elsif @delegate_to_klass && @klass.respond_to?(method, true)
+ ActiveSupport::Deprecation.warn \
+ "Delegating missing #{method} method to #{@klass}. " \
+ "Accessibility of private/protected class methods in :scope is deprecated and will be removed in Rails 6.0."
+ @klass.send(method, *args, &block)
elsif arel.respond_to?(method)
ActiveSupport::Deprecation.warn \
"Delegating #{method} to arel is deprecated and will be removed in Rails 6.0."
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index f7613a187d..c5562c1ff0 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -371,16 +371,14 @@ module ActiveRecord
relation
end
- def construct_join_dependency
- including = eager_load_values + includes_values
- joins = joins_values.select { |join| join.is_a?(Arel::Nodes::Join) }
+ def construct_join_dependency(associations)
ActiveRecord::Associations::JoinDependency.new(
- klass, table, including, alias_tracker(joins)
+ klass, table, associations
)
end
- def apply_join_dependency(eager_loading: true)
- join_dependency = construct_join_dependency
+ def apply_join_dependency(eager_loading: group_values.empty?)
+ join_dependency = construct_join_dependency(eager_load_values + includes_values)
relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
if eager_loading && !using_limitable_reflections?(join_dependency.reflections)
@@ -392,7 +390,6 @@ module ActiveRecord
end
if block_given?
- relation._select!(join_dependency.aliases.columns)
yield relation, join_dependency
else
relation
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 25510d4a57..07b16a0740 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -117,14 +117,10 @@ module ActiveRecord
if other.klass == relation.klass
relation.joins!(*other.joins_values)
else
- alias_tracker = nil
joins_dependency = other.joins_values.map do |join|
case join
when Hash, Symbol, Array
- alias_tracker ||= other.alias_tracker
- ActiveRecord::Associations::JoinDependency.new(
- other.klass, other.table, join, alias_tracker
- )
+ other.send(:construct_join_dependency, join)
else
join
end
@@ -140,14 +136,10 @@ module ActiveRecord
if other.klass == relation.klass
relation.left_outer_joins!(*other.left_outer_joins_values)
else
- alias_tracker = nil
joins_dependency = other.left_outer_joins_values.map do |join|
case join
when Hash, Symbol, Array
- alias_tracker ||= other.alias_tracker
- ActiveRecord::Associations::JoinDependency.new(
- other.klass, other.table, join, alias_tracker
- )
+ other.send(:construct_join_dependency, join)
else
join
end
@@ -160,10 +152,10 @@ module ActiveRecord
def merge_multi_values
if other.reordering_value
# override any order specified in the original relation
- relation.reorder! other.order_values
+ relation.reorder!(*other.order_values)
elsif other.order_values.any?
# merge in order_values from relation
- relation.order! other.order_values
+ relation.order!(*other.order_values)
end
extensions = other.extensions - relation.extensions
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 7a0edcbc33..f734cd0ad8 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -48,7 +48,12 @@ module ActiveRecord
end
def build(attribute, value)
- handler_for(value).call(attribute, value)
+ if table.type(attribute.name).force_equality?(value)
+ bind = build_bind_attribute(attribute.name, value)
+ attribute.eq(bind)
+ else
+ handler_for(value).call(attribute, value)
+ end
end
def build_bind_attribute(column_name, value)
@@ -95,10 +100,6 @@ module ActiveRecord
end.reduce(&:and)
end
queries.reduce(&:or)
- # FIXME: Deprecate this and provide a public API to force equality
- elsif (value.is_a?(Range) || value.is_a?(Array)) &&
- table.type(key.to_s).respond_to?(:subtype)
- BasicObjectHandler.new(self).call(table.arel_attribute(key), value)
else
build(table.arel_attribute(key), value)
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
index 64bf83e3c1..e5191fa38a 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/array/extract"
+
module ActiveRecord
class PredicateBuilder
class ArrayHandler # :nodoc:
@@ -11,8 +13,8 @@ module ActiveRecord
return attribute.in([]) if value.empty?
values = value.map { |x| x.is_a?(Base) ? x.id : x }
- nils, values = values.partition(&:nil?)
- ranges, values = values.partition { |v| v.is_a?(Range) }
+ nils = values.extract!(&:nil?)
+ ranges = values.extract! { |v| v.is_a?(Range) }
values_predicate =
case values.length
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 4e60863e52..52405f21a1 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -894,8 +894,8 @@ module ActiveRecord
self
end
- def skip_query_cache! # :nodoc:
- self.skip_query_cache_value = true
+ def skip_query_cache!(value = true) # :nodoc:
+ self.skip_query_cache_value = value
self
end
@@ -909,11 +909,12 @@ module ActiveRecord
@arel ||= build_arel(aliases)
end
+ # Returns a relation value with a given name
+ def get_value(name) # :nodoc:
+ @values.fetch(name, DEFAULT_VALUES[name])
+ end
+
protected
- # Returns a relation value with a given name
- def get_value(name) # :nodoc:
- @values.fetch(name, DEFAULT_VALUES[name])
- end
# Sets the relation value with the given name
def set_value(name, value) # :nodoc:
@@ -1017,19 +1018,17 @@ module ActiveRecord
def build_join_query(manager, buckets, join_type, aliases)
buckets.default = []
- association_joins = buckets[:association_join]
- stashed_association_joins = buckets[:stashed_join]
- join_nodes = buckets[:join_node].uniq
- string_joins = buckets[:string_join].map(&:strip).uniq
+ association_joins = buckets[:association_join]
+ stashed_joins = buckets[:stashed_join]
+ join_nodes = buckets[:join_node].uniq
+ string_joins = buckets[:string_join].map(&:strip).uniq
join_list = join_nodes + convert_join_strings_to_ast(string_joins)
alias_tracker = alias_tracker(join_list, aliases)
- join_dependency = ActiveRecord::Associations::JoinDependency.new(
- klass, table, association_joins, alias_tracker
- )
+ join_dependency = construct_join_dependency(association_joins)
- joins = join_dependency.join_constraints(stashed_association_joins, join_type)
+ joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker)
joins.each { |join| manager.from(join) }
manager.join_sources.concat(join_list)
@@ -1055,11 +1054,13 @@ module ActiveRecord
end
def arel_columns(columns)
- columns.map do |field|
+ columns.flat_map do |field|
if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value
arel_attribute(field)
elsif Symbol === field
connection.quote_table_name(field.to_s)
+ elsif Proc === field
+ field.call
else
field
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index e54e8086dd..7f1c2fd7eb 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -43,6 +43,11 @@ module ActiveRecord
@column_types = column_types
end
+ # Returns true if this result set includes the column named +name+
+ def includes_column?(name)
+ @columns.include? name
+ end
+
# Returns the number of elements in the rows array.
def length
@rows.length
@@ -97,12 +102,21 @@ module ActiveRecord
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.deserialize(value) }
- end
+ if columns.one?
+ # Separated to avoid allocating an array per row
- columns.one? ? result.map!(&:first) : result
+ type = column_type(columns.first, type_overrides)
+
+ rows.map do |(value)|
+ type.deserialize(value)
+ end
+ else
+ types = columns.map { |name| column_type(name, type_overrides) }
+
+ rows.map do |values|
+ Array.new(values.size) { |i| types[i].deserialize(values[i]) }
+ end
+ end
end
def initialize_copy(other)
@@ -125,7 +139,7 @@ module ActiveRecord
begin
# We freeze the strings to prevent them getting duped when
# used as keys in ActiveRecord::Base's @attributes hash
- columns = @columns.map { |c| c.dup.freeze }
+ columns = @columns.map(&:-@)
@rows.map { |row|
# In the past we used Hash[columns.zip(row)]
# though elegant, the verbose way is much more efficient
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index b8d848b999..d475e77444 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -17,6 +17,12 @@ module ActiveRecord
# Only strings are accepted if ActiveRecord::Base.schema_format == :sql.
cattr_accessor :ignore_tables, default: []
+ ##
+ # :singleton-method:
+ # Specify a custom regular expression matching foreign keys which name
+ # should not be dumped to db/schema.rb.
+ cattr_accessor :fk_ignore_pattern, default: /^fk_rails_[0-9a-f]{10}$/
+
class << self
def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base)
connection.create_schema_dumper(generate_options(config)).dump(stream)
@@ -65,11 +71,11 @@ module ActiveRecord
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
-# Note that this schema.rb definition is the authoritative source for your
-# database schema. If you need to create the application database on another
-# system, you should be using db:schema:load, not running all the migrations
-# from scratch. The latter is a flawed and unsustainable approach (the more migrations
-# you'll amass, the slower it'll run and the greater likelihood for issues).
+# This file is the source Rails uses to define your schema when running `rails
+# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
@@ -210,7 +216,7 @@ HEADER
parts << "primary_key: #{foreign_key.primary_key.inspect}"
end
- if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
+ if foreign_key.export_name_on_schema_dump?
parts << "name: #{foreign_key.name.inspect}"
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 752655aa05..a784001587 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -183,7 +183,7 @@ module ActiveRecord
if body.respond_to?(:to_proc)
singleton_class.send(:define_method, name) do |*args|
scope = all
- scope = scope.instance_exec(*args, &body) || scope
+ scope = scope._exec_scope(*args, &body)
scope = scope.extending(extension) if extension
scope
end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 3290675338..3537e2d008 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -31,10 +31,18 @@ module ActiveRecord
#
# class User < ActiveRecord::Base
# store :settings, accessors: [ :color, :homepage ], coder: JSON
+ # store :parent, accessors: [ :name ], coder: JSON, prefix: true
+ # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner
+ # store :settings, accessors: [ :two_factor_auth ], suffix: true
+ # store :settings, accessors: [ :login_retry ], suffix: :config
# end
#
- # u = User.new(color: 'black', homepage: '37signals.com')
+ # u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily')
# u.color # Accessor stored attribute
+ # u.parent_name # Accessor stored attribute with prefix
+ # u.partner_name # Accessor stored attribute with custom prefix
+ # u.two_factor_auth_settings # Accessor stored attribute with suffix
+ # u.login_retry_config # Accessor stored attribute with custom suffix
# u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
#
# # There is no difference between strings and symbols for accessing custom attributes
@@ -44,11 +52,13 @@ module ActiveRecord
# # Add additional accessors to an existing store through store_accessor
# class SuperUser < User
# store_accessor :settings, :privileges, :servants
+ # store_accessor :parent, :birthday, prefix: true
+ # store_accessor :settings, :secret_question, suffix: :config
# end
#
# The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes].
#
- # User.stored_attributes[:settings] # [:color, :homepage]
+ # User.stored_attributes[:settings] # [:color, :homepage, :two_factor_auth, :login_retry]
#
# == Overwriting default accessors
#
@@ -81,19 +91,40 @@ module ActiveRecord
module ClassMethods
def store(store_attribute, options = {})
serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder])
- store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
+ store_accessor(store_attribute, options[:accessors], options.slice(:prefix, :suffix)) if options.has_key? :accessors
end
- def store_accessor(store_attribute, *keys)
+ def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil)
keys = keys.flatten
+ accessor_prefix =
+ case prefix
+ when String, Symbol
+ "#{prefix}_"
+ when TrueClass
+ "#{store_attribute}_"
+ else
+ ""
+ end
+ accessor_suffix =
+ case suffix
+ when String, Symbol
+ "_#{suffix}"
+ when TrueClass
+ "_#{store_attribute}"
+ else
+ ""
+ end
+
_store_accessors_module.module_eval do
keys.each do |key|
- define_method("#{key}=") do |value|
+ accessor_key = "#{accessor_prefix}#{key}#{accessor_suffix}"
+
+ define_method("#{accessor_key}=") do |value|
write_store_attribute(store_attribute, key, value)
end
- define_method(key) do
+ define_method(accessor_key) do
read_store_attribute(store_attribute, key)
end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index af1bbc7e93..fd36c0abd2 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -8,7 +8,7 @@ module ActiveRecord
# ActiveRecord::Tasks::DatabaseTasks 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.
+ # The tasks defined here are used with Rails commands 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
@@ -134,6 +134,18 @@ module ActiveRecord
end
end
+ def for_each
+ databases = Rails.application.config.load_database_yaml
+ database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases)
+
+ # if this is a single database application we don't want tasks for each primary database
+ return if database_configs.count == 1
+
+ database_configs.each do |db_config|
+ yield db_config.spec_name
+ end
+ end
+
def create_current(environment = env)
each_current_configuration(environment) { |configuration|
create configuration
@@ -233,9 +245,10 @@ module ActiveRecord
class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename, structure_load_flags)
end
- def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env) # :nodoc:
- file ||= schema_file(format)
+ def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary") # :nodoc:
+ file ||= dump_filename(spec_name, format)
+ verbose_was, Migration.verbose = Migration.verbose, verbose? && ENV["VERBOSE"]
check_schema_file(file)
ActiveRecord::Base.establish_connection(configuration)
@@ -249,20 +262,36 @@ module ActiveRecord
end
ActiveRecord::InternalMetadata.create_table
ActiveRecord::InternalMetadata[:environment] = environment
+ ensure
+ Migration.verbose = verbose_was
end
def schema_file(format = ActiveRecord::Base.schema_format)
+ File.join(db_dir, schema_file_type(format))
+ end
+
+ def schema_file_type(format = ActiveRecord::Base.schema_format)
case format
when :ruby
- File.join(db_dir, "schema.rb")
+ "schema.rb"
when :sql
- File.join(db_dir, "structure.sql")
+ "structure.sql"
end
end
+ def dump_filename(namespace, format = ActiveRecord::Base.schema_format)
+ filename = if namespace == "primary"
+ schema_file_type(format)
+ else
+ "#{namespace}_#{schema_file_type(format)}"
+ end
+
+ ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename)
+ end
+
def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
- each_current_configuration(environment) { |configuration, configuration_environment|
- load_schema configuration, format, file, configuration_environment
+ each_current_configuration(environment) { |configuration, spec_name, env|
+ load_schema(configuration, format, file, env, spec_name)
}
ActiveRecord::Base.establish_connection(environment.to_sym)
end
@@ -312,10 +341,10 @@ module ActiveRecord
environments = [environment]
environments << "test" if environment == "development"
- ActiveRecord::Base.configurations.slice(*environments).each do |configuration_environment, configuration|
- next unless configuration["database"]
-
- yield configuration, configuration_environment
+ environments.each do |env|
+ ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration|
+ yield configuration, spec_name, env
+ end
end
end
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index e697fa6def..eddc6fa223 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -68,9 +68,7 @@ module ActiveRecord
private
- def configuration
- @configuration
- end
+ attr_reader :configuration
def configuration_without_database
configuration.merge("database" => nil)
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 647e066137..533e6953a4 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -90,9 +90,7 @@ module ActiveRecord
private
- def configuration
- @configuration
- end
+ attr_reader :configuration
def encoding
configuration["encoding"] || DEFAULT_ENCODING
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index dfe599c4dd..d0bad05176 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -60,13 +60,7 @@ module ActiveRecord
private
- def configuration
- @configuration
- end
-
- def root
- @root
- end
+ attr_reader :configuration, :root
def run_cmd(cmd, args, out)
fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out)
diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb
index 606a3b0fb5..5b4efe22c9 100644
--- a/activerecord/lib/active_record/test_databases.rb
+++ b/activerecord/lib/active_record/test_databases.rb
@@ -5,28 +5,27 @@ require "active_support/testing/parallelization"
module ActiveRecord
module TestDatabases # :nodoc:
ActiveSupport::Testing::Parallelization.after_fork_hook do |i|
- create_and_migrate(i, spec_name: Rails.env)
+ create_and_load_schema(i, spec_name: Rails.env)
end
- ActiveSupport::Testing::Parallelization.run_cleanup_hook do |i|
- drop(i, spec_name: Rails.env)
+ ActiveSupport::Testing::Parallelization.run_cleanup_hook do |_|
+ drop(spec_name: Rails.env)
end
- def self.create_and_migrate(i, spec_name:)
+ def self.create_and_load_schema(i, spec_name:)
old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
connection_spec = ActiveRecord::Base.configurations[spec_name]
connection_spec["database"] += "-#{i}"
ActiveRecord::Tasks::DatabaseTasks.create(connection_spec)
- ActiveRecord::Base.establish_connection(connection_spec)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(connection_spec)
ensure
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
ENV["VERBOSE"] = old
end
- def self.drop(i, spec_name:)
+ def self.drop(spec_name:)
old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
connection_spec = ActiveRecord::Base.configurations[spec_name]
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 5da3759e5a..d32f971ad1 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -52,7 +52,13 @@ module ActiveRecord
clear_timestamp_attributes
end
- class_methods do
+ module ClassMethods # :nodoc:
+ def touch_attributes_with_time(*names, time: nil)
+ attribute_names = timestamp_attributes_for_update_in_model
+ attribute_names |= names.map(&:to_s)
+ attribute_names.index_with(time ||= current_time_from_proper_timezone)
+ end
+
private
def timestamp_attributes_for_create_in_model
timestamp_attributes_for_create.select { |c| column_names.include?(c) }
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 97cba5d1c7..c5d5fca672 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -306,9 +306,7 @@ module ActiveRecord
end
def save(*) #:nodoc:
- rollback_active_record_state! do
- with_transaction_returning_status { super }
- end
+ with_transaction_returning_status { super }
end
def save!(*) #:nodoc:
@@ -319,17 +317,6 @@ module ActiveRecord
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
- yield
- rescue Exception
- restore_transaction_record_state
- raise
- ensure
- clear_transaction_record_state
- end
-
def before_committed! # :nodoc:
_run_before_commit_without_transaction_enrollment_callbacks
_run_before_commit_callbacks
@@ -340,11 +327,13 @@ module ActiveRecord
# 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:
- if should_run_callbacks && destroyed? || persisted?
+ if should_run_callbacks && (destroyed? || persisted?)
+ @_committed_already_called = true
_run_commit_without_transaction_enrollment_callbacks
_run_commit_callbacks
end
ensure
+ @_committed_already_called = false
force_clear_transaction_record_state
end
@@ -382,13 +371,7 @@ module ActiveRecord
status = nil
self.class.transaction do
add_to_transaction
- begin
- status = yield
- rescue ActiveRecord::Rollback
- clear_transaction_record_state
- status = nil
- end
-
+ status = yield
raise ActiveRecord::Rollback unless status
end
status
@@ -399,16 +382,26 @@ module ActiveRecord
end
private
+ attr_reader :_committed_already_called, :_trigger_update_callback, :_trigger_destroy_callback
# 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
- @_start_transaction_state[:id] = id
@_start_transaction_state.reverse_merge!(
+ id: id,
new_record: @new_record,
destroyed: @destroyed,
frozen?: frozen?,
)
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
+ remember_new_record_before_last_commit
+ end
+
+ def remember_new_record_before_last_commit
+ if _committed_already_called
+ @_new_record_before_last_commit = false
+ else
+ @_new_record_before_last_commit = @_start_transaction_state[:new_record]
+ end
end
# Clear the new record state and id of a record.
@@ -440,22 +433,16 @@ module ActiveRecord
end
end
- # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
- def transaction_record_state(state)
- @_start_transaction_state[state]
- end
-
# Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
def transaction_include_any_action?(actions)
actions.any? do |action|
case action
when :create
- transaction_record_state(:new_record)
- when :destroy
- defined?(@_trigger_destroy_callback) && @_trigger_destroy_callback
+ persisted? && @_new_record_before_last_commit
when :update
- !(transaction_record_state(:new_record) || destroyed?) &&
- (defined?(@_trigger_update_callback) && @_trigger_update_callback)
+ !(@_new_record_before_last_commit || destroyed?) && _trigger_update_callback
+ when :destroy
+ _trigger_destroy_callback
end
end
end
@@ -491,7 +478,8 @@ module ActiveRecord
def update_attributes_from_transaction_state(transaction_state)
if transaction_state && transaction_state.finalized?
- restore_transaction_record_state if transaction_state.rolledback?
+ restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback?
+ force_clear_transaction_record_state if transaction_state.fully_committed?
clear_transaction_record_state if transaction_state.fully_completed?
end
end
diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb
index 3cf70eafb8..82661a328a 100644
--- a/activerecord/lib/active_record/translation.rb
+++ b/activerecord/lib/active_record/translation.rb
@@ -10,7 +10,7 @@ module ActiveRecord
classes = [klass]
return classes if klass == ActiveRecord::Base
- while klass != klass.base_class
+ while !klass.base_class?
classes << klass = klass.superclass
end
classes
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index e882784691..0a2f6cb9fb 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -51,6 +51,10 @@ module ActiveRecord
end
end
+ def force_equality?(value)
+ coder.respond_to?(:object_class) && value.is_a?(coder.object_class)
+ end
+
private
def default_value?(value)
diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb
new file mode 100644
index 0000000000..7d04e1cac6
--- /dev/null
+++ b/activerecord/lib/arel.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "arel/errors"
+
+require "arel/crud"
+require "arel/factory_methods"
+
+require "arel/expressions"
+require "arel/predications"
+require "arel/window_predications"
+require "arel/math"
+require "arel/alias_predication"
+require "arel/order_predications"
+require "arel/table"
+require "arel/attributes"
+require "arel/compatibility/wheres"
+
+require "arel/visitors"
+require "arel/collectors/sql_string"
+
+require "arel/tree_manager"
+require "arel/insert_manager"
+require "arel/select_manager"
+require "arel/update_manager"
+require "arel/delete_manager"
+require "arel/nodes"
+
+module Arel # :nodoc: all
+ VERSION = "10.0.0"
+
+ def self.sql(raw_sql)
+ Arel::Nodes::SqlLiteral.new raw_sql
+ end
+
+ def self.star
+ sql "*"
+ end
+ ## Convenience Alias
+ Node = Arel::Nodes::Node
+end
diff --git a/activerecord/lib/arel/alias_predication.rb b/activerecord/lib/arel/alias_predication.rb
new file mode 100644
index 0000000000..4abbbb7ef6
--- /dev/null
+++ b/activerecord/lib/arel/alias_predication.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module AliasPredication
+ def as(other)
+ Nodes::As.new self, Nodes::SqlLiteral.new(other)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/attributes.rb b/activerecord/lib/arel/attributes.rb
new file mode 100644
index 0000000000..35d586c948
--- /dev/null
+++ b/activerecord/lib/arel/attributes.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "arel/attributes/attribute"
+
+module Arel # :nodoc: all
+ module Attributes
+ ###
+ # Factory method to wrap a raw database +column+ to an Arel Attribute.
+ def self.for(column)
+ case column.type
+ when :string, :text, :binary then String
+ when :integer then Integer
+ when :float then Float
+ when :decimal then Decimal
+ when :date, :datetime, :timestamp, :time then Time
+ when :boolean then Boolean
+ else
+ Undefined
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/attributes/attribute.rb b/activerecord/lib/arel/attributes/attribute.rb
new file mode 100644
index 0000000000..ecf499a23e
--- /dev/null
+++ b/activerecord/lib/arel/attributes/attribute.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Attributes
+ class Attribute < Struct.new :relation, :name
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+ include Arel::Math
+
+ ###
+ # Create a node for lowering this attribute
+ def lower
+ relation.lower self
+ end
+
+ def type_cast_for_database(value)
+ relation.type_cast_for_database(name, value)
+ end
+
+ def able_to_type_cast?
+ relation.able_to_type_cast?
+ end
+ end
+
+ class String < Attribute; end
+ class Time < Attribute; end
+ class Boolean < Attribute; end
+ class Decimal < Attribute; end
+ class Float < Attribute; end
+ class Integer < Attribute; end
+ class Undefined < Attribute; end
+ end
+
+ Attribute = Attributes::Attribute
+end
diff --git a/activerecord/lib/arel/collectors/bind.rb b/activerecord/lib/arel/collectors/bind.rb
new file mode 100644
index 0000000000..6f8912575d
--- /dev/null
+++ b/activerecord/lib/arel/collectors/bind.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class Bind
+ def initialize
+ @binds = []
+ end
+
+ def <<(str)
+ self
+ end
+
+ def add_bind(bind)
+ @binds << bind
+ self
+ end
+
+ def value
+ @binds
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/composite.rb b/activerecord/lib/arel/collectors/composite.rb
new file mode 100644
index 0000000000..d040d8598d
--- /dev/null
+++ b/activerecord/lib/arel/collectors/composite.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class Composite
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+
+ def <<(str)
+ left << str
+ right << str
+ self
+ end
+
+ def add_bind(bind, &block)
+ left.add_bind bind, &block
+ right.add_bind bind, &block
+ self
+ end
+
+ def value
+ [left.value, right.value]
+ end
+
+ protected
+
+ attr_reader :left, :right
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/plain_string.rb b/activerecord/lib/arel/collectors/plain_string.rb
new file mode 100644
index 0000000000..687d7fbf2f
--- /dev/null
+++ b/activerecord/lib/arel/collectors/plain_string.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class PlainString
+ def initialize
+ @str = "".dup
+ end
+
+ def value
+ @str
+ end
+
+ def <<(str)
+ @str << str
+ self
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/sql_string.rb b/activerecord/lib/arel/collectors/sql_string.rb
new file mode 100644
index 0000000000..c293a89a74
--- /dev/null
+++ b/activerecord/lib/arel/collectors/sql_string.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "arel/collectors/plain_string"
+
+module Arel # :nodoc: all
+ module Collectors
+ class SQLString < PlainString
+ def initialize(*)
+ super
+ @bind_index = 1
+ end
+
+ def add_bind(bind)
+ self << yield(@bind_index)
+ @bind_index += 1
+ self
+ end
+
+ def compile(bvs)
+ value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/substitute_binds.rb b/activerecord/lib/arel/collectors/substitute_binds.rb
new file mode 100644
index 0000000000..3f40eec8a8
--- /dev/null
+++ b/activerecord/lib/arel/collectors/substitute_binds.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class SubstituteBinds
+ def initialize(quoter, delegate_collector)
+ @quoter = quoter
+ @delegate = delegate_collector
+ end
+
+ def <<(str)
+ delegate << str
+ self
+ end
+
+ def add_bind(bind)
+ self << quoter.quote(bind)
+ end
+
+ def value
+ delegate.value
+ end
+
+ protected
+
+ attr_reader :quoter, :delegate
+ end
+ end
+end
diff --git a/activerecord/lib/arel/compatibility/wheres.rb b/activerecord/lib/arel/compatibility/wheres.rb
new file mode 100644
index 0000000000..c8a73f0dae
--- /dev/null
+++ b/activerecord/lib/arel/compatibility/wheres.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Compatibility # :nodoc:
+ class Wheres # :nodoc:
+ include Enumerable
+
+ module Value # :nodoc:
+ attr_accessor :visitor
+ def value
+ visitor.accept self
+ end
+
+ def name
+ super.to_sym
+ end
+ end
+
+ def initialize(engine, collection)
+ @engine = engine
+ @collection = collection
+ end
+
+ def each
+ to_sql = Visitors::ToSql.new @engine
+
+ @collection.each { |c|
+ c.extend(Value)
+ c.visitor = to_sql
+ yield c
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/crud.rb b/activerecord/lib/arel/crud.rb
new file mode 100644
index 0000000000..e8a563ca4a
--- /dev/null
+++ b/activerecord/lib/arel/crud.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ ###
+ # FIXME hopefully we can remove this
+ module Crud
+ def compile_update(values, pk)
+ um = UpdateManager.new
+
+ if Nodes::SqlLiteral === values
+ relation = @ctx.from
+ else
+ relation = values.first.first.relation
+ end
+ um.key = pk
+ um.table relation
+ um.set values
+ um.take @ast.limit.expr if @ast.limit
+ um.order(*@ast.orders)
+ um.wheres = @ctx.wheres
+ um
+ end
+
+ def compile_insert(values)
+ im = create_insert
+ im.insert values
+ im
+ end
+
+ def create_insert
+ InsertManager.new
+ end
+
+ def compile_delete
+ dm = DeleteManager.new
+ dm.take @ast.limit.expr if @ast.limit
+ dm.wheres = @ctx.wheres
+ dm.from @ctx.froms
+ dm
+ end
+ end
+end
diff --git a/activerecord/lib/arel/delete_manager.rb b/activerecord/lib/arel/delete_manager.rb
new file mode 100644
index 0000000000..2def581009
--- /dev/null
+++ b/activerecord/lib/arel/delete_manager.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class DeleteManager < Arel::TreeManager
+ def initialize
+ super
+ @ast = Nodes::DeleteStatement.new
+ @ctx = @ast
+ end
+
+ def from(relation)
+ @ast.relation = relation
+ self
+ end
+
+ def take(limit)
+ @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit
+ self
+ end
+
+ def wheres=(list)
+ @ast.wheres = list
+ end
+ end
+end
diff --git a/activerecord/lib/arel/errors.rb b/activerecord/lib/arel/errors.rb
new file mode 100644
index 0000000000..2f8d5e3c02
--- /dev/null
+++ b/activerecord/lib/arel/errors.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class ArelError < StandardError
+ end
+
+ class EmptyJoinError < ArelError
+ end
+end
diff --git a/activerecord/lib/arel/expressions.rb b/activerecord/lib/arel/expressions.rb
new file mode 100644
index 0000000000..da8afb338c
--- /dev/null
+++ b/activerecord/lib/arel/expressions.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Expressions
+ def count(distinct = false)
+ Nodes::Count.new [self], distinct
+ end
+
+ def sum
+ Nodes::Sum.new [self]
+ end
+
+ def maximum
+ Nodes::Max.new [self]
+ end
+
+ def minimum
+ Nodes::Min.new [self]
+ end
+
+ def average
+ Nodes::Avg.new [self]
+ end
+
+ def extract(field)
+ Nodes::Extract.new [self], field
+ end
+ end
+end
diff --git a/activerecord/lib/arel/factory_methods.rb b/activerecord/lib/arel/factory_methods.rb
new file mode 100644
index 0000000000..b828bc274e
--- /dev/null
+++ b/activerecord/lib/arel/factory_methods.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ ###
+ # Methods for creating various nodes
+ module FactoryMethods
+ def create_true
+ Arel::Nodes::True.new
+ end
+
+ def create_false
+ Arel::Nodes::False.new
+ end
+
+ def create_table_alias(relation, name)
+ Nodes::TableAlias.new(relation, name)
+ end
+
+ def create_join(to, constraint = nil, klass = Nodes::InnerJoin)
+ klass.new(to, constraint)
+ end
+
+ def create_string_join(to)
+ create_join to, nil, Nodes::StringJoin
+ end
+
+ def create_and(clauses)
+ Nodes::And.new clauses
+ end
+
+ def create_on(expr)
+ Nodes::On.new expr
+ end
+
+ def grouping(expr)
+ Nodes::Grouping.new expr
+ end
+
+ ###
+ # Create a LOWER() function
+ def lower(column)
+ Nodes::NamedFunction.new "LOWER", [Nodes.build_quoted(column)]
+ end
+ end
+end
diff --git a/activerecord/lib/arel/insert_manager.rb b/activerecord/lib/arel/insert_manager.rb
new file mode 100644
index 0000000000..c90fc33a48
--- /dev/null
+++ b/activerecord/lib/arel/insert_manager.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class InsertManager < Arel::TreeManager
+ def initialize
+ super
+ @ast = Nodes::InsertStatement.new
+ end
+
+ def into(table)
+ @ast.relation = table
+ self
+ end
+
+ def columns; @ast.columns end
+ def values=(val); @ast.values = val; end
+
+ def select(select)
+ @ast.select = select
+ end
+
+ def insert(fields)
+ return if fields.empty?
+
+ if String === fields
+ @ast.values = Nodes::SqlLiteral.new(fields)
+ else
+ @ast.relation ||= fields.first.first.relation
+
+ values = []
+
+ fields.each do |column, value|
+ @ast.columns << column
+ values << value
+ end
+ @ast.values = create_values values, @ast.columns
+ end
+ self
+ end
+
+ def create_values(values, columns)
+ Nodes::Values.new values, columns
+ end
+
+ def create_values_list(rows)
+ Nodes::ValuesList.new(rows)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/math.rb b/activerecord/lib/arel/math.rb
new file mode 100644
index 0000000000..2359f13148
--- /dev/null
+++ b/activerecord/lib/arel/math.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Math
+ def *(other)
+ Arel::Nodes::Multiplication.new(self, other)
+ end
+
+ def +(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::Addition.new(self, other))
+ end
+
+ def -(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::Subtraction.new(self, other))
+ end
+
+ def /(other)
+ Arel::Nodes::Division.new(self, other)
+ end
+
+ def &(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(self, other))
+ end
+
+ def |(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(self, other))
+ end
+
+ def ^(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseXor.new(self, other))
+ end
+
+ def <<(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftLeft.new(self, other))
+ end
+
+ def >>(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftRight.new(self, other))
+ end
+
+ def ~@
+ Arel::Nodes::BitwiseNot.new(self)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes.rb b/activerecord/lib/arel/nodes.rb
new file mode 100644
index 0000000000..5af0e532e2
--- /dev/null
+++ b/activerecord/lib/arel/nodes.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+# node
+require "arel/nodes/node"
+require "arel/nodes/node_expression"
+require "arel/nodes/select_statement"
+require "arel/nodes/select_core"
+require "arel/nodes/insert_statement"
+require "arel/nodes/update_statement"
+require "arel/nodes/bind_param"
+
+# terminal
+
+require "arel/nodes/terminal"
+require "arel/nodes/true"
+require "arel/nodes/false"
+
+# unary
+require "arel/nodes/unary"
+require "arel/nodes/grouping"
+require "arel/nodes/ascending"
+require "arel/nodes/descending"
+require "arel/nodes/unqualified_column"
+require "arel/nodes/with"
+
+# binary
+require "arel/nodes/binary"
+require "arel/nodes/equality"
+require "arel/nodes/in" # Why is this subclassed from equality?
+require "arel/nodes/join_source"
+require "arel/nodes/delete_statement"
+require "arel/nodes/table_alias"
+require "arel/nodes/infix_operation"
+require "arel/nodes/unary_operation"
+require "arel/nodes/over"
+require "arel/nodes/matches"
+require "arel/nodes/regexp"
+
+# nary
+require "arel/nodes/and"
+
+# function
+# FIXME: Function + Alias can be rewritten as a Function and Alias node.
+# We should make Function a Unary node and deprecate the use of "aliaz"
+require "arel/nodes/function"
+require "arel/nodes/count"
+require "arel/nodes/extract"
+require "arel/nodes/values"
+require "arel/nodes/values_list"
+require "arel/nodes/named_function"
+
+# windows
+require "arel/nodes/window"
+
+# conditional expressions
+require "arel/nodes/case"
+
+# joins
+require "arel/nodes/full_outer_join"
+require "arel/nodes/inner_join"
+require "arel/nodes/outer_join"
+require "arel/nodes/right_outer_join"
+require "arel/nodes/string_join"
+
+require "arel/nodes/sql_literal"
+
+require "arel/nodes/casted"
diff --git a/activerecord/lib/arel/nodes/and.rb b/activerecord/lib/arel/nodes/and.rb
new file mode 100644
index 0000000000..c530a77bfb
--- /dev/null
+++ b/activerecord/lib/arel/nodes/and.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class And < Arel::Nodes::Node
+ attr_reader :children
+
+ def initialize(children)
+ super()
+ @children = children
+ end
+
+ def left
+ children.first
+ end
+
+ def right
+ children[1]
+ end
+
+ def hash
+ children.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.children == other.children
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/ascending.rb b/activerecord/lib/arel/nodes/ascending.rb
new file mode 100644
index 0000000000..8b617f4df5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/ascending.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Ascending < Ordering
+ def reverse
+ Descending.new(expr)
+ end
+
+ def direction
+ :asc
+ end
+
+ def ascending?
+ true
+ end
+
+ def descending?
+ false
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/binary.rb b/activerecord/lib/arel/nodes/binary.rb
new file mode 100644
index 0000000000..e184e99c73
--- /dev/null
+++ b/activerecord/lib/arel/nodes/binary.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Binary < Arel::Nodes::NodeExpression
+ attr_accessor :left, :right
+
+ def initialize(left, right)
+ super()
+ @left = left
+ @right = right
+ end
+
+ def initialize_copy(other)
+ super
+ @left = @left.clone if @left
+ @right = @right.clone if @right
+ end
+
+ def hash
+ [self.class, @left, @right].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.left == other.left &&
+ self.right == other.right
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ As
+ Assignment
+ Between
+ GreaterThan
+ GreaterThanOrEqual
+ Join
+ LessThan
+ LessThanOrEqual
+ NotEqual
+ NotIn
+ Or
+ Union
+ UnionAll
+ Intersect
+ Except
+ }.each do |name|
+ const_set name, Class.new(Binary)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb
new file mode 100644
index 0000000000..53c5563d93
--- /dev/null
+++ b/activerecord/lib/arel/nodes/bind_param.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class BindParam < Node
+ attr_accessor :value
+
+ def initialize(value)
+ @value = value
+ super()
+ end
+
+ def hash
+ [self.class, self.value].hash
+ end
+
+ def eql?(other)
+ other.is_a?(BindParam) &&
+ value == other.value
+ end
+ alias :== :eql?
+
+ def nil?
+ value.nil?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/case.rb b/activerecord/lib/arel/nodes/case.rb
new file mode 100644
index 0000000000..654a54825e
--- /dev/null
+++ b/activerecord/lib/arel/nodes/case.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Case < Arel::Nodes::Node
+ attr_accessor :case, :conditions, :default
+
+ def initialize(expression = nil, default = nil)
+ @case = expression
+ @conditions = []
+ @default = default
+ end
+
+ def when(condition, expression = nil)
+ @conditions << When.new(Nodes.build_quoted(condition), expression)
+ self
+ end
+
+ def then(expression)
+ @conditions.last.right = Nodes.build_quoted(expression)
+ self
+ end
+
+ def else(expression)
+ @default = Else.new Nodes.build_quoted(expression)
+ self
+ end
+
+ def initialize_copy(other)
+ super
+ @case = @case.clone if @case
+ @conditions = @conditions.map { |x| x.clone }
+ @default = @default.clone if @default
+ end
+
+ def hash
+ [@case, @conditions, @default].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.case == other.case &&
+ self.conditions == other.conditions &&
+ self.default == other.default
+ end
+ alias :== :eql?
+ end
+
+ class When < Binary # :nodoc:
+ end
+
+ class Else < Unary # :nodoc:
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/casted.rb b/activerecord/lib/arel/nodes/casted.rb
new file mode 100644
index 0000000000..c1e6e97d6d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/casted.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Casted < Arel::Nodes::NodeExpression # :nodoc:
+ attr_reader :val, :attribute
+ def initialize(val, attribute)
+ @val = val
+ @attribute = attribute
+ super()
+ end
+
+ def nil?; @val.nil?; end
+
+ def hash
+ [self.class, val, attribute].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.val == other.val &&
+ self.attribute == other.attribute
+ end
+ alias :== :eql?
+ end
+
+ class Quoted < Arel::Nodes::Unary # :nodoc:
+ alias :val :value
+ def nil?; val.nil?; end
+ end
+
+ def self.build_quoted(other, attribute = nil)
+ case other
+ when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::Nodes::BindParam, Arel::SelectManager, Arel::Nodes::Quoted, Arel::Nodes::SqlLiteral
+ other
+ else
+ case attribute
+ when Arel::Attributes::Attribute
+ Casted.new other, attribute
+ else
+ Quoted.new other
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/count.rb b/activerecord/lib/arel/nodes/count.rb
new file mode 100644
index 0000000000..880464639d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/count.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Count < Arel::Nodes::Function
+ def initialize(expr, distinct = false, aliaz = nil)
+ super(expr, aliaz)
+ @distinct = distinct
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb
new file mode 100644
index 0000000000..eaac05e2f6
--- /dev/null
+++ b/activerecord/lib/arel/nodes/delete_statement.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class DeleteStatement < Arel::Nodes::Node
+ attr_accessor :left, :right
+ attr_accessor :limit
+
+ alias :relation :left
+ alias :relation= :left=
+ alias :wheres :right
+ alias :wheres= :right=
+
+ def initialize(relation = nil, wheres = [])
+ super()
+ @left = relation
+ @right = wheres
+ end
+
+ def initialize_copy(other)
+ super
+ @left = @left.clone if @left
+ @right = @right.clone if @right
+ end
+
+ def hash
+ [self.class, @left, @right].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.left == other.left &&
+ self.right == other.right
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/descending.rb b/activerecord/lib/arel/nodes/descending.rb
new file mode 100644
index 0000000000..f3f6992ca8
--- /dev/null
+++ b/activerecord/lib/arel/nodes/descending.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Descending < Ordering
+ def reverse
+ Ascending.new(expr)
+ end
+
+ def direction
+ :desc
+ end
+
+ def ascending?
+ false
+ end
+
+ def descending?
+ true
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/equality.rb b/activerecord/lib/arel/nodes/equality.rb
new file mode 100644
index 0000000000..2aa85a977e
--- /dev/null
+++ b/activerecord/lib/arel/nodes/equality.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Equality < Arel::Nodes::Binary
+ def operator; :== end
+ alias :operand1 :left
+ alias :operand2 :right
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/extract.rb b/activerecord/lib/arel/nodes/extract.rb
new file mode 100644
index 0000000000..5799ee9b8f
--- /dev/null
+++ b/activerecord/lib/arel/nodes/extract.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Extract < Arel::Nodes::Unary
+ attr_accessor :field
+
+ def initialize(expr, field)
+ super(expr)
+ @field = field
+ end
+
+ def hash
+ super ^ @field.hash
+ end
+
+ def eql?(other)
+ super &&
+ self.field == other.field
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/false.rb b/activerecord/lib/arel/nodes/false.rb
new file mode 100644
index 0000000000..1e5bf04be5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/false.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class False < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/full_outer_join.rb b/activerecord/lib/arel/nodes/full_outer_join.rb
new file mode 100644
index 0000000000..91bb81f2e3
--- /dev/null
+++ b/activerecord/lib/arel/nodes/full_outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class FullOuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/function.rb b/activerecord/lib/arel/nodes/function.rb
new file mode 100644
index 0000000000..0a439b39f5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/function.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Function < Arel::Nodes::NodeExpression
+ include Arel::WindowPredications
+ attr_accessor :expressions, :alias, :distinct
+
+ def initialize(expr, aliaz = nil)
+ super()
+ @expressions = expr
+ @alias = aliaz && SqlLiteral.new(aliaz)
+ @distinct = false
+ end
+
+ def as(aliaz)
+ self.alias = SqlLiteral.new(aliaz)
+ self
+ end
+
+ def hash
+ [@expressions, @alias, @distinct].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.expressions == other.expressions &&
+ self.alias == other.alias &&
+ self.distinct == other.distinct
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ Sum
+ Exists
+ Max
+ Min
+ Avg
+ }.each do |name|
+ const_set(name, Class.new(Function))
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/grouping.rb b/activerecord/lib/arel/nodes/grouping.rb
new file mode 100644
index 0000000000..4d0bd69d4d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/grouping.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Grouping < Unary
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/in.rb b/activerecord/lib/arel/nodes/in.rb
new file mode 100644
index 0000000000..2be45d6f99
--- /dev/null
+++ b/activerecord/lib/arel/nodes/in.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class In < Equality
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/infix_operation.rb b/activerecord/lib/arel/nodes/infix_operation.rb
new file mode 100644
index 0000000000..bc7e20dcc6
--- /dev/null
+++ b/activerecord/lib/arel/nodes/infix_operation.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InfixOperation < Binary
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::OrderPredications
+ include Arel::AliasPredication
+ include Arel::Math
+
+ attr_reader :operator
+
+ def initialize(operator, left, right)
+ super(left, right)
+ @operator = operator
+ end
+ end
+
+ class Multiplication < InfixOperation
+ def initialize(left, right)
+ super(:*, left, right)
+ end
+ end
+
+ class Division < InfixOperation
+ def initialize(left, right)
+ super(:/, left, right)
+ end
+ end
+
+ class Addition < InfixOperation
+ def initialize(left, right)
+ super(:+, left, right)
+ end
+ end
+
+ class Subtraction < InfixOperation
+ def initialize(left, right)
+ super(:-, left, right)
+ end
+ end
+
+ class Concat < InfixOperation
+ def initialize(left, right)
+ super("||", left, right)
+ end
+ end
+
+ class BitwiseAnd < InfixOperation
+ def initialize(left, right)
+ super(:&, left, right)
+ end
+ end
+
+ class BitwiseOr < InfixOperation
+ def initialize(left, right)
+ super(:|, left, right)
+ end
+ end
+
+ class BitwiseXor < InfixOperation
+ def initialize(left, right)
+ super(:^, left, right)
+ end
+ end
+
+ class BitwiseShiftLeft < InfixOperation
+ def initialize(left, right)
+ super(:<<, left, right)
+ end
+ end
+
+ class BitwiseShiftRight < InfixOperation
+ def initialize(left, right)
+ super(:>>, left, right)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/inner_join.rb b/activerecord/lib/arel/nodes/inner_join.rb
new file mode 100644
index 0000000000..519fafad09
--- /dev/null
+++ b/activerecord/lib/arel/nodes/inner_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InnerJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/insert_statement.rb b/activerecord/lib/arel/nodes/insert_statement.rb
new file mode 100644
index 0000000000..d28fd1f6c8
--- /dev/null
+++ b/activerecord/lib/arel/nodes/insert_statement.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InsertStatement < Arel::Nodes::Node
+ attr_accessor :relation, :columns, :values, :select
+
+ def initialize
+ super()
+ @relation = nil
+ @columns = []
+ @values = nil
+ @select = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @columns = @columns.clone
+ @values = @values.clone if @values
+ @select = @select.clone if @select
+ end
+
+ def hash
+ [@relation, @columns, @values, @select].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.relation == other.relation &&
+ self.columns == other.columns &&
+ self.select == other.select &&
+ self.values == other.values
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/join_source.rb b/activerecord/lib/arel/nodes/join_source.rb
new file mode 100644
index 0000000000..abf0944623
--- /dev/null
+++ b/activerecord/lib/arel/nodes/join_source.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ ###
+ # Class that represents a join source
+ #
+ # http://www.sqlite.org/syntaxdiagrams.html#join-source
+
+ class JoinSource < Arel::Nodes::Binary
+ def initialize(single_source, joinop = [])
+ super
+ end
+
+ def empty?
+ !left && right.empty?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/matches.rb b/activerecord/lib/arel/nodes/matches.rb
new file mode 100644
index 0000000000..fd5734f4bd
--- /dev/null
+++ b/activerecord/lib/arel/nodes/matches.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Matches < Binary
+ attr_reader :escape
+ attr_accessor :case_sensitive
+
+ def initialize(left, right, escape = nil, case_sensitive = false)
+ super(left, right)
+ @escape = escape && Nodes.build_quoted(escape)
+ @case_sensitive = case_sensitive
+ end
+ end
+
+ class DoesNotMatch < Matches; end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/named_function.rb b/activerecord/lib/arel/nodes/named_function.rb
new file mode 100644
index 0000000000..126462d6d6
--- /dev/null
+++ b/activerecord/lib/arel/nodes/named_function.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class NamedFunction < Arel::Nodes::Function
+ attr_accessor :name
+
+ def initialize(name, expr, aliaz = nil)
+ super(expr, aliaz)
+ @name = name
+ end
+
+ def hash
+ super ^ @name.hash
+ end
+
+ def eql?(other)
+ super && self.name == other.name
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb
new file mode 100644
index 0000000000..2b9b1e9828
--- /dev/null
+++ b/activerecord/lib/arel/nodes/node.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ ###
+ # Abstract base class for all AST nodes
+ class Node
+ include Arel::FactoryMethods
+ include Enumerable
+
+ if $DEBUG
+ def _caller
+ @caller
+ end
+
+ def initialize
+ @caller = caller.dup
+ end
+ end
+
+ ###
+ # Factory method to create a Nodes::Not node that has the recipient of
+ # the caller as a child.
+ def not
+ Nodes::Not.new self
+ end
+
+ ###
+ # Factory method to create a Nodes::Grouping node that has an Nodes::Or
+ # node as a child.
+ def or(right)
+ Nodes::Grouping.new Nodes::Or.new(self, right)
+ end
+
+ ###
+ # Factory method to create an Nodes::And node.
+ def and(right)
+ Nodes::And.new [self, right]
+ end
+
+ # FIXME: this method should go away. I don't like people calling
+ # to_sql on non-head nodes. This forces us to walk the AST until we
+ # can find a node that has a "relation" member.
+ #
+ # Maybe we should just use `Table.engine`? :'(
+ def to_sql(engine = Table.engine)
+ collector = Arel::Collectors::SQLString.new
+ collector = engine.connection.visitor.accept self, collector
+ collector.value
+ end
+
+ # Iterate through AST, nodes will be yielded depth-first
+ def each(&block)
+ return enum_for(:each) unless block_given?
+
+ ::Arel::Visitors::DepthFirst.new(block).accept self
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/node_expression.rb b/activerecord/lib/arel/nodes/node_expression.rb
new file mode 100644
index 0000000000..cbcfaba37c
--- /dev/null
+++ b/activerecord/lib/arel/nodes/node_expression.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class NodeExpression < Arel::Nodes::Node
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+ include Arel::Math
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/outer_join.rb b/activerecord/lib/arel/nodes/outer_join.rb
new file mode 100644
index 0000000000..0a3042be61
--- /dev/null
+++ b/activerecord/lib/arel/nodes/outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class OuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/over.rb b/activerecord/lib/arel/nodes/over.rb
new file mode 100644
index 0000000000..91176764a9
--- /dev/null
+++ b/activerecord/lib/arel/nodes/over.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Over < Binary
+ include Arel::AliasPredication
+
+ def initialize(left, right = nil)
+ super(left, right)
+ end
+
+ def operator; "OVER" end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/regexp.rb b/activerecord/lib/arel/nodes/regexp.rb
new file mode 100644
index 0000000000..7c25095569
--- /dev/null
+++ b/activerecord/lib/arel/nodes/regexp.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Regexp < Binary
+ attr_accessor :case_sensitive
+
+ def initialize(left, right, case_sensitive = true)
+ super(left, right)
+ @case_sensitive = case_sensitive
+ end
+ end
+
+ class NotRegexp < Regexp; end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/right_outer_join.rb b/activerecord/lib/arel/nodes/right_outer_join.rb
new file mode 100644
index 0000000000..04ed4aaa78
--- /dev/null
+++ b/activerecord/lib/arel/nodes/right_outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class RightOuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb
new file mode 100644
index 0000000000..2defe61974
--- /dev/null
+++ b/activerecord/lib/arel/nodes/select_core.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SelectCore < Arel::Nodes::Node
+ attr_accessor :top, :projections, :wheres, :groups, :windows
+ attr_accessor :havings, :source, :set_quantifier
+
+ def initialize
+ super()
+ @source = JoinSource.new nil
+ @top = nil
+
+ # https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier
+ @set_quantifier = nil
+ @projections = []
+ @wheres = []
+ @groups = []
+ @havings = []
+ @windows = []
+ end
+
+ def from
+ @source.left
+ end
+
+ def from=(value)
+ @source.left = value
+ end
+
+ alias :froms= :from=
+ alias :froms :from
+
+ def initialize_copy(other)
+ super
+ @source = @source.clone if @source
+ @projections = @projections.clone
+ @wheres = @wheres.clone
+ @groups = @groups.clone
+ @havings = @havings.clone
+ @windows = @windows.clone
+ end
+
+ def hash
+ [
+ @source, @top, @set_quantifier, @projections,
+ @wheres, @groups, @havings, @windows
+ ].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.source == other.source &&
+ self.top == other.top &&
+ self.set_quantifier == other.set_quantifier &&
+ self.projections == other.projections &&
+ self.wheres == other.wheres &&
+ self.groups == other.groups &&
+ self.havings == other.havings &&
+ self.windows == other.windows
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/select_statement.rb b/activerecord/lib/arel/nodes/select_statement.rb
new file mode 100644
index 0000000000..eff5dad939
--- /dev/null
+++ b/activerecord/lib/arel/nodes/select_statement.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SelectStatement < Arel::Nodes::NodeExpression
+ attr_reader :cores
+ attr_accessor :limit, :orders, :lock, :offset, :with
+
+ def initialize(cores = [SelectCore.new])
+ super()
+ @cores = cores
+ @orders = []
+ @limit = nil
+ @lock = nil
+ @offset = nil
+ @with = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @cores = @cores.map { |x| x.clone }
+ @orders = @orders.map { |x| x.clone }
+ end
+
+ def hash
+ [@cores, @orders, @limit, @lock, @offset, @with].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.cores == other.cores &&
+ self.orders == other.orders &&
+ self.limit == other.limit &&
+ self.lock == other.lock &&
+ self.offset == other.offset &&
+ self.with == other.with
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/sql_literal.rb b/activerecord/lib/arel/nodes/sql_literal.rb
new file mode 100644
index 0000000000..d25a8521b7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/sql_literal.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SqlLiteral < String
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+
+ def encode_with(coder)
+ coder.scalar = self.to_s
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/string_join.rb b/activerecord/lib/arel/nodes/string_join.rb
new file mode 100644
index 0000000000..86027fcab7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/string_join.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class StringJoin < Arel::Nodes::Join
+ def initialize(left, right = nil)
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/table_alias.rb b/activerecord/lib/arel/nodes/table_alias.rb
new file mode 100644
index 0000000000..f95ca16a3d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/table_alias.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class TableAlias < Arel::Nodes::Binary
+ alias :name :right
+ alias :relation :left
+ alias :table_alias :name
+
+ def [](name)
+ Attribute.new(self, name)
+ end
+
+ def table_name
+ relation.respond_to?(:name) ? relation.name : name
+ end
+
+ def type_cast_for_database(*args)
+ relation.type_cast_for_database(*args)
+ end
+
+ def able_to_type_cast?
+ relation.respond_to?(:able_to_type_cast?) && relation.able_to_type_cast?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/terminal.rb b/activerecord/lib/arel/nodes/terminal.rb
new file mode 100644
index 0000000000..d84c453f1a
--- /dev/null
+++ b/activerecord/lib/arel/nodes/terminal.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Distinct < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/true.rb b/activerecord/lib/arel/nodes/true.rb
new file mode 100644
index 0000000000..c891012969
--- /dev/null
+++ b/activerecord/lib/arel/nodes/true.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class True < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb
new file mode 100644
index 0000000000..a3c0045897
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unary.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Unary < Arel::Nodes::NodeExpression
+ attr_accessor :expr
+ alias :value :expr
+
+ def initialize(expr)
+ super()
+ @expr = expr
+ end
+
+ def hash
+ @expr.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.expr == other.expr
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ Bin
+ Cube
+ DistinctOn
+ Group
+ GroupingElement
+ GroupingSet
+ Lateral
+ Limit
+ Lock
+ Not
+ Offset
+ On
+ Ordering
+ RollUp
+ Top
+ }.each do |name|
+ const_set(name, Class.new(Unary))
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unary_operation.rb b/activerecord/lib/arel/nodes/unary_operation.rb
new file mode 100644
index 0000000000..524282ac84
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unary_operation.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UnaryOperation < Unary
+ attr_reader :operator
+
+ def initialize(operator, operand)
+ super(operand)
+ @operator = operator
+ end
+ end
+
+ class BitwiseNot < UnaryOperation
+ def initialize(operand)
+ super(:~, operand)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unqualified_column.rb b/activerecord/lib/arel/nodes/unqualified_column.rb
new file mode 100644
index 0000000000..7c3e0720d7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unqualified_column.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UnqualifiedColumn < Arel::Nodes::Unary
+ alias :attribute :expr
+ alias :attribute= :expr=
+
+ def relation
+ @expr.relation
+ end
+
+ def column
+ @expr.column
+ end
+
+ def name
+ @expr.name
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb
new file mode 100644
index 0000000000..5184b1180f
--- /dev/null
+++ b/activerecord/lib/arel/nodes/update_statement.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UpdateStatement < Arel::Nodes::Node
+ attr_accessor :relation, :wheres, :values, :orders, :limit
+ attr_accessor :key
+
+ def initialize
+ @relation = nil
+ @wheres = []
+ @values = []
+ @orders = []
+ @limit = nil
+ @key = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @wheres = @wheres.clone
+ @values = @values.clone
+ end
+
+ def hash
+ [@relation, @wheres, @values, @orders, @limit, @key].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.relation == other.relation &&
+ self.wheres == other.wheres &&
+ self.values == other.values &&
+ self.orders == other.orders &&
+ self.limit == other.limit &&
+ self.key == other.key
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/values.rb b/activerecord/lib/arel/nodes/values.rb
new file mode 100644
index 0000000000..650248dc04
--- /dev/null
+++ b/activerecord/lib/arel/nodes/values.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Values < Arel::Nodes::Binary
+ alias :expressions :left
+ alias :expressions= :left=
+ alias :columns :right
+ alias :columns= :right=
+
+ def initialize(exprs, columns = [])
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/values_list.rb b/activerecord/lib/arel/nodes/values_list.rb
new file mode 100644
index 0000000000..27109848e4
--- /dev/null
+++ b/activerecord/lib/arel/nodes/values_list.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class ValuesList < Node
+ attr_reader :rows
+
+ def initialize(rows)
+ @rows = rows
+ super()
+ end
+
+ def hash
+ @rows.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.rows == other.rows
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/window.rb b/activerecord/lib/arel/nodes/window.rb
new file mode 100644
index 0000000000..4916fc7fbe
--- /dev/null
+++ b/activerecord/lib/arel/nodes/window.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Window < Arel::Nodes::Node
+ attr_accessor :orders, :framing, :partitions
+
+ def initialize
+ @orders = []
+ @partitions = []
+ @framing = nil
+ end
+
+ def order(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @orders.concat expr.map { |x|
+ String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def partition(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @partitions.concat expr.map { |x|
+ String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def frame(expr)
+ @framing = expr
+ end
+
+ def rows(expr = nil)
+ if @framing
+ Rows.new(expr)
+ else
+ frame(Rows.new(expr))
+ end
+ end
+
+ def range(expr = nil)
+ if @framing
+ Range.new(expr)
+ else
+ frame(Range.new(expr))
+ end
+ end
+
+ def initialize_copy(other)
+ super
+ @orders = @orders.map { |x| x.clone }
+ end
+
+ def hash
+ [@orders, @framing].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.orders == other.orders &&
+ self.framing == other.framing &&
+ self.partitions == other.partitions
+ end
+ alias :== :eql?
+ end
+
+ class NamedWindow < Window
+ attr_accessor :name
+
+ def initialize(name)
+ super()
+ @name = name
+ end
+
+ def initialize_copy(other)
+ super
+ @name = other.name.clone
+ end
+
+ def hash
+ super ^ @name.hash
+ end
+
+ def eql?(other)
+ super && self.name == other.name
+ end
+ alias :== :eql?
+ end
+
+ class Rows < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class Range < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class CurrentRow < Node
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+
+ class Preceding < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class Following < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/with.rb b/activerecord/lib/arel/nodes/with.rb
new file mode 100644
index 0000000000..157bdcaa08
--- /dev/null
+++ b/activerecord/lib/arel/nodes/with.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class With < Arel::Nodes::Unary
+ alias children expr
+ end
+
+ class WithRecursive < With; end
+ end
+end
diff --git a/activerecord/lib/arel/order_predications.rb b/activerecord/lib/arel/order_predications.rb
new file mode 100644
index 0000000000..d785bbba92
--- /dev/null
+++ b/activerecord/lib/arel/order_predications.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module OrderPredications
+ def asc
+ Nodes::Ascending.new self
+ end
+
+ def desc
+ Nodes::Descending.new self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb
new file mode 100644
index 0000000000..e83a6f162f
--- /dev/null
+++ b/activerecord/lib/arel/predications.rb
@@ -0,0 +1,241 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Predications
+ def not_eq(other)
+ Nodes::NotEqual.new self, quoted_node(other)
+ end
+
+ def not_eq_any(others)
+ grouping_any :not_eq, others
+ end
+
+ def not_eq_all(others)
+ grouping_all :not_eq, others
+ end
+
+ def eq(other)
+ Nodes::Equality.new self, quoted_node(other)
+ end
+
+ def eq_any(others)
+ grouping_any :eq, others
+ end
+
+ def eq_all(others)
+ grouping_all :eq, quoted_array(others)
+ end
+
+ def between(other)
+ if equals_quoted?(other.begin, -Float::INFINITY)
+ if equals_quoted?(other.end, Float::INFINITY)
+ not_in([])
+ elsif other.exclude_end?
+ lt(other.end)
+ else
+ lteq(other.end)
+ end
+ elsif equals_quoted?(other.end, Float::INFINITY)
+ gteq(other.begin)
+ elsif other.exclude_end?
+ gteq(other.begin).and(lt(other.end))
+ else
+ left = quoted_node(other.begin)
+ right = quoted_node(other.end)
+ Nodes::Between.new(self, left.and(right))
+ end
+ end
+
+ def in(other)
+ case other
+ when Arel::SelectManager
+ Arel::Nodes::In.new(self, other.ast)
+ when Range
+ if $VERBOSE
+ warn <<-eowarn
+Passing a range to `#in` is deprecated. Call `#between`, instead.
+ eowarn
+ end
+ between(other)
+ when Enumerable
+ Nodes::In.new self, quoted_array(other)
+ else
+ Nodes::In.new self, quoted_node(other)
+ end
+ end
+
+ def in_any(others)
+ grouping_any :in, others
+ end
+
+ def in_all(others)
+ grouping_all :in, others
+ end
+
+ def not_between(other)
+ if equals_quoted?(other.begin, -Float::INFINITY)
+ if equals_quoted?(other.end, Float::INFINITY)
+ self.in([])
+ elsif other.exclude_end?
+ gteq(other.end)
+ else
+ gt(other.end)
+ end
+ elsif equals_quoted?(other.end, Float::INFINITY)
+ lt(other.begin)
+ else
+ left = lt(other.begin)
+ right = if other.exclude_end?
+ gteq(other.end)
+ else
+ gt(other.end)
+ end
+ left.or(right)
+ end
+ end
+
+ def not_in(other)
+ case other
+ when Arel::SelectManager
+ Arel::Nodes::NotIn.new(self, other.ast)
+ when Range
+ if $VERBOSE
+ warn <<-eowarn
+Passing a range to `#not_in` is deprecated. Call `#not_between`, instead.
+ eowarn
+ end
+ not_between(other)
+ when Enumerable
+ Nodes::NotIn.new self, quoted_array(other)
+ else
+ Nodes::NotIn.new self, quoted_node(other)
+ end
+ end
+
+ def not_in_any(others)
+ grouping_any :not_in, others
+ end
+
+ def not_in_all(others)
+ grouping_all :not_in, others
+ end
+
+ def matches(other, escape = nil, case_sensitive = false)
+ Nodes::Matches.new self, quoted_node(other), escape, case_sensitive
+ end
+
+ def matches_regexp(other, case_sensitive = true)
+ Nodes::Regexp.new self, quoted_node(other), case_sensitive
+ end
+
+ def matches_any(others, escape = nil, case_sensitive = false)
+ grouping_any :matches, others, escape, case_sensitive
+ end
+
+ def matches_all(others, escape = nil, case_sensitive = false)
+ grouping_all :matches, others, escape, case_sensitive
+ end
+
+ def does_not_match(other, escape = nil, case_sensitive = false)
+ Nodes::DoesNotMatch.new self, quoted_node(other), escape, case_sensitive
+ end
+
+ def does_not_match_regexp(other, case_sensitive = true)
+ Nodes::NotRegexp.new self, quoted_node(other), case_sensitive
+ end
+
+ def does_not_match_any(others, escape = nil)
+ grouping_any :does_not_match, others, escape
+ end
+
+ def does_not_match_all(others, escape = nil)
+ grouping_all :does_not_match, others, escape
+ end
+
+ def gteq(right)
+ Nodes::GreaterThanOrEqual.new self, quoted_node(right)
+ end
+
+ def gteq_any(others)
+ grouping_any :gteq, others
+ end
+
+ def gteq_all(others)
+ grouping_all :gteq, others
+ end
+
+ def gt(right)
+ Nodes::GreaterThan.new self, quoted_node(right)
+ end
+
+ def gt_any(others)
+ grouping_any :gt, others
+ end
+
+ def gt_all(others)
+ grouping_all :gt, others
+ end
+
+ def lt(right)
+ Nodes::LessThan.new self, quoted_node(right)
+ end
+
+ def lt_any(others)
+ grouping_any :lt, others
+ end
+
+ def lt_all(others)
+ grouping_all :lt, others
+ end
+
+ def lteq(right)
+ Nodes::LessThanOrEqual.new self, quoted_node(right)
+ end
+
+ def lteq_any(others)
+ grouping_any :lteq, others
+ end
+
+ def lteq_all(others)
+ grouping_all :lteq, others
+ end
+
+ def when(right)
+ Nodes::Case.new(self).when quoted_node(right)
+ end
+
+ def concat(other)
+ Nodes::Concat.new self, other
+ end
+
+ private
+
+ def grouping_any(method_id, others, *extras)
+ nodes = others.map { |expr| send(method_id, expr, *extras) }
+ Nodes::Grouping.new nodes.inject { |memo, node|
+ Nodes::Or.new(memo, node)
+ }
+ end
+
+ def grouping_all(method_id, others, *extras)
+ nodes = others.map { |expr| send(method_id, expr, *extras) }
+ Nodes::Grouping.new Nodes::And.new(nodes)
+ end
+
+ def quoted_node(other)
+ Nodes.build_quoted(other, self)
+ end
+
+ def quoted_array(others)
+ others.map { |v| quoted_node(v) }
+ end
+
+ def equals_quoted?(maybe_quoted, value)
+ if maybe_quoted.is_a?(Nodes::Quoted)
+ maybe_quoted.val == value
+ else
+ maybe_quoted == value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb
new file mode 100644
index 0000000000..22a04b00c6
--- /dev/null
+++ b/activerecord/lib/arel/select_manager.rb
@@ -0,0 +1,273 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class SelectManager < Arel::TreeManager
+ include Arel::Crud
+
+ STRING_OR_SYMBOL_CLASS = [Symbol, String]
+
+ def initialize(table = nil)
+ super()
+ @ast = Nodes::SelectStatement.new
+ @ctx = @ast.cores.last
+ from table
+ end
+
+ def initialize_copy(other)
+ super
+ @ctx = @ast.cores.last
+ end
+
+ def limit
+ @ast.limit && @ast.limit.expr
+ end
+ alias :taken :limit
+
+ def constraints
+ @ctx.wheres
+ end
+
+ def offset
+ @ast.offset && @ast.offset.expr
+ end
+
+ def skip(amount)
+ if amount
+ @ast.offset = Nodes::Offset.new(amount)
+ else
+ @ast.offset = nil
+ end
+ self
+ end
+ alias :offset= :skip
+
+ ###
+ # Produces an Arel::Nodes::Exists node
+ def exists
+ Arel::Nodes::Exists.new @ast
+ end
+
+ def as(other)
+ create_table_alias grouping(@ast), Nodes::SqlLiteral.new(other)
+ end
+
+ def lock(locking = Arel.sql("FOR UPDATE"))
+ case locking
+ when true
+ locking = Arel.sql("FOR UPDATE")
+ when Arel::Nodes::SqlLiteral
+ when String
+ locking = Arel.sql locking
+ end
+
+ @ast.lock = Nodes::Lock.new(locking)
+ self
+ end
+
+ def locked
+ @ast.lock
+ end
+
+ def on(*exprs)
+ @ctx.source.right.last.right = Nodes::On.new(collapse(exprs))
+ self
+ end
+
+ def group(*columns)
+ columns.each do |column|
+ # FIXME: backwards compat
+ column = Nodes::SqlLiteral.new(column) if String === column
+ column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column
+
+ @ctx.groups.push Nodes::Group.new column
+ end
+ self
+ end
+
+ def from(table)
+ table = Nodes::SqlLiteral.new(table) if String === table
+
+ case table
+ when Nodes::Join
+ @ctx.source.right << table
+ else
+ @ctx.source.left = table
+ end
+
+ self
+ end
+
+ def froms
+ @ast.cores.map { |x| x.from }.compact
+ end
+
+ def join(relation, klass = Nodes::InnerJoin)
+ return self unless relation
+
+ case relation
+ when String, Nodes::SqlLiteral
+ raise EmptyJoinError if relation.empty?
+ klass = Nodes::StringJoin
+ end
+
+ @ctx.source.right << create_join(relation, nil, klass)
+ self
+ end
+
+ def outer_join(relation)
+ join(relation, Nodes::OuterJoin)
+ end
+
+ def having(expr)
+ @ctx.havings << expr
+ self
+ end
+
+ def window(name)
+ window = Nodes::NamedWindow.new(name)
+ @ctx.windows.push window
+ window
+ end
+
+ def project(*projections)
+ # FIXME: converting these to SQLLiterals is probably not good, but
+ # rails tests require it.
+ @ctx.projections.concat projections.map { |x|
+ STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def projections
+ @ctx.projections
+ end
+
+ def projections=(projections)
+ @ctx.projections = projections
+ end
+
+ def distinct(value = true)
+ if value
+ @ctx.set_quantifier = Arel::Nodes::Distinct.new
+ else
+ @ctx.set_quantifier = nil
+ end
+ self
+ end
+
+ def distinct_on(value)
+ if value
+ @ctx.set_quantifier = Arel::Nodes::DistinctOn.new(value)
+ else
+ @ctx.set_quantifier = nil
+ end
+ self
+ end
+
+ def order(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @ast.orders.concat expr.map { |x|
+ STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def orders
+ @ast.orders
+ end
+
+ def where_sql(engine = Table.engine)
+ return if @ctx.wheres.empty?
+
+ viz = Visitors::WhereSql.new(engine.connection.visitor, engine.connection)
+ Nodes::SqlLiteral.new viz.accept(@ctx, Collectors::SQLString.new).value
+ end
+
+ def union(operation, other = nil)
+ if other
+ node_class = Nodes.const_get("Union#{operation.to_s.capitalize}")
+ else
+ other = operation
+ node_class = Nodes::Union
+ end
+
+ node_class.new self.ast, other.ast
+ end
+
+ def intersect(other)
+ Nodes::Intersect.new ast, other.ast
+ end
+
+ def except(other)
+ Nodes::Except.new ast, other.ast
+ end
+ alias :minus :except
+
+ def lateral(table_name = nil)
+ base = table_name.nil? ? ast : as(table_name)
+ Nodes::Lateral.new(base)
+ end
+
+ def with(*subqueries)
+ if subqueries.first.is_a? Symbol
+ node_class = Nodes.const_get("With#{subqueries.shift.to_s.capitalize}")
+ else
+ node_class = Nodes::With
+ end
+ @ast.with = node_class.new(subqueries.flatten)
+
+ self
+ end
+
+ def take(limit)
+ if limit
+ @ast.limit = Nodes::Limit.new(limit)
+ @ctx.top = Nodes::Top.new(limit)
+ else
+ @ast.limit = nil
+ @ctx.top = nil
+ end
+ self
+ end
+ alias limit= take
+
+ def join_sources
+ @ctx.source.right
+ end
+
+ def source
+ @ctx.source
+ end
+
+ class Row < Struct.new(:data) # :nodoc:
+ def id
+ data["id"]
+ end
+
+ def method_missing(name, *args)
+ name = name.to_s
+ return data[name] if data.key?(name)
+ super
+ end
+ end
+
+ private
+ def collapse(exprs, existing = nil)
+ exprs = exprs.unshift(existing.expr) if existing
+ exprs = exprs.compact.map { |expr|
+ if String === expr
+ # FIXME: Don't do this automatically
+ Arel.sql(expr)
+ else
+ expr
+ end
+ }
+
+ if exprs.length == 1
+ exprs.first
+ else
+ create_and exprs
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/table.rb b/activerecord/lib/arel/table.rb
new file mode 100644
index 0000000000..686fcdf962
--- /dev/null
+++ b/activerecord/lib/arel/table.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class Table
+ include Arel::Crud
+ include Arel::FactoryMethods
+
+ @engine = nil
+ class << self; attr_accessor :engine; end
+
+ attr_accessor :name, :table_alias
+
+ # TableAlias and Table both have a #table_name which is the name of the underlying table
+ alias :table_name :name
+
+ def initialize(name, as: nil, type_caster: nil)
+ @name = name.to_s
+ @type_caster = type_caster
+
+ # Sometime AR sends an :as parameter to table, to let the table know
+ # that it is an Alias. We may want to override new, and return a
+ # TableAlias node?
+ if as.to_s == @name
+ as = nil
+ end
+ @table_alias = as
+ end
+
+ def alias(name = "#{self.name}_2")
+ Nodes::TableAlias.new(self, name)
+ end
+
+ def from
+ SelectManager.new(self)
+ end
+
+ def join(relation, klass = Nodes::InnerJoin)
+ return from unless relation
+
+ case relation
+ when String, Nodes::SqlLiteral
+ raise EmptyJoinError if relation.empty?
+ klass = Nodes::StringJoin
+ end
+
+ from.join(relation, klass)
+ end
+
+ def outer_join(relation)
+ join(relation, Nodes::OuterJoin)
+ end
+
+ def group(*columns)
+ from.group(*columns)
+ end
+
+ def order(*expr)
+ from.order(*expr)
+ end
+
+ def where(condition)
+ from.where condition
+ end
+
+ def project(*things)
+ from.project(*things)
+ end
+
+ def take(amount)
+ from.take amount
+ end
+
+ def skip(amount)
+ from.skip amount
+ end
+
+ def having(expr)
+ from.having expr
+ end
+
+ def [](name)
+ ::Arel::Attribute.new self, name
+ end
+
+ def hash
+ # Perf note: aliases and table alias is excluded from the hash
+ # aliases can have a loop back to this table breaking hashes in parent
+ # relations, for the vast majority of cases @name is unique to a query
+ @name.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.name == other.name &&
+ self.table_alias == other.table_alias
+ end
+ alias :== :eql?
+
+ def type_cast_for_database(attribute_name, value)
+ type_caster.type_cast_for_database(attribute_name, value)
+ end
+
+ def able_to_type_cast?
+ !type_caster.nil?
+ end
+
+ protected
+
+ attr_reader :type_caster
+ end
+end
diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb
new file mode 100644
index 0000000000..ed47b09a37
--- /dev/null
+++ b/activerecord/lib/arel/tree_manager.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class TreeManager
+ include Arel::FactoryMethods
+
+ attr_reader :ast
+
+ def initialize
+ @ctx = nil
+ end
+
+ def to_dot
+ collector = Arel::Collectors::PlainString.new
+ collector = Visitors::Dot.new.accept @ast, collector
+ collector.value
+ end
+
+ def to_sql(engine = Table.engine)
+ collector = Arel::Collectors::SQLString.new
+ collector = engine.connection.visitor.accept @ast, collector
+ collector.value
+ end
+
+ def initialize_copy(other)
+ super
+ @ast = @ast.clone
+ end
+
+ def where(expr)
+ if Arel::TreeManager === expr
+ expr = expr.ast
+ end
+ @ctx.wheres << expr
+ self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/update_manager.rb b/activerecord/lib/arel/update_manager.rb
new file mode 100644
index 0000000000..fe444343ba
--- /dev/null
+++ b/activerecord/lib/arel/update_manager.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class UpdateManager < Arel::TreeManager
+ def initialize
+ super
+ @ast = Nodes::UpdateStatement.new
+ @ctx = @ast
+ end
+
+ def take(limit)
+ @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit
+ self
+ end
+
+ def key=(key)
+ @ast.key = Nodes.build_quoted(key)
+ end
+
+ def key
+ @ast.key
+ end
+
+ def order(*expr)
+ @ast.orders = expr
+ self
+ end
+
+ ###
+ # UPDATE +table+
+ def table(table)
+ @ast.relation = table
+ self
+ end
+
+ def wheres=(exprs)
+ @ast.wheres = exprs
+ end
+
+ def where(expr)
+ @ast.wheres << expr
+ self
+ end
+
+ def set(values)
+ if String === values
+ @ast.values = [values]
+ else
+ @ast.values = values.map { |column, value|
+ Nodes::Assignment.new(
+ Nodes::UnqualifiedColumn.new(column),
+ value
+ )
+ }
+ end
+ self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors.rb b/activerecord/lib/arel/visitors.rb
new file mode 100644
index 0000000000..e350f52e65
--- /dev/null
+++ b/activerecord/lib/arel/visitors.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require "arel/visitors/visitor"
+require "arel/visitors/depth_first"
+require "arel/visitors/to_sql"
+require "arel/visitors/sqlite"
+require "arel/visitors/postgresql"
+require "arel/visitors/mysql"
+require "arel/visitors/mssql"
+require "arel/visitors/oracle"
+require "arel/visitors/oracle12"
+require "arel/visitors/where_sql"
+require "arel/visitors/dot"
+require "arel/visitors/ibm_db"
+require "arel/visitors/informix"
+
+module Arel # :nodoc: all
+ module Visitors
+ end
+end
diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb
new file mode 100644
index 0000000000..bcf8f8f980
--- /dev/null
+++ b/activerecord/lib/arel/visitors/depth_first.rb
@@ -0,0 +1,200 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class DepthFirst < Arel::Visitors::Visitor
+ def initialize(block = nil)
+ @block = block || Proc.new
+ super()
+ end
+
+ private
+
+ def visit(o)
+ super
+ @block.call o
+ end
+
+ def unary(o)
+ visit o.expr
+ end
+ alias :visit_Arel_Nodes_Else :unary
+ alias :visit_Arel_Nodes_Group :unary
+ alias :visit_Arel_Nodes_Cube :unary
+ alias :visit_Arel_Nodes_RollUp :unary
+ alias :visit_Arel_Nodes_GroupingSet :unary
+ alias :visit_Arel_Nodes_GroupingElement :unary
+ alias :visit_Arel_Nodes_Grouping :unary
+ alias :visit_Arel_Nodes_Having :unary
+ alias :visit_Arel_Nodes_Lateral :unary
+ alias :visit_Arel_Nodes_Limit :unary
+ alias :visit_Arel_Nodes_Not :unary
+ alias :visit_Arel_Nodes_Offset :unary
+ alias :visit_Arel_Nodes_On :unary
+ alias :visit_Arel_Nodes_Ordering :unary
+ alias :visit_Arel_Nodes_Ascending :unary
+ alias :visit_Arel_Nodes_Descending :unary
+ alias :visit_Arel_Nodes_Top :unary
+ alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+
+ def function(o)
+ visit o.expressions
+ visit o.alias
+ visit o.distinct
+ end
+ alias :visit_Arel_Nodes_Avg :function
+ alias :visit_Arel_Nodes_Exists :function
+ alias :visit_Arel_Nodes_Max :function
+ alias :visit_Arel_Nodes_Min :function
+ alias :visit_Arel_Nodes_Sum :function
+
+ def visit_Arel_Nodes_NamedFunction(o)
+ visit o.name
+ visit o.expressions
+ visit o.distinct
+ visit o.alias
+ end
+
+ def visit_Arel_Nodes_Count(o)
+ visit o.expressions
+ visit o.alias
+ visit o.distinct
+ end
+
+ def visit_Arel_Nodes_Case(o)
+ visit o.case
+ visit o.conditions
+ visit o.default
+ end
+
+ def nary(o)
+ o.children.each { |child| visit child }
+ end
+ alias :visit_Arel_Nodes_And :nary
+
+ def binary(o)
+ visit o.left
+ visit o.right
+ end
+ alias :visit_Arel_Nodes_As :binary
+ alias :visit_Arel_Nodes_Assignment :binary
+ alias :visit_Arel_Nodes_Between :binary
+ alias :visit_Arel_Nodes_Concat :binary
+ alias :visit_Arel_Nodes_DeleteStatement :binary
+ alias :visit_Arel_Nodes_DoesNotMatch :binary
+ alias :visit_Arel_Nodes_Equality :binary
+ alias :visit_Arel_Nodes_FullOuterJoin :binary
+ alias :visit_Arel_Nodes_GreaterThan :binary
+ alias :visit_Arel_Nodes_GreaterThanOrEqual :binary
+ alias :visit_Arel_Nodes_In :binary
+ alias :visit_Arel_Nodes_InfixOperation :binary
+ alias :visit_Arel_Nodes_JoinSource :binary
+ alias :visit_Arel_Nodes_InnerJoin :binary
+ alias :visit_Arel_Nodes_LessThan :binary
+ alias :visit_Arel_Nodes_LessThanOrEqual :binary
+ alias :visit_Arel_Nodes_Matches :binary
+ alias :visit_Arel_Nodes_NotEqual :binary
+ alias :visit_Arel_Nodes_NotIn :binary
+ alias :visit_Arel_Nodes_NotRegexp :binary
+ alias :visit_Arel_Nodes_Or :binary
+ alias :visit_Arel_Nodes_OuterJoin :binary
+ alias :visit_Arel_Nodes_Regexp :binary
+ alias :visit_Arel_Nodes_RightOuterJoin :binary
+ alias :visit_Arel_Nodes_TableAlias :binary
+ alias :visit_Arel_Nodes_Values :binary
+ alias :visit_Arel_Nodes_When :binary
+
+ def visit_Arel_Nodes_StringJoin(o)
+ visit o.left
+ end
+
+ def visit_Arel_Attribute(o)
+ visit o.relation
+ visit o.name
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute
+
+ def visit_Arel_Table(o)
+ visit o.name
+ end
+
+ def terminal(o)
+ end
+ alias :visit_ActiveSupport_Multibyte_Chars :terminal
+ alias :visit_ActiveSupport_StringInquirer :terminal
+ alias :visit_Arel_Nodes_Lock :terminal
+ alias :visit_Arel_Nodes_Node :terminal
+ alias :visit_Arel_Nodes_SqlLiteral :terminal
+ alias :visit_Arel_Nodes_BindParam :terminal
+ alias :visit_Arel_Nodes_Window :terminal
+ alias :visit_Arel_Nodes_True :terminal
+ alias :visit_Arel_Nodes_False :terminal
+ alias :visit_BigDecimal :terminal
+ alias :visit_Bignum :terminal
+ alias :visit_Class :terminal
+ alias :visit_Date :terminal
+ alias :visit_DateTime :terminal
+ alias :visit_FalseClass :terminal
+ alias :visit_Fixnum :terminal
+ alias :visit_Float :terminal
+ alias :visit_Integer :terminal
+ alias :visit_NilClass :terminal
+ alias :visit_String :terminal
+ alias :visit_Symbol :terminal
+ alias :visit_Time :terminal
+ alias :visit_TrueClass :terminal
+
+ def visit_Arel_Nodes_InsertStatement(o)
+ visit o.relation
+ visit o.columns
+ visit o.values
+ end
+
+ def visit_Arel_Nodes_SelectCore(o)
+ visit o.projections
+ visit o.source
+ visit o.wheres
+ visit o.groups
+ visit o.windows
+ visit o.havings
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o)
+ visit o.cores
+ visit o.orders
+ visit o.limit
+ visit o.lock
+ visit o.offset
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o)
+ visit o.relation
+ visit o.values
+ visit o.wheres
+ visit o.orders
+ visit o.limit
+ end
+
+ def visit_Array(o)
+ o.each { |i| visit i }
+ end
+ alias :visit_Set :visit_Array
+
+ def visit_Hash(o)
+ o.each { |k, v| visit(k); visit(v) }
+ end
+
+ DISPATCH = dispatch_cache
+
+ def get_dispatch_cache
+ DISPATCH
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb
new file mode 100644
index 0000000000..d352b81914
--- /dev/null
+++ b/activerecord/lib/arel/visitors/dot.rb
@@ -0,0 +1,292 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Dot < Arel::Visitors::Visitor
+ class Node # :nodoc:
+ attr_accessor :name, :id, :fields
+
+ def initialize(name, id, fields = [])
+ @name = name
+ @id = id
+ @fields = fields
+ end
+ end
+
+ class Edge < Struct.new :name, :from, :to # :nodoc:
+ end
+
+ def initialize
+ super()
+ @nodes = []
+ @edges = []
+ @node_stack = []
+ @edge_stack = []
+ @seen = {}
+ end
+
+ def accept(object, collector)
+ visit object
+ collector << to_dot
+ end
+
+ private
+
+ def visit_Arel_Nodes_Ordering(o)
+ visit_edge o, "expr"
+ end
+
+ def visit_Arel_Nodes_TableAlias(o)
+ visit_edge o, "name"
+ visit_edge o, "relation"
+ end
+
+ def visit_Arel_Nodes_Count(o)
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ end
+
+ def visit_Arel_Nodes_Values(o)
+ visit_edge o, "expressions"
+ end
+
+ def visit_Arel_Nodes_StringJoin(o)
+ visit_edge o, "left"
+ end
+
+ def visit_Arel_Nodes_InnerJoin(o)
+ visit_edge o, "left"
+ visit_edge o, "right"
+ end
+ alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_InnerJoin
+ alias :visit_Arel_Nodes_OuterJoin :visit_Arel_Nodes_InnerJoin
+ alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_InnerJoin
+
+ def visit_Arel_Nodes_DeleteStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "wheres"
+ end
+
+ def unary(o)
+ visit_edge o, "expr"
+ end
+ alias :visit_Arel_Nodes_Group :unary
+ alias :visit_Arel_Nodes_Cube :unary
+ alias :visit_Arel_Nodes_RollUp :unary
+ alias :visit_Arel_Nodes_GroupingSet :unary
+ alias :visit_Arel_Nodes_GroupingElement :unary
+ alias :visit_Arel_Nodes_Grouping :unary
+ alias :visit_Arel_Nodes_Having :unary
+ alias :visit_Arel_Nodes_Limit :unary
+ alias :visit_Arel_Nodes_Not :unary
+ alias :visit_Arel_Nodes_Offset :unary
+ alias :visit_Arel_Nodes_On :unary
+ alias :visit_Arel_Nodes_Top :unary
+ alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+ alias :visit_Arel_Nodes_Preceding :unary
+ alias :visit_Arel_Nodes_Following :unary
+ alias :visit_Arel_Nodes_Rows :unary
+ alias :visit_Arel_Nodes_Range :unary
+
+ def window(o)
+ visit_edge o, "partitions"
+ visit_edge o, "orders"
+ visit_edge o, "framing"
+ end
+ alias :visit_Arel_Nodes_Window :window
+
+ def named_window(o)
+ visit_edge o, "partitions"
+ visit_edge o, "orders"
+ visit_edge o, "framing"
+ visit_edge o, "name"
+ end
+ alias :visit_Arel_Nodes_NamedWindow :named_window
+
+ def function(o)
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ visit_edge o, "alias"
+ end
+ alias :visit_Arel_Nodes_Exists :function
+ alias :visit_Arel_Nodes_Min :function
+ alias :visit_Arel_Nodes_Max :function
+ alias :visit_Arel_Nodes_Avg :function
+ alias :visit_Arel_Nodes_Sum :function
+
+ def extract(o)
+ visit_edge o, "expressions"
+ visit_edge o, "alias"
+ end
+ alias :visit_Arel_Nodes_Extract :extract
+
+ def visit_Arel_Nodes_NamedFunction(o)
+ visit_edge o, "name"
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ visit_edge o, "alias"
+ end
+
+ def visit_Arel_Nodes_InsertStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "columns"
+ visit_edge o, "values"
+ end
+
+ def visit_Arel_Nodes_SelectCore(o)
+ visit_edge o, "source"
+ visit_edge o, "projections"
+ visit_edge o, "wheres"
+ visit_edge o, "windows"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o)
+ visit_edge o, "cores"
+ visit_edge o, "limit"
+ visit_edge o, "orders"
+ visit_edge o, "offset"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "wheres"
+ visit_edge o, "values"
+ end
+
+ def visit_Arel_Table(o)
+ visit_edge o, "name"
+ end
+
+ def visit_Arel_Nodes_Casted(o)
+ visit_edge o, "val"
+ visit_edge o, "attribute"
+ end
+
+ def visit_Arel_Attribute(o)
+ visit_edge o, "relation"
+ visit_edge o, "name"
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute
+
+ def nary(o)
+ o.children.each_with_index do |x, i|
+ edge(i) { visit x }
+ end
+ end
+ alias :visit_Arel_Nodes_And :nary
+
+ def binary(o)
+ visit_edge o, "left"
+ visit_edge o, "right"
+ end
+ alias :visit_Arel_Nodes_As :binary
+ alias :visit_Arel_Nodes_Assignment :binary
+ alias :visit_Arel_Nodes_Between :binary
+ alias :visit_Arel_Nodes_Concat :binary
+ alias :visit_Arel_Nodes_DoesNotMatch :binary
+ alias :visit_Arel_Nodes_Equality :binary
+ alias :visit_Arel_Nodes_GreaterThan :binary
+ alias :visit_Arel_Nodes_GreaterThanOrEqual :binary
+ alias :visit_Arel_Nodes_In :binary
+ alias :visit_Arel_Nodes_JoinSource :binary
+ alias :visit_Arel_Nodes_LessThan :binary
+ alias :visit_Arel_Nodes_LessThanOrEqual :binary
+ alias :visit_Arel_Nodes_Matches :binary
+ alias :visit_Arel_Nodes_NotEqual :binary
+ alias :visit_Arel_Nodes_NotIn :binary
+ alias :visit_Arel_Nodes_Or :binary
+ alias :visit_Arel_Nodes_Over :binary
+
+ def visit_String(o)
+ @node_stack.last.fields << o
+ end
+ alias :visit_Time :visit_String
+ alias :visit_Date :visit_String
+ alias :visit_DateTime :visit_String
+ alias :visit_NilClass :visit_String
+ alias :visit_TrueClass :visit_String
+ alias :visit_FalseClass :visit_String
+ alias :visit_Integer :visit_String
+ alias :visit_Fixnum :visit_String
+ alias :visit_BigDecimal :visit_String
+ alias :visit_Float :visit_String
+ alias :visit_Symbol :visit_String
+ alias :visit_Arel_Nodes_SqlLiteral :visit_String
+
+ def visit_Arel_Nodes_BindParam(o); end
+
+ def visit_Hash(o)
+ o.each_with_index do |pair, i|
+ edge("pair_#{i}") { visit pair }
+ end
+ end
+
+ def visit_Array(o)
+ o.each_with_index do |x, i|
+ edge(i) { visit x }
+ end
+ end
+ alias :visit_Set :visit_Array
+
+ def visit_edge(o, method)
+ edge(method) { visit o.send(method) }
+ end
+
+ def visit(o)
+ if node = @seen[o.object_id]
+ @edge_stack.last.to = node
+ return
+ end
+
+ node = Node.new(o.class.name, o.object_id)
+ @seen[node.id] = node
+ @nodes << node
+ with_node node do
+ super
+ end
+ end
+
+ def edge(name)
+ edge = Edge.new(name, @node_stack.last)
+ @edge_stack.push edge
+ @edges << edge
+ yield
+ @edge_stack.pop
+ end
+
+ def with_node(node)
+ if edge = @edge_stack.last
+ edge.to = node
+ end
+
+ @node_stack.push node
+ yield
+ @node_stack.pop
+ end
+
+ def quote(string)
+ string.to_s.gsub('"', '\"')
+ end
+
+ def to_dot
+ "digraph \"Arel\" {\nnode [width=0.375,height=0.25,shape=record];\n" +
+ @nodes.map { |node|
+ label = "<f0>#{node.name}"
+
+ node.fields.each_with_index do |field, i|
+ label += "|<f#{i + 1}>#{quote field}"
+ end
+
+ "#{node.id} [label=\"#{label}\"];"
+ }.join("\n") + "\n" + @edges.map { |edge|
+ "#{edge.from.id} -> #{edge.to.id} [label=\"#{edge.name}\"];"
+ }.join("\n") + "\n}"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb
new file mode 100644
index 0000000000..0a06aef60b
--- /dev/null
+++ b/activerecord/lib/arel/visitors/ibm_db.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class IBM_DB < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FETCH FIRST "
+ collector = visit o.expr, collector
+ collector << " ROWS ONLY"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb
new file mode 100644
index 0000000000..0a9713794e
--- /dev/null
+++ b/activerecord/lib/arel/visitors/informix.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Informix < Arel::Visitors::ToSql
+ private
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ collector << "SELECT "
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.limit, collector
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore x, c
+ }
+ if o.orders.any?
+ collector << "ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+ collector = maybe_visit o.lock, collector
+ end
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector = inject_join o.projections, collector, ", "
+ if o.source && !o.source.empty?
+ collector << " FROM "
+ collector = visit o.source, collector
+ end
+
+ if o.wheres.any?
+ collector << " WHERE "
+ collector = inject_join o.wheres, collector, " AND "
+ end
+
+ if o.groups.any?
+ collector << "GROUP BY "
+ collector = inject_join o.groups, collector, ", "
+ end
+
+ if o.havings.any?
+ collector << " HAVING "
+ collector = inject_join o.havings, collector, " AND "
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "SKIP "
+ visit o.expr, collector
+ end
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FIRST "
+ visit o.expr, collector
+ collector << " "
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb
new file mode 100644
index 0000000000..9aedc51d15
--- /dev/null
+++ b/activerecord/lib/arel/visitors/mssql.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class MSSQL < Arel::Visitors::ToSql
+ RowNumber = Struct.new :children
+
+ def initialize(*)
+ @primary_keys = {}
+ super
+ end
+
+ private
+
+ # `top` wouldn't really work here. I.e. User.select("distinct first_name").limit(10) would generate
+ # "select top 10 distinct first_name from users", which is invalid query! it should be
+ # "select distinct top 10 first_name from users"
+ def visit_Arel_Nodes_Top(o)
+ ""
+ end
+
+ def visit_Arel_Visitors_MSSQL_RowNumber(o, collector)
+ collector << "ROW_NUMBER() OVER (ORDER BY "
+ inject_join(o.children, collector, ", ") << ") as _row_num"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if !o.limit && !o.offset
+ return super
+ end
+
+ is_select_count = false
+ o.cores.each { |x|
+ core_order_by = row_num_literal determine_order_by(o.orders, x)
+ if select_count? x
+ x.projections = [core_order_by]
+ is_select_count = true
+ else
+ x.projections << core_order_by
+ end
+ }
+
+ if is_select_count
+ # fixme count distinct wouldn't work with limit or offset
+ collector << "SELECT COUNT(1) as count_id FROM ("
+ end
+
+ collector << "SELECT _t.* FROM ("
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore x, c
+ }
+ collector << ") as _t WHERE #{get_offset_limit_clause(o)}"
+
+ if is_select_count
+ collector << ") AS subquery"
+ else
+ collector
+ end
+ end
+
+ def get_offset_limit_clause(o)
+ first_row = o.offset ? o.offset.expr.to_i + 1 : 1
+ last_row = o.limit ? o.limit.expr.to_i - 1 + first_row : nil
+ if last_row
+ " _row_num BETWEEN #{first_row} AND #{last_row}"
+ else
+ " _row_num >= #{first_row}"
+ end
+ end
+
+ def visit_Arel_Nodes_DeleteStatement(o, collector)
+ collector << "DELETE "
+ if o.limit
+ collector << "TOP ("
+ visit o.limit.expr, collector
+ collector << ") "
+ end
+ collector << "FROM "
+ collector = visit o.relation, collector
+ if o.wheres.any?
+ collector << " WHERE "
+ inject_join o.wheres, collector, AND
+ else
+ collector
+ end
+ end
+
+ def determine_order_by(orders, x)
+ if orders.any?
+ orders
+ elsif x.groups.any?
+ x.groups
+ else
+ pk = find_left_table_pk(x.froms)
+ pk ? [pk] : []
+ end
+ end
+
+ def row_num_literal(order_by)
+ RowNumber.new order_by
+ end
+
+ def select_count?(x)
+ x.projections.length == 1 && Arel::Nodes::Count === x.projections.first
+ end
+
+ # FIXME raise exception of there is no pk?
+ def find_left_table_pk(o)
+ if o.kind_of?(Arel::Nodes::Join)
+ find_left_table_pk(o.left)
+ elsif o.instance_of?(Arel::Table)
+ find_primary_key(o)
+ end
+ end
+
+ def find_primary_key(o)
+ @primary_keys[o.name] ||= begin
+ primary_key_name = @connection.primary_key(o.name)
+ # some tables might be without primary key
+ primary_key_name && o[primary_key_name]
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb
new file mode 100644
index 0000000000..37bfb661f0
--- /dev/null
+++ b/activerecord/lib/arel/visitors/mysql.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class MySQL < Arel::Visitors::ToSql
+ private
+ def visit_Arel_Nodes_Union(o, collector, suppress_parens = false)
+ unless suppress_parens
+ collector << "( "
+ end
+
+ collector = case o.left
+ when Arel::Nodes::Union
+ visit_Arel_Nodes_Union o.left, collector, true
+ else
+ visit o.left, collector
+ end
+
+ collector << " UNION "
+
+ collector = case o.right
+ when Arel::Nodes::Union
+ visit_Arel_Nodes_Union o.right, collector, true
+ else
+ visit o.right, collector
+ end
+
+ if suppress_parens
+ collector
+ else
+ collector << " )"
+ end
+ end
+
+ def visit_Arel_Nodes_Bin(o, collector)
+ collector << "BINARY "
+ visit o.expr, collector
+ end
+
+ ###
+ # :'(
+ # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if o.offset && !o.limit
+ o.limit = Arel::Nodes::Limit.new(18446744073709551615)
+ end
+ super
+ end
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ o.froms ||= Arel.sql("DUAL")
+ super
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ collector << "UPDATE "
+ collector = visit o.relation, collector
+
+ unless o.values.empty?
+ collector << " SET "
+ collector = inject_join o.values, collector, ", "
+ end
+
+ unless o.wheres.empty?
+ collector << " WHERE "
+ collector = inject_join o.wheres, collector, " AND "
+ end
+
+ unless o.orders.empty?
+ collector << " ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+
+ maybe_visit o.limit, collector
+ end
+
+ def visit_Arel_Nodes_Concat(o, collector)
+ collector << " CONCAT("
+ visit o.left, collector
+ collector << ", "
+ visit o.right, collector
+ collector << ") "
+ collector
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb
new file mode 100644
index 0000000000..30a1529d46
--- /dev/null
+++ b/activerecord/lib/arel/visitors/oracle.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Oracle < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ o = order_hacks(o)
+
+ # if need to select first records without ORDER BY and GROUP BY and without DISTINCT
+ # then can use simple ROWNUM in WHERE clause
+ if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && o.cores.first.set_quantifier.class.to_s !~ /Distinct/
+ o.cores.last.wheres.push Nodes::LessThanOrEqual.new(
+ Nodes::SqlLiteral.new("ROWNUM"), o.limit.expr
+ )
+ return super
+ end
+
+ if o.limit && o.offset
+ o = o.dup
+ limit = o.limit.expr
+ offset = o.offset
+ o.offset = nil
+ collector << "
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM ("
+
+ collector = super(o, collector)
+
+ if offset.expr.is_a? Nodes::BindParam
+ collector << ") raw_sql_ WHERE rownum <= ("
+ collector = visit offset.expr, collector
+ collector << " + "
+ collector = visit limit, collector
+ collector << ") ) WHERE raw_rnum_ > "
+ collector = visit offset.expr, collector
+ return collector
+ else
+ collector << ") raw_sql_
+ WHERE rownum <= #{offset.expr.to_i + limit}
+ )
+ WHERE "
+ return visit(offset, collector)
+ end
+ end
+
+ if o.limit
+ o = o.dup
+ limit = o.limit.expr
+ collector << "SELECT * FROM ("
+ collector = super(o, collector)
+ collector << ") WHERE ROWNUM <= "
+ return visit limit, collector
+ end
+
+ if o.offset
+ o = o.dup
+ offset = o.offset
+ o.offset = nil
+ collector << "SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM ("
+ collector = super(o, collector)
+ collector << ") raw_sql_
+ )
+ WHERE "
+ return visit offset, collector
+ end
+
+ super
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "raw_rnum_ > "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ collector = infix_value o, collector, " MINUS "
+ collector << " )"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ # Oracle does not allow ORDER BY/LIMIT in UPDATEs.
+ if o.orders.any? && o.limit.nil?
+ # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided,
+ # otherwise let the user deal with the error
+ o = o.dup
+ o.orders = []
+ end
+
+ super
+ end
+
+ ###
+ # Hacks for the order clauses specific to Oracle
+ def order_hacks(o)
+ return o if o.orders.empty?
+ return o unless o.cores.any? do |core|
+ core.projections.any? do |projection|
+ /FIRST_VALUE/ === projection
+ end
+ end
+ # Previous version with join and split broke ORDER BY clause
+ # if it contained functions with several arguments (separated by ',').
+ #
+ # orders = o.orders.map { |x| visit x }.join(', ').split(',')
+ orders = o.orders.map do |x|
+ string = visit(x, Arel::Collectors::SQLString.new).value
+ if string.include?(",")
+ split_order_string(string)
+ else
+ string
+ end
+ end.flatten
+ o.orders = []
+ orders.each_with_index do |order, i|
+ o.orders <<
+ Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i === order}")
+ end
+ o
+ end
+
+ # Split string by commas but count opening and closing brackets
+ # and ignore commas inside brackets.
+ def split_order_string(string)
+ array = []
+ i = 0
+ string.split(",").each do |part|
+ if array[i]
+ array[i] << "," << part
+ else
+ # to ensure that array[i] will be String and not Arel::Nodes::SqlLiteral
+ array[i] = part.to_s
+ end
+ i += 1 if array[i].count("(") == array[i].count(")")
+ end
+ array
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| ":a#{i}" }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb
new file mode 100644
index 0000000000..7061f06087
--- /dev/null
+++ b/activerecord/lib/arel/visitors/oracle12.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Oracle12 < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ # Oracle does not allow LIMIT clause with select for update
+ if o.limit && o.lock
+ raise ArgumentError, <<-MSG
+ 'Combination of limit and lock is not supported.
+ because generated SQL statements
+ `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014.`
+ MSG
+ end
+ super
+ end
+
+ def visit_Arel_Nodes_SelectOptions(o, collector)
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.limit, collector
+ collector = maybe_visit o.lock, collector
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FETCH FIRST "
+ collector = visit o.expr, collector
+ collector << " ROWS ONLY"
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "OFFSET "
+ visit o.expr, collector
+ collector << " ROWS"
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ collector = infix_value o, collector, " MINUS "
+ collector << " )"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ # Oracle does not allow ORDER BY/LIMIT in UPDATEs.
+ if o.orders.any? && o.limit.nil?
+ # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided,
+ # otherwise let the user deal with the error
+ o = o.dup
+ o.orders = []
+ end
+
+ super
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| ":a#{i}" }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb
new file mode 100644
index 0000000000..c5110fa89c
--- /dev/null
+++ b/activerecord/lib/arel/visitors/postgresql.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class PostgreSQL < Arel::Visitors::ToSql
+ CUBE = "CUBE"
+ ROLLUP = "ROLLUP"
+ GROUPING_SETS = "GROUPING SETS"
+ LATERAL = "LATERAL"
+
+ private
+
+ def visit_Arel_Nodes_Matches(o, collector)
+ op = o.case_sensitive ? " LIKE " : " ILIKE "
+ collector = infix_value o, collector, op
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_DoesNotMatch(o, collector)
+ op = o.case_sensitive ? " NOT LIKE " : " NOT ILIKE "
+ collector = infix_value o, collector, op
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Regexp(o, collector)
+ op = o.case_sensitive ? " ~ " : " ~* "
+ infix_value o, collector, op
+ end
+
+ def visit_Arel_Nodes_NotRegexp(o, collector)
+ op = o.case_sensitive ? " !~ " : " !~* "
+ infix_value o, collector, op
+ end
+
+ def visit_Arel_Nodes_DistinctOn(o, collector)
+ collector << "DISTINCT ON ( "
+ visit(o.expr, collector) << " )"
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| "$#{i}" }
+ end
+
+ def visit_Arel_Nodes_GroupingElement(o, collector)
+ collector << "( "
+ visit(o.expr, collector) << " )"
+ end
+
+ def visit_Arel_Nodes_Cube(o, collector)
+ collector << CUBE
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_RollUp(o, collector)
+ collector << ROLLUP
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_GroupingSet(o, collector)
+ collector << GROUPING_SETS
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_Lateral(o, collector)
+ collector << LATERAL
+ collector << SPACE
+ grouping_parentheses o, collector
+ end
+
+ # Used by Lateral visitor to enclose select queries in parentheses
+ def grouping_parentheses(o, collector)
+ if o.expr.is_a? Nodes::SelectStatement
+ collector << "("
+ visit o.expr, collector
+ collector << ")"
+ else
+ visit o.expr, collector
+ end
+ end
+
+ # Utilized by GroupingSet, Cube & RollUp visitors to
+ # handle grouping aggregation semantics
+ def grouping_array_or_grouping_element(o, collector)
+ if o.expr.is_a? Array
+ collector << "( "
+ visit o.expr, collector
+ collector << " )"
+ else
+ visit o.expr, collector
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb
new file mode 100644
index 0000000000..cb1d2424ad
--- /dev/null
+++ b/activerecord/lib/arel/visitors/sqlite.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class SQLite < Arel::Visitors::ToSql
+ private
+
+ # Locks are not supported in SQLite
+ def visit_Arel_Nodes_Lock(o, collector)
+ collector
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ o.limit = Arel::Nodes::Limit.new(-1) if o.offset && !o.limit
+ super
+ end
+
+ def visit_Arel_Nodes_True(o, collector)
+ collector << "1"
+ end
+
+ def visit_Arel_Nodes_False(o, collector)
+ collector << "0"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb
new file mode 100644
index 0000000000..5986fd5576
--- /dev/null
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -0,0 +1,847 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class UnsupportedVisitError < StandardError
+ def initialize(object)
+ super "Unsupported argument type: #{object.class.name}. Construct an Arel node instead."
+ end
+ end
+
+ class ToSql < Arel::Visitors::Visitor
+ ##
+ # This is some roflscale crazy stuff. I'm roflscaling this because
+ # building SQL queries is a hotspot. I will explain the roflscale so that
+ # others will not rm this code.
+ #
+ # In YARV, string literals in a method body will get duped when the byte
+ # code is executed. Let's take a look:
+ #
+ # > puts RubyVM::InstructionSequence.new('def foo; "bar"; end').disasm
+ #
+ # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>=====
+ # 0000 trace 8
+ # 0002 trace 1
+ # 0004 putstring "bar"
+ # 0006 trace 16
+ # 0008 leave
+ #
+ # The `putstring` bytecode will dup the string and push it on the stack.
+ # In many cases in our SQL visitor, that string is never mutated, so there
+ # is no need to dup the literal.
+ #
+ # If we change to a constant lookup, the string will not be duped, and we
+ # can reduce the objects in our system:
+ #
+ # > puts RubyVM::InstructionSequence.new('BAR = "bar"; def foo; BAR; end').disasm
+ #
+ # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>========
+ # 0000 trace 8
+ # 0002 trace 1
+ # 0004 getinlinecache 11, <ic:0>
+ # 0007 getconstant :BAR
+ # 0009 setinlinecache <ic:0>
+ # 0011 trace 16
+ # 0013 leave
+ #
+ # `getconstant` should be a hash lookup, and no object is duped when the
+ # value of the constant is pushed on the stack. Hence the crazy
+ # constants below.
+ #
+ # `matches` and `doesNotMatch` operate case-insensitively via Visitor subclasses
+ # specialized for specific databases when necessary.
+ #
+
+ WHERE = " WHERE " # :nodoc:
+ SPACE = " " # :nodoc:
+ COMMA = ", " # :nodoc:
+ GROUP_BY = " GROUP BY " # :nodoc:
+ ORDER_BY = " ORDER BY " # :nodoc:
+ WINDOW = " WINDOW " # :nodoc:
+ AND = " AND " # :nodoc:
+
+ DISTINCT = "DISTINCT" # :nodoc:
+
+ def initialize(connection)
+ super()
+ @connection = connection
+ end
+
+ def compile(node, &block)
+ accept(node, Arel::Collectors::SQLString.new, &block).value
+ end
+
+ private
+
+ def visit_Arel_Nodes_DeleteStatement(o, collector)
+ collector << "DELETE FROM "
+ collector = visit o.relation, collector
+ if o.wheres.any?
+ collector << WHERE
+ collector = inject_join o.wheres, collector, AND
+ end
+
+ maybe_visit o.limit, collector
+ end
+
+ # FIXME: we should probably have a 2-pass visitor for this
+ def build_subselect(key, o)
+ stmt = Nodes::SelectStatement.new
+ core = stmt.cores.first
+ core.froms = o.relation
+ core.wheres = o.wheres
+ core.projections = [key]
+ stmt.limit = o.limit
+ stmt.orders = o.orders
+ stmt
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ if o.orders.empty? && o.limit.nil?
+ wheres = o.wheres
+ else
+ wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
+ end
+
+ collector << "UPDATE "
+ collector = visit o.relation, collector
+ unless o.values.empty?
+ collector << " SET "
+ collector = inject_join o.values, collector, ", "
+ end
+
+ unless wheres.empty?
+ collector << " WHERE "
+ collector = inject_join wheres, collector, " AND "
+ end
+
+ collector
+ end
+
+ def visit_Arel_Nodes_InsertStatement(o, collector)
+ collector << "INSERT INTO "
+ collector = visit o.relation, collector
+ if o.columns.any?
+ collector << " (#{o.columns.map { |x|
+ quote_column_name x.name
+ }.join ', '})"
+ end
+
+ if o.values
+ maybe_visit o.values, collector
+ elsif o.select
+ maybe_visit o.select, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Exists(o, collector)
+ collector << "EXISTS ("
+ collector = visit(o.expressions, collector) << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Casted(o, collector)
+ collector << quoted(o.val, o.attribute).to_s
+ end
+
+ def visit_Arel_Nodes_Quoted(o, collector)
+ collector << quoted(o.expr, nil).to_s
+ end
+
+ def visit_Arel_Nodes_True(o, collector)
+ collector << "TRUE"
+ end
+
+ def visit_Arel_Nodes_False(o, collector)
+ collector << "FALSE"
+ end
+
+ def visit_Arel_Nodes_ValuesList(o, collector)
+ collector << "VALUES "
+
+ len = o.rows.length - 1
+ o.rows.each_with_index { |row, i|
+ collector << "("
+ row_len = row.length - 1
+ row.each_with_index do |value, k|
+ case value
+ when Nodes::SqlLiteral, Nodes::BindParam
+ collector = visit(value, collector)
+ else
+ collector << quote(value)
+ end
+ collector << COMMA unless k == row_len
+ end
+ collector << ")"
+ collector << COMMA unless i == len
+ }
+ collector
+ end
+
+ def visit_Arel_Nodes_Values(o, collector)
+ collector << "VALUES ("
+
+ len = o.expressions.length - 1
+ o.expressions.each_with_index { |value, i|
+ case value
+ when Nodes::SqlLiteral, Nodes::BindParam
+ collector = visit value, collector
+ else
+ collector << quote(value).to_s
+ end
+ unless i == len
+ collector << COMMA
+ end
+ }
+
+ collector << ")"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if o.with
+ collector = visit o.with, collector
+ collector << SPACE
+ end
+
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore(x, c)
+ }
+
+ unless o.orders.empty?
+ collector << ORDER_BY
+ len = o.orders.length - 1
+ o.orders.each_with_index { |x, i|
+ collector = visit(x, collector)
+ collector << COMMA unless len == i
+ }
+ end
+
+ visit_Arel_Nodes_SelectOptions(o, collector)
+
+ collector
+ end
+
+ def visit_Arel_Nodes_SelectOptions(o, collector)
+ collector = maybe_visit o.limit, collector
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.lock, collector
+ end
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector << "SELECT"
+
+ collector = maybe_visit o.top, collector
+
+ collector = maybe_visit o.set_quantifier, collector
+
+ collect_nodes_for o.projections, collector, SPACE
+
+ if o.source && !o.source.empty?
+ collector << " FROM "
+ collector = visit o.source, collector
+ end
+
+ collect_nodes_for o.wheres, collector, WHERE, AND
+ collect_nodes_for o.groups, collector, GROUP_BY
+ unless o.havings.empty?
+ collector << " HAVING "
+ inject_join o.havings, collector, AND
+ end
+ collect_nodes_for o.windows, collector, WINDOW
+
+ collector
+ end
+
+ def collect_nodes_for(nodes, collector, spacer, connector = COMMA)
+ unless nodes.empty?
+ collector << spacer
+ len = nodes.length - 1
+ nodes.each_with_index do |x, i|
+ collector = visit(x, collector)
+ collector << connector unless len == i
+ end
+ end
+ end
+
+ def visit_Arel_Nodes_Bin(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Distinct(o, collector)
+ collector << DISTINCT
+ end
+
+ def visit_Arel_Nodes_DistinctOn(o, collector)
+ raise NotImplementedError, "DISTINCT ON not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_With(o, collector)
+ collector << "WITH "
+ inject_join o.children, collector, COMMA
+ end
+
+ def visit_Arel_Nodes_WithRecursive(o, collector)
+ collector << "WITH RECURSIVE "
+ inject_join o.children, collector, COMMA
+ end
+
+ def visit_Arel_Nodes_Union(o, collector)
+ collector << "( "
+ infix_value(o, collector, " UNION ") << " )"
+ end
+
+ def visit_Arel_Nodes_UnionAll(o, collector)
+ collector << "( "
+ infix_value(o, collector, " UNION ALL ") << " )"
+ end
+
+ def visit_Arel_Nodes_Intersect(o, collector)
+ collector << "( "
+ infix_value(o, collector, " INTERSECT ") << " )"
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ infix_value(o, collector, " EXCEPT ") << " )"
+ end
+
+ def visit_Arel_Nodes_NamedWindow(o, collector)
+ collector << quote_column_name(o.name)
+ collector << " AS "
+ visit_Arel_Nodes_Window o, collector
+ end
+
+ def visit_Arel_Nodes_Window(o, collector)
+ collector << "("
+
+ if o.partitions.any?
+ collector << "PARTITION BY "
+ collector = inject_join o.partitions, collector, ", "
+ end
+
+ if o.orders.any?
+ collector << SPACE if o.partitions.any?
+ collector << "ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+
+ if o.framing
+ collector << SPACE if o.partitions.any? || o.orders.any?
+ collector = visit o.framing, collector
+ end
+
+ collector << ")"
+ end
+
+ def visit_Arel_Nodes_Rows(o, collector)
+ if o.expr
+ collector << "ROWS "
+ visit o.expr, collector
+ else
+ collector << "ROWS"
+ end
+ end
+
+ def visit_Arel_Nodes_Range(o, collector)
+ if o.expr
+ collector << "RANGE "
+ visit o.expr, collector
+ else
+ collector << "RANGE"
+ end
+ end
+
+ def visit_Arel_Nodes_Preceding(o, collector)
+ collector = if o.expr
+ visit o.expr, collector
+ else
+ collector << "UNBOUNDED"
+ end
+
+ collector << " PRECEDING"
+ end
+
+ def visit_Arel_Nodes_Following(o, collector)
+ collector = if o.expr
+ visit o.expr, collector
+ else
+ collector << "UNBOUNDED"
+ end
+
+ collector << " FOLLOWING"
+ end
+
+ def visit_Arel_Nodes_CurrentRow(o, collector)
+ collector << "CURRENT ROW"
+ end
+
+ def visit_Arel_Nodes_Over(o, collector)
+ case o.right
+ when nil
+ visit(o.left, collector) << " OVER ()"
+ when Arel::Nodes::SqlLiteral
+ infix_value o, collector, " OVER "
+ when String, Symbol
+ visit(o.left, collector) << " OVER #{quote_column_name o.right.to_s}"
+ else
+ infix_value o, collector, " OVER "
+ end
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "OFFSET "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "LIMIT "
+ visit o.expr, collector
+ end
+
+ # FIXME: this does nothing on most databases, but does on MSSQL
+ def visit_Arel_Nodes_Top(o, collector)
+ collector
+ end
+
+ def visit_Arel_Nodes_Lock(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Grouping(o, collector)
+ if o.expr.is_a? Nodes::Grouping
+ visit(o.expr, collector)
+ else
+ collector << "("
+ visit(o.expr, collector) << ")"
+ end
+ end
+
+ def visit_Arel_SelectManager(o, collector)
+ collector << "("
+ visit(o.ast, collector) << ")"
+ end
+
+ def visit_Arel_Nodes_Ascending(o, collector)
+ visit(o.expr, collector) << " ASC"
+ end
+
+ def visit_Arel_Nodes_Descending(o, collector)
+ visit(o.expr, collector) << " DESC"
+ end
+
+ def visit_Arel_Nodes_Group(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_NamedFunction(o, collector)
+ collector << o.name
+ collector << "("
+ collector << "DISTINCT " if o.distinct
+ collector = inject_join(o.expressions, collector, ", ") << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Extract(o, collector)
+ collector << "EXTRACT(#{o.field.to_s.upcase} FROM "
+ visit(o.expr, collector) << ")"
+ end
+
+ def visit_Arel_Nodes_Count(o, collector)
+ aggregate "COUNT", o, collector
+ end
+
+ def visit_Arel_Nodes_Sum(o, collector)
+ aggregate "SUM", o, collector
+ end
+
+ def visit_Arel_Nodes_Max(o, collector)
+ aggregate "MAX", o, collector
+ end
+
+ def visit_Arel_Nodes_Min(o, collector)
+ aggregate "MIN", o, collector
+ end
+
+ def visit_Arel_Nodes_Avg(o, collector)
+ aggregate "AVG", o, collector
+ end
+
+ def visit_Arel_Nodes_TableAlias(o, collector)
+ collector = visit o.relation, collector
+ collector << " "
+ collector << quote_table_name(o.name)
+ end
+
+ def visit_Arel_Nodes_Between(o, collector)
+ collector = visit o.left, collector
+ collector << " BETWEEN "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_GreaterThanOrEqual(o, collector)
+ collector = visit o.left, collector
+ collector << " >= "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_GreaterThan(o, collector)
+ collector = visit o.left, collector
+ collector << " > "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_LessThanOrEqual(o, collector)
+ collector = visit o.left, collector
+ collector << " <= "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_LessThan(o, collector)
+ collector = visit o.left, collector
+ collector << " < "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Matches(o, collector)
+ collector = visit o.left, collector
+ collector << " LIKE "
+ collector = visit o.right, collector
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_DoesNotMatch(o, collector)
+ collector = visit o.left, collector
+ collector << " NOT LIKE "
+ collector = visit o.right, collector
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_JoinSource(o, collector)
+ if o.left
+ collector = visit o.left, collector
+ end
+ if o.right.any?
+ collector << SPACE if o.left
+ collector = inject_join o.right, collector, SPACE
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_Regexp(o, collector)
+ raise NotImplementedError, "~ not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_NotRegexp(o, collector)
+ raise NotImplementedError, "!~ not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_StringJoin(o, collector)
+ visit o.left, collector
+ end
+
+ def visit_Arel_Nodes_FullOuterJoin(o, collector)
+ collector << "FULL OUTER JOIN "
+ collector = visit o.left, collector
+ collector << SPACE
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_OuterJoin(o, collector)
+ collector << "LEFT OUTER JOIN "
+ collector = visit o.left, collector
+ collector << " "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_RightOuterJoin(o, collector)
+ collector << "RIGHT OUTER JOIN "
+ collector = visit o.left, collector
+ collector << SPACE
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_InnerJoin(o, collector)
+ collector << "INNER JOIN "
+ collector = visit o.left, collector
+ if o.right
+ collector << SPACE
+ visit(o.right, collector)
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_On(o, collector)
+ collector << "ON "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Not(o, collector)
+ collector << "NOT ("
+ visit(o.expr, collector) << ")"
+ end
+
+ def visit_Arel_Table(o, collector)
+ if o.table_alias
+ collector << "#{quote_table_name o.name} #{quote_table_name o.table_alias}"
+ else
+ collector << quote_table_name(o.name)
+ end
+ end
+
+ def visit_Arel_Nodes_In(o, collector)
+ if Array === o.right && o.right.empty?
+ collector << "1=0"
+ else
+ collector = visit o.left, collector
+ collector << " IN ("
+ visit(o.right, collector) << ")"
+ end
+ end
+
+ def visit_Arel_Nodes_NotIn(o, collector)
+ if Array === o.right && o.right.empty?
+ collector << "1=1"
+ else
+ collector = visit o.left, collector
+ collector << " NOT IN ("
+ collector = visit o.right, collector
+ collector << ")"
+ end
+ end
+
+ def visit_Arel_Nodes_And(o, collector)
+ inject_join o.children, collector, " AND "
+ end
+
+ def visit_Arel_Nodes_Or(o, collector)
+ collector = visit o.left, collector
+ collector << " OR "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Assignment(o, collector)
+ case o.right
+ when Arel::Nodes::UnqualifiedColumn, Arel::Attributes::Attribute, Arel::Nodes::BindParam
+ collector = visit o.left, collector
+ collector << " = "
+ visit o.right, collector
+ else
+ collector = visit o.left, collector
+ collector << " = "
+ collector << quote(o.right).to_s
+ end
+ end
+
+ def visit_Arel_Nodes_Equality(o, collector)
+ right = o.right
+
+ collector = visit o.left, collector
+
+ if right.nil?
+ collector << " IS NULL"
+ else
+ collector << " = "
+ visit right, collector
+ end
+ end
+
+ def visit_Arel_Nodes_NotEqual(o, collector)
+ right = o.right
+
+ collector = visit o.left, collector
+
+ if right.nil?
+ collector << " IS NOT NULL"
+ else
+ collector << " != "
+ visit right, collector
+ end
+ end
+
+ def visit_Arel_Nodes_As(o, collector)
+ collector = visit o.left, collector
+ collector << " AS "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Case(o, collector)
+ collector << "CASE "
+ if o.case
+ visit o.case, collector
+ collector << " "
+ end
+ o.conditions.each do |condition|
+ visit condition, collector
+ collector << " "
+ end
+ if o.default
+ visit o.default, collector
+ collector << " "
+ end
+ collector << "END"
+ end
+
+ def visit_Arel_Nodes_When(o, collector)
+ collector << "WHEN "
+ visit o.left, collector
+ collector << " THEN "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Else(o, collector)
+ collector << "ELSE "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_UnqualifiedColumn(o, collector)
+ collector << "#{quote_column_name o.name}"
+ collector
+ end
+
+ def visit_Arel_Attributes_Attribute(o, collector)
+ join_name = o.relation.table_alias || o.relation.name
+ collector << "#{quote_table_name join_name}.#{quote_column_name o.name}"
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
+
+ def literal(o, collector); collector << o.to_s; end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { "?" }
+ end
+
+ alias :visit_Arel_Nodes_SqlLiteral :literal
+ alias :visit_Bignum :literal
+ alias :visit_Fixnum :literal
+ alias :visit_Integer :literal
+
+ def quoted(o, a)
+ if a && a.able_to_type_cast?
+ quote(a.type_cast_for_database(o))
+ else
+ quote(o)
+ end
+ end
+
+ def unsupported(o, collector)
+ raise UnsupportedVisitError.new(o)
+ end
+
+ alias :visit_ActiveSupport_Multibyte_Chars :unsupported
+ alias :visit_ActiveSupport_StringInquirer :unsupported
+ alias :visit_BigDecimal :unsupported
+ alias :visit_Class :unsupported
+ alias :visit_Date :unsupported
+ alias :visit_DateTime :unsupported
+ alias :visit_FalseClass :unsupported
+ alias :visit_Float :unsupported
+ alias :visit_Hash :unsupported
+ alias :visit_NilClass :unsupported
+ alias :visit_String :unsupported
+ alias :visit_Symbol :unsupported
+ alias :visit_Time :unsupported
+ alias :visit_TrueClass :unsupported
+
+ def visit_Arel_Nodes_InfixOperation(o, collector)
+ collector = visit o.left, collector
+ collector << " #{o.operator} "
+ visit o.right, collector
+ end
+
+ alias :visit_Arel_Nodes_Addition :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Subtraction :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Multiplication :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Division :visit_Arel_Nodes_InfixOperation
+
+ def visit_Arel_Nodes_UnaryOperation(o, collector)
+ collector << " #{o.operator} "
+ visit o.expr, collector
+ end
+
+ def visit_Array(o, collector)
+ inject_join o, collector, ", "
+ end
+ alias :visit_Set :visit_Array
+
+ def quote(value)
+ return value if Arel::Nodes::SqlLiteral === value
+ @connection.quote value
+ end
+
+ def quote_table_name(name)
+ return name if Arel::Nodes::SqlLiteral === name
+ @connection.quote_table_name(name)
+ end
+
+ def quote_column_name(name)
+ return name if Arel::Nodes::SqlLiteral === name
+ @connection.quote_column_name(name)
+ end
+
+ def maybe_visit(thing, collector)
+ return collector unless thing
+ collector << " "
+ visit thing, collector
+ end
+
+ def inject_join(list, collector, join_str)
+ len = list.length - 1
+ list.each_with_index.inject(collector) { |c, (x, i)|
+ if i == len
+ visit x, c
+ else
+ visit(x, c) << join_str
+ end
+ }
+ end
+
+ def infix_value(o, collector, value)
+ collector = visit o.left, collector
+ collector << value
+ visit o.right, collector
+ end
+
+ def aggregate(name, o, collector)
+ collector << "#{name}("
+ if o.distinct
+ collector << "DISTINCT "
+ end
+ collector = inject_join(o.expressions, collector, ", ") << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/visitor.rb b/activerecord/lib/arel/visitors/visitor.rb
new file mode 100644
index 0000000000..1c17184e86
--- /dev/null
+++ b/activerecord/lib/arel/visitors/visitor.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Visitor
+ def initialize
+ @dispatch = get_dispatch_cache
+ end
+
+ def accept(object, *args)
+ visit object, *args
+ end
+
+ private
+
+ attr_reader :dispatch
+
+ def self.dispatch_cache
+ Hash.new do |hash, klass|
+ hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
+ end
+ end
+
+ def get_dispatch_cache
+ self.class.dispatch_cache
+ end
+
+ def visit(object, *args)
+ dispatch_method = dispatch[object.class]
+ send dispatch_method, object, *args
+ rescue NoMethodError => e
+ raise e if respond_to?(dispatch_method, true)
+ superklass = object.class.ancestors.find { |klass|
+ respond_to?(dispatch[klass], true)
+ }
+ raise(TypeError, "Cannot visit #{object.class}") unless superklass
+ dispatch[object.class] = dispatch[superklass]
+ retry
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/where_sql.rb b/activerecord/lib/arel/visitors/where_sql.rb
new file mode 100644
index 0000000000..c6caf5e7c9
--- /dev/null
+++ b/activerecord/lib/arel/visitors/where_sql.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class WhereSql < Arel::Visitors::ToSql
+ def initialize(inner_visitor, *args, &block)
+ @inner_visitor = inner_visitor
+ super(*args, &block)
+ end
+
+ private
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector << "WHERE "
+ wheres = o.wheres.map do |where|
+ Nodes::SqlLiteral.new(@inner_visitor.accept(where, collector.class.new).value)
+ end
+
+ inject_join wheres, collector, " AND "
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/window_predications.rb b/activerecord/lib/arel/window_predications.rb
new file mode 100644
index 0000000000..3a8ee41f8a
--- /dev/null
+++ b/activerecord/lib/arel/window_predications.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module WindowPredications
+ def over(expr = nil)
+ Nodes::Over.new(self, expr)
+ end
+ end
+end