Skip to content

Commit

Permalink
Switch from with.recursive to with_recursive
Browse files Browse the repository at this point in the history
  • Loading branch information
ClearlyClaire committed Apr 19, 2024
1 parent 5708b0c commit cab51fc
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 50 deletions.
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/querying.rb
Expand Up @@ -17,7 +17,7 @@ module Querying
:and, :or, :annotate, :optimizer_hints, :extending,
:having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only,
:count, :average, :minimum, :maximum, :sum, :calculate,
:pluck, :pick, :ids, :async_ids, :strict_loading, :excluding, :without, :with,
:pluck, :pick, :ids, :async_ids, :strict_loading, :excluding, :without, :with, :with_recursive,
:async_count, :async_average, :async_minimum, :async_maximum, :async_sum, :async_pluck, :async_pick,
].freeze # :nodoc:
delegate(*QUERYING_METHODS, to: :all)
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/relation.rb
Expand Up @@ -60,7 +60,7 @@ def exec_explain(&block)
:reverse_order, :distinct, :create_with, :skip_query_cache]

CLAUSE_METHODS = [:where, :having, :from]
INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :with]
INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :with, :with_recursive]

VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS

Expand Down
73 changes: 27 additions & 46 deletions activerecord/lib/active_record/relation/query_methods.rb
Expand Up @@ -144,23 +144,6 @@ def scope_association_reflection(association)
end
end

# +WithChain+ objects act as placeholder for queries in which +with+ does not have any parameter.
# In this case, +with+ can be chained to return a new relation.
class WithChain
def initialize(scope) # :nodoc:
@scope = scope
end

# Returns a new relation in which Common Table Expressions (CTEs) are flagged as recursive.
#
# See QueryMethods#with for more details.
def recursive(*args)
@scope.with_values += args
@scope.with_is_recursive = true
@scope
end
end

# A wrapper to distinguish CTE joins from other nodes.
class CTEJoin # :nodoc:
attr_reader :name
Expand Down Expand Up @@ -196,18 +179,6 @@ def #{method_name}=(value) # def includes_values=(value)
CODE
end

# TODO: This is akin to how `Relation::VALUE_METHODS` are defined,
# but this does not neatly fit into one of the existing categories.
# Maybe we should make a full-fledged `WithClause`
def with_is_recursive
@values.fetch(:with_is_recursive, false)
end

def with_is_recursive=(value)
assert_mutability!
@values[:with_is_recursive] = value
end

alias extensions extending_values

# Specify associations +args+ to be eager loaded to prevent N + 1 queries.
Expand Down Expand Up @@ -475,17 +446,6 @@ def _select!(*fields) # :nodoc:
# # )
# # SELECT * FROM posts
#
# This can be used to write recursive CTEs:
#
# Post.with.recursive(post_and_replies: [Post.where(id: 42), Post.joins('JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id')])
# # => ActiveRecord::Relation
# # WITH post_and_replies AS (
# # (SELECT * FROM posts WHERE id = 42)
# # UNION ALL
# # (SELECT * FROM posts JOIN posts_and_replies ON posts.in_reply_to_id = posts_and_replies.id)
# # )
# # SELECT * FROM posts
#
# Once you define Common Table Expression you can use custom +FROM+ value or +JOIN+ to reference it.
#
# Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).from("posts_with_tags AS posts")
Expand Down Expand Up @@ -524,11 +484,8 @@ def _select!(*fields) # :nodoc:
# .with(posts_with_comments: Post.where("comments_count > ?", 0))
# .with(posts_with_tags: Post.where("tags_count > ?", 0))
def with(*args)
if args.empty?
WithChain.new(spawn)
else
spawn.with!(*args)
end
check_if_method_has_arguments!(__callee__, args)
spawn.with!(*args)
end

# Like #with, but modifies relation in place.
Expand All @@ -537,6 +494,30 @@ def with!(*args) # :nodoc:
self
end

# Add a recursive Common Table Expression (CTE) that you can then reference within another SELECT statement.
#
# Post.with_recursive(post_and_replies: [Post.where(id: 42), Post.joins('JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id')])
# # => ActiveRecord::Relation
# # WITH post_and_replies AS (
# # (SELECT * FROM posts WHERE id = 42)
# # UNION ALL
# # (SELECT * FROM posts JOIN posts_and_replies ON posts.in_reply_to_id = posts_and_replies.id)
# # )
# # SELECT * FROM posts
#
# See `#with` for more information.
def with_recursive(*args)
check_if_method_has_arguments!(__callee__, args)
spawn.with_recursive!(*args)
end

# Like #with_recursive but modifies the relation in place.
def with_recursive!(*args) # :nodoc:
self.with_values += args
@with_is_recursive = true
self
end

# Allows you to change a previously set select statement.
#
# Post.select(:title, :body)
Expand Down Expand Up @@ -1899,7 +1880,7 @@ def build_with(arel)
build_with_value_from_hash(with_value)
end

with_is_recursive ? arel.with(:recursive, with_statements) : arel.with(with_statements)
@with_is_recursive ? arel.with(:recursive, with_statements) : arel.with(with_statements)
end

def build_with_value_from_hash(hash)
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/cases/relation/delegation_test.rb
Expand Up @@ -62,7 +62,7 @@ class QueryingMethodsDelegationTest < ActiveRecord::TestCase
ActiveRecord::SpawnMethods.public_instance_methods(false) - [:spawn, :merge!] +
ActiveRecord::QueryMethods.public_instance_methods(false).reject { |method|
method.end_with?("=", "!", "?", "value", "values", "clause")
} - [:reverse_order, :arel, :extensions, :construct_join_dependency, :with_is_recursive] + [
} - [:reverse_order, :arel, :extensions, :construct_join_dependency] + [
:any?, :many?, :none?, :one?,
:first_or_create, :first_or_create!, :first_or_initialize,
:find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/cases/relation/with_test.rb
Expand Up @@ -75,7 +75,7 @@ def test_with_when_passing_arrays
def test_with_recursive
# TODO: actually test recursive behavior
relation = Post
.with.recursive(posts_with_comments: Post.where("legacy_comments_count > 0"))
.with_recursive(posts_with_comments: Post.where("legacy_comments_count > 0"))
.from("posts_with_comments AS posts")

assert_equal POSTS_WITH_COMMENTS, relation.order(:id).pluck(:id)
Expand Down

0 comments on commit cab51fc

Please sign in to comment.