activerecord-sqlserver-adapter 7.2.9 → 8.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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +10 -6
  3. data/CHANGELOG.md +5 -67
  4. data/Dockerfile.ci +1 -1
  5. data/Gemfile +2 -0
  6. data/README.md +16 -17
  7. data/VERSION +1 -1
  8. data/activerecord-sqlserver-adapter.gemspec +2 -2
  9. data/docker-compose.ci.yml +0 -1
  10. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +1 -1
  11. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +50 -56
  12. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +129 -118
  13. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +1 -0
  14. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +3 -2
  15. data/lib/active_record/connection_adapters/sqlserver/utils.rb +0 -4
  16. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +17 -27
  17. data/test/cases/adapter_test_sqlserver.rb +36 -38
  18. data/test/cases/coerced_tests.rb +153 -60
  19. data/test/cases/helper_sqlserver.rb +0 -8
  20. data/test/cases/optimizer_hints_test_sqlserver.rb +1 -2
  21. data/test/cases/schema_test_sqlserver.rb +0 -6
  22. data/test/cases/showplan_test_sqlserver.rb +2 -2
  23. data/test/cases/specific_schema_test_sqlserver.rb +6 -6
  24. data/test/cases/view_test_sqlserver.rb +3 -9
  25. data/test/support/query_assertions.rb +0 -22
  26. metadata +11 -15
  27. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +0 -29
  28. data/test/cases/temp_test_sqlserver.rb +0 -9
  29. data/test/cases/temporary_table_test_sqlserver.rb +0 -19
  30. data/test/fixtures/sst_customers_view.yml +0 -6
@@ -248,7 +248,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
248
248
  def test_belongs_to_coerced
249
249
  client = Client.find(3)
250
250
  first_firm = companies(:first_firm)
251
- assert_queries_match(/FETCH NEXT @(\d) ROWS ONLY/) do
251
+ assert_queries_match(/FETCH NEXT @(\d) ROWS ONLY(.)*@\1 = 1/) do
252
252
  assert_equal first_firm, client.firm
253
253
  assert_equal first_firm.name, client.firm.name
254
254
  end
@@ -257,6 +257,21 @@ end
257
257
 
258
258
  module ActiveRecord
259
259
  class BindParameterTest < ActiveRecord::TestCase
260
+ # Same as original coerced test except log is found using `EXEC sp_executesql` wrapper.
261
+ coerce_tests! :test_binds_are_logged
262
+ def test_binds_are_logged_coerced
263
+ sub = Arel::Nodes::BindParam.new(1)
264
+ binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)]
265
+ sql = "select * from topics where id = #{sub.to_sql}"
266
+
267
+ @connection.exec_query(sql, "SQL", binds)
268
+
269
+ logged_sql = "EXEC sp_executesql N'#{sql}', N'#{sub.to_sql} int', #{sub.to_sql} = 1"
270
+ message = @subscriber.calls.find { |args| args[4][:sql] == logged_sql }
271
+
272
+ assert_equal binds, message[4][:binds]
273
+ end
274
+
260
275
  # SQL Server adapter does not use a statement cache as query plans are already reused using `EXEC sp_executesql`.
261
276
  coerce_tests! :test_statement_cache
262
277
  coerce_tests! :test_statement_cache_with_query_cache
@@ -264,6 +279,55 @@ module ActiveRecord
264
279
  coerce_tests! :test_statement_cache_with_find_by
265
280
  coerce_tests! :test_statement_cache_with_in_clause
266
281
  coerce_tests! :test_statement_cache_with_sql_string_literal
282
+
283
+ # Same as original coerced test except prepared statements include `EXEC sp_executesql` wrapper.
284
+ coerce_tests! :test_bind_params_to_sql_with_prepared_statements, :test_bind_params_to_sql_with_unprepared_statements
285
+ def test_bind_params_to_sql_with_prepared_statements_coerced
286
+ assert_bind_params_to_sql_coerced(prepared: true)
287
+ end
288
+
289
+ def test_bind_params_to_sql_with_unprepared_statements_coerced
290
+ @connection.unprepared_statement do
291
+ assert_bind_params_to_sql_coerced(prepared: false)
292
+ end
293
+ end
294
+
295
+ private
296
+
297
+ def assert_bind_params_to_sql_coerced(prepared:)
298
+ table = Author.quoted_table_name
299
+ pk = "#{table}.#{Author.quoted_primary_key}"
300
+
301
+ # prepared_statements: true
302
+ #
303
+ # EXEC sp_executesql N'SELECT [authors].* FROM [authors] WHERE [authors].[id] IN (@0, @1, @2) OR [authors].[id] IS NULL)', N'@0 bigint, @1 bigint, @2 bigint', @0 = 1, @1 = 2, @2 = 3
304
+ #
305
+ # prepared_statements: false
306
+ #
307
+ # SELECT [authors].* FROM [authors] WHERE ([authors].[id] IN (1, 2, 3) OR [authors].[id] IS NULL)
308
+ #
309
+ sql_unprepared = "SELECT #{table}.* FROM #{table} WHERE (#{pk} IN (#{bind_params(1..3)}) OR #{pk} IS NULL)"
310
+ sql_prepared = "EXEC sp_executesql N'SELECT #{table}.* FROM #{table} WHERE (#{pk} IN (#{bind_params(1..3)}) OR #{pk} IS NULL)', N'@0 bigint, @1 bigint, @2 bigint', @0 = 1, @1 = 2, @2 = 3"
311
+
312
+ authors = Author.where(id: [1, 2, 3, nil])
313
+ assert_equal sql_unprepared, @connection.to_sql(authors.arel)
314
+ assert_queries_match(prepared ? sql_prepared : sql_unprepared) { assert_equal 3, authors.length }
315
+
316
+ # prepared_statements: true
317
+ #
318
+ # EXEC sp_executesql N'SELECT [authors].* FROM [authors] WHERE [authors].[id] IN (@0, @1, @2)', N'@0 bigint, @1 bigint, @2 bigint', @0 = 1, @1 = 2, @2 = 3
319
+ #
320
+ # prepared_statements: false
321
+ #
322
+ # SELECT [authors].* FROM [authors] WHERE [authors].[id] IN (1, 2, 3)
323
+ #
324
+ sql_unprepared = "SELECT #{table}.* FROM #{table} WHERE #{pk} IN (#{bind_params(1..3)})"
325
+ sql_prepared = "EXEC sp_executesql N'SELECT #{table}.* FROM #{table} WHERE #{pk} IN (#{bind_params(1..3)})', N'@0 bigint, @1 bigint, @2 bigint', @0 = 1, @1 = 2, @2 = 3"
326
+
327
+ authors = Author.where(id: [1, 2, 3, 9223372036854775808])
328
+ assert_equal sql_unprepared, @connection.to_sql(authors.arel)
329
+ assert_queries_match(prepared ? sql_prepared : sql_unprepared) { assert_equal 3, authors.length }
330
+ end
267
331
  end
