Skip to content

Commit 81ebfcb

Browse files
committed
Make inner sql for limit/offset eager limiting friendly. Also put in some doc comments for extensions.
1 parent cadb967 commit 81ebfcb

File tree

2 files changed

+57
-33
lines changed

2 files changed

+57
-33
lines changed

lib/arel/visitors/sqlserver.rb

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,27 @@ module Arel
22

33
module Nodes
44

5+
# See the SelectManager#lock method on why this custom class is needed.
56
class LockWithSQLServer < Arel::Nodes::Unary
67
end
78

9+
# In versions of ActiveRecord prior to v3.0.3 limits and offset were always integers via
10+
# a #to_i. Somewhere between ActiveRecord and ARel this is not happening anymore nor are they
11+
# in agreement which should be responsible. Since we need to make sure that these are visited
12+
# correctly and that we can do math with them, these are here to cast to integers.
813
class Limit < Arel::Nodes::Unary
914
def initialize expr
1015
@expr = expr.to_i
1116
end
1217
end
13-
1418
class Offset < Arel::Nodes::Unary
1519
def initialize expr
1620
@expr = expr.to_i
1721
end
1822
end
1923

24+
# Extending the Ordering class to be comparrison friendly which allows us to call #uniq on a
25+
# collection of them. See SelectManager#order for more details.
2026
class Ordering < Arel::Nodes::Binary
2127
def hash
2228
expr.hash
@@ -35,32 +41,38 @@ class SelectManager < Arel::TreeManager
3541

3642
alias :lock_without_sqlserver :lock
3743

44+
# Getting real Ordering objects is very important for us. We need to be able to call #uniq on
45+
# a colleciton of them reliably as well as using their true object attributes to mutate them
46+
# to grouping objects for the inner sql during a select statment with an offset/rownumber. So this
47+
# is here till ActiveRecord & ARel does this for us instead of using SqlLiteral objects.
3848
def order(*exprs)
3949
@ast.orders.concat(exprs.map{ |x|
4050
case x
4151
when Arel::Attributes::Attribute
4252
c = engine.connection
4353
tn = x.relation.table_alias || x.relation.name
44-
expr = Nodes::SqlLiteral.new "#{c.quote_table_name(tn)}.#{c.quote_column_name(x.name)}"
45-
Nodes::Ordering.new expr
54+
expr = Arel::Nodes::SqlLiteral.new "#{c.quote_table_name(tn)}.#{c.quote_column_name(x.name)}"
55+
Arel::Nodes::Ordering.new expr
4656
when String
4757
x.split(',').map do |s|
4858
expr, direction = s.split
49-
expr = Nodes::SqlLiteral.new(expr)
59+
expr = Arel::Nodes::SqlLiteral.new(expr)
5060
direction = direction =~ /desc/i ? :desc : :asc
51-
Nodes::Ordering.new expr, direction
61+
Arel::Nodes::Ordering.new expr, direction
5262
end
5363
else
54-
expr = Nodes::SqlLiteral.new x.to_s
55-
Nodes::Ordering.new expr
64+
expr = Arel::Nodes::SqlLiteral.new x.to_s
65+
Arel::Nodes::Ordering.new expr
5666
end
5767
}.flatten)
5868
self
5969
end
6070

