Skip to content

Commit 484ba21

Browse files
authored
Merge pull request #749 from rails-sqlserver/5-2-backport
Backport master to 5-2-stable
2 parents c2916d7 + c2be57c commit 484ba21

24 files changed

+806
-656
lines changed

lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb

+19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,25 @@ module ConnectionAdapters
66
module SQLServer
77
module CoreExt
88
module Calculations
9+
10+
# Same as original except we don't perform PostgreSQL hack that removes ordering.
11+
def calculate(operation, column_name)
12+
if has_include?(column_name)
13+
relation = apply_join_dependency
14+
15+
if operation.to_s.downcase == "count"
16+
unless distinct_value || distinct_select?(column_name || select_for_count)
17+
relation.distinct!
18+
relation.select_values = [ klass.primary_key || table[Arel.star] ]
19+
end
20+
end
21+
22+
relation.calculate(operation, column_name)
23+
else
24+
perform_calculation(operation, column_name)
25+
end
26+
end
27+
928
private
1029

1130
def build_count_subquery(relation, column_name, distinct)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
require 'active_record/relation'
2+
require 'active_record/version'
3+
4+
module ActiveRecord
5+
module ConnectionAdapters
6+
module SQLServer
7+
module CoreExt
8+
module FinderMethods
9+
10+
private
11+
12+
# Same as original except we order by values in distinct select if present.
13+
def construct_relation_for_exists(conditions)
14+
if distinct_value && offset_value
15+
relation = limit!(1)
16+
17+
if select_values.present?
18+
relation = relation.order(*select_values)
19+
else
20+
relation = relation.except(:order)
21+
end
22+
else
23+
relation = except(:select, :distinct, :order)._select!(::ActiveRecord::FinderMethods::ONE_AS_ONE).limit!(1)
24+
end
25+
26+
case conditions
27+
when Array, Hash
28+
relation.where!(conditions) unless conditions.empty?
29+
else
30+
relation.where!(primary_key => conditions) unless conditions == :none
31+
end
32+
33+
relation
34+
end
35+
end
36+
end
37+
end
38+
end
39+
end
40+
41+
ActiveSupport.on_load(:active_record) do
42+
ActiveRecord::Relation.include(ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::FinderMethods)
43+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require 'active_record/relation'
2+
require 'active_record/version'
3+
4+
module ActiveRecord
5+
module ConnectionAdapters
6+
module SQLServer
7+
module CoreExt
8+
module QueryMethods
9+
10+
private
11+
12+
# Copy of original from Rails master. This patch can be removed when adapter supports Rails 6.
13+
def table_name_matches?(from)
14+
table_name = Regexp.escape(table.name)
15+
quoted_table_name = Regexp.escape(connection.quote_table_name(table.name))
16+
/(?:\A|(?<!FROM)\s)(?:\b#{table_name}\b|#{quoted_table_name})(?!\.)/i.match?(from.to_s)
17+
end
18+
end
19+
end
20+
end
21+
end
22+
end
23+
24+
ActiveSupport.on_load(:active_record) do
25+
ActiveRecord::Relation.include(ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::QueryMethods)
26+
end

lib/active_record/connection_adapters/sqlserver_adapter.rb

+6-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
require 'active_record/connection_adapters/sqlserver/core_ext/explain'
88
require 'active_record/connection_adapters/sqlserver/core_ext/explain_subscriber'
99
require 'active_record/connection_adapters/sqlserver/core_ext/attribute_methods'
10+
require 'active_record/connection_adapters/sqlserver/core_ext/finder_methods'
11+
require 'active_record/connection_adapters/sqlserver/core_ext/query_methods'
1012
require 'active_record/connection_adapters/sqlserver/version'
1113
require 'active_record/connection_adapters/sqlserver/type'
1214
require 'active_record/connection_adapters/sqlserver/database_limits'
@@ -40,6 +42,9 @@ class SQLServerAdapter < AbstractAdapter
4042

4143
ADAPTER_NAME = 'SQLServer'.freeze
4244

45+
# Default precision for 'time' (See https://docs.microsoft.com/en-us/sql/t-sql/data-types/time-transact-sql)
46+
DEFAULT_TIME_PRECISION = 7
47+
4348
attr_reader :spid
4449

4550
cattr_accessor :cs_equality_operator, instance_accessor: false
@@ -296,8 +301,7 @@ def initialize_type_map(m = type_map)
296301
end
297302
m.register_type 'smalldatetime', SQLServer::Type::SmallDateTime.new
298303
m.register_type %r{\Atime}i do |sql_type|
299-
scale = extract_scale(sql_type)
300-
precision = extract_precision(sql_type)
304+
precision = extract_precision(sql_type) || DEFAULT_TIME_PRECISION
301305
SQLServer::Type::Time.new precision: precision
302306
end
303307
# Character Strings

test/cases/adapter_test_sqlserver.rb

+18-18
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
1515

1616
it 'has basic and non-senstive information in the adpaters inspect method' do
1717
string = connection.inspect
18-
string.must_match %r{ActiveRecord::ConnectionAdapters::SQLServerAdapter}
19-
string.must_match %r{version\: \d.\d}
20-
string.must_match %r{mode: dblib}
21-
string.must_match %r{azure: (true|false)}
22-
string.wont_match %r{host}
23-
string.wont_match %r{password}
24-
string.wont_match %r{username}
25-
string.wont_match %r{port}
18+
_(string).must_match %r{ActiveRecord::ConnectionAdapters::SQLServerAdapter}
19+
_(string).must_match %r{version\: \d.\d}
20+
_(string).must_match %r{mode: dblib}
21+
_(string).must_match %r{azure: (true|false)}
22+
_(string).wont_match %r{host}
23+
_(string).wont_match %r{password}
24+
_(string).wont_match %r{username}
25+
_(string).wont_match %r{port}
2626
end
2727

2828
it 'has a 128 max #table_alias_length' do
@@ -161,7 +161,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
161161
end
162162

163163
it 'return an empty array when calling #identity_columns for a table_name with no identity' do
164-
connection.send(:identity_columns, Subscriber.table_name).must_equal []
164+
_(connection.send(:identity_columns, Subscriber.table_name)).must_equal []
165165
end
166166

167167
end
@@ -303,7 +303,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
303303
end
304304

305305
it 'find SSTestCustomersView table name' do
306-
connection.views.must_include 'sst_customers_view'
306+
_(connection.views).must_include 'sst_customers_view'
307307
end
308308

309309
it 'work with dynamic finders' do
@@ -344,9 +344,9 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
344344
end
345345

346346
it 'find identity column' do
347-
SSTestCustomersView.primary_key.must_equal 'id'
348-
connection.primary_key(SSTestCustomersView.table_name).must_equal 'id'
349-
SSTestCustomersView.columns_hash['id'].must_be :is_identity?
347+
_(SSTestCustomersView.primary_key).must_equal 'id'
348+
_(connection.primary_key(SSTestCustomersView.table_name)).must_equal 'id'
349+
_(SSTestCustomersView.columns_hash['id']).must_be :is_identity?
350350
end
351351

352352
it 'find default values' do
@@ -371,9 +371,9 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
371371
end
372372

373373
it 'find identity column' do
374-
SSTestStringDefaultsView.primary_key.must_equal 'id'
375-
connection.primary_key(SSTestStringDefaultsView.table_name).must_equal 'id'
376-
SSTestStringDefaultsView.columns_hash['id'].must_be :is_identity?
374+
_(SSTestStringDefaultsView.primary_key).must_equal 'id'
375+
_(connection.primary_key(SSTestStringDefaultsView.table_name)).must_equal 'id'
376+
_(SSTestStringDefaultsView.columns_hash['id']).must_be :is_identity?
377377
end
378378

379379
it 'find default values' do
@@ -422,8 +422,8 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
422422

423423
it 'in_memory_oltp' do
424424
if ENV['IN_MEMORY_OLTP'] && connection.supports_in_memory_oltp?
425-
SSTMemory.primary_key.must_equal 'id'
426-
SSTMemory.columns_hash['id'].must_be :is_identity?
425+
_(SSTMemory.primary_key).must_equal 'id'
426+
_(SSTMemory.columns_hash['id']).must_be :is_identity?
427427
else
428428
skip 'supports_in_memory_oltp? => false'
429429
end

test/cases/change_column_null_test_sqlserver.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,19 @@ def find_column(table, name)
2424

2525
describe '#change_column_null' do
2626
it 'does not change the column limit' do
27-
name_column.limit.must_equal 15
27+
_(name_column.limit).must_equal 15
2828
end
2929

3030
it 'does not change the column default' do
31-
code_column.default.must_equal 'n/a'
31+
_(code_column.default).must_equal 'n/a'
3232
end
3333

3434
it 'does not change the column precision' do
35-
value_column.precision.must_equal 32
35+
_(value_column.precision).must_equal 32
3636
end
3737

3838
it 'does not change the column scale' do
39-
value_column.scale.must_equal 8
39+
_(value_column.scale).must_equal 8
4040
end
4141
end
4242
end

test/cases/coerced_tests.rb

+28-9
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,27 @@ def test_belongs_to_with_primary_key_joins_on_correct_column_coerced
145145

146146
module ActiveRecord
147147
class BindParameterTest < ActiveRecord::TestCase
148-
# Never finds `sql` since we use `EXEC sp_executesql` wrappers.
148+
# Same as original coerced test except log is found using `EXEC sp_executesql` wrapper.
149149
coerce_tests! :test_binds_are_logged
150+
def test_binds_are_logged_coerced
151+
sub = Arel::Nodes::BindParam.new(1)
152+
binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)]
153+
sql = "select * from topics where id = #{sub.to_sql}"
154+
155+
@connection.exec_query(sql, "SQL", binds)
156+
157+
logged_sql = "EXEC sp_executesql N'#{sql}', N'#{sub.to_sql} int', #{sub.to_sql} = 1"
158+
message = @subscriber.calls.find { |args| args[4][:sql] == logged_sql }
159+
160+
assert_equal binds, message[4][:binds]
161+
end
162+
163+
# SQL Server adapter does not use a statement cache as query plans are already reused using `EXEC sp_executesql`.
164+
coerce_tests! :test_statement_cache
165+
coerce_tests! :test_statement_cache_with_query_cache
166+
coerce_tests! :test_statement_cache_with_find_by
167+
coerce_tests! :test_statement_cache_with_in_clause
168+
coerce_tests! :test_statement_cache_with_sql_string_literal
150169
end
151170
end
152171