268
332
  end
269
333
 
@@ -312,18 +376,6 @@ module ActiveRecord
312
376
  end
313
377
 
314
378
  class CalculationsTest < ActiveRecord::TestCase
315
- # SELECT columns must be in the GROUP clause.
316
- coerce_tests! :test_should_count_with_group_by_qualified_name_on_loaded
317
- def test_should_count_with_group_by_qualified_name_on_loaded_coerced
318
- accounts = Account.group("accounts.id").select("accounts.id")
319
- expected = { 1 => 1, 2 => 1, 3 => 1, 4 => 1, 5 => 1, 6 => 1 }
320
- assert_not_predicate accounts, :loaded?
321
- assert_equal expected, accounts.count
322
- accounts.load
323
- assert_predicate accounts, :loaded?
324
- assert_equal expected, accounts.count(:id)
325
- end
326
-
327
379
  # Fix randomly failing test. The loading of the model's schema was affecting the test.
328
380
  coerce_tests! :test_offset_is_kept
329
381
  def test_offset_is_kept_coerced
@@ -444,7 +496,7 @@ class CalculationsTest < ActiveRecord::TestCase
444
496
  def test_limit_is_kept_coerced
445
497
  queries = capture_sql { Account.limit(1).count }
446
498
  assert_equal 1, queries.length
447
- assert_match(/ORDER BY \[accounts\]\.\[id\] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY/, queries.first)
499
+ assert_match(/ORDER BY \[accounts\]\.\[id\] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY.*@0 = 1/, queries.first)
448
500
  end
449
501
 
450
502
  # Match SQL Server limit implementation
@@ -452,7 +504,7 @@ class CalculationsTest < ActiveRecord::TestCase
452
504
  def test_limit_with_offset_is_kept_coerced
453
505
  queries = capture_sql { Account.limit(1).offset(1).count }
454
506
  assert_equal 1, queries.length
455
- assert_match(/ORDER BY \[accounts\]\.\[id\] ASC OFFSET @0 ROWS FETCH NEXT @1 ROWS ONLY/, queries.first)
507
+ assert_match(/ORDER BY \[accounts\]\.\[id\] ASC OFFSET @0 ROWS FETCH NEXT @1 ROWS ONLY.*@0 = 1, @1 = 1/, queries.first)
456
508
  end
457
509
 
458
510
  # SQL Server needs an alias for the calculated column
@@ -468,6 +520,9 @@ class CalculationsTest < ActiveRecord::TestCase
468
520
 
469
521
  # SELECT columns must be in the GROUP clause. Since since `ids` only selects the primary key you cannot perform this query in SQL Server.
470
522
  coerce_tests! :test_ids_with_includes_and_non_primary_key_order
523
+
524
+ # To limit the results in SQL Server we use `FETCH NEXT @0 ROWS ONLY` instead of `LIMIT @0`. To use `FETCH NEXT` an order must be provided.
525
+ coerce_tests! :test_no_order_by_when_counting_all
471
526
  end
472
527
 
473
528
  module ActiveRecord
@@ -916,9 +971,9 @@ class FinderTest < ActiveRecord::TestCase
916
971
  # Assert SQL Server limit implementation
917
972
  coerce_tests! :test_take_and_first_and_last_with_integer_should_use_sql_limit
918
973
  def test_take_and_first_and_last_with_integer_should_use_sql_limit_coerced
919
- assert_queries_and_values_match(/OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY/, [3]) { Topic.take(3).entries }
920
- assert_queries_and_values_match(/OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY/, [2]) { Topic.first(2).entries }
921
- assert_queries_and_values_match(/OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY/, [5]) { Topic.last(5).entries }
974
+ assert_queries_match(/OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY.* @0 = 3/) { Topic.take(3).entries }
975
+ assert_queries_match(/OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY.* @0 = 2/) { Topic.first(2).entries }
976
+ assert_queries_match(/OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY.* @0 = 5/) { Topic.last(5).entries }
922
977
  end
