KEMBAR78
Add support for annotating queries generated by ActiveRecord::Relation with SQL comments by mattyoho · Pull Request #35617 · rails/rails · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
* Add `ActiveRecord::Relation#annotate` for adding SQL comments to its queries.

For example:

```
Post.where(id: 123).annotate("this is a comment").to_sql
# SELECT "posts".* FROM "posts" WHERE "posts"."id" = 123 /* this is a comment */
```

This can be useful in instrumentation or other analysis of issued queries.

*Matt Yoho*

* Support Optimizer Hints.

In most databases, there is a way to control the optimizer is by using optimizer hints,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,15 @@ def through_scope
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)
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/querying.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module Querying
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
:where, :rewhere, :preload, :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)
Expand Down
5 changes: 4 additions & 1 deletion activerecord/lib/active_record/relation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -389,6 +389,8 @@ def update_all(updates)
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

Expand Down Expand Up @@ -504,6 +506,7 @@ def delete_all
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")

Expand Down
23 changes: 22 additions & 1 deletion activerecord/lib/active_record/relation/query_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ def reorder!(*args) # :nodoc:
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.
Expand Down Expand Up @@ -948,6 +948,26 @@ def skip_preloading! # :nodoc:
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)
Expand Down Expand Up @@ -1004,6 +1024,7 @@ def build_arel(aliases)
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
Expand Down
2 changes: 2 additions & 0 deletions activerecord/lib/arel/nodes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
require "arel/nodes/right_outer_join"
require "arel/nodes/string_join"

require "arel/nodes/comment"

require "arel/nodes/sql_literal"

require "arel/nodes/casted"
29 changes: 29 additions & 0 deletions activerecord/lib/arel/nodes/comment.rb
Original file line number Diff line number Diff line change
@@ -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
9 changes: 6 additions & 3 deletions activerecord/lib/arel/nodes/delete_statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -18,16 +18,18 @@ def initialize(relation = nil, wheres = [])
@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)
Expand All @@ -37,7 +39,8 @@ def eql?(other)
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
Expand Down
9 changes: 6 additions & 3 deletions activerecord/lib/arel/nodes/insert_statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,36 @@
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()
@relation = nil
@columns = []
@values = nil
@select = nil
@comment = nil
end

def initialize_copy(other)
super
@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)
self.class == other.class &&
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
Expand Down
9 changes: 6 additions & 3 deletions activerecord/lib/arel/nodes/select_core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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
Expand All @@ -18,6 +18,7 @@ def initialize
@groups = []
@havings = []
@windows = []
@comment = nil
end

def from
Expand All @@ -39,12 +40,13 @@ def initialize_copy(other)
@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

Expand All @@ -57,7 +59,8 @@ def eql?(other)
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
Expand Down
9 changes: 6 additions & 3 deletions activerecord/lib/arel/nodes/update_statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,16 +13,18 @@ def initialize
@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)
Expand All @@ -33,7 +35,8 @@ def eql?(other)
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
Expand Down
9 changes: 9 additions & 0 deletions activerecord/lib/arel/select_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,15 @@ def source
@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
Expand Down
5 changes: 5 additions & 0 deletions activerecord/lib/arel/tree_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ def where(expr)
@ast.wheres << expr
self
end

def comment(*values)
@ast.comment = Nodes::Comment.new(values)
self
end
end

attr_reader :ast
Expand Down
4 changes: 4 additions & 0 deletions activerecord/lib/arel/visitors/depth_first.rb
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ def visit_Arel_Nodes_UpdateStatement(o)
visit o.limit
end

def visit_Arel_Nodes_Comment(o)
visit o.values
end

def visit_Array(o)
o.each { |i| visit i }
end
Expand Down
4 changes: 4 additions & 0 deletions activerecord/lib/arel/visitors/dot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ def visit_Array(o)
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
Expand Down
Loading