@@ -192,14 +211,14 @@ def test_should_return_decimal_average_of_integer_field_coerced
192211
def test_limit_is_kept_coerced
193212
queries = capture_sql_ss { Account.limit(1).count }
194213
assert_equal 1, queries.length
195-
queries.first.must_match %r{ORDER BY \[accounts\]\.\[id\] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY.*@0 = 1}
214+
_(queries.first).must_match %r{ORDER BY \[accounts\]\.\[id\] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY.*@0 = 1}
196215
end
197216

198217
coerce_tests! :test_limit_with_offset_is_kept
199218
def test_limit_with_offset_is_kept_coerced
200219
queries = capture_sql_ss { Account.limit(1).offset(1).count }
201220
assert_equal 1, queries.length
202-
queries.first.must_match %r{ORDER BY \[accounts\]\.\[id\] ASC OFFSET @0 ROWS FETCH NEXT @1 ROWS ONLY.*@0 = 1, @1 = 1}
221+
_(queries.first).must_match %r{ORDER BY \[accounts\]\.\[id\] ASC OFFSET @0 ROWS FETCH NEXT @1 ROWS ONLY.*@0 = 1, @1 = 1}
203222
end
204223

205224
# SQL Server needs an alias for the calculated column
@@ -265,7 +284,7 @@ class ColumnAttributesTest < ActiveRecord::TestCase
265284
def test_add_column_without_limit_coerced
266285
add_column :test_models, :description, :string, limit: nil
267286
TestModel.reset_column_information
268-
TestModel.columns_hash["description"].limit.must_equal 4000
287+
_(TestModel.columns_hash["description"].limit).must_equal 4000
269288
end
270289
end
271290
end
@@ -615,14 +634,14 @@ class PersistenceTest < ActiveRecord::TestCase
615634
coerce_tests! :test_update_all_doesnt_ignore_order
616635
def test_update_all_doesnt_ignore_order_coerced
617636
david, mary = authors(:david), authors(:mary)
618-
david.id.must_equal 1
619-
mary.id.must_equal 2
620-
david.name.wont_equal mary.name
637+
_(david.id).must_equal 1
638+
_(mary.id).must_equal 2
639+
_(david.name).wont_equal mary.name
621640
assert_sql(/UPDATE.*\(SELECT \[authors\].\[id\] FROM \[authors\].*ORDER BY \[authors\].\[id\]/i) do
622641
Author.where('[id] > 1').order(:id).update_all(name: 'Test')
623642
end
624-
david.reload.name.must_equal 'David'
625-
mary.reload.name.must_equal 'Test'
643+
_(david.reload.name).must_equal 'David'
644+
_(mary.reload.name).must_equal 'Test'
626645
end
627646

628647
# We can not UPDATE identity columns.

0 commit comments

Comments
 (0)