923
978
 
924
979
  # This fails only when run in the full test suite task. Just taking it out of the mix.
@@ -949,7 +1004,7 @@ class FinderTest < ActiveRecord::TestCase
949
1004
  # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
950
1005
  coerce_tests! :test_include_on_unloaded_relation_with_match
951
1006
  def test_include_on_unloaded_relation_with_match_coerced
952
- assert_queries_match(/1 AS one.*FETCH NEXT @2 ROWS ONLY/) do
1007
+ assert_queries_match(/1 AS one.*FETCH NEXT @2 ROWS ONLY.*@2 = 1/) do
953
1008
  assert_equal true, Customer.where(name: "David").include?(customers(:david))
954
1009
  end
955
1010
  end
@@ -957,7 +1012,7 @@ class FinderTest < ActiveRecord::TestCase
957
1012
  # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
958
1013
  coerce_tests! :test_include_on_unloaded_relation_without_match
959
1014
  def test_include_on_unloaded_relation_without_match_coerced
960
- assert_queries_match(/1 AS one.*FETCH NEXT @2 ROWS ONLY/) do
1015
+ assert_queries_match(/1 AS one.*FETCH NEXT @2 ROWS ONLY.*@2 = 1/) do
961
1016
  assert_equal false, Customer.where(name: "David").include?(customers(:mary))
962
1017
  end
963
1018
  end
@@ -965,7 +1020,7 @@ class FinderTest < ActiveRecord::TestCase
965
1020
  # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
966
1021
  coerce_tests! :test_member_on_unloaded_relation_with_match
967
1022
  def test_member_on_unloaded_relation_with_match_coerced
968
- assert_queries_match(/1 AS one.*FETCH NEXT @2 ROWS ONLY/) do
1023
+ assert_queries_match(/1 AS one.*FETCH NEXT @2 ROWS ONLY.*@2 = 1/) do
969
1024
  assert_equal true, Customer.where(name: "David").member?(customers(:david))
970
1025
  end
971
1026
  end
@@ -973,7 +1028,7 @@ class FinderTest < ActiveRecord::TestCase
973
1028
  # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
974
1029
  coerce_tests! :test_member_on_unloaded_relation_without_match
975
1030
  def test_member_on_unloaded_relation_without_match_coerced
976
- assert_queries_match(/1 AS one.*FETCH NEXT @2 ROWS ONLY/) do
1031
+ assert_queries_match(/1 AS one.*FETCH NEXT @2 ROWS ONLY.*@2 = 1/) do
977
1032
  assert_equal false, Customer.where(name: "David").member?(customers(:mary))
978
1033
  end
979
1034
  end
@@ -988,7 +1043,7 @@ class FinderTest < ActiveRecord::TestCase
988
1043
  assert_equal topics(:third), Topic.last
989
1044
 
990
1045
  c = Topic.lease_connection
