Skip to content

Commit aa55d2d

Browse files
skipkayhilbyroot
authored andcommitted
Fix SqlLiterals overriding query's retryable value
Most `visit` functions in the `ToSql` visitor that modify the `collector`'s `retryable` value will change it unconditionally to `false`. For example, if the visitor ever encounters a `DeleteStatement`, we never want to retry the query, so `retryable = false`. However, the `SqlLiteral` `visit` method would copy the `SqlLiteral`s `retryable` attribute to the `collector`, whether it was `true` or `false`. This is an issue because the `collector`'s final `retryable` value ends up dependent on the order of `SqlLiteral`s visited and can be incorrectly changed from `false` to `true`. This commit fixes the issue by changing the `SqlLiteral` `visit` method to only change the `collector`'s `retryable` value to `false` (and only if the `SqlLiteral`'s `retryable` value is also `false`). This ensures that `retryable` `SqlLiteral`s can never override a `collector`s `false` `retryable` value. A new test was added to exercise the bug, specifically that a `retryable: true` `SqlLiteral` is visited after a `Node` that marks the `collector` `retryable: false`. Additionally, the fix exposed the fact that one of the tests incorrectly relied on the previous behavior because none of the tests used a `collector` with `retryable: true`. To be more realistic, `collector.retryable = true` was added to all of the tests concerned with `retryable`, even though most of them are valid tests without it (since `retryable: nil` is the default, asserting `retryable: false` still correctly covers `ToSql` changing the value to `false`).
1 parent 7c86c22 commit aa55d2d

File tree

2 files changed

+22
-3
lines changed

2 files changed

+22
-3
lines changed

activerecord/lib/arel/visitors/to_sql.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ def visit_Arel_Nodes_BindParam(o, collector)
763763

764764
def visit_Arel_Nodes_SqlLiteral(o, collector)
765765
collector.preparable = false
766-
collector.retryable = o.retryable
766+
collector.retryable &&= o.retryable
767767
collector << o.to_s
768768
end
769769

activerecord/test/cases/arel/visitors/to_sql_test.rb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def dispatch
7373
it "should mark collector as non-retryable when visiting named function" do
7474
function = Nodes::NamedFunction.new("ABS", [@table])
7575
collector = Collectors::SQLString.new
76+
collector.retryable = true
7677
@visitor.accept(function, collector)
7778

7879
assert_equal false, collector.retryable
@@ -81,22 +82,37 @@ def dispatch
8182
it "should mark collector as non-retryable when visiting SQL literal" do
8283
node = Nodes::SqlLiteral.new("COUNT(*)")
8384
collector = Collectors::SQLString.new
85+
collector.retryable = true
8486
@visitor.accept(node, collector)
8587

8688
assert_equal false, collector.retryable
8789
end
8890

89-
it "should mark collector as retryable if SQL literal is marked as retryable" do
91+
it "should not change retryable if SQL literal is marked as retryable" do
9092
node = Nodes::SqlLiteral.new("COUNT(*)", retryable: true)
9193
collector = Collectors::SQLString.new
94+
collector.retryable = true
9295
@visitor.accept(node, collector)
9396

94-
assert collector.retryable
97+
assert_predicate collector, :retryable
98+
end
99+
100+
it "should mark collector as non-retryable if SQL literal is not retryable" do
101+
node = Nodes::As.new(
102+
Nodes::SqlLiteral.new("`product.id`"),
103+
Nodes::SqlLiteral.new("`product.id`", retryable: true)
104+
)
105+
collector = Collectors::SQLString.new
106+
collector.retryable = true
107+
@visitor.accept(node, collector)
108+
109+
assert_equal false, collector.retryable
95110
end
96111

97112
it "should mark collector as non-retryable when visiting bound SQL literal" do
98113
node = Nodes::BoundSqlLiteral.new("id IN (?)", [[1, 2, 3]], {})
99114
collector = Collectors::SQLString.new
115+
collector.retryable = true
100116
@visitor.accept(node, collector)
101117

102118
assert_equal false, collector.retryable
@@ -105,6 +121,7 @@ def dispatch
105121
it "should mark collector as non-retryable when visiting insert statement node" do
106122
statement = Arel::Nodes::InsertStatement.new(@table)
107123
collector = Collectors::SQLString.new
124+
collector.retryable = true
108125
@visitor.accept(statement, collector)
109126

110127
assert_equal false, collector.retryable
@@ -113,6 +130,7 @@ def dispatch
113130
it "should mark collector as non-retryable when visiting update statement node" do
114131
statement = Arel::Nodes::UpdateStatement.new(@table)
115132
collector = Collectors::SQLString.new
133+
collector.retryable = true
116134
@visitor.accept(statement, collector)
117135

118136
assert_equal false, collector.retryable
@@ -121,6 +139,7 @@ def dispatch
121139
it "should mark collector as non-retryable when visiting delete statement node" do
122140
statement = Arel::Nodes::DeleteStatement.new(@table)
123141
collector = Collectors::SQLString.new
142+
collector.retryable = true
124143
@visitor.accept(statement, collector)
125144

126145
assert_equal false, collector.retryable

0 commit comments

Comments
 (0)