Skip to content

Commit cd9c5e7

Browse files
authored
Merge pull request #2777 from yahonda/backport-2664-to-release81
2 parents 6a29763 + 3c9e949 commit cd9c5e7

2 files changed

Lines changed: 106 additions & 1 deletion

File tree

lib/arel/visitors/oracle_common.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,35 @@ def cached_column_for(attr)
4646
def schema_cache
4747
@connection.schema_cache
4848
end
49+
50+
def visit_Arel_Nodes_In(o, collector)
51+
attr, values = o.left, o.right
52+
return super unless values.is_a?(Array)
53+
54+
in_clause_length = @connection.in_clause_length
55+
return super if values.length <= in_clause_length
56+
57+
# Split into multiple IN nodes and combine with OR
58+
in_nodes = values.each_slice(in_clause_length).map do |slice|
59+
Arel::Nodes::In.new(attr, slice)
60+
end
61+
or_node = Arel::Nodes::Or.new(in_nodes)
62+
visit(Arel::Nodes::Grouping.new(or_node), collector)
63+
end
64+
65+
def visit_Arel_Nodes_NotIn(o, collector)
66+
attr, values = o.left, o.right
67+
return super unless values.is_a?(Array)
68+
69+
in_clause_length = @connection.in_clause_length
70+
return super if values.length <= in_clause_length
71+
72+
# Split into multiple NOT IN nodes and combine with AND
73+
not_in_nodes = values.each_slice(in_clause_length).map do |slice|
74+
Arel::Nodes::NotIn.new(attr, slice)
75+
end
76+
visit(Arel::Nodes::And.new(not_in_nodes), collector)
77+
end
4978
end
5079
end
5180
end

spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ class ::TestComment < ActiveRecord::Base
244244
class ::TestPost < ActiveRecord::Base
245245
has_many :test_comments
246246
end
247-
@ids = (1..1010).to_a
247+
@ids = (1..2010).to_a
248248
TestPost.transaction do
249249
@ids.each do |id|
250250
TestPost.create!(id: id, title: "Title #{id}")
@@ -272,6 +272,38 @@ class ::TestPost < ActiveRecord::Base
272272
posts = TestPost.where(id: [*@ids, nil]).to_a
273273
expect(posts.size).to eq(@ids.size)
274274
end
275+
276+
# some frameworks like baby_squeel construct Arel objects directly
277+
it "should allow more than 1000 items using Arel::Nodes::In" do
278+
table = TestPost.arel_table
279+
in_node = Arel::Nodes::In.new(table[:id], @ids)
280+
query = table.where(in_node).project(Arel.star)
281+
282+
sql = query.to_sql
283+
posts = TestPost.connection.select_all(sql).to_a
284+
expect(posts.size).to eq(@ids.size)
285+
286+
# SQL contains multiple IN clauses (split due to 1000 limit)
287+
expect(sql.scan(/IN \(/).size).to be > 1
288+
end
289+
290+
it "should allow more than 1000 items using Arel::Nodes::NotIn" do
291+
ids = @ids.dup
292+
non_not_in = ids.pop
293+
294+
table = TestPost.arel_table
295+
not_in_node = Arel::Nodes::NotIn.new(table[:id], ids)
296+
query = table.where(not_in_node).project(Arel.star)
297+
298+
sql = query.to_sql
299+
posts = TestPost.connection.select_all(sql).to_a
300+
301+
expect(posts.size).to eq(1)
302+
expect(posts.first["id"]).to eq(non_not_in)
303+
304+
# SQL contains multiple NOT IN clauses (split due to 1000 limit)
305+
expect(sql.scan(/NOT IN \(/).size).to be > 1
306+
end
275307
end
276308

277309
describe "with statement pool" do
@@ -806,10 +838,54 @@ class ::TestComment < ActiveRecord::Base
806838
ActiveRecord::Base.clear_cache!
807839
end
808840

841+
before(:each) do
842+
TestPost.delete_all
843+
TestComment.delete_all
844+
end
845+
809846
it "should not raise undefined method length" do
810847
post = TestPost.create!
811848
post.test_comments << TestComment.create!
812849
expect(TestComment.where(test_post_id: TestPost.select(:id)).size).to eq(1)
813850
end
851+
852+
it "should handle IN with subquery using Arel::Nodes::In" do
853+
post = TestPost.create!
854+
post.test_comments << TestComment.create!
855+
856+
table = TestComment.arel_table
857+
subquery = TestPost.select(:id).arel
858+
in_node = Arel::Nodes::In.new(table[:test_post_id], subquery)
859+
query = table.where(in_node).project(Arel.star)
860+
861+
sql = query.to_sql
862+
comments = TestComment.connection.select_all(sql).to_a
863+
expect(comments.size).to eq(1)
864+
865+
# SQL should contain IN with subquery, not split into multiple IN clauses
866+
expect(sql).to match(/IN \(+SELECT/)
867+
expect(sql.scan(/IN \(/).size).to eq(1)
868+
end
869+
870+
it "should handle NOT IN with subquery using Arel::Nodes::NotIn" do
871+
post = TestPost.create!
872+
TestComment.create!(test_post_id: post.id)
873+
orphan_comment = TestComment.create!(test_post_id: post.id + 1)
874+
875+
table = TestComment.arel_table
876+
subquery = TestPost.select(:id).arel
877+
not_in_node = Arel::Nodes::NotIn.new(table[:test_post_id], subquery)
878+
query = table.where(not_in_node).project(Arel.star)
879+
880+
sql = query.to_sql
881+
comments = TestComment.connection.select_all(sql).to_a
882+
883+
expect(comments.size).to eq(1)
884+
expect(comments.first["id"]).to eq(orphan_comment.id)
885+
886+
# SQL should contain NOT IN with subquery, not split into multiple NOT IN clauses
887+
expect(sql).to match(/NOT IN \(+SELECT/)
888+
expect(sql.scan(/NOT IN \(/).size).to eq(1)
889+
end
814890
end
815891
end

0 commit comments

Comments
 (0)