991
- assert_queries_match(/ORDER BY #{Regexp.escape(c.quote_table_name("topics.title"))} DESC, #{Regexp.escape(c.quote_table_name("topics.id"))} DESC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY/i) {
1046
+ assert_queries_match(/ORDER BY #{Regexp.escape(c.quote_table_name("topics.title"))} DESC, #{Regexp.escape(c.quote_table_name("topics.id"))} DESC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY.*@0 = 1/i) {
992
1047
  Topic.last
993
1048
  }
994
1049
  ensure
@@ -1002,7 +1057,7 @@ class FinderTest < ActiveRecord::TestCase
1002
1057
  Topic.implicit_order_column = "id"
1003
1058
 
1004
1059
  c = Topic.lease_connection
1005
- assert_queries_match(/ORDER BY #{Regexp.escape(c.quote_table_name("topics.id"))} DESC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY/i) {
1060
+ assert_queries_match(/ORDER BY #{Regexp.escape(c.quote_table_name("topics.id"))} DESC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY.*@0 = 1/i) {
1006
1061
  Topic.last
1007
1062
  }
1008
1063
  ensure
@@ -1017,7 +1072,7 @@ class FinderTest < ActiveRecord::TestCase
1017
1072
 
1018
1073
  c = NonPrimaryKey.lease_connection
1019
1074
 
1020
- assert_queries_match(/ORDER BY #{Regexp.escape(c.quote_table_name("non_primary_keys.created_at"))} DESC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY/i) {
1075
+ assert_queries_match(/ORDER BY #{Regexp.escape(c.quote_table_name("non_primary_keys.created_at"))} DESC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY.*@0 = 1/i) {
1021
1076
  NonPrimaryKey.last
1022
1077
  }
1023
1078
  ensure
@@ -1027,7 +1082,7 @@ class FinderTest < ActiveRecord::TestCase
1027
1082
  # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
1028
1083
  coerce_tests! :test_member_on_unloaded_relation_with_composite_primary_key
1029
1084
  def test_member_on_unloaded_relation_with_composite_primary_key_coerced
1030
- assert_queries_match(/1 AS one.* FETCH NEXT @(\d) ROWS ONLY/) do
1085
+ assert_queries_match(/1 AS one.* FETCH NEXT @(\d) ROWS ONLY.*@\1 = 1/) do
1031
1086
  book = cpk_books(:cpk_great_author_first_book)
1032
1087
  assert Cpk::Book.where(title: "The first book").member?(book)
1033
1088
  end
@@ -1042,7 +1097,7 @@ class FinderTest < ActiveRecord::TestCase
1042
1097
  quoted_color = Regexp.escape(c.quote_table_name("clothing_items.color"))
1043
1098
  quoted_descrption = Regexp.escape(c.quote_table_name("clothing_items.description"))
1044
1099
 
1045
- assert_queries_match(/ORDER BY #{quoted_descrption} ASC, #{quoted_type} ASC, #{quoted_color} ASC OFFSET 0 ROWS FETCH NEXT @(\d) ROWS ONLY/i) do
1100
+ assert_queries_match(/ORDER BY #{quoted_descrption} ASC, #{quoted_type} ASC, #{quoted_color} ASC OFFSET 0 ROWS FETCH NEXT @(\d) ROWS ONLY.*@\1 = 1/i) do
1046
1101
  assert_kind_of ClothingItem, ClothingItem.first
1047
1102
  end
1048
1103
  ensure
@@ -1056,7 +1111,7 @@ class FinderTest < ActiveRecord::TestCase
1056
1111
  quoted_type = Regexp.escape(c.quote_table_name("clothing_items.clothing_type"))
1057
1112
  quoted_color = Regexp.escape(c.quote_table_name("clothing_items.color"))
1058
1113
 
1059
- assert_queries_match(/ORDER BY #{quoted_type} DESC, #{quoted_color} DESC OFFSET 0 ROWS FETCH NEXT @(\d) ROWS ONLY/i) do
1114
+ assert_queries_match(/ORDER BY #{quoted_type} DESC, #{quoted_color} DESC OFFSET 0 ROWS FETCH NEXT @(\d) ROWS ONLY.*@\1 = 1/i) do
1060
1115
  assert_kind_of ClothingItem, ClothingItem.last
1061
1116
  end
1062
1117
  end
@@ -1068,7 +1123,7 @@ class FinderTest < ActiveRecord::TestCase
1068
1123
  quoted_type = Regexp.escape(c.quote_table_name("clothing_items.clothing_type"))
1069
1124
  quoted_color = Regexp.escape(c.quote_table_name("clothing_items.color"))
1070
1125
 
1071
- assert_queries_match(/ORDER BY #{quoted_type} ASC, #{quoted_color} ASC OFFSET 0 ROWS FETCH NEXT @(\d) ROWS ONLY/i) do
1126
+ assert_queries_match(/ORDER BY #{quoted_type} ASC, #{quoted_color} ASC OFFSET 0 ROWS FETCH NEXT @(\d) ROWS ONLY.*@\1 = 1/i) do
1072
1127
  assert_kind_of ClothingItem, ClothingItem.first
1073
1128
  end
1074
1129
  end
@@ -1081,7 +1136,7 @@ class FinderTest < ActiveRecord::TestCase
1081
1136
  quoted_type = Regexp.escape(c.quote_table_name("clothing_items.clothing_type"))
1082
1137
  quoted_color = Regexp.escape(c.quote_table_name("clothing_items.color"))
1083
1138
 
1084
- assert_queries_match(/ORDER BY #{quoted_color} ASC, #{quoted_type} ASC OFFSET 0 ROWS FETCH NEXT @(\d) ROWS ONLY/i) do
1139
+ assert_queries_match(/ORDER BY #{quoted_color} ASC, #{quoted_type} ASC OFFSET 0 ROWS FETCH NEXT @(\d) ROWS ONLY.*@\1 = 1/i) do
1085
1140
  assert_kind_of ClothingItem, ClothingItem.first
1086
1141
  end
1087
1142
  ensure
@@ -1091,7 +1146,7 @@ class FinderTest < ActiveRecord::TestCase
1091
1146
  # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
1092
1147
  coerce_tests! :test_include_on_unloaded_relation_with_composite_primary_key
1093
1148
  def test_include_on_unloaded_relation_with_composite_primary_key_coerced
1094
- assert_queries_match(/1 AS one.*OFFSET 0 ROWS FETCH NEXT @(\d) ROWS ONLY/) do
1149
+ assert_queries_match(/1 AS one.*OFFSET 0 ROWS FETCH NEXT @(\d) ROWS ONLY.*@\1 = 1/) do
1095
1150
  book = cpk_books(:cpk_great_author_first_book)
1096
1151
  assert Cpk::Book.where(title: "The first book").include?(book)
1097
1152
  end
@@ -1101,11 +1156,11 @@ class FinderTest < ActiveRecord::TestCase
1101
1156
  coerce_tests! :test_nth_to_last_with_order_uses_limit
1102
1157
  def test_nth_to_last_with_order_uses_limit_coerced
1103
1158
  c = Topic.lease_connection
1104
- assert_queries_match(/ORDER BY #{Regexp.escape(c.quote_table_name("topics.id"))} DESC OFFSET @(\d) ROWS FETCH NEXT @(\d) ROWS ONLY/i) do
1159
+ assert_queries_match(/ORDER BY #{Regexp.escape(c.quote_table_name("topics.id"))} DESC OFFSET @(\d) ROWS FETCH NEXT @(\d) ROWS ONLY.*@\1 = 1.*@\2 = 1/i) do
1105
1160
  Topic.second_to_last
1106
1161
  end
1107
1162
 
1108
- assert_queries_match(/ORDER BY #{Regexp.escape(c.quote_table_name("topics.updated_at"))} DESC OFFSET @(\d) ROWS FETCH NEXT @(\d) ROWS ONLY/i) do
1163
+ assert_queries_match(/ORDER BY #{Regexp.escape(c.quote_table_name("topics.updated_at"))} DESC OFFSET @(\d) ROWS FETCH NEXT @(\d) ROWS ONLY.*@\1 = 1.*@\2 = 1/i) do
1109
1164
  Topic.order(:updated_at).second_to_last
1110
1165
  end
1111
1166
  end
@@ -1153,7 +1208,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
1153
1208
  def test_has_one_coerced
1154
1209
  firm = companies(:first_firm)
1155
1210
  first_account = Account.find(1)
1156
- assert_queries_match(/FETCH NEXT @(\d) ROWS ONLY/) do
1211
+ assert_queries_match(/FETCH NEXT @(\d) ROWS ONLY(.)*@\1 = 1/) do
1157
1212
  assert_equal first_account, firm.account
1158
1213
  assert_equal first_account.credit_limit, firm.account.credit_limit
1159
1214
  end
@@ -1165,7 +1220,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
1165
1220
  coerce_tests! :test_has_one_through_executes_limited_query
1166
1221
  def test_has_one_through_executes_limited_query_coerced
1167
1222
  boring_club = clubs(:boring_club)
1168
- assert_queries_match(/FETCH NEXT @(\d) ROWS ONLY/) do
1223
+ assert_queries_match(/FETCH NEXT @(\d) ROWS ONLY(.)*@\1 = 1/) do
1169
1224
  assert_equal boring_club, @member.general_club
1170
1225
  end
1171
1226
  end
@@ -1285,6 +1340,20 @@ module ActiveRecord
1285
1340
  def test_registering_new_handlers_for_association_coerced
1286
1341
  assert_match %r{#{Regexp.escape(topic_title)} ~ N'rails'}i, Reply.joins(:topic).where(topics: { title: /rails/ }).to_sql
1287
1342
  end
1343
+
1344
+ # Same as original test except string has `N` prefix to indicate unicode string.
1345
+ coerce_tests! :test_registering_new_handlers_for_joins
1346
+ def test_registering_new_handlers_for_joins_coerced
1347
+ Reply.belongs_to :regexp_topic, -> { where(title: /rails/) }, class_name: "Topic", foreign_key: "parent_id"
1348
+
1349
+ assert_match %r{#{Regexp.escape(quote_table_name("regexp_topic.title"))} ~ N'rails'}i, Reply.joins(:regexp_topic).references(Arel.sql("regexp_topic")).to_sql
1350
+ end
1351
+
1352
+ private
1353
+
1354
+ def topic_title
1355
+ Topic.lease_connection.quote_table_name("topics.title")
1356
+ end
1288
1357
  end
1289
1358
  end
1290
1359
 
@@ -1372,7 +1441,7 @@ class RelationTest < ActiveRecord::TestCase
1372
1441
  # Find any limit via our expression.
1373
1442
  coerce_tests! %r{relations don't load all records in #inspect}
1374
1443
  def test_relations_dont_load_all_records_in_inspect_coerced
1375
- assert_queries_match(/NEXT @0 ROWS/) do
1444
+ assert_queries_match(/NEXT @0 ROWS.*@0 = \d+/) do
1376
1445
  Post.all.inspect
1377
1446
  end
1378
1447
  end
@@ -2060,7 +2129,7 @@ class RelationMergingTest < ActiveRecord::TestCase
2060
2129
  non_mary_and_bob = Author.where.not(id: [mary, bob])
2061
2130
 
2062
2131
  author_id = Author.lease_connection.quote_table_name("authors.id")
2063
- assert_queries_match(/WHERE #{Regexp.escape(author_id)} NOT IN \((@\d), \g<1>\)/) do
2132
+ assert_queries_match(/WHERE #{Regexp.escape(author_id)} NOT IN \((@\d), \g<1>\)'/) do
2064
2133
  assert_equal [david], non_mary_and_bob.merge(non_mary_and_bob)
2065
2134
  end
2066
2135
 
@@ -2116,17 +2185,6 @@ class EnumTest < ActiveRecord::TestCase
2116
2185
  Book.lease_connection.add_index(:books, [:author_id, :name], unique: true)
2117
2186
  end
2118
2187
 
2119
- # Need to remove index as SQL Server considers NULLs on a unique-index to be equal unlike PostgreSQL/MySQL/SQLite.
2120
- coerce_tests! %r{declare multiple enums at a time}
2121
- test "declare multiple enums at a time coerced" do
2122
- Book.lease_connection.remove_index(:books, column: [:author_id, :name])
2123
-
2124
- send(:'original_declare multiple enums at a time')
2125
- ensure
2126
- Book.where(author_id: nil, name: nil).delete_all
2127
- Book.lease_connection.add_index(:books, [:author_id, :name], unique: true)
2128
- end
2129
-
2130
2188
  # Need to remove index as SQL Server considers NULLs on a unique-index to be equal unlike PostgreSQL/MySQL/SQLite.
2131
2189
  coerce_tests! %r{serializable\? with large number label}
2132
2190
  test "serializable? with large number label coerced" do
@@ -2265,7 +2323,7 @@ class PreloaderTest < ActiveRecord::TestCase
2265
2323
 
2266
2324
  c = Cpk::OrderAgreement.lease_connection
2267
2325
  order_id_column = Regexp.escape(c.quote_table_name("cpk_order_agreements.order_id"))
2268
- order_id_constraint = /#{order_id_column} = @0$/
2326
+ order_id_constraint = /#{order_id_column} = @0.*@0 = \d+$/
2269
2327
  expectation = /SELECT.*WHERE.* #{order_id_constraint}/
2270
2328
 
2271
2329
  assert_match(expectation, preload_sql)
@@ -2289,7 +2347,7 @@ class PreloaderTest < ActiveRecord::TestCase
2289
2347
 
2290
2348
  c = Cpk::Order.lease_connection
2291
2349
  order_id = Regexp.escape(c.quote_table_name("cpk_orders.id"))
2292
- order_constraint = /#{order_id} = @0$/
2350
+ order_constraint = /#{order_id} = @0.*@0 = \d+$/
2293
2351
  expectation = /SELECT.*WHERE.* #{order_constraint}/
2294
2352
 
2295
2353
  assert_match(expectation, preload_sql)
@@ -2359,8 +2417,10 @@ class QueryLogsTest < ActiveRecord::TestCase
2359
2417
  # SQL requires double single-quotes.
2360
2418
  coerce_tests! :test_sql_commenter_format
2361
2419
  def test_sql_commenter_format_coerced
2362
- ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
2363
- assert_queries_match(%r{/\*application='active_record'\*/}) do
2420
+ ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
2421
+ ActiveRecord::QueryLogs.tags = [:application]
2422
+
2423
+ assert_queries_match(%r{/\*application=''active_record''\*/}) do
2364
2424
  Dashboard.first
2365
2425
  end
2366
2426
  end
@@ -2368,14 +2428,14 @@ class QueryLogsTest < ActiveRecord::TestCase
2368
2428
  # SQL requires double single-quotes.
2369
2429
  coerce_tests! :test_sqlcommenter_format_value
2370
2430
  def test_sqlcommenter_format_value_coerced
2371
- ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
2431
+ ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
2372
2432
 
2373
2433
  ActiveRecord::QueryLogs.tags = [
2374
2434
  :application,
2375
2435
  { tracestate: "congo=t61rcWkgMzE,rojo=00f067aa0ba902b7", custom_proc: -> { "Joe's Shack" } },
2376
2436
  ]
2377
2437
 
2378
- assert_queries_match(%r{custom_proc='Joe%27s%20Shack',tracestate='congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7'\*/}) do
2438
+ assert_queries_match(%r{custom_proc=''Joe%27s%20Shack'',tracestate=''congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7''\*/}) do
2379
2439
  Dashboard.first
2380
2440
  end
2381
2441
  end
@@ -2383,14 +2443,14 @@ class QueryLogsTest < ActiveRecord::TestCase
2383
2443
  # SQL requires double single-quotes.
2384
2444
  coerce_tests! :test_sqlcommenter_format_value_string_coercible
2385
2445
  def test_sqlcommenter_format_value_string_coercible_coerced
2386
- ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
2446
+ ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
2387
2447
 
2388
2448
  ActiveRecord::QueryLogs.tags = [
2389
2449
  :application,
2390
2450
  { custom_proc: -> { 1234 } },
2391
2451
  ]
2392
2452
 
2393
- assert_queries_match(%r{custom_proc='1234'\*/}) do
2453
+ assert_queries_match(%r{custom_proc=''1234''\*/}) do
2394
2454
  Dashboard.first
2395
2455
  end
2396
2456
  end
@@ -2398,7 +2458,7 @@ class QueryLogsTest < ActiveRecord::TestCase
2398
2458
  # SQL requires double single-quotes.
2399
2459
  coerce_tests! :test_sqlcommenter_format_allows_string_keys
2400
2460
  def test_sqlcommenter_format_allows_string_keys_coerced
2401
- ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
2461
+ ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
2402
2462
 
2403
2463
  ActiveRecord::QueryLogs.tags = [
2404
2464
  :application,
@@ -2409,7 +2469,7 @@ class QueryLogsTest < ActiveRecord::TestCase
2409
2469
  },
2410
2470
  ]
2411
2471
 
2412
- assert_queries_match(%r{custom_proc='Joe%27s%20Shack',string='value',tracestate='congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7'\*/}) do
2472
+ assert_queries_match(%r{custom_proc=''Joe%27s%20Shack'',string=''value'',tracestate=''congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7''\*/}) do
2413
2473
  Dashboard.first
2414
2474
  end
2415
2475
  end
@@ -2433,6 +2493,17 @@ class InsertAllTest < ActiveRecord::TestCase
2433
2493
  result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: Arel.sql("UPPER(INSERTED.name) as name")
2434
2494
  assert_equal %w[ REWORK ], result.pluck("name")
2435
2495
  end
2496
+
2497
+ # Need to remove index as SQL Server considers NULLs on a unique-index to be equal unlike PostgreSQL/MySQL/SQLite.
2498
+ coerce_tests! :test_insert_with_type_casting_and_serialize_is_consistent
2499
+ def test_insert_with_type_casting_and_serialize_is_consistent_coerced
2500
+ connection.remove_index(:books, column: [:author_id, :name])
2501
+
2502
+ original_test_insert_with_type_casting_and_serialize_is_consistent
2503
+ ensure
2504
+ Book.where(author_id: nil, name: '["Array"]').delete_all
2505
+ Book.lease_connection.add_index(:books, [:author_id, :name], unique: true)
2506
+ end
2436
2507
  end
2437
2508
 
2438
2509
  module ActiveRecord
@@ -2595,6 +2666,28 @@ module ActiveRecord
2595
2666
  end
2596
2667
  end
2597
2668
 
2669
+ module ActiveRecord
2670
+ module ConnectionAdapters
2671
+ class RegistrationIsolatedTest < ActiveRecord::TestCase
2672
+ # SQL Server was not included in the list of available adapters in the error message.
2673
+ coerce_tests! %r{resolve raises if the adapter is using the pre 7.2 adapter registration API}
2674
+ def resolve_raises_if_the_adapter_is_using_the_pre_7_2_adapter_registration_API
2675
+ exception = assert_raises(ActiveRecord::AdapterNotFound) do
2676
+ ActiveRecord::ConnectionAdapters.resolve("fake_legacy")
2677
+ end
2678
+
2679
+ assert_equal(
2680
+ "Database configuration specifies nonexistent 'ridiculous' adapter. Available adapters are: abstract, fake, mysql2, postgresql, sqlite3, sqlserver, trilogy. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile if it's not in the list of available adapters.",
2681
+ exception.message
2682
+ )
2683
+ ensure
2684
+ ActiveRecord::ConnectionAdapters.instance_variable_get(:@adapters).delete("fake_legacy")
2685
+ end
2686
+ end
2687
+ end
2688
+ end
2689
+
2690
+
2598
2691
  module ActiveRecord
2599
2692
  class TableMetadataTest < ActiveSupport::TestCase
2600
2693
  # Adapter returns an object that is subclass of what is expected in the original test.
@@ -2633,7 +2726,7 @@ class ExplainTest < ActiveRecord::TestCase
2633
2726
  def test_relation_explain_with_first_coerced
2634
2727
  expected_query = capture_sql {
2635
2728
  Car.all.first
2636
- }.first[/(.*?) NEXT/, 1]
2729
+ }.first[/EXEC sp_executesql N'(.*?) NEXT/, 1]
2637
2730
  message = Car.all.explain.first
2638
2731
  assert_match(/^EXPLAIN/, message)
2639
2732
  assert_match(expected_query, message)
@@ -2644,7 +2737,7 @@ class ExplainTest < ActiveRecord::TestCase
2644
2737
  def test_relation_explain_with_last_coerced
2645
2738
  expected_query = capture_sql {
2646
2739
  Car.all.last
2647
- }.first[/(.*?) NEXT/, 1]
2740
+ }.first[/EXEC sp_executesql N'(.*?) NEXT/, 1]
2648
2741
  expected_query = expected_query
2649
2742
  message = Car.all.explain.last
2650
2743
 
@@ -15,14 +15,6 @@ require "support/connection_reflection"
15
15
  require "support/query_assertions"
16
16
  require "mocha/minitest"
17
17
 
18
- Minitest.after_run do
19
- puts "\n\n"
20
- puts "=" * 80
21
- puts ActiveRecord::Base.lease_connection.send(:sqlserver_version)
22
- puts "\nSQL Server Version Year: #{ActiveRecord::Base.lease_connection.get_database_version}"
23
- puts "=" * 80
24
- end
25
-
26
18
  module ActiveSupport
27
19
  class TestCase < ::Minitest::Test
28
20
  include ARTest::SQLServer::CoerceableTest
@@ -29,14 +29,13 @@ class OptimizerHitsTestSQLServer < ActiveRecord::TestCase
29
29
  end
30
30
 
31
31
  it "support subqueries" do
32
- assert_queries_match(%r{SELECT COUNT\(count_column\) FROM \(SELECT .*\) subquery_for_count OPTION \(MAXDOP 2\)}) do
32
+ assert_queries_match(%r{.*'SELECT COUNT\(count_column\) FROM \(SELECT .*\) subquery_for_count OPTION \(MAXDOP 2\)'.*}) do
33
33
  companies = Company.optimizer_hints("MAXDOP 2")
34
34
  companies = companies.select(:id).where(firm_id: [0, 1]).limit(3)
35
35
  assert_equal 3, companies.count
36
36
  end
37
37
  end
38
38
 
39
-
40
39
  it "support order" do
41
40
  assert_queries_match(%r{\ASELECT .+ FROM .+ ORDER .+ OPTION .+\z}) do
42
41
  companies = Company.optimizer_hints("LABEL='FindCompanies'")
@@ -101,11 +101,5 @@ class SchemaTestSQLServer < ActiveRecord::TestCase
101
101
  assert_equal "[with].[select notation]", connection.send(:get_raw_table_name, "INSERT INTO [with].[select notation] SELECT * FROM [table_name]")
102
102
  end
103
103
  end
104
-
105
- describe 'CREATE VIEW statements' do
106
- it do
107
- assert_equal "test_table_as", connection.send(:get_raw_table_name, "CREATE VIEW test_views ( test_table_a_id, test_table_b_id ) AS SELECT test_table_as.id as test_table_a_id, test_table_bs.id as test_table_b_id FROM (test_table_as with(nolock) LEFT JOIN test_table_bs with(nolock) ON (test_table_as.id = test_table_bs.test_table_a_id))")
108
- end
109
- end
110
104
  end
111
105
  end
@@ -28,13 +28,13 @@ class ShowplanTestSQLServer < ActiveRecord::TestCase
28
28
 
29
29
  it "from array condition using index" do
30
30
  plan = Car.where(id: [1, 2]).explain.inspect
31
- _(plan).must_include "SELECT [cars].* FROM [cars] WHERE [cars].[id]"
31
+ _(plan).must_include "SELECT [cars].* FROM [cars] WHERE [cars].[id] IN (1, 2)"
32
32
  _(plan).must_include "Clustered Index Seek", "make sure we do not showplan the sp_executesql"
33
33
  end
34
34
 
35
35
  it "from array condition" do
36
36
  plan = Car.where(name: ["honda", "zyke"]).explain.inspect
37
- _(plan).must_include " SELECT [cars].* FROM [cars] WHERE [cars].[name]"
37
+ _(plan).must_include " SELECT [cars].* FROM [cars] WHERE [cars].[name] IN (N'honda', N'zyke')"
38
38
  _(plan).must_include "Clustered Index Scan", "make sure we do not showplan the sp_executesql"
39
39
  end
40
40
  end
@@ -116,16 +116,16 @@ class SpecificSchemaTestSQLServer < ActiveRecord::TestCase
116
116
  end
117
117
  end
118
118
  # Using ActiveRecord's quoted_id feature for objects.
119
- assert_queries_and_values_match(/.*/, ["'T'", 1]) { SSTestDatatypeMigration.where(char_col: value.new).first }
120
- assert_queries_and_values_match(/.*/, ["'T'", 1]) { SSTestDatatypeMigration.where(varchar_col: value.new).first }
119
+ assert_queries_match(/@0 = 'T'/) { SSTestDatatypeMigration.where(char_col: value.new).first }
120
+ assert_queries_match(/@0 = 'T'/) { SSTestDatatypeMigration.where(varchar_col: value.new).first }
121
121
  # Using our custom char type data.
122
122
  type = ActiveRecord::Type::SQLServer::Char
123
123
  data = ActiveRecord::Type::SQLServer::Data
124
- assert_queries_and_values_match(/.*/, ["'T'", 1]) { SSTestDatatypeMigration.where(char_col: data.new("T", type.new)).first }
125
- assert_queries_and_values_match(/.*/, ["'T'", 1]) { SSTestDatatypeMigration.where(varchar_col: data.new("T", type.new)).first }
124
+ assert_queries_match(/@0 = 'T'/) { SSTestDatatypeMigration.where(char_col: data.new("T", type.new)).first }
125
+ assert_queries_match(/@0 = 'T'/) { SSTestDatatypeMigration.where(varchar_col: data.new("T", type.new)).first }
126
126
  # Taking care of everything.
127
- assert_queries_and_values_match(/.*/, ["'T'", 1]) { SSTestDatatypeMigration.where(char_col: "T").first }
128
- assert_queries_and_values_match(/.*/, ["'T'", 1]) { SSTestDatatypeMigration.where(varchar_col: "T").first }
127
+ assert_queries_match(/@0 = 'T'/) { SSTestDatatypeMigration.where(char_col: "T").first }
128
+ assert_queries_match(/@0 = 'T'/) { SSTestDatatypeMigration.where(varchar_col: "T").first }
129
129
  end
130
130
 
131
131
  it "can update and hence properly quoted non-national char/varchar columns" do
@@ -48,17 +48,11 @@ class ViewTestSQLServer < ActiveRecord::TestCase
48
48
  end
49
49
  end
50
50
 
51
- describe "identity insert" do
52
- it "creates table record through a view" do
53
- assert_difference("SSTestCustomersView.count", 2) do
51
+ describe 'identity insert' do
52
+ it "identity insert works with views" do
53
+ assert_difference("SSTestCustomersView.count", 1) do
54
54
  SSTestCustomersView.create!(id: 5, name: "Bob")
55
- SSTestCustomersView.create!(id: 6, name: "Tim")
56
55
  end
57
56
  end
58
-
59
- it "creates table records through a view using fixtures" do
60
- ActiveRecord::FixtureSet.create_fixtures(File.join(ARTest::SQLServer.test_root_sqlserver, "fixtures"), ["sst_customers_view"])
61
- assert_equal SSTestCustomersView.all.count, 2
62
- end
63
57
  end
64
58
  end
@@ -22,28 +22,6 @@ module ARTest
22
22
  end
23
23
  end
24
24
 
25
- def assert_queries_and_values_match(match, bound_values=[], count: nil, &block)
26
- ActiveRecord::Base.lease_connection.materialize_transactions
27
-
28
- counter = ActiveRecord::Assertions::QueryAssertions::SQLCounter.new
29
- ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
30
- result = _assert_nothing_raised_or_warn("assert_queries_match", &block)
31
- queries = counter.log_full
32
- matched_queries = queries.select do |query, values|
33
- values = values.map { |v| v.respond_to?(:quoted) ? v.quoted : v }
34
- match === query && bound_values === values
35
- end
36
-
37
- if count
38
- assert_equal count, matched_queries.size, "#{matched_queries.size} instead of #{count} queries were executed.#{count.log.empty? ? '' : "\nQueries:\n#{counter.log.join("\n")}"}"
39
- else
40
- assert_operator matched_queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{counter.log.empty? ? '' : "\nQueries:\n#{counter.log.join("\n")}"}"
41
- end
42
-
43
- result
44
- end
45
- end
46
-
47
25
  private
48
26
 
49
27
  # Rails tests expect a save-point to be created and released. SQL Server does not release