Skip to content

Commit 06464ec

Browse files
committed
[mssql] there are 3 hard problems in it: naming, caching and SQLServer AREL visitors !
1 parent 93b8b53 commit 06464ec

File tree

4 files changed

+192
-54
lines changed

4 files changed

+192
-54
lines changed

lib/arel/visitors/sql_server.rb

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
require 'arel/visitors/compat'
22

3-
loaded = nil
4-
if ArJdbc::AR42
5-
require 'arel/visitors/sql_server/ng42.rb'
6-
loaded = true
7-
end
8-
93
module Arel
104
module Visitors
5+
ToSql.class_eval do
6+
alias_method :_visit_Arel_Nodes_SelectStatement, :visit_Arel_Nodes_SelectStatement
7+
end
118
# @note AREL set's up `Arel::Visitors::MSSQL` but its not usable as is ...
129
# @private
1310
class SQLServer < const_defined?(:MSSQL) ? MSSQL : ToSql
1411

12+
private
13+
1514
def visit_Arel_Nodes_SelectStatement(*args) # [o] AR <= 4.0 [o, a] on 4.1
1615
o, a = args.first, args.last
1716

18-
return super if ! o.limit && ! o.offset
17+
return _visit_Arel_Nodes_SelectStatement(*args) if ! o.limit && ! o.offset
1918

2019
unless o.orders.empty?
2120
select_order_by = do_visit_columns o.orders, a, 'ORDER BY '
@@ -27,14 +26,14 @@ def visit_Arel_Nodes_SelectStatement(*args) # [o] AR <= 4.0 [o, a] on 4.1
2726
core_order_by = select_order_by || determine_order_by(x, a)
2827
if select_count? x
2928
x.projections = [
30-
core_order_by ? _row_num_literal(core_order_by) : Arel::Nodes::SqlLiteral.new("*")
29+
Arel::Nodes::SqlLiteral.new(core_order_by ? over_row_num(core_order_by) : '*')
3130
]
3231
select_count = true
3332
else
3433
# NOTE: this should really be added here and we should built the
3534
# wrapping SQL but than #replace_limit_offset! assumes it does that
3635
# ... MS-SQL adapter code seems to be 'hacked' by a lot of people
37-
#x.projections << _row_num_literal(order_by)
36+
#x.projections << Arel::Nodes::SqlLiteral.new over_row_num(order_by)
3837
end
3938
sql << do_visit_select_core(x, a)
4039
end
@@ -48,7 +47,7 @@ def visit_Arel_Nodes_SelectStatement(*args) # [o] AR <= 4.0 [o, a] on 4.1
4847
add_lock!(sql, :lock => o.lock && true)
4948

5049
sql
51-
end
50+
end unless ArJdbc::AR42
5251

5352
# @private
5453
MAX_LIMIT_VALUE = 9_223_372_036_854_775_807
@@ -67,19 +66,19 @@ def visit_Arel_Nodes_Lock o, a = nil
6766
#
6867
# we return nothing here and add the appropriate stuff with #add_lock!
6968
#do_visit o.expr, a
70-
end
69+
end unless ArJdbc::AR42
7170

7271
def visit_Arel_Nodes_Top o, a = nil
7372
# `top` wouldn't really work here:
7473
# User.select("distinct first_name").limit(10)
7574
# would generate "select top 10 distinct first_name from users",
7675
# which is invalid should be "select distinct top 10 first_name ..."
77-
''
76+
a || ''
7877
end
7978

8079
def visit_Arel_Nodes_Limit o, a = nil
8180
"TOP (#{do_visit o.expr, a})"
82-
end
81+
end unless ArJdbc::AR42
8382

8483
def visit_Arel_Nodes_Ordering o, a = nil
8584
expr = do_visit o.expr, a
@@ -88,7 +87,7 @@ def visit_Arel_Nodes_Ordering o, a = nil
8887
else
8988
expr
9089
end
91-
end
90+
end unless ArJdbc::AR42
9291

9392
def visit_Arel_Nodes_Bin o, a = nil
9493
expr = o.expr; sql = do_visit expr, a
@@ -98,7 +97,7 @@ def visit_Arel_Nodes_Bin o, a = nil
9897
sql << " #{::ArJdbc::MSSQL.cs_equality_operator} "
9998
sql
10099
end
101-
end
100+
end unless ArJdbc::AR42
102101

103102
private
104103

@@ -110,19 +109,17 @@ def select_count? x
110109
x.projections.length == 1 && Arel::Nodes::Count === x.projections.first
111110
end unless possibly_private_method_defined? :select_count?
112111

113-
unless possibly_private_method_defined? :determine_order_by
114-
def determine_order_by x, a
115-
unless x.groups.empty?
116-
do_visit_columns x.groups, a, 'ORDER BY '
117-
else
118-
table_pk = find_left_table_pk(x, a)
119-
table_pk == 'NULL' ? nil : "ORDER BY #{table_pk}"
120-
end
112+
def determine_order_by x, a
113+
unless x.groups.empty?
114+
do_visit_columns x.groups, a, 'ORDER BY '
115+
else
116+
table_pk = find_left_table_pk(x)
117+
table_pk && "ORDER BY #{table_pk}"
121118
end
119+
end
122120