71+
# A friendly over ride that allows us to put a special lock object that can have a default or pass
72+
# custom string hints down. See the visit_Arel_Nodes_LockWithSQLServer delegation method.
6173
def lock(locking=true)
6274
if Arel::Visitors::SQLServer === @visitor
63-
@ast.lock = Nodes::LockWithSQLServer.new(locking)
75+
@ast.lock = Arel::Nodes::LockWithSQLServer.new(locking)
6476
self
6577
else
6678
lock_without_sqlserver(locking)
@@ -121,17 +133,12 @@ def visit_Arel_Nodes_SelectStatementWithOutOffset(o, windowed=false)
121133
if windowed
122134
projections = function_select_statement?(o) ? projections : projections.map { |x| projection_without_expression(x) }
123135
elsif eager_limiting_select_statement?(o)
124-
raise "visit_Arel_Nodes_SelectStatementWithOutOffset - eager_limiting_select_statement?"
125136
groups = projections.map { |x| projection_without_expression(x) }
126137
projections = projections.map { |x| projection_without_expression(x) }
127-
# TODO: Let's alter objects vs strings and make new order objects
128138
orders = orders.map do |x|
129-
Arel::SqlLiteral.new(x.split(',').reject(&:blank?).map do |c|
130-
max = c =~ /desc\s*/i
131-
c = clause_without_expression(c).sub(/(asc|desc)/i,'').strip
132-
max ? "MAX(#{c})" : "MIN(#{c})"
133-
end.join(', '))
134-
end
139+
expr = Arel::Nodes::SqlLiteral.new projection_without_expression(x.expr)
140+
x.descending? ? Arel::Nodes::Max.new([expr]) : Arel::Nodes::Min.new([expr])
141+
end
135142
end
136143
[ ("SELECT" if !windowed),
137144
(visit(o.limit) if o.limit && !windowed),
@@ -266,8 +273,9 @@ def rowtable_orders(o)
266273
end.reverse.uniq.reverse
267274
end
268275

276+
# TODO: We use this for grouping too, maybe make Grouping objects vs SqlLiteral.
269277
def projection_without_expression(projection)
270-
Arel::SqlLiteral.new(projection.split(',').map do |x|
278+
Arel::Nodes::SqlLiteral.new(projection.split(',').map do |x|
271279
x.strip!
272280
x.sub!(/^(COUNT|SUM|MAX|MIN|AVG)\s*(\((.*)\))?/,'\3')
273281
x.sub!(/^DISTINCT\s*/,'')

test/cases/scratch_test_sqlserver.rb

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,46 @@
11
require 'cases/sqlserver_helper'
2-
require 'models/tagging'
32
require 'models/post'
4-
require 'models/topic'
3+
require 'models/tagging'
4+
require 'models/tag'
55
require 'models/comment'
6-
require 'models/reply'
76
require 'models/author'
8-
require 'models/comment'
9-
require 'models/entrant'
10-
require 'models/developer'
7+
require 'models/category'
118
require 'models/company'
12-
require 'models/bird'
13-
require 'models/car'
14-
require 'models/engine'
15-
require 'models/tyre'
9+
require 'models/person'
10+
require 'models/reader'
11+
require 'models/owner'
12+
require 'models/pet'
13+
require 'models/reference'
14+
require 'models/job'
15+
require 'models/subscriber'
16+
require 'models/subscription'
17+
require 'models/book'
18+
require 'models/developer'
19+
require 'models/project'
1620

1721
class ScratchTestSqlserver < ActiveRecord::TestCase
1822

19-
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things',
20-
:authors, :categorizations, :categories, :posts
23+
fixtures :posts, :comments, :authors, :author_addresses, :categories, :categories_posts,
24+
:companies, :accounts, :tags, :taggings, :people, :readers,
25+
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
26+
:developers, :projects, :developers_projects
2127

2228
should 'pass' do
23-
topics = Topic.send(:with_scope, :find => {:limit => 1, :offset => 1, :include => :replies}) do
24-
Topic.find(:all, :order => "topics.id")
29+
Post.send(:with_scope, :find => { :conditions => "1=1" }) do
30+
posts = authors(:david).posts.find(:all,
31+
:include => :comments,
32+
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
33+
:limit => 2
34+
)
35+
assert_equal 2, posts.size
36+
37+
count = Post.count(
38+
:include => [ :comments, :author ],
39+
:conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
40+
:limit => 2
41+
)
42+
assert_equal count, posts.size
2543
end
26-
assert_equal 1, topics.size
27-
assert_equal 2, topics.first.id
2844
end
2945

3046

0 commit comments

Comments
 (0)