Skip to content

Commit 38a943e

Browse files
committed
When using LIMIT/OFFSET without ordering try to order using projection rather than with the primary key
1 parent 96e3b04 commit 38a943e

File tree

3 files changed

+108
-8
lines changed

3 files changed

+108
-8
lines changed

‎CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#### Fixed
44

55
- [#1318](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1318) Reverse order of values when upserting
6+
- []() When using `LIMIT`/`OFFSET` without ordering try to order using projection rather than with the primary key.
67

78
## v8.0.5
89

‎lib/arel/visitors/sqlserver.rb

+36-8
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,11 @@ def visit_Arel_Nodes_SelectStatement_SQLServer_Lock(collector, options = {})
252252
collector
253253
end
254254

255+
# AIDO
255256
def visit_Orders_And_Let_Fetch_Happen(o, collector)
257+
258+
# binding.pry if $DEBUG
259+
256260
make_Fetch_Possible_And_Deterministic o
257261
if o.orders.any?
258262
collector << " ORDER BY "
@@ -300,24 +304,48 @@ def select_statement_lock?
300304
@select_statement && @select_statement.lock
301305
end
302306

307+
# If LIMIT/OFFSET is used without ORDER BY, SQLServer will return an error.
308+
# This method will add a deterministic ORDER BY clause to the query using following rules:
309+
# 1. If the query has projections, use the first projection as the ORDER BY clause.
310+
# 2. If the query has SQL literal projection, use the first part of the SQL literal as the ORDER BY clause.
311+
# 3. If the query has a table with a primary key, use the primary key as the ORDER BY clause.
303312
def make_Fetch_Possible_And_Deterministic(o)
304313
return if o.limit.nil? && o.offset.nil?
305314
return if o.orders.any?
306315

307-
t = table_From_Statement o
308-
pk = primary_Key_From_Table t
309-
return unless pk
316+
# TODO: Refactor to list all projections and then find the first one that looks good.
317+
318+
projection = o.cores.first.projections.first
319+
320+
321+
binding.pry if $DEBUG
322+
323+
324+
if projection.is_a?(Arel::Attributes::Attribute) && !projection.name.include?("*")
325+
o.orders = [projection.asc]
310326

311-
# Prefer deterministic vs a simple `(SELECT NULL)` expr.
312-
o.orders = [pk.asc]
327+
# TODO: Use better logic to find first projection that is usable for ordering.
328+
elsif projection.is_a?(Arel::Nodes::SqlLiteral) && !projection.match?(/^\s*(1 as ONE|\*)(\s|,)*/i)
329+
330+
first_projection = Arel::Nodes::SqlLiteral.new(projection.split(",").first.split(/\sAS\s/i).first)
331+
o.orders = [first_projection.asc]
332+
else
333+
334+
pk = primary_Key_From_Table(table_From_Statement(o))
335+
o.orders = [pk.asc] if pk
336+
end
337+
338+
# rescue => e
339+
# binding.pry
313340
end
314341

315342
def distinct_One_As_One_Is_So_Not_Fetch(o)
316343
core = o.cores.first
317344
distinct = Nodes::Distinct === core.set_quantifier
318-
oneasone = core.projections.all? { |x| x == ActiveRecord::FinderMethods::ONE_AS_ONE }
319-
limitone = [nil, 0, 1].include? node_value(o.limit)
320-
if distinct && oneasone && limitone && !o.offset
345+
one_as_one = core.projections.all? { |x| x == ActiveRecord::FinderMethods::ONE_AS_ONE }
346+
limit_one = [nil, 0, 1].include? node_value(o.limit)
347+
348+
if distinct && one_as_one && limit_one && !o.offset
321349
core.projections = [Arel.sql("TOP(1) 1 AS [one]")]
322350
o.limit = nil
323351
end

‎test/cases/order_test_sqlserver.rb

+71
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,75 @@ class OrderTestSQLServer < ActiveRecord::TestCase
150150
sql = Post.order(:id).order("posts.id ASC").to_sql
151151
assert_equal "SELECT [posts].* FROM [posts] ORDER BY [posts].[id] ASC, posts.id ASC", sql
152152
end
153+
154+
describe "simple query containing limit" do
155+
it "order by primary key if no projections" do
156+
$DEBUG = false
157+
158+
sql = Post.limit(5).to_sql
159+
160+
assert_equal "SELECT [posts].* FROM [posts] ORDER BY [posts].[id] ASC OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY", sql
161+
162+
$DEBUG = false
163+
end
164+
165+
it "use order provided" do
166+
# $DEBUG = true
167+
168+
sql = Post.select(:legacy_comments_count).order(:tags_count).limit(5).to_sql
169+
170+
assert_equal "SELECT [posts].[legacy_comments_count] FROM [posts] ORDER BY [posts].[tags_count] ASC OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY", sql
171+
172+
# binding.pry
173+
174+
end
175+
176+
it "order by first projection if no order provided" do
177+
# $DEBUG = true
178+
179+
sql = Post.select(:legacy_comments_count).limit(5).to_sql
180+
181+
assert_equal "SELECT [posts].[legacy_comments_count] FROM [posts] ORDER BY [posts].[legacy_comments_count] ASC OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY", sql
182+
183+
# binding.pry
184+
185+
end
186+
187+
it "order by first projection (when multiple projections) if no order provided" do
188+
sql = Post.select(:legacy_comments_count, :tags_count).limit(5).to_sql
189+
190+
assert_equal "SELECT [posts].[legacy_comments_count], [posts].[tags_count] FROM [posts] ORDER BY [posts].[legacy_comments_count] ASC OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY", sql
191+
end
192+
end
193+
194+
describe "query containing FROM and limit" do
195+
it "uses the provided orderings" do
196+
sql = "SELECT sum(legacy_comments_count), count(*), min(legacy_comments_count) FROM (SELECT [posts].[legacy_comments_count] FROM [posts] ORDER BY [posts].[legacy_comments_count] DESC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY) subquery ORDER BY sum(legacy_comments_count) ASC OFFSET 0 ROWS FETCH NEXT @1 ROWS ONLY"
197+
198+
assert_queries_match(/#{Regexp.escape(sql)}/) do
199+
result = Post.from(Post.order(legacy_comments_count: :desc).limit(5).select(:legacy_comments_count)).pick(Arel.sql("sum(legacy_comments_count), count(*), min(legacy_comments_count)"))
200+
assert_equal result, [11, 5, 1]
201+
end
202+
end
203+
#
204+
it "in the subquery the first projection is used for ordering if none provided" do
205+
sql = "SELECT sum(legacy_comments_count), count(*), min(legacy_comments_count) FROM (SELECT [posts].[legacy_comments_count], [posts].[tags_count] FROM [posts] ORDER BY [posts].[legacy_comments_count] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY) subquery ORDER BY sum(legacy_comments_count) ASC OFFSET 0 ROWS FETCH NEXT @1 ROWS ONLY"
206+
207+
# binding.pry
208+
209+
assert_queries_match(/#{Regexp.escape(sql)}/) do
210+
result = Post.from(Post.limit(5).select(:legacy_comments_count, :tags_count)).pick(Arel.sql("sum(legacy_comments_count), count(*), min(legacy_comments_count)"))
211+
assert_equal result, [0, 5, 0]
212+
end
213+
end
214+
215+
it "in the subquery the primary key is used for ordering if none provided" do
216+
sql = "SELECT sum(legacy_comments_count), count(*), min(legacy_comments_count) FROM (SELECT [posts].* FROM [posts] ORDER BY [posts].[id] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY) subquery ORDER BY sum(legacy_comments_count) ASC OFFSET 0 ROWS FETCH NEXT @1 ROWS ONLY"
217+
218+
assert_queries_match(/#{Regexp.escape(sql)}/) do
219+
result = Post.from(Post.limit(5)).pick(Arel.sql("sum(legacy_comments_count), count(*), min(legacy_comments_count)"))
220+
assert_equal result, [10, 5, 0]
221+
end
222+
end
223+
end
153224
end

0 commit comments

Comments
 (0)