123-
def find_left_table_pk o, a
124-
primary_key_from_table table_from_select_core(o)
125-
end
121+
def find_left_table_pk o
122+
primary_key_from_table table_from_select_core(o)
126123
end
127124

128125
def do_visit_columns(colls, a, sql)
@@ -133,6 +130,18 @@ def do_visit_columns(colls, a, sql)
133130
sql
134131
end
135132

133+
def do_visit_columns(colls, a, sql)
134+
prefix = sql
135+
sql = Arel::Collectors::PlainString.new
136+
sql << prefix if prefix
137+
138+
last = colls.size - 1
139+
colls.each_with_index do |x, i|
140+
visit(x, sql); sql << ', ' unless i == last
141+
end
142+
sql.value
143+
end if ArJdbc::AR42
144+
136145
def do_visit_columns(colls, a, sql)
137146
non_simple_order = /\sASC|\sDESC|\sCASE|\sCOLLATE|[\.,\[\(]/i # MIN(width)
138147

@@ -151,9 +160,9 @@ def do_visit_columns(colls, a, sql)
151160
sql
152161
end if Arel::VERSION < '4.0.0'
153162

154-
def _row_num_literal order_by
155-
Arel::Nodes::SqlLiteral.new("ROW_NUMBER() OVER (#{order_by}) as _row_num")
156-
end
163+
def over_row_num order_by
164+
"ROW_NUMBER() OVER (#{order_by}) as _row_num"
165+
end # unless possibly_private_method_defined? :row_num_literal
157166

158167
def table_from_select_core core
159168
if Arel::Table === core.from
@@ -189,14 +198,14 @@ def primary_key_from_table t
189198
return pk if pk
190199
end
191200

192-
pk = @primary_keys[table_name = engine.table_name] ||= begin
201+
pk = (@primary_keys ||= {}).fetch(table_name = engine.table_name) do
193202
pk_name = @connection.primary_key(table_name)
194203
# some tables might be without primary key
195-
pk_name && t[pk_name]
204+
@primary_keys[table_name] = pk_name && t[pk_name]
196205
end
197206
return pk if pk
198207

199-
column_name = engine.columns.first
208+
column_name = engine.columns.first.try(:name)
200209
column_name && t[column_name]
201210
end
202211

@@ -209,5 +218,8 @@ def primary_key_from_table t
209218
class SQLServer2000 < SQLServer
210219
include ArJdbc::MSSQL::LimitHelpers::SqlServer2000ReplaceLimitOffset
211220
end
221+
222+
load 'arel/visitors/sql_server/ng42.rb' if ArJdbc::AR42
223+
212224
end
213-
end unless loaded
225+
end

lib/arel/visitors/sql_server/ng42.rb

Lines changed: 116 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Arel
22
module Visitors
3-
class SQLServer < Arel::Visitors::ToSql
3+
class SQLServerNG < SQLServer # Arel::Visitors::ToSql
44

55
OFFSET = " OFFSET "
66
ROWS = " ROWS"
@@ -56,18 +56,55 @@ def visit_Arel_Nodes_Limit o, collector
5656
end
5757

5858
def visit_Arel_Nodes_SelectStatement o, collector
59-
@select_statement = o
6059
distinct_One_As_One_Is_So_Not_Fetch o
60+
61+
@select_statement = o
62+
6163
if o.with
6264
collector = visit o.with, collector
6365
collector << SPACE
6466
end
65-
collector = o.cores.inject(collector) { |c,x|
67+
68+
return _visit_Arel_Nodes_SelectStatement(o, collector) if ! o.limit && ! o.offset
69+
70+
# collector = o.cores.inject(collector) { |c,x|
71+
# visit_Arel_Nodes_SelectCore(x, c)
72+
# }
73+
74+
unless o.orders.empty?
75+
select_order_by = do_visit_columns o.orders, collector, 'ORDER BY '
76+
end
77+
78+
select_count = false
79+
collector = o.cores.inject(collector) do |c, x|
80+
unless core_order_by = select_order_by
81+
core_order_by = generate_order_by determine_order_by(o, x)
82+
end
83+
84+
if select_count? x
85+
x.projections = [ Arel::Nodes::SqlLiteral.new(over_row_num(core_order_by)) ]
86+
select_count = true
87+
else
88+
# NOTE: this should really be added here and we should built the
89+
# wrapping SQL but than #replace_limit_offset! assumes it does that
90+
# ... MS-SQL adapter code seems to be 'hacked' by a lot of people
91+
#x.projections << Arel::Nodes::SqlLiteral.new(over_row_num(select_order_by))
92+
end if core_order_by
6693
visit_Arel_Nodes_SelectCore(x, c)
67-
}
68-
collector = visit_Orders_And_Let_Fetch_Happen o, collector
69-
collector = visit_Make_Fetch_Happen o, collector
70-
collector
94+
end
95+
# END collector = o.cores.inject(collector) { |c,x|
96+
97+
# collector = visit_Orders_And_Let_Fetch_Happen o, collector
98+
# collector = visit_Make_Fetch_Happen o, collector
99+
# collector # __method__ END
100+
101+
self.class.collector_proxy(collector) do |sql|
102+
select_order_by ||= "ORDER BY #{@connection.determine_order_clause(sql)}"
103+
replace_limit_offset!(sql, limit_for(o.limit), o.offset && o.offset.value.to_i, select_order_by)
104+
sql = "SELECT COUNT(*) AS count_id FROM (#{sql}) AS subquery" if select_count
105+
sql
106+
end
107+
71108
ensure
72109
@select_statement = nil
73110
end
@@ -125,22 +162,13 @@ def visit_Make_Fetch_Happen o, collector
125162

126163
# SQLServer Helpers
127164

128-
def node_value(node)
129-
return nil unless node
130-
case node.expr
131-
when NilClass then nil
132-
when Numeric then node.expr
133-
when Arel::Nodes::Unary then node.expr.expr
134-
end
135-
end
136-
137165
def select_statement_lock?
138-
@select_statement && @select_statement.lock
166+
@select_statement && @select_statement.lock # AVOID INSTANCE var
139167
end
140168

141169
def make_Fetch_Possible_And_Deterministic o
142170
return if o.limit.nil? && o.offset.nil?
143-
if o.orders.empty?
171+
if o.orders.empty? # ORDER BY mandatory with OFFSET FETCH clause
144172
t = table_From_Statement o
145173
pk = primary_Key_From_Table t
146174
return unless pk
@@ -184,8 +212,77 @@ def primary_Key_From_Table t
184212
column_name ? t[column_name] : nil
185213
end
186214

215+
def determine_order_by o, x
216+
if o.orders.any?
217+
o.orders
218+
elsif x.groups.any?
219+
x.groups
220+
else
221+
pk = find_left_table_pk(x)
222+
pk ? [ pk ] : nil # []
223+
end
224+
end
225+
226+
def generate_order_by orders
227+
do_visit_columns orders, nil, 'ORDER BY '
228+
end
229+
230+
SQLString = ActiveRecord::ConnectionAdapters::AbstractAdapter::SQLString
231+
# BindCollector = ActiveRecord::ConnectionAdapters::AbstractAdapter::BindCollector
232+
233+
def self.collector_proxy(collector, &block)
234+
if collector.is_a?(SQLString)
235+
return SQLStringProxy.new(collector, block)
236+
end
237+
BindCollectorProxy.new(collector, block)
238+
end
239+
240+
class BindCollectorProxy < ActiveRecord::ConnectionAdapters::AbstractAdapter::BindCollector
241+
242+
def initialize(collector, block); @delegate = collector; @block = block end
243+
244+
def << str; @delegate << str; self end
245+
246+
def add_bind bind; @delegate.add_bind bind; self end
247+
248+
def value; @delegate.value; end
249+
250+
#def substitute_binds bvs; @delegate.substitute_binds(bvs); self end
251+
252+
def compile(bvs, conn)
253+
_yield_str @delegate.compile(bvs, conn)
254+
end
255+
256+
private
257+
258+
def method_missing(name, *args, &block); @delegate.send(name, args, &block) end
259+
260+
def _yield_str(str); @block ? @block.call(str) : str end
261+
262+
end
263+
264+
class SQLStringProxy < ActiveRecord::ConnectionAdapters::AbstractAdapter::SQLString
265+
266+
def initialize(collector, block); @delegate = collector; @block = block end
267+
268+
def << str; @delegate << str; self end
269+
270+
def add_bind bind; @delegate.add_bind bind; self end
271+
272+
def compile(bvs, conn)
273+
_yield_str @delegate.compile(bvs, conn)
274+
end
275+
276+
private
277+
278+
def method_missing(name, *args, &block); @delegate.send(name, args, &block) end
279+
280+
def _yield_str(str); @block ? @block.call(str) : str end
281+
282+
end
283+
187284
end
188285
end
189286
end
190287

191-
Arel::Visitors::VISITORS['mssql'] = Arel::Visitors::VISITORS['sqlserver'] = Arel::Visitors::SQLServer
288+
Arel::Visitors::VISITORS['mssql'] = Arel::Visitors::VISITORS['sqlserver'] = Arel::Visitors::SQLServerNG

lib/arjdbc/mssql/adapter.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ def self.arel_visitor_type(config)
108108
::Arel::Visitors::SQLServer2000 : ::Arel::Visitors::SQLServer
109109
end
110110

111+
def self.arel_visitor_type(config)
112+
require 'arel/visitors/sql_server'; ::Arel::Visitors::SQLServerNG
113+
end if AR42
114+
111115
# @deprecated no longer used
112116
# @see ActiveRecord::ConnectionAdapters::JdbcAdapter#arel2_visitors
113117
def self.arel2_visitors(config)

0 commit comments

Comments
 (0)