diff options
Diffstat (limited to 'activerecord/lib')
27 files changed, 300 insertions, 145 deletions
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 84a9797aa5..0d384950fe 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -57,21 +57,14 @@ module ActiveRecord @through_records[record.object_id] ||= begin ensure_mutable - through_record = through_association.build(*options_for_through_record) - through_record.send("#{source_reflection.name}=", record) + attributes = through_scope_attributes + attributes[source_reflection.name] = record + attributes[source_reflection.foreign_type] = options[:source_type] if options[:source_type] - if options[:source_type] - through_record.send("#{source_reflection.foreign_type}=", options[:source_type]) - end - - through_record + through_association.build(attributes) end end - def options_for_through_record - [through_scope_attributes] - end - def through_scope_attributes scope.where_values_hash(through_association.reflection.name.to_s). except!(through_association.reflection.foreign_key, diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 8997579527..c7cd87f9d4 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -143,9 +143,7 @@ module ActiveRecord def preloaders_for_reflection(reflection, records, scope) records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs| - loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope) - loader.run self - loader + preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope).run end end @@ -166,10 +164,18 @@ module ActiveRecord @reflection = reflection end - def run(preloader); end + def run + self + end def preloaded_records - owners.flat_map { |owner| owner.association(reflection.name).target } + @preloaded_records ||= records_by_owner.flat_map(&:last) + end + + def records_by_owner + @records_by_owner ||= owners.each_with_object({}) do |owner, result| + result[owner] = Array(owner.association(reflection.name).target) + end end private diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 7048ff43b8..46532f651e 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -4,26 +4,44 @@ module ActiveRecord module Associations class Preloader class Association #:nodoc: - attr_reader :preloaded_records - def initialize(klass, owners, reflection, preload_scope) @klass = klass @owners = owners @reflection = reflection @preload_scope = preload_scope @model = owners.first && owners.first.class - @preloaded_records = [] end - def run(preloader) - records = load_records do |record| - owner = owners_by_key[convert_key(record[association_key_name])] - association = owner.association(reflection.name) - association.set_inverse_instance(record) + def run + if !preload_scope || preload_scope.empty_scope? + owners.each do |owner| + associate_records_to_owner(owner, records_by_owner[owner] || []) + end + else + # Custom preload scope is used and + # the association can not be marked as loaded + # Loading into a Hash instead + records_by_owner end + self + end - owners.each do |owner| - associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || []) + def records_by_owner + @records_by_owner ||= preloaded_records.each_with_object({}) do |record, result| + owners_by_key[convert_key(record[association_key_name])].each do |owner| + (result[owner] ||= []) << record + end + end + end + + def preloaded_records + return @preloaded_records if defined?(@preloaded_records) + return [] if owner_keys.empty? + # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) + # Make several smaller queries if necessary or make one query if the adapter supports it + slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) + @preloaded_records = slices.flat_map do |slice| + records_for(slice) end end @@ -54,13 +72,10 @@ module ActiveRecord end def owners_by_key - unless defined?(@owners_by_key) - @owners_by_key = owners.each_with_object({}) do |owner, h| - key = convert_key(owner[owner_key_name]) - h[key] = owner if key - end + @owners_by_key ||= owners.each_with_object({}) do |owner, result| + key = convert_key(owner[owner_key_name]) + (result[key] ||= []) << owner if key end - @owners_by_key end def key_conversion_required? @@ -87,23 +102,16 @@ module ActiveRecord @model.type_for_attribute(owner_key_name).type end - def load_records(&block) - return {} if owner_keys.empty? - # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) - # Make several smaller queries if necessary or make one query if the adapter supports it - slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) - @preloaded_records = slices.flat_map do |slice| - records_for(slice, &block) - end - @preloaded_records.group_by do |record| - convert_key(record[association_key_name]) + def records_for(ids) + scope.where(association_key_name => ids).load do |record| + # Processing only the first owner + # because the record is modified but not an owner + owner = owners_by_key[convert_key(record[association_key_name])].first + association = owner.association(reflection.name) + association.set_inverse_instance(record) end end - def records_for(ids, &block) - scope.where(association_key_name => ids).load(&block) - end - def scope @scope ||= build_scope end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 32653956b2..bec1c4c94a 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -4,45 +4,57 @@ module ActiveRecord module Associations class Preloader class ThroughAssociation < Association # :nodoc: - def run(preloader) - already_loaded = owners.first.association(through_reflection.name).loaded? - through_scope = through_scope() - through_preloaders = preloader.preload(owners, through_reflection.name, through_scope) - middle_records = through_preloaders.flat_map(&:preloaded_records) - preloaders = preloader.preload(middle_records, source_reflection.name, scope) - @preloaded_records = preloaders.flat_map(&:preloaded_records) - - owners.each do |owner| - through_records = Array(owner.association(through_reflection.name).target) - - if already_loaded + PRELOADER = ActiveRecord::Associations::Preloader.new + + def initialize(*) + super + @already_loaded = owners.first.association(through_reflection.name).loaded? + end + + def preloaded_records + @preloaded_records ||= source_preloaders.flat_map(&:preloaded_records) + end + + def records_by_owner + return @records_by_owner if defined?(@records_by_owner) + source_records_by_owner = source_preloaders.map(&:records_by_owner).reduce(:merge) + through_records_by_owner = through_preloaders.map(&:records_by_owner).reduce(:merge) + + @records_by_owner = owners.each_with_object({}) do |owner, result| + through_records = through_records_by_owner[owner] || [] + + if @already_loaded if source_type = reflection.options[:source_type] through_records = through_records.select do |record| record[reflection.foreign_type] == source_type end end - else - owner.association(through_reflection.name).reset if through_scope end - result = through_records.flat_map do |record| - record.association(source_reflection.name).target + records = through_records.flat_map do |record| + source_records_by_owner[record] end - result.compact! - result.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any? - result.uniq! if scope.distinct_value - associate_records_to_owner(owner, result) - end - - unless scope.empty_scope? - middle_records.each do |owner| - owner.association(source_reflection.name).reset if owner - end + records.compact! + records.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any? + records.uniq! if scope.distinct_value + result[owner] = records end end private + def source_preloaders + @source_preloaders ||= PRELOADER.preload(middle_records, source_reflection.name, scope) + end + + def middle_records + through_preloaders.flat_map(&:preloaded_records) + end + + def through_preloaders + @through_preloaders ||= PRELOADER.preload(owners, through_reflection.name, through_scope) + end + def through_reflection reflection.through_reflection end @@ -52,8 +64,8 @@ module ActiveRecord end def preload_index - @preload_index ||= @preloaded_records.each_with_object({}).with_index do |(id, result), index| - result[id] = index + @preload_index ||= preloaded_records.each_with_object({}).with_index do |(record, result), index| + result[record] = index end end @@ -61,11 +73,15 @@ module ActiveRecord scope = through_reflection.klass.unscoped options = reflection.options + values = reflection_scope.values + if annotations = values[:annotate] + scope.annotate!(*annotations) + end + if options[:source_type] scope.where! reflection.foreign_type => options[:source_type] elsif !reflection_scope.where_clause.empty? scope.where_clause = reflection_scope.where_clause - values = reflection_scope.values if includes = values[:includes] scope.includes!(source_reflection.name => includes) @@ -92,7 +108,7 @@ module ActiveRecord end end - scope unless scope.empty_scope? + scope end end end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 35150889d9..7cf421c184 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -41,6 +41,9 @@ module ActiveRecord # +range+ (PostgreSQL only) specifies that the type should be a range (see the # examples below). # + # When using a symbol for +cast_type+, extra options are forwarded to the + # constructor of the type object. + # # ==== Examples # # The type detected by Active Record can be overridden. @@ -112,6 +115,16 @@ module ActiveRecord # my_float_range: 1.0..3.5 # } # + # Passing options to the type constructor + # + # # app/models/my_model.rb + # class MyModel < ActiveRecord::Base + # attribute :small_int, :integer, limit: 2 + # end + # + # MyModel.create(small_int: 65537) + # # => Error: 65537 is out of range for the limit of two bytes + # # ==== Creating Custom Types # # Users may also define their own custom types, as long as they respond diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index d77d76cb1e..fe94662543 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -457,10 +457,16 @@ module ActiveRecord # If the record is new or it has changed, returns true. def record_changed?(reflection, record, key) record.new_record? || - (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) || + association_foreign_key_changed?(reflection, record, key) || record.will_save_change_to_attribute?(reflection.foreign_key) end + def association_foreign_key_changed?(reflection, record, key) + return false if reflection.through_reflection? + + record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key + end + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. # # In addition, it will destroy the association if it was marked for destruction. diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index 4656045fe5..7431a1c759 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -17,22 +17,22 @@ module ActiveRecord end # Collects the configs for the environment and optionally the specification - # name passed in. To include replica configurations pass `include_replicas: true`. + # name passed in. To include replica configurations pass <tt>include_replicas: true</tt>. # # If a spec name is provided a single DatabaseConfig object will be # returned, otherwise an array of DatabaseConfig objects will be # returned that corresponds with the environment and type requested. # - # Options: + # ==== Options # - # <tt>env_name:</tt> The environment name. Defaults to +nil+ which will collect - # configs for all environments. - # <tt>spec_name:</tt> The specification name (i.e. primary, animals, etc.). Defaults - # to +nil+. - # <tt>include_replicas:</tt> Determines whether to include replicas in - # the returned list. Most of the time we're only iterating over the write - # connection (i.e. migrations don't need to run for the write and read connection). - # Defaults to +false+. + # * <tt>env_name:</tt> The environment name. Defaults to +nil+ which will collect + # configs for all environments. + # * <tt>spec_name:</tt> The specification name (i.e. primary, animals, etc.). Defaults + # to +nil+. + # * <tt>include_replicas:</tt> Determines whether to include replicas in + # the returned list. Most of the time we're only iterating over the write + # connection (i.e. migrations don't need to run for the write and read connection). + # Defaults to +false+. def configs_for(env_name: nil, spec_name: nil, include_replicas: false) configs = env_with_configs(env_name) @@ -53,7 +53,7 @@ module ActiveRecord # Returns the config hash that corresponds with the environment # - # If the application has multiple databases `default_hash` will + # If the application has multiple databases +default_hash+ will # return the first config hash for the environment. # # { database: "my_db", adapter: "mysql2" } @@ -65,7 +65,7 @@ module ActiveRecord # Returns a single DatabaseConfig object based on the requested environment. # - # If the application has multiple databases `find_db_config` will return + # If the application has multiple databases +find_db_config+ will return # the first DatabaseConfig for the environment. def find_db_config(env) configurations.find do |db_config| diff --git a/activerecord/lib/active_record/database_configurations/hash_config.rb b/activerecord/lib/active_record/database_configurations/hash_config.rb index 574cb6bf15..e31ff09391 100644 --- a/activerecord/lib/active_record/database_configurations/hash_config.rb +++ b/activerecord/lib/active_record/database_configurations/hash_config.rb @@ -14,16 +14,16 @@ module ActiveRecord # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 # @env_name="development", @spec_name="primary", @config={"database"=>"db_name"}> # - # Options are: + # ==== Options # - # <tt>:env_name</tt> - The Rails environment, i.e. "development" - # <tt>:spec_name</tt> - The specification name. In a standard two-tier - # database configuration this will default to "primary". In a multiple - # database three-tier database configuration this corresponds to the name - # used in the second tier, for example "primary_readonly". - # <tt>:config</tt> - The config hash. This is the hash that contains the - # database adapter, name, and other important information for database - # connections. + # * <tt>:env_name</tt> - The Rails environment, i.e. "development". + # * <tt>:spec_name</tt> - The specification name. In a standard two-tier + # database configuration this will default to "primary". In a multiple + # database three-tier database configuration this corresponds to the name + # used in the second tier, for example "primary_readonly". + # * <tt>:config</tt> - The config hash. This is the hash that contains the + # database adapter, name, and other important information for database + # connections. class HashConfig < DatabaseConfig attr_reader :config @@ -33,14 +33,14 @@ module ActiveRecord end # Determines whether a database configuration is for a replica / readonly - # connection. If the `replica` key is present in the config, `replica?` will + # connection. If the +replica+ key is present in the config, +replica?+ will # return +true+. def replica? config["replica"] end # The migrations paths for a database configuration. If the - # `migrations_paths` key is present in the config, `migrations_paths` + # +migrations_paths+ key is present in the config, +migrations_paths+ # will return its value. def migrations_paths config["migrations_paths"] diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb index 8e8aa69478..e2d30ae416 100644 --- a/activerecord/lib/active_record/database_configurations/url_config.rb +++ b/activerecord/lib/active_record/database_configurations/url_config.rb @@ -17,17 +17,17 @@ module ActiveRecord # @config={"adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost"}, # @url="postgres://localhost/foo"> # - # Options are: + # ==== Options # - # <tt>:env_name</tt> - The Rails environment, ie "development" - # <tt>:spec_name</tt> - The specification name. In a standard two-tier - # database configuration this will default to "primary". In a multiple - # database three-tier database configuration this corresponds to the name - # used in the second tier, for example "primary_readonly". - # <tt>:url</tt> - The database URL. - # <tt>:config</tt> - The config hash. This is the hash that contains the - # database adapter, name, and other important information for database - # connections. + # * <tt>:env_name</tt> - The Rails environment, ie "development". + # * <tt>:spec_name</tt> - The specification name. In a standard two-tier + # database configuration this will default to "primary". In a multiple + # database three-tier database configuration this corresponds to the name + # used in the second tier, for example "primary_readonly". + # * <tt>:url</tt> - The database URL. + # * <tt>:config</tt> - The config hash. This is the hash that contains the + # database adapter, name, and other important information for database + # connections. class UrlConfig < DatabaseConfig attr_reader :url, :config @@ -42,14 +42,14 @@ module ActiveRecord end # Determines whether a database configuration is for a replica / readonly - # connection. If the `replica` key is present in the config, `replica?` will + # connection. If the +replica+ key is present in the config, +replica?+ will # return +true+. def replica? config["replica"] end # The migrations paths for a database configuration. If the - # `migrations_paths` key is present in the config, `migrations_paths` + # +migrations_paths+ key is present in the config, +migrations_paths+ # will return its value. def migrations_paths config["migrations_paths"] diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index ba03a3773a..7705cefa59 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -88,7 +88,7 @@ module ActiveRecord # (Postgres-only) An array of attributes to return for all successfully # inserted records, which by default is the primary key. # Pass <tt>returning: %w[ id name ]</tt> for both id and name - # or <tt>returning: false</tt> to omit the underlying RETURNING SQL + # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL # clause entirely. # # [:unique_by] @@ -157,7 +157,7 @@ module ActiveRecord # (Postgres-only) An array of attributes to return for all successfully # inserted records, which by default is the primary key. # Pass <tt>returning: %w[ id name ]</tt> for both id and name - # or <tt>returning: false</tt> to omit the underlying RETURNING SQL + # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL # clause entirely. # # ==== Examples @@ -205,7 +205,7 @@ module ActiveRecord # (Postgres-only) An array of attributes to return for all successfully # inserted records, which by default is the primary key. # Pass <tt>returning: %w[ id name ]</tt> for both id and name - # or <tt>returning: false</tt> to omit the underlying RETURNING SQL + # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL # clause entirely. # # [:unique_by] diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 86021f0a80..ae1501f5a1 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -13,9 +13,9 @@ module ActiveRecord :destroy_all, :delete_all, :update_all, :destroy_by, :delete_by, :find_each, :find_in_batches, :in_batches, :select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins, - :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :or, + :where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or, :having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only, - :count, :average, :minimum, :maximum, :sum, :calculate, + :count, :average, :minimum, :maximum, :sum, :calculate, :annotate, :pluck, :pick, :ids ].freeze # :nodoc: delegate(*QUERYING_METHODS, to: :all) diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 6d954a2b2e..36c2422d84 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -5,7 +5,7 @@ module ActiveRecord class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, :order, :joins, :left_outer_joins, :references, - :extending, :unscope, :optimizer_hints] + :extending, :unscope, :optimizer_hints, :annotate] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :skip_query_cache] @@ -389,6 +389,8 @@ module ActiveRecord stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name)) end + stmt.comment(*arel.comment_node.values) if arel.comment_node + @klass.connection.update stmt, "#{@klass} Update All" end @@ -504,6 +506,7 @@ module ActiveRecord stmt.offset(arel.offset) stmt.order(*arel.orders) stmt.wheres = arel.constraints + stmt.comment(*arel.comment_node.values) if arel.comment_node affected = @klass.connection.delete(stmt, "#{@klass} Destroy") diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 05ea0850d3..c37855172b 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -154,6 +154,19 @@ module ActiveRecord self end + # Extracts a named +association+ from the relation. The named association is first preloaded, + # then the individual association records are collected from the relation. Like so: + # + # account.memberships.extract_associated(:user) + # # => Returns collection of User records + # + # This is short-hand for: + # + # account.memberships.preload(:user).collect(&:user) + def extract_associated(association) + preload(association).collect(&association) + end + # Use to indicate that the given +table_names+ are referenced by an SQL string, # and should therefore be JOINed in any query rather than loaded separately. # This method only works in conjunction with #includes. @@ -349,7 +362,7 @@ module ActiveRecord end VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock, - :limit, :offset, :joins, :left_outer_joins, + :limit, :offset, :joins, :left_outer_joins, :annotate, :includes, :from, :readonly, :having, :optimizer_hints]) # Removes an unwanted relation that is already defined on a chain of relations. @@ -948,6 +961,26 @@ module ActiveRecord self end + # Adds an SQL comment to queries generated from this relation. For example: + # + # User.annotate("selecting user names").select(:name) + # # SELECT "users"."name" FROM "users" /* selecting user names */ + # + # User.annotate("selecting", "user", "names").select(:name) + # # SELECT "users"."name" FROM "users" /* selecting */ /* user */ /* names */ + # + # The SQL block comment delimiters, "/*" and "*/", will be added automatically. + def annotate(*args) + check_if_method_has_arguments!(:annotate, args) + spawn.annotate!(*args) + end + + # Like #annotate, but modifies relation in place. + def annotate!(*args) # :nodoc: + self.annotate_values += args + self + end + # Returns the Arel object associated with the relation. def arel(aliases = nil) # :nodoc: @arel ||= build_arel(aliases) @@ -1004,6 +1037,7 @@ module ActiveRecord arel.distinct(distinct_value) arel.from(build_from) unless from_clause.empty? arel.lock(lock_value) if lock_value + arel.comment(*annotate_values) unless annotate_values.empty? arel end diff --git a/activerecord/lib/arel/nodes.rb b/activerecord/lib/arel/nodes.rb index 2f6dd9bc45..f994754620 100644 --- a/activerecord/lib/arel/nodes.rb +++ b/activerecord/lib/arel/nodes.rb @@ -61,6 +61,8 @@ require "arel/nodes/outer_join" require "arel/nodes/right_outer_join" require "arel/nodes/string_join" +require "arel/nodes/comment" + require "arel/nodes/sql_literal" require "arel/nodes/casted" diff --git a/activerecord/lib/arel/nodes/comment.rb b/activerecord/lib/arel/nodes/comment.rb new file mode 100644 index 0000000000..237ff27e7e --- /dev/null +++ b/activerecord/lib/arel/nodes/comment.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Arel # :nodoc: all + module Nodes + class Comment < Arel::Nodes::Node + attr_reader :values + + def initialize(values) + super() + @values = values + end + + def initialize_copy(other) + super + @values = @values.clone + end + + def hash + [@values].hash + end + + def eql?(other) + self.class == other.class && + self.values == other.values + end + alias :== :eql? + end + end +end diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb index a419975335..56249b2bad 100644 --- a/activerecord/lib/arel/nodes/delete_statement.rb +++ b/activerecord/lib/arel/nodes/delete_statement.rb @@ -3,7 +3,7 @@ module Arel # :nodoc: all module Nodes class DeleteStatement < Arel::Nodes::Node - attr_accessor :left, :right, :orders, :limit, :offset, :key + attr_accessor :left, :right, :orders, :limit, :offset, :key, :comment alias :relation :left alias :relation= :left= @@ -18,16 +18,18 @@ module Arel # :nodoc: all @limit = nil @offset = nil @key = nil + @comment = nil end def initialize_copy(other) super @left = @left.clone if @left @right = @right.clone if @right + @comment = @comment.clone if @comment end def hash - [self.class, @left, @right, @orders, @limit, @offset, @key].hash + [self.class, @left, @right, @orders, @limit, @offset, @key, @comment].hash end def eql?(other) @@ -37,7 +39,8 @@ module Arel # :nodoc: all self.orders == other.orders && self.limit == other.limit && self.offset == other.offset && - self.key == other.key + self.key == other.key && + self.comment == other.comment end alias :== :eql? end diff --git a/activerecord/lib/arel/nodes/insert_statement.rb b/activerecord/lib/arel/nodes/insert_statement.rb index d28fd1f6c8..8430dd23da 100644 --- a/activerecord/lib/arel/nodes/insert_statement.rb +++ b/activerecord/lib/arel/nodes/insert_statement.rb @@ -3,7 +3,7 @@ module Arel # :nodoc: all module Nodes class InsertStatement < Arel::Nodes::Node - attr_accessor :relation, :columns, :values, :select + attr_accessor :relation, :columns, :values, :select, :comment def initialize super() @@ -11,6 +11,7 @@ module Arel # :nodoc: all @columns = [] @values = nil @select = nil + @comment = nil end def initialize_copy(other) @@ -18,10 +19,11 @@ module Arel # :nodoc: all @columns = @columns.clone @values = @values.clone if @values @select = @select.clone if @select + @comment = @comment.clone if @comment end def hash - [@relation, @columns, @values, @select].hash + [@relation, @columns, @values, @select, @comment].hash end def eql?(other) @@ -29,7 +31,8 @@ module Arel # :nodoc: all self.relation == other.relation && self.columns == other.columns && self.select == other.select && - self.values == other.values + self.values == other.values && + self.comment == other.comment end alias :== :eql? end diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb index 6585f9b3ec..b6154b7ff4 100644 --- a/activerecord/lib/arel/nodes/select_core.rb +++ b/activerecord/lib/arel/nodes/select_core.rb @@ -3,21 +3,22 @@ module Arel # :nodoc: all module Nodes class SelectCore < Arel::Nodes::Node - attr_accessor :projections, :wheres, :groups, :windows + attr_accessor :projections, :wheres, :groups, :windows, :comment attr_accessor :havings, :source, :set_quantifier, :optimizer_hints def initialize super() - @source = JoinSource.new nil + @source = JoinSource.new nil - @optimizer_hints = nil # https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier - @set_quantifier = nil - @projections = [] - @wheres = [] - @groups = [] - @havings = [] - @windows = [] + @set_quantifier = nil + @optimizer_hints = nil + @projections = [] + @wheres = [] + @groups = [] + @havings = [] + @windows = [] + @comment = nil end def from @@ -39,12 +40,13 @@ module Arel # :nodoc: all @groups = @groups.clone @havings = @havings.clone @windows = @windows.clone + @comment = @comment.clone if @comment end def hash [ @source, @set_quantifier, @projections, @optimizer_hints, - @wheres, @groups, @havings, @windows + @wheres, @groups, @havings, @windows, @comment ].hash end @@ -57,7 +59,8 @@ module Arel # :nodoc: all self.wheres == other.wheres && self.groups == other.groups && self.havings == other.havings && - self.windows == other.windows + self.windows == other.windows && + self.comment == other.comment end alias :== :eql? end diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb index cfaa19e392..015bcd7613 100644 --- a/activerecord/lib/arel/nodes/update_statement.rb +++ b/activerecord/lib/arel/nodes/update_statement.rb @@ -3,7 +3,7 @@ module Arel # :nodoc: all module Nodes class UpdateStatement < Arel::Nodes::Node - attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key + attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key, :comment def initialize @relation = nil @@ -13,16 +13,18 @@ module Arel # :nodoc: all @limit = nil @offset = nil @key = nil + @comment = nil end def initialize_copy(other) super @wheres = @wheres.clone @values = @values.clone + @comment = @comment.clone if @comment end def hash - [@relation, @wheres, @values, @orders, @limit, @offset, @key].hash + [@relation, @wheres, @values, @orders, @limit, @offset, @key, @comment].hash end def eql?(other) @@ -33,7 +35,8 @@ module Arel # :nodoc: all self.orders == other.orders && self.limit == other.limit && self.offset == other.offset && - self.key == other.key + self.key == other.key && + self.comment == other.comment end alias :== :eql? end diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb index 32286b67f4..4e9f527235 100644 --- a/activerecord/lib/arel/select_manager.rb +++ b/activerecord/lib/arel/select_manager.rb @@ -244,6 +244,15 @@ module Arel # :nodoc: all @ctx.source end + def comment(*values) + @ctx.comment = Nodes::Comment.new(values) + self + end + + def comment_node + @ctx.comment + end + private def collapse(exprs) exprs = exprs.compact diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb index 0476399618..326c4f995c 100644 --- a/activerecord/lib/arel/tree_manager.rb +++ b/activerecord/lib/arel/tree_manager.rb @@ -36,6 +36,11 @@ module Arel # :nodoc: all @ast.wheres << expr self end + + def comment(*values) + @ast.comment = Nodes::Comment.new(values) + self + end end attr_reader :ast diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb index 109afb7402..d696edc507 100644 --- a/activerecord/lib/arel/visitors/depth_first.rb +++ b/activerecord/lib/arel/visitors/depth_first.rb @@ -181,6 +181,10 @@ module Arel # :nodoc: all visit o.limit end + def visit_Arel_Nodes_Comment(o) + visit o.values + end + def visit_Array(o) o.each { |i| visit i } end diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb index 37803ce0c0..ecc386de07 100644 --- a/activerecord/lib/arel/visitors/dot.rb +++ b/activerecord/lib/arel/visitors/dot.rb @@ -234,6 +234,10 @@ module Arel # :nodoc: all end alias :visit_Set :visit_Array + def visit_Arel_Nodes_Comment(o) + visit_edge(o, "values") + end + def visit_edge(o, method) edge(method) { visit o.send(method) } end diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 1630226085..4192d9efdc 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -35,6 +35,7 @@ module Arel # :nodoc: all collect_nodes_for o.wheres, collector, " WHERE ", " AND " collect_nodes_for o.orders, collector, " ORDER BY " maybe_visit o.limit, collector + maybe_visit o.comment, collector end def visit_Arel_Nodes_UpdateStatement(o, collector) @@ -47,6 +48,7 @@ module Arel # :nodoc: all collect_nodes_for o.wheres, collector, " WHERE ", " AND " collect_nodes_for o.orders, collector, " ORDER BY " maybe_visit o.limit, collector + maybe_visit o.comment, collector end def visit_Arel_Nodes_InsertStatement(o, collector) @@ -62,9 +64,9 @@ module Arel # :nodoc: all maybe_visit o.values, collector elsif o.select maybe_visit o.select, collector - else - collector end + + maybe_visit o.comment, collector end def visit_Arel_Nodes_Exists(o, collector) @@ -162,7 +164,7 @@ module Arel # :nodoc: all collect_nodes_for o.havings, collector, " HAVING ", " AND " collect_nodes_for o.windows, collector, " WINDOW " - collector + maybe_visit o.comment, collector end def visit_Arel_Nodes_OptimizerHints(o, collector) @@ -170,6 +172,10 @@ module Arel # :nodoc: all collector << "/*+ #{hints} */" end + def visit_Arel_Nodes_Comment(o, collector) + collector << o.values.map { |v| "/* #{sanitize_as_sql_comment(v)} */" }.join(" ") + end + def collect_nodes_for(nodes, collector, spacer, connector = ", ") unless nodes.empty? collector << spacer diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt index 5f7201cfe1..562543f981 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt @@ -6,7 +6,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi t.string :password_digest<%= attribute.inject_options %> <% elsif attribute.token? -%> t.string :<%= attribute.name %><%= attribute.inject_options %> -<% else -%> +<% elsif !attribute.virtual? -%> t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> <% end -%> <% end -%> diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt index 481c70201b..c07380bec9 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt @@ -7,7 +7,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi <%- elsif attribute.token? -%> add_column :<%= table_name %>, :<%= attribute.name %>, :string<%= attribute.inject_options %> add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true - <%- else -%> + <%- elsif !attribute.virtual? -%> add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> <%- if attribute.has_index? -%> add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> @@ -21,7 +21,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi <%- attributes.each do |attribute| -%> <%- if attribute.reference? -%> t.references :<%= attribute.name %><%= attribute.inject_options %> - <%- else -%> + <%- elsif !attribute.virtual? -%> <%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %> <%- end -%> <%- end -%> @@ -37,7 +37,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi <%- if attribute.has_index? -%> remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> <%- end -%> + <%- if !attribute.virtual? %> remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- end -%> <%- end -%> <%- end -%> <%- end -%> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt index 55dc65c8ad..cdd029735a 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt @@ -3,6 +3,9 @@ class <%= class_name %> < <%= parent_class_name.classify %> <% attributes.select(&:reference?).each do |attribute| -%> belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %> <% end -%> +<% attributes.select(&:rich_text?).each do |attribute| -%> + has_rich_text :<%= attribute.name %> +<% end -%> <% attributes.select(&:token?).each do |attribute| -%> has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %> <% end -%> |