activerecord-sqlserver-adapter 3.2.18 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +4 -28
- data/VERSION +1 -1
- data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +2 -7
- data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +6 -9
- data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +3 -25
- data/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb +4 -14
- data/lib/active_record/connection_adapters/sqlserver/core_ext/relation.rb +1 -3
- data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +2 -4
- data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +74 -80
- data/lib/active_record/connection_adapters/sqlserver/errors.rb +10 -14
- data/lib/active_record/connection_adapters/sqlserver/quoting.rb +24 -15
- data/lib/active_record/connection_adapters/sqlserver/schema_cache.rb +24 -19
- data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +28 -0
- data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +118 -77
- data/lib/active_record/connection_adapters/sqlserver/showplan.rb +10 -13
- data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +8 -11
- data/lib/active_record/connection_adapters/sqlserver/showplan/printer_xml.rb +2 -5
- data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +23 -0
- data/lib/active_record/connection_adapters/sqlserver/utils.rb +4 -10
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +121 -247
- data/lib/active_record/connection_adapters/sqlserver_column.rb +116 -0
- data/lib/active_record/sqlserver_base.rb +28 -0
- data/lib/active_record/sqlserver_test_case.rb +17 -0
- data/lib/arel/arel_sqlserver.rb +5 -0
- data/lib/arel/nodes_sqlserver.rb +14 -0
- data/lib/arel/select_manager_sqlserver.rb +62 -0
- data/lib/arel/visitors/sqlserver.rb +251 -188
- metadata +32 -10
- data/lib/active_record/connection_adapters/sqlserver/core_ext/database_statements.rb +0 -97
@@ -0,0 +1,116 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class SQLServerColumn < Column
|
4
|
+
def initialize(name, default, sql_type = nil, null = true, sqlserver_options = {})
|
5
|
+
@sqlserver_options = sqlserver_options.symbolize_keys
|
6
|
+
super(name, default, sql_type, null)
|
7
|
+
@primary = @sqlserver_options[:is_identity] || @sqlserver_options[:is_primary]
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def string_to_binary(value)
|
12
|
+
"0x#{value.unpack("H*")[0]}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def binary_to_string(value)
|
16
|
+
if value.encoding != Encoding::ASCII_8BIT
|
17
|
+
value = value.force_encoding(Encoding::ASCII_8BIT)
|
18
|
+
end
|
19
|
+
value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def is_identity?
|
24
|
+
@sqlserver_options[:is_identity]
|
25
|
+
end
|
26
|
+
|
27
|
+
def is_primary?
|
28
|
+
@sqlserver_options[:is_primary]
|
29
|
+
end
|
30
|
+
|
31
|
+
def is_utf8?
|
32
|
+
@sql_type =~ /nvarchar|ntext|nchar/i
|
33
|
+
end
|
34
|
+
|
35
|
+
def is_integer?
|
36
|
+
@sql_type =~ /int/i
|
37
|
+
end
|
38
|
+
|
39
|
+
def is_real?
|
40
|
+
@sql_type =~ /real/i
|
41
|
+
end
|
42
|
+
|
43
|
+
def sql_type_for_statement
|
44
|
+
if is_integer? || is_real?
|
45
|
+
sql_type.sub(/\((\d+)?\)/, '')
|
46
|
+
else
|
47
|
+
sql_type
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def default_function
|
52
|
+
@sqlserver_options[:default_function]
|
53
|
+
end
|
54
|
+
|
55
|
+
def table_name
|
56
|
+
@sqlserver_options[:table_name]
|
57
|
+
end
|
58
|
+
|
59
|
+
def table_klass
|
60
|
+
@table_klass ||= begin
|
61
|
+
table_name.classify.constantize
|
62
|
+
rescue StandardError, NameError, LoadError
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
(@table_klass && @table_klass < ActiveRecord::Base) ? @table_klass : nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def database_year
|
69
|
+
@sqlserver_options[:database_year]
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def extract_limit(sql_type)
|
75
|
+
case sql_type
|
76
|
+
when /^smallint/i
|
77
|
+
2
|
78
|
+
when /^int/i
|
79
|
+
4
|
80
|
+
when /^bigint/i
|
81
|
+
8
|
82
|
+
when /\(max\)/, /decimal/, /numeric/
|
83
|
+
nil
|
84
|
+
else
|
85
|
+
super
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def simplified_type(field_type)
|
90
|
+
case field_type
|
91
|
+
when /real/i then :float
|
92
|
+
when /money/i then :decimal
|
93
|
+
when /image/i then :binary
|
94
|
+
when /bit/i then :boolean
|
95
|
+
when /uniqueidentifier/i then :uuid
|
96
|
+
when /datetime/i then simplified_datetime
|
97
|
+
when /varchar\(max\)/ then :text
|
98
|
+
when /timestamp/ then :binary
|
99
|
+
else super
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def simplified_datetime
|
104
|
+
if database_year >= 2008
|
105
|
+
:datetime
|
106
|
+
elsif table_klass && table_klass.coerced_sqlserver_date_columns.include?(name)
|
107
|
+
:date
|
108
|
+
elsif table_klass && table_klass.coerced_sqlserver_time_columns.include?(name)
|
109
|
+
:time
|
110
|
+
else
|
111
|
+
:datetime
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end # class SQLServerColumn
|
115
|
+
end # module ConnectionAdapters
|
116
|
+
end # module ActiveRecord
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Base
|
3
|
+
def self.sqlserver_connection(config) #:nodoc:
|
4
|
+
config = config.symbolize_keys
|
5
|
+
config.reverse_merge! mode: :dblib
|
6
|
+
mode = config[:mode].to_s.downcase.underscore.to_sym
|
7
|
+
case mode
|
8
|
+
when :dblib
|
9
|
+
require 'tiny_tds'
|
10
|
+
when :odbc
|
11
|
+
raise ArgumentError, 'Missing :dsn configuration.' unless config.key?(:dsn)
|
12
|
+
require 'odbc'
|
13
|
+
require 'active_record/connection_adapters/sqlserver/core_ext/odbc'
|
14
|
+
else
|
15
|
+
raise ArgumentError, "Unknown connection mode in #{config.inspect}."
|
16
|
+
end
|
17
|
+
ConnectionAdapters::SQLServerAdapter.new(nil, logger, nil, config.merge(mode: mode))
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.did_retry_sqlserver_connection(connection, count)
|
21
|
+
logger.info "CONNECTION RETRY: #{connection.class.name} retry ##{count}."
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.did_lose_sqlserver_connection(connection)
|
25
|
+
logger.info "CONNECTION LOST: #{connection.class.name}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_record/test_case.rb'
|
2
|
+
|
3
|
+
# TODO: I'm struggling to figure out how to unsubscribe from only one 'sql.active_record'
|
4
|
+
# This is a temporary hack until we can just get the sqlserver_ignored regex in rails
|
5
|
+
ActiveSupport::Notifications.notifier.listeners_for('sql.active_record').each do |listener|
|
6
|
+
if listener.inspect =~ /ActiveRecord::SQLCounter/
|
7
|
+
ActiveSupport::Notifications.unsubscribe(listener)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ActiveRecord
|
12
|
+
class SQLCounter
|
13
|
+
sqlserver_ignored = [/SELECT SCOPE_IDENTITY/, /INFORMATION_SCHEMA\.(TABLES|VIEWS|COLUMNS)/, /SELECT @@version/, /SELECT @@TRANCOUNT/, /(BEGIN|COMMIT|ROLLBACK|SAVE) TRANSACTION/]
|
14
|
+
ignored_sql.concat sqlserver_ignored
|
15
|
+
end
|
16
|
+
ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Arel
|
2
|
+
module Nodes
|
3
|
+
# Extending the Ordering class to be comparison friendly which allows us to call #uniq on a
|
4
|
+
# collection of them. See SelectManager#order for more details.
|
5
|
+
class Ordering < Arel::Nodes::Unary
|
6
|
+
def eql?(other)
|
7
|
+
# Arel::Nodes::Ascending or Arel::Nodes::Desecnding
|
8
|
+
other.is_a?(Arel::Nodes::Ordering) &&
|
9
|
+
expr == other.expr
|
10
|
+
end
|
11
|
+
alias_method :==, :eql?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Arel
|
2
|
+
class SelectManager < Arel::TreeManager
|
3
|
+
AR_CA_SQLSA_NAME = 'ActiveRecord::ConnectionAdapters::SQLServerAdapter'.freeze
|
4
|
+
|
5
|
+
# Getting real Ordering objects is very important for us. We need to be able to call #uniq on
|
6
|
+
# a colleciton of them reliably as well as using their true object attributes to mutate them
|
7
|
+
# to grouping objects for the inner sql during a select statment with an offset/rownumber. So this
|
8
|
+
# is here till ActiveRecord & ARel does this for us instead of using SqlLiteral objects.
|
9
|
+
alias_method :order_without_sqlserver, :order
|
10
|
+
def order(*expr)
|
11
|
+
return order_without_sqlserver(*expr) unless engine_activerecord_sqlserver_adapter?
|
12
|
+
@ast.orders.concat(expr.map do |x|
|
13
|
+
case x
|
14
|
+
when Arel::Attributes::Attribute
|
15
|
+
table = Arel::Table.new(x.relation.table_alias || x.relation.name)
|
16
|
+
e = table[x.name]
|
17
|
+
Arel::Nodes::Ascending.new e
|
18
|
+
when Arel::Nodes::Ordering
|
19
|
+
x
|
20
|
+
when String
|
21
|
+
x.split(',').map do |s|
|
22
|
+
s = x if x.strip =~ /\A\b\w+\b\(.*,.*\)(\s+(ASC|DESC))?\Z/i # Allow functions with comma(s) to pass thru.
|
23
|
+
s.strip!
|
24
|
+
d = s =~ /(ASC|DESC)\Z/i ? Regexp.last_match[1].upcase : nil
|
25
|
+
e = d.nil? ? s : s.mb_chars[0...-d.length].strip
|
26
|
+
e = Arel.sql(e)
|
27
|
+
d && d == 'DESC' ? Arel::Nodes::Descending.new(e) : Arel::Nodes::Ascending.new(e)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
e = Arel.sql(x.to_s)
|
31
|
+
Arel::Nodes::Ascending.new e
|
32
|
+
end
|
33
|
+
end.flatten)
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# A friendly over ride that allows us to put a special lock object that can have a default or pass
|
38
|
+
# custom string hints down. See the visit_Arel_Nodes_LockWithSQLServer delegation method.
|
39
|
+
alias_method :lock_without_sqlserver, :lock
|
40
|
+
def lock(locking = true)
|
41
|
+
if engine_activerecord_sqlserver_adapter?
|
42
|
+
case locking
|
43
|
+
when true
|
44
|
+
locking = Arel.sql('WITH(HOLDLOCK, ROWLOCK)')
|
45
|
+
when Arel::Nodes::SqlLiteral
|
46
|
+
when String
|
47
|
+
locking = Arel.sql locking
|
48
|
+
end
|
49
|
+
@ast.lock = Arel::Nodes::Lock.new(locking)
|
50
|
+
self
|
51
|
+
else
|
52
|
+
lock_without_sqlserver(locking)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def engine_activerecord_sqlserver_adapter?
|
59
|
+
@engine.connection && @engine.connection.class.name == AR_CA_SQLSA_NAME
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -1,214 +1,217 @@
|
|
1
|
-
require 'arel'
|
2
|
-
|
3
1
|
module Arel
|
4
|
-
|
5
|
-
module Nodes
|
6
|
-
|
7
|
-
# Extending the Ordering class to be comparrison friendly which allows us to call #uniq on a
|
8
|
-
# collection of them. See SelectManager#order for more details.
|
9
|
-
class Ordering < Arel::Nodes::Unary
|
10
|
-
def hash
|
11
|
-
expr.hash
|
12
|
-
end
|
13
|
-
def ==(other)
|
14
|
-
other.is_a?(Arel::Nodes::Ordering) && self.expr == other.expr
|
15
|
-
end
|
16
|
-
def eql?(other)
|
17
|
-
self == other
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
end
|
22
|
-
|
23
|
-
class SelectManager < Arel::TreeManager
|
24
|
-
|
25
|
-
AR_CA_SQLSA_NAME = 'ActiveRecord::ConnectionAdapters::SQLServerAdapter'.freeze
|
26
|
-
|
27
|
-
# Getting real Ordering objects is very important for us. We need to be able to call #uniq on
|
28
|
-
# a colleciton of them reliably as well as using their true object attributes to mutate them
|
29
|
-
# to grouping objects for the inner sql during a select statment with an offset/rownumber. So this
|
30
|
-
# is here till ActiveRecord & ARel does this for us instead of using SqlLiteral objects.
|
31
|
-
alias :order_without_sqlserver :order
|
32
|
-
def order(*expr)
|
33
|
-
return order_without_sqlserver(*expr) unless engine_activerecord_sqlserver_adapter?
|
34
|
-
@ast.orders.concat(expr.map{ |x|
|
35
|
-
case x
|
36
|
-
when Arel::Attributes::Attribute
|
37
|
-
table = Arel::Table.new(x.relation.table_alias || x.relation.name)
|
38
|
-
e = table[x.name]
|
39
|
-
Arel::Nodes::Ascending.new e
|
40
|
-
when Arel::Nodes::Ordering
|
41
|
-
x
|
42
|
-
when String
|
43
|
-
x.split(',').map do |s|
|
44
|
-
s = x if x.strip =~ /\A\b\w+\b\(.*,.*\)(\s+(ASC|DESC))?\Z/i # Allow functions with comma(s) to pass thru.
|
45
|
-
s.strip!
|
46
|
-
d = s =~ /(ASC|DESC)\Z/i ? $1.upcase : nil
|
47
|
-
e = d.nil? ? s : s.mb_chars[0...-d.length].strip
|
48
|
-
e = Arel.sql(e)
|
49
|
-
d && d == "DESC" ? Arel::Nodes::Descending.new(e) : Arel::Nodes::Ascending.new(e)
|
50
|
-
end
|
51
|
-
else
|
52
|
-
e = Arel.sql(x.to_s)
|
53
|
-
Arel::Nodes::Ascending.new e
|
54
|
-
end
|
55
|
-
}.flatten)
|
56
|
-
self
|
57
|
-
end
|
58
|
-
|
59
|
-
# A friendly over ride that allows us to put a special lock object that can have a default or pass
|
60
|
-
# custom string hints down. See the visit_Arel_Nodes_LockWithSQLServer delegation method.
|
61
|
-
alias :lock_without_sqlserver :lock
|
62
|
-
def lock(locking=true)
|
63
|
-
if engine_activerecord_sqlserver_adapter?
|
64
|
-
case locking
|
65
|
-
when true
|
66
|
-
locking = Arel.sql('WITH(HOLDLOCK, ROWLOCK)')
|
67
|
-
when Arel::Nodes::SqlLiteral
|
68
|
-
when String
|
69
|
-
locking = Arel.sql locking
|
70
|
-
end
|
71
|
-
@ast.lock = Arel::Nodes::Lock.new(locking)
|
72
|
-
self
|
73
|
-
else
|
74
|
-
lock_without_sqlserver(locking)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
private
|
79
|
-
|
80
|
-
def engine_activerecord_sqlserver_adapter?
|
81
|
-
@engine.connection && @engine.connection.class.name == AR_CA_SQLSA_NAME
|
82
|
-
end
|
83
|
-
|
84
|
-
end
|
85
|
-
|
86
2
|
module Visitors
|
87
3
|
class SQLServer < Arel::Visitors::ToSql
|
88
|
-
|
89
4
|
private
|
90
5
|
|
91
6
|
# SQLServer ToSql/Visitor (Overides)
|
92
|
-
|
93
|
-
def visit_Arel_Nodes_SelectStatement(o)
|
7
|
+
def visit_Arel_Nodes_SelectStatement(o, a)
|
94
8
|
if complex_count_sql?(o)
|
95
|
-
visit_Arel_Nodes_SelectStatementForComplexCount(o)
|
9
|
+
visit_Arel_Nodes_SelectStatementForComplexCount(o, a)
|
10
|
+
elsif distinct_non_present_orders?(o, a)
|
11
|
+
visit_Arel_Nodes_SelectStatementDistinctNonPresentOrders(o, a)
|
96
12
|
elsif o.offset
|
97
|
-
visit_Arel_Nodes_SelectStatementWithOffset(o)
|
13
|
+
visit_Arel_Nodes_SelectStatementWithOffset(o, a)
|
98
14
|
else
|
99
|
-
visit_Arel_Nodes_SelectStatementWithOutOffset(o)
|
15
|
+
visit_Arel_Nodes_SelectStatementWithOutOffset(o, a)
|
100
16
|
end
|
101
17
|
end
|
102
|
-
|
103
|
-
def visit_Arel_Nodes_UpdateStatement(o)
|
18
|
+
|
19
|
+
def visit_Arel_Nodes_UpdateStatement(o, a)
|
104
20
|
if o.orders.any? && o.limit.nil?
|
105
|
-
o.limit = Nodes::Limit.new(
|
21
|
+
o.limit = Nodes::Limit.new(9_223_372_036_854_775_807)
|
106
22
|
end
|
107
23
|
super
|
108
24
|
end
|
109
25
|
|
110
|
-
def visit_Arel_Nodes_Offset(o)
|
111
|
-
"WHERE [__rnt].[__rn] > (#{visit o.expr})"
|
26
|
+
def visit_Arel_Nodes_Offset(o, a)
|
27
|
+
"WHERE [__rnt].[__rn] > (#{visit o.expr, a})"
|
112
28
|
end
|
113
29
|
|
114
|
-
def visit_Arel_Nodes_Limit(o)
|
115
|
-
"TOP (#{visit o.expr})"
|
30
|
+
def visit_Arel_Nodes_Limit(o, a)
|
31
|
+
"TOP (#{visit o.expr, a})"
|
116
32
|
end
|
117
33
|
|
118
|
-
def visit_Arel_Nodes_Lock(o)
|
119
|
-
visit o.expr
|
34
|
+
def visit_Arel_Nodes_Lock(o, a)
|
35
|
+
visit o.expr, a
|
120
36
|
end
|
121
|
-
|
122
|
-
def visit_Arel_Nodes_Ordering(o)
|
37
|
+
|
38
|
+
def visit_Arel_Nodes_Ordering(o, a)
|
123
39
|
if o.respond_to?(:direction)
|
124
|
-
"#{visit o.expr} #{o.ascending? ? 'ASC' : 'DESC'}"
|
40
|
+
"#{visit o.expr, a} #{o.ascending? ? 'ASC' : 'DESC'}"
|
125
41
|
else
|
126
|
-
visit o.expr
|
42
|
+
visit o.expr, a
|
127
43
|
end
|
128
44
|
end
|
129
|
-
|
130
|
-
def visit_Arel_Nodes_Bin(o)
|
131
|
-
"#{visit o.expr} #{@connection.cs_equality_operator}"
|
45
|
+
|
46
|
+
def visit_Arel_Nodes_Bin(o, a)
|
47
|
+
"#{visit o.expr, a} #{@connection.cs_equality_operator}"
|
132
48
|
end
|
133
49
|
|
134
50
|
# SQLServer ToSql/Visitor (Additions)
|
135
51
|
|
136
|
-
|
52
|
+
# This constructs a query using DENSE_RANK() and ROW_NUMBER() to allow
|
53
|
+
# ordering a DISTINCT set of data by columns in another table that are
|
54
|
+
# not part of what we actually want to be DISTINCT. Without this, it is
|
55
|
+
# possible for the DISTINCT qualifier combined with TOP to return fewer
|
56
|
+
# rows than were requested.
|
57
|
+
def visit_Arel_Nodes_SelectStatementDistinctNonPresentOrders(o, a)
|
58
|
+
core = o.cores.first
|
59
|
+
projections = core.projections
|
60
|
+
groups = core.groups
|
61
|
+
orders = o.orders.uniq
|
62
|
+
|
63
|
+
select_frags = projections.map do |x|
|
64
|
+
frag = projection_to_sql_remove_distinct(x, core, a)
|
65
|
+
# Remove the table specifier
|
66
|
+
frag.gsub!(/^[^\.]*\./, '')
|
67
|
+
# If there is an alias, remove everything but
|
68
|
+
frag.gsub(/^.*\sAS\s+/i, '')
|
69
|
+
end
|
70
|
+
|
71
|
+
if o.offset
|
72
|
+
select_frags << 'ROW_NUMBER() OVER (ORDER BY __order) AS __offset'
|
73
|
+
else
|
74
|
+
select_frags << '__order'
|
75
|
+
end
|
76
|
+
|
77
|
+
projection_list = projections.map { |x| projection_to_sql_remove_distinct(x, core, a) }.join(', ')
|
78
|
+
|
79
|
+
sql = [
|
80
|
+
('SELECT'),
|
81
|
+
(visit(core.set_quantifier, a) if core.set_quantifier && !o.offset),
|
82
|
+
(visit(o.limit, a) if o.limit && !o.offset),
|
83
|
+
(select_frags.join(', ')),
|
84
|
+
('FROM ('),
|
85
|
+
('SELECT'),
|
86
|
+
(
|
87
|
+
[
|
88
|
+
(projection_list),
|
89
|
+
(', DENSE_RANK() OVER ('),
|
90
|
+
("ORDER BY #{orders.map { |x| visit(x, a) }.join(', ')}" unless orders.empty?),
|
91
|
+
(') AS __order'),
|
92
|
+
(', ROW_NUMBER() OVER ('),
|
93
|
+
("PARTITION BY #{projection_list}" if !orders.empty?),
|
94
|
+
(" ORDER BY #{orders.map { |x| visit(x, a) }.join(', ')}" unless orders.empty?),
|
95
|
+
(') AS __joined_row_num')
|
96
|
+
].join('')
|
97
|
+
),
|
98
|
+
(source_with_lock_for_select_statement(o, a)),
|
99
|
+
("WHERE #{core.wheres.map { |x| visit(x, a) }.join ' AND ' }" unless core.wheres.empty?),
|
100
|
+
("GROUP BY #{groups.map { |x| visit(x, a) }.join ', ' }" unless groups.empty?),
|
101
|
+
(visit(core.having, a) if core.having),
|
102
|
+
(') AS __sq'),
|
103
|
+
('WHERE __joined_row_num = 1'),
|
104
|
+
('ORDER BY __order' unless o.offset)
|
105
|
+
].compact.join(' ')
|
106
|
+
|
107
|
+
if o.offset
|
108
|
+
sql = [
|
109
|
+
('SELECT'),
|
110
|
+
(visit(core.set_quantifier, a) if core.set_quantifier),
|
111
|
+
(visit(o.limit, a) if o.limit),
|
112
|
+
('*'),
|
113
|
+
('FROM (' + sql + ') AS __osq'),
|
114
|
+
("WHERE __offset > #{visit(o.offset.expr, a)}"),
|
115
|
+
('ORDER BY __offset')
|
116
|
+
].join(' ')
|
117
|
+
end
|
118
|
+
|
119
|
+
sql
|
120
|
+
end
|
121
|
+
|
122
|
+
def visit_Arel_Nodes_SelectStatementWithOutOffset(o, a, windowed = false)
|
137
123
|
find_and_fix_uncorrelated_joins_in_select_statement(o)
|
138
124
|
core = o.cores.first
|
139
125
|
projections = core.projections
|
140
126
|
groups = core.groups
|
141
127
|
orders = o.orders.uniq
|
142
128
|
if windowed
|
143
|
-
projections = function_select_statement?(o) ? projections : projections.map { |x| projection_without_expression(x) }
|
144
|
-
groups = projections.map { |x| projection_without_expression(x) } if windowed_single_distinct_select_statement?(o) && groups.empty?
|
129
|
+
projections = function_select_statement?(o) ? projections : projections.map { |x| projection_without_expression(x, a) }
|
130
|
+
groups = projections.map { |x| projection_without_expression(x, a) } if windowed_single_distinct_select_statement?(o) && groups.empty?
|
145
131
|
groups += orders.map { |x| Arel.sql(x.expr) } if windowed_single_distinct_select_statement?(o)
|
146
|
-
elsif eager_limiting_select_statement?(o)
|
147
|
-
projections = projections.map { |x| projection_without_expression(x) }
|
148
|
-
groups = projections.map { |x| projection_without_expression(x) }
|
132
|
+
elsif eager_limiting_select_statement?(o, a)
|
133
|
+
projections = projections.map { |x| projection_without_expression(x, a) }
|
134
|
+
groups = projections.map { |x| projection_without_expression(x, a) }
|
149
135
|
orders = orders.map do |x|
|
150
|
-
expr = Arel.sql projection_without_expression(x.expr)
|
136
|
+
expr = Arel.sql projection_without_expression(x.expr, a)
|
151
137
|
x.descending? ? Arel::Nodes::Max.new([expr]) : Arel::Nodes::Min.new([expr])
|
152
138
|
end
|
153
|
-
elsif top_one_everything_for_through_join?(o)
|
154
|
-
projections = projections.map { |x| projection_without_expression(x) }
|
139
|
+
elsif top_one_everything_for_through_join?(o, a)
|
140
|
+
projections = projections.map { |x| projection_without_expression(x, a) }
|
155
141
|
end
|
156
|
-
[
|
157
|
-
(
|
158
|
-
(visit(
|
159
|
-
(
|
160
|
-
(
|
161
|
-
|
162
|
-
|
163
|
-
(
|
164
|
-
(
|
142
|
+
[
|
143
|
+
('SELECT' unless windowed),
|
144
|
+
(visit(core.set_quantifier, a) if core.set_quantifier && !windowed),
|
145
|
+
(visit(o.limit, a) if o.limit && !windowed),
|
146
|
+
(projections.map do |x|
|
147
|
+
v = visit(x, a)
|
148
|
+
v == '1' ? '1 AS [__wrp]' : v
|
149
|
+
end.join(', ')),
|
150
|
+
(source_with_lock_for_select_statement(o, a)),
|
151
|
+
("WHERE #{core.wheres.map { |x| visit(x, a) }.join ' AND ' }" unless core.wheres.empty?),
|
152
|
+
("GROUP BY #{groups.map { |x| visit(x, a) }.join ', ' }" unless groups.empty?),
|
153
|
+
(visit(core.having, a) if core.having),
|
154
|
+
("ORDER BY #{orders.map { |x| visit(x, a) }.join(', ')}" if !orders.empty? && !windowed)
|
165
155
|
].compact.join ' '
|
166
156
|
end
|
167
157
|
|
168
|
-
def visit_Arel_Nodes_SelectStatementWithOffset(o)
|
158
|
+
def visit_Arel_Nodes_SelectStatementWithOffset(o, a)
|
169
159
|
core = o.cores.first
|
170
|
-
o.limit ||= Arel::Nodes::Limit.new(
|
160
|
+
o.limit ||= Arel::Nodes::Limit.new(9_223_372_036_854_775_807)
|
171
161
|
orders = rowtable_orders(o)
|
172
|
-
[
|
173
|
-
|
174
|
-
(
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
162
|
+
[
|
163
|
+
'SELECT',
|
164
|
+
(visit(o.limit, a) if o.limit && !windowed_single_distinct_select_statement?(o)),
|
165
|
+
(rowtable_projections(o, a).map { |x| visit(x, a) }.join(', ')),
|
166
|
+
'FROM (',
|
167
|
+
"SELECT #{core.set_quantifier ? 'DISTINCT DENSE_RANK()' : 'ROW_NUMBER()'} OVER (ORDER BY #{orders.map { |x| visit(x, a) }.join(', ')}) AS [__rn],",
|
168
|
+
visit_Arel_Nodes_SelectStatementWithOutOffset(o, a, true),
|
169
|
+
') AS [__rnt]',
|
170
|
+
(visit(o.offset, a) if o.offset),
|
171
|
+
'ORDER BY [__rnt].[__rn] ASC'
|
181
172
|
].compact.join ' '
|
182
173
|
end
|
183
174
|
|
184
|
-
def visit_Arel_Nodes_SelectStatementForComplexCount(o)
|
175
|
+
def visit_Arel_Nodes_SelectStatementForComplexCount(o, a)
|
185
176
|
core = o.cores.first
|
186
177
|
o.limit.expr = Arel.sql("#{o.limit.expr} + #{o.offset ? o.offset.expr : 0}") if o.limit
|
187
178
|
orders = rowtable_orders(o)
|
188
|
-
[
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
")
|
200
|
-
|
179
|
+
[
|
180
|
+
'SELECT COUNT([count]) AS [count_id]',
|
181
|
+
'FROM (',
|
182
|
+
'SELECT',
|
183
|
+
(visit(o.limit, a) if o.limit),
|
184
|
+
"ROW_NUMBER() OVER (ORDER BY #{orders.map { |x| visit(x, a) }.join(', ')}) AS [__rn],",
|
185
|
+
'1 AS [count]',
|
186
|
+
(source_with_lock_for_select_statement(o, a)),
|
187
|
+
("WHERE #{core.wheres.map { |x| visit(x, a) }.join ' AND ' }" unless core.wheres.empty?),
|
188
|
+
("GROUP BY #{core.groups.map { |x| visit(x, a) }.join ', ' }" unless core.groups.empty?),
|
189
|
+
(visit(core.having, a) if core.having),
|
190
|
+
("ORDER BY #{o.orders.map { |x| visit(x, a) }.join(', ')}" unless o.orders.empty?),
|
191
|
+
') AS [__rnt]',
|
192
|
+
(visit(o.offset, a) if o.offset)
|
201
193
|
].compact.join ' '
|
202
194
|
end
|
203
195
|
|
204
|
-
|
205
196
|
# SQLServer Helpers
|
206
197
|
|
207
|
-
def
|
198
|
+
def projection_to_sql_remove_distinct(x, core, a)
|
199
|
+
frag = Arel.sql(visit(x, a))
|
200
|
+
# In Rails 4.0.0, DISTINCT was in a projection, whereas with 4.0.1
|
201
|
+
# it is now stored in the set_quantifier. This moves it to the correct
|
202
|
+
# place so the code works on both 4.0.0 and 4.0.1.
|
203
|
+
if frag =~ /^\s*DISTINCT\s+/i
|
204
|
+
core.set_quantifier = Arel::Nodes::Distinct.new
|
205
|
+
frag.gsub!(/\s*DISTINCT\s+/, '')
|
206
|
+
end
|
207
|
+
frag
|
208
|
+
end
|
209
|
+
|
210
|
+
def source_with_lock_for_select_statement(o, a)
|
208
211
|
core = o.cores.first
|
209
|
-
source = "FROM #{visit(core.source).strip}" if core.source
|
212
|
+
source = "FROM #{visit(core.source, a).strip}" if core.source
|
210
213
|
if source && o.lock
|
211
|
-
lock = visit o.lock
|
214
|
+
lock = visit o.lock, a
|
212
215
|
index = source.match(/FROM [\w\[\]\.]+/)[0].mb_chars.length
|
213
216
|
source.insert index, " #{lock}"
|
214
217
|
else
|
@@ -226,7 +229,7 @@ module Arel
|
|
226
229
|
# elsif Arel::Nodes::JoinSource === core.source
|
227
230
|
# Arel::Nodes::SqlLiteral === core.source.left ? Arel::Table.new(core.source.left, @engine) : core.source.left
|
228
231
|
# end
|
229
|
-
table_finder = lambda
|
232
|
+
table_finder = lambda do |x|
|
230
233
|
case x
|
231
234
|
when Arel::Table
|
232
235
|
x
|
@@ -235,7 +238,7 @@ module Arel
|
|
235
238
|
when Arel::Nodes::Join
|
236
239
|
table_finder.call(x.left)
|
237
240
|
end
|
238
|
-
|
241
|
+
end
|
239
242
|
table_finder.call(core.froms)
|
240
243
|
end
|
241
244
|
|
@@ -246,25 +249,88 @@ module Arel
|
|
246
249
|
((p1.respond_to?(:distinct) && p1.distinct) ||
|
247
250
|
p1.respond_to?(:include?) && p1.include?('DISTINCT'))
|
248
251
|
end
|
249
|
-
|
252
|
+
|
253
|
+
# Determine if the SELECT statement is asking for DISTINCT results,
|
254
|
+
# but is using columns not part of the SELECT list in the ORDER BY.
|
255
|
+
# This is necessary because SQL Server requires all ORDER BY entries
|
256
|
+
# be in the SELECT list with DISTINCT. However, these ordering columns
|
257
|
+
# can cause duplicate rows, which affect when using a limit.
|
258
|
+
def distinct_non_present_orders?(o, a)
|
259
|
+
projections = o.cores.first.projections
|
260
|
+
|
261
|
+
sq = o.cores.first.set_quantifier
|
262
|
+
p1 = projections.first
|
263
|
+
|
264
|
+
found_distinct = sq && sq.class.to_s =~ /Distinct/
|
265
|
+
if (p1.respond_to?(:distinct) && p1.distinct) || (p1.respond_to?(:include?) && p1.include?('DISTINCT'))
|
266
|
+
found_distinct = true
|
267
|
+
end
|
268
|
+
|
269
|
+
return false if !found_distinct || o.orders.uniq.empty?
|
270
|
+
|
271
|
+
tables_all_columns = []
|
272
|
+
expressions = projections.map do |p|
|
273
|
+
visit(p, a).split(',').map do |x|
|
274
|
+
x.strip!
|
275
|
+
# Rails 4.0.0 included DISTINCT in the first projection
|
276
|
+
x.gsub!(/\s*DISTINCT\s+/, '')
|
277
|
+
# Aliased column names
|
278
|
+
x.gsub!(/\s+AS\s+\w+/i, '')
|
279
|
+
# Identifier quoting
|
280
|
+
x.gsub!(/\[|\]/, '')
|
281
|
+
star_match = /^(\w+)\.\*$/.match(x)
|
282
|
+
tables_all_columns << star_match[1] if star_match
|
283
|
+
x.strip.downcase
|
284
|
+
end.join(', ')
|
285
|
+
end
|
286
|
+
|
287
|
+
# Make sure each order by is in the select list, otherwise there needs
|
288
|
+
# to be a subquery with row_numbe()
|
289
|
+
o.orders.uniq.each do |order|
|
290
|
+
order = visit(order, a)
|
291
|
+
order.strip!
|
292
|
+
|
293
|
+
order.gsub!(/\s+(asc|desc)/i, '')
|
294
|
+
# Identifier quoting
|
295
|
+
order.gsub!(/\[|\]/, '')
|
296
|
+
|
297
|
+
order.strip!
|
298
|
+
order.downcase!
|
299
|
+
|
300
|
+
# If we selected all columns from a table, the order is ok
|
301
|
+
table_match = /^(\w+)\.\w+$/.match(order)
|
302
|
+
next if table_match && tables_all_columns.include?(table_match[1])
|
303
|
+
|
304
|
+
next if expressions.include?(order)
|
305
|
+
|
306
|
+
return true
|
307
|
+
end
|
308
|
+
|
309
|
+
# We didn't find anything in the order by no being selected
|
310
|
+
false
|
311
|
+
end
|
312
|
+
|
250
313
|
def windowed_single_distinct_select_statement?(o)
|
251
|
-
o.limit &&
|
314
|
+
o.limit &&
|
315
|
+
o.offset &&
|
316
|
+
single_distinct_select_statement?(o)
|
252
317
|
end
|
253
|
-
|
254
|
-
def single_distinct_select_everything_statement?(o)
|
255
|
-
single_distinct_select_statement?(o) &&
|
318
|
+
|
319
|
+
def single_distinct_select_everything_statement?(o, a)
|
320
|
+
single_distinct_select_statement?(o) &&
|
321
|
+
visit(o.cores.first.projections.first, a).ends_with?('.*')
|
256
322
|
end
|
257
|
-
|
258
|
-
def top_one_everything_for_through_join?(o)
|
259
|
-
single_distinct_select_everything_statement?(o) &&
|
260
|
-
(o.limit && !o.offset) &&
|
323
|
+
|
324
|
+
def top_one_everything_for_through_join?(o, a)
|
325
|
+
single_distinct_select_everything_statement?(o, a) &&
|
326
|
+
(o.limit && !o.offset) &&
|
261
327
|
join_in_select_statement?(o)
|
262
328
|
end
|
263
329
|
|
264
|
-
def all_projections_aliased_in_select_statement?(o)
|
330
|
+
def all_projections_aliased_in_select_statement?(o, a)
|
265
331
|
projections = o.cores.first.projections
|
266
332
|
projections.all? do |x|
|
267
|
-
visit(x).split(',').all? { |y| y.include?(' AS ') }
|
333
|
+
visit(x, a).split(',').all? { |y| y.include?(' AS ') }
|
268
334
|
end
|
269
335
|
end
|
270
336
|
|
@@ -273,12 +339,12 @@ module Arel
|
|
273
339
|
core.projections.any? { |x| Arel::Nodes::Function === x }
|
274
340
|
end
|
275
341
|
|
276
|
-
def eager_limiting_select_statement?(o)
|
342
|
+
def eager_limiting_select_statement?(o, a)
|
277
343
|
core = o.cores.first
|
278
|
-
single_distinct_select_statement?(o) &&
|
279
|
-
(o.limit && !o.offset) &&
|
280
|
-
core.groups.empty? &&
|
281
|
-
!single_distinct_select_everything_statement?(o)
|
344
|
+
single_distinct_select_statement?(o) &&
|
345
|
+
(o.limit && !o.offset) &&
|
346
|
+
core.groups.empty? &&
|
347
|
+
!single_distinct_select_everything_statement?(o, a)
|
282
348
|
end
|
283
349
|
|
284
350
|
def join_in_select_statement?(o)
|
@@ -293,7 +359,7 @@ module Arel
|
|
293
359
|
o.limit &&
|
294
360
|
!join_in_select_statement?(o)
|
295
361
|
end
|
296
|
-
|
362
|
+
|
297
363
|
def select_primary_key_sql?(o)
|
298
364
|
core = o.cores.first
|
299
365
|
return false if core.projections.size != 1
|
@@ -320,22 +386,22 @@ module Arel
|
|
320
386
|
j2 = core.froms.right
|
321
387
|
return unless Arel::Nodes::OuterJoin === j1 && Arel::Nodes::SqlLiteral === j2 && j2.include?('JOIN ')
|
322
388
|
j1_tn = j1.right.name
|
323
|
-
j2_tn = j2.match(/JOIN \[(.*)\].*ON/).try(:[],1)
|
389
|
+
j2_tn = j2.match(/JOIN \[(.*)\].*ON/).try(:[], 1)
|
324
390
|
return unless j1_tn == j2_tn
|
325
391
|
on_index = j2.index(' ON ')
|
326
392
|
j2.insert on_index, " AS [#{j2_tn}_crltd]"
|
327
393
|
j2.sub! "[#{j2_tn}].", "[#{j2_tn}_crltd]."
|
328
394
|
end
|
329
395
|
|
330
|
-
def rowtable_projections(o)
|
396
|
+
def rowtable_projections(o, a)
|
331
397
|
core = o.cores.first
|
332
398
|
if windowed_single_distinct_select_statement?(o) && core.groups.blank?
|
333
399
|
tn = table_from_select_statement(o).name
|
334
400
|
core.projections.map do |x|
|
335
401
|
x.dup.tap do |p|
|
336
402
|
p.sub! 'DISTINCT', ''
|
337
|
-
p.insert 0, visit(o.limit) if o.limit
|
338
|
-
p.gsub!
|
403
|
+
p.insert 0, visit(o.limit, a) if o.limit
|
404
|
+
p.gsub!(/\[?#{tn}\]?\./, '[__rnt].')
|
339
405
|
p.strip!
|
340
406
|
end
|
341
407
|
end
|
@@ -343,14 +409,14 @@ module Arel
|
|
343
409
|
tn = table_from_select_statement(o).name
|
344
410
|
core.projections.map do |x|
|
345
411
|
x.dup.tap do |p|
|
346
|
-
p.sub! 'DISTINCT', "DISTINCT #{visit(o.limit)}".strip if o.limit
|
347
|
-
p.gsub!
|
412
|
+
p.sub! 'DISTINCT', "DISTINCT #{visit(o.limit, a)}".strip if o.limit
|
413
|
+
p.gsub!(/\[?#{tn}\]?\./, '[__rnt].')
|
348
414
|
p.strip!
|
349
415
|
end
|
350
416
|
end
|
351
|
-
elsif join_in_select_statement?(o) && all_projections_aliased_in_select_statement?(o)
|
417
|
+
elsif join_in_select_statement?(o) && all_projections_aliased_in_select_statement?(o, a)
|
352
418
|
core.projections.map do |x|
|
353
|
-
Arel.sql visit(x).split(',').map{ |y| y.split(' AS ').last.strip }.join(', ')
|
419
|
+
Arel.sql visit(x, a).split(',').map { |y| y.split(' AS ').last.strip }.join(', ')
|
354
420
|
end
|
355
421
|
elsif select_primary_key_sql?(o)
|
356
422
|
[Arel.sql("[__rnt].#{quote_column_name(core.projections.first.name)}")]
|
@@ -360,7 +426,6 @@ module Arel
|
|
360
426
|
end
|
361
427
|
|
362
428
|
def rowtable_orders(o)
|
363
|
-
core = o.cores.first
|
364
429
|
if !o.orders.empty?
|
365
430
|
o.orders
|
366
431
|
else
|
@@ -371,19 +436,17 @@ module Arel
|
|
371
436
|
end
|
372
437
|
|
373
438
|
# TODO: We use this for grouping too, maybe make Grouping objects vs SqlLiteral.
|
374
|
-
def projection_without_expression(projection)
|
375
|
-
Arel.sql(visit(projection).split(',').map do |x|
|
439
|
+
def projection_without_expression(projection, a)
|
440
|
+
Arel.sql(visit(projection, a).split(',').map do |x|
|
376
441
|
x.strip!
|
377
|
-
x.sub!(/^(COUNT|SUM|MAX|MIN|AVG)\s*(\((.*)\))?/,'\3')
|
378
|
-
x.sub!(/^DISTINCT\s*/,'')
|
379
|
-
x.sub!(/TOP\s*\(\d+\)\s*/i,'')
|
442
|
+
x.sub!(/^(COUNT|SUM|MAX|MIN|AVG)\s*(\((.*)\))?/, '\3')
|
443
|
+
x.sub!(/^DISTINCT\s*/, '')
|
444
|
+
x.sub!(/TOP\s*\(\d+\)\s*/i, '')
|
380
445
|
x.strip
|
381
446
|
end.join(', '))
|
382
447
|
end
|
383
|
-
|
384
448
|
end
|
385
449
|
end
|
386
|
-
|
387
450
|
end
|
388
451
|
|
389
452
|
Arel::Visitors::VISITORS['sqlserver'] = Arel::Visitors::SQLServer
|