Skip to content

Commit

Permalink
Support for multi-table updates with limits, offsets and orders
Browse files Browse the repository at this point in the history
  • Loading branch information
jonleighton committed Aug 15, 2011
1 parent 128d006 commit 43b99f2
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 2 deletions.
Expand Up @@ -312,6 +312,9 @@ def sanitize_limit(limit)
def join_to_update(update, select) #:nodoc:
subselect = select.clone
subselect.ast.cores.last.projections = [update.ast.key]

update.ast.limit = nil
update.ast.orders = []
update.wheres = [update.ast.key.in(subselect)]
end

Expand Down
Expand Up @@ -577,8 +577,29 @@ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
where_sql
end

# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
# these, we must use a subquery. However, MySQL is too stupid to create a
# temporary table for this automatically, so we have to give it some prompting
# in the form of a subsubquery. Ugh!
def join_to_update(update, select) #:nodoc:
update.table select.ast.cores.last.source
if select.limit || select.offset || select.orders.any?
subsubselect = select.ast.clone
subsubselect.cores.last.projections = [update.ast.key]
subsubselect = Arel::Nodes::TableAlias.new(
Arel::Nodes::Grouping.new(subsubselect),
'__active_record_temp'
)

subselect = Arel::SelectManager.new(select.engine, subsubselect)
subselect.project(Arel::Table.new('__active_record_temp')[update.ast.key.name])

update.ast.limit = nil
update.ast.orders = []
update.wheres = [update.ast.key.in(subselect)]
else
update.table select.ast.cores.last.source
end
end

protected
Expand Down
Expand Up @@ -491,8 +491,29 @@ def release_savepoint
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end

# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
# these, we must use a subquery. However, MySQL is too stupid to create a
# temporary table for this automatically, so we have to give it some prompting
# in the form of a subsubquery. Ugh!
def join_to_update(update, select) #:nodoc:
update.table select.ast.cores.last.source
if select.limit || select.offset || select.orders.any?
subsubselect = select.ast.clone
subsubselect.cores.last.projections = [update.ast.key]
subsubselect = Arel::Nodes::TableAlias.new(
Arel::Nodes::Grouping.new(subsubselect),
'__active_record_temp'
)

subselect = Arel::SelectManager.new(select.engine, subsubselect)
subselect.project(Arel::Table.new('__active_record_temp')[update.ast.key.name])

update.ast.limit = nil
update.ast.orders = []
update.wheres = [update.ast.key.in(subselect)]
else
update.table select.ast.cores.last.source
end
end

# SCHEMA STATEMENTS ========================================
Expand Down
30 changes: 30 additions & 0 deletions activerecord/test/cases/relations_test.rb
Expand Up @@ -973,4 +973,34 @@ def test_update_all_with_joins
assert_equal count, comments.update_all(:post_id => posts(:thinking).id)
assert_equal posts(:thinking), comments(:greetings).post
end

def test_update_all_with_joins_and_limit
comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).limit(1)
assert_equal 1, comments.update_all(:post_id => posts(:thinking).id)
end

def test_update_all_with_joins_and_limit_and_order
comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).order('comments.id').limit(1)
assert_equal 1, comments.update_all(:post_id => posts(:thinking).id)
assert_equal posts(:thinking), comments(:greetings).post
assert_equal posts(:welcome), comments(:more_greetings).post
end

def test_update_all_with_joins_and_offset
all_comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id)
count = all_comments.count
comments = all_comments.offset(1)

assert_equal count - 1, comments.update_all(:post_id => posts(:thinking).id)
end

def test_update_all_with_joins_and_offset_and_order
all_comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).order('posts.id')
count = all_comments.count
comments = all_comments.offset(1)

assert_equal count - 1, comments.update_all(:post_id => posts(:thinking).id)
assert_equal posts(:thinking), comments(:more_greetings).post
assert_equal posts(:welcome), comments(:greetings).post
end
end

0 comments on commit 43b99f2

Please sign in to comment.