activerecord-sqlserver-adapter 6.0.1 → 6.1.1.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +26 -0
  3. data/CHANGELOG.md +29 -46
  4. data/README.md +32 -3
  5. data/RUNNING_UNIT_TESTS.md +1 -1
  6. data/VERSION +1 -1
  7. data/activerecord-sqlserver-adapter.gemspec +1 -1
  8. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +2 -0
  9. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +5 -10
  10. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +9 -2
  11. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +2 -0
  12. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +2 -0
  13. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -4
  14. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +28 -16
  15. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +8 -7
  16. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +22 -1
  17. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +9 -3
  18. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +31 -9
  19. data/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +36 -7
  20. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +0 -1
  21. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +2 -2
  22. data/lib/active_record/connection_adapters/sqlserver/type.rb +1 -0
  23. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +2 -1
  24. data/lib/active_record/connection_adapters/sqlserver/type/decimal_without_scale.rb +22 -0
  25. data/lib/active_record/connection_adapters/sqlserver/utils.rb +1 -1
  26. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +100 -69
  27. data/lib/active_record/connection_adapters/sqlserver_column.rb +75 -19
  28. data/lib/active_record/sqlserver_base.rb +9 -15
  29. data/lib/active_record/tasks/sqlserver_database_tasks.rb +17 -14
  30. data/lib/arel/visitors/sqlserver.rb +125 -40
  31. data/test/cases/adapter_test_sqlserver.rb +50 -16
  32. data/test/cases/change_column_collation_test_sqlserver.rb +33 -0
  33. data/test/cases/coerced_tests.rb +611 -78
  34. data/test/cases/column_test_sqlserver.rb +9 -2
  35. data/test/cases/disconnected_test_sqlserver.rb +39 -0
  36. data/test/cases/execute_procedure_test_sqlserver.rb +9 -0
  37. data/test/cases/fetch_test_sqlserver.rb +18 -0
  38. data/test/cases/in_clause_test_sqlserver.rb +27 -0
  39. data/test/cases/lateral_test_sqlserver.rb +35 -0
  40. data/test/cases/migration_test_sqlserver.rb +51 -0
  41. data/test/cases/optimizer_hints_test_sqlserver.rb +72 -0
  42. data/test/cases/order_test_sqlserver.rb +7 -0
  43. data/test/cases/primary_keys_test_sqlserver.rb +103 -0
  44. data/test/cases/rake_test_sqlserver.rb +38 -2
  45. data/test/cases/schema_dumper_test_sqlserver.rb +14 -3
  46. data/test/migrations/create_clients_and_change_column_collation.rb +19 -0
  47. data/test/models/sqlserver/composite_pk.rb +9 -0
  48. data/test/models/sqlserver/sst_string_collation.rb +3 -0
  49. data/test/schema/sqlserver_specific_schema.rb +25 -0
  50. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic.dump +0 -0
  51. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic_associations.dump +0 -0
  52. data/test/support/sql_counter_sqlserver.rb +14 -12
  53. metadata +29 -9
  54. data/.travis.yml +0 -23
  55. data/lib/active_record/connection_adapters/sqlserver/core_ext/query_methods.rb +0 -28
@@ -6,6 +6,7 @@ require "models/task"
6
6
  require "models/post"
7
7
  require "models/subscriber"
8
8
  require "models/minimalistic"
9
+ require "models/college"
9
10
 
10
11
  class AdapterTestSQLServer < ActiveRecord::TestCase
11
12
  fixtures :tasks
@@ -42,17 +43,49 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
42
43
  assert connection.supports_ddl_transactions?
43
44
  end
44
45
 
45
- it "allow owner table name prefixs like dbo to still allow table exists to return true" do
46
+ it "table exists works if table name prefixed by schema and owner" do
46
47
  begin
47
48
  assert_equal "topics", Topic.table_name
48
49
  assert Topic.table_exists?
50
+
51
+ # Test when owner included in table name.
49
52
  Topic.table_name = "dbo.topics"
50
- assert Topic.table_exists?, "Tasks table name of dbo.topics should return true for exists."
53
+ assert Topic.table_exists?, "Topics table name of 'dbo.topics' should return true for exists."
54
+
55
+ # Test when database and owner included in table name.
56
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
57
+ Topic.table_name = "#{db_config.database}.dbo.topics"
58
+ assert Topic.table_exists?, "Topics table name of '[DATABASE].dbo.topics' should return true for exists."
51
59
  ensure
52
60
  Topic.table_name = "topics"
53
61
  end
54
62
  end
55
63
 
64
+ it "test table existence across database schemas" do
65
+ arunit_connection = Topic.connection
66
+ arunit2_connection = College.connection
67
+
68
+ arunit_database = arunit_connection.pool.db_config.database
69
+ arunit2_database = arunit2_connection.pool.db_config.database
70
+
71
+ # Assert that connections use different default databases schemas.
72
+ assert_not_equal arunit_database, arunit2_database
73
+
74
+ # Assert that the Topics table exists when using the Topics connection.
75
+ assert arunit_connection.table_exists?('topics'), 'Topics table exists using table name'
76
+ assert arunit_connection.table_exists?('dbo.topics'), 'Topics table exists using owner and table name'
77
+ assert arunit_connection.table_exists?("#{arunit_database}.dbo.topics"), 'Topics table exists using database, owner and table name'
78
+
79
+ # Assert that the Colleges table exists when using the Colleges connection.
80
+ assert arunit2_connection.table_exists?('colleges'), 'College table exists using table name'
81
+ assert arunit2_connection.table_exists?('dbo.colleges'), 'College table exists using owner and table name'
82
+ assert arunit2_connection.table_exists?("#{arunit2_database}.dbo.colleges"), 'College table exists using database, owner and table name'
83
+
84
+ # Assert that the tables exist when using each others connection.
85
+ assert arunit_connection.table_exists?("#{arunit2_database}.dbo.colleges"), 'Colleges table exists using Topics connection'
86
+ assert arunit2_connection.table_exists?("#{arunit_database}.dbo.topics"), 'Topics table exists using Colleges connection'
87
+ end
88
+
56
89
  it "return true to insert sql query for inserts only" do
57
90
  assert connection.send(:insert_sql?, "INSERT...")
58
91
  assert connection.send(:insert_sql?, "EXEC sp_executesql N'INSERT INTO [fk_test_has_fks] ([fk_id]) VALUES (@0); SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident', N'@0 int', @0 = 0")
@@ -68,21 +101,23 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
68
101
 
69
102
  it "test bad connection" do
70
103
  assert_raise ActiveRecord::NoDatabaseError do
71
- config = ActiveRecord::Base.configurations["arunit"].merge(database: "inexistent_activerecord_unittest")
72
- ActiveRecord::Base.sqlserver_connection config
104
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
105
+ configuration = db_config.configuration_hash.merge(database: "inexistent_activerecord_unittest")
106
+ ActiveRecord::Base.sqlserver_connection configuration
73
107
  end
74
108
  end
75
109
 
76
110
  it "test database exists returns false if database does not exist" do
77
- config = ActiveRecord::Base.configurations["arunit"].merge(database: "inexistent_activerecord_unittest")
78
- assert_not ActiveRecord::ConnectionAdapters::SQLServerAdapter.database_exists?(config),
111
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
112
+ configuration = db_config.configuration_hash.merge(database: "inexistent_activerecord_unittest")
113
+ assert_not ActiveRecord::ConnectionAdapters::SQLServerAdapter.database_exists?(configuration),
79
114
  "expected database to not exist"
80
115
  end
81
116
 
82
117
  it "test database exists returns true when the database exists" do
83
- config = ActiveRecord::Base.configurations["arunit"]
84
- assert ActiveRecord::ConnectionAdapters::SQLServerAdapter.database_exists?(config),
85
- "expected database #{config[:database]} to exist"
118
+ db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
119
+ assert ActiveRecord::ConnectionAdapters::SQLServerAdapter.database_exists?(db_config.configuration_hash),
120
+ "expected database #{db_config.database} to exist"
86
121
  end
87
122
 
88
123
  describe "with different language" do
@@ -342,7 +377,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
342
377
  assert !SSTestCustomersView.columns.blank?
343
378
  assert_equal columns.size, SSTestCustomersView.columns.size
344
379
  columns.each do |colname|
345
- assert_instance_of ActiveRecord::ConnectionAdapters::SQLServerColumn,
380
+ assert_instance_of ActiveRecord::ConnectionAdapters::SQLServer::Column,
346
381
  SSTestCustomersView.columns_hash[colname],
347
382
  "Column name #{colname.inspect} was not found in these columns #{SSTestCustomersView.columns.map(&:name).inspect}"
348
383
  end
@@ -369,7 +404,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
369
404
  assert !SSTestStringDefaultsView.columns.blank?
370
405
  assert_equal columns.size, SSTestStringDefaultsView.columns.size
371
406
  columns.each do |colname|
372
- assert_instance_of ActiveRecord::ConnectionAdapters::SQLServerColumn,
407
+ assert_instance_of ActiveRecord::ConnectionAdapters::SQLServer::Column,
373
408
  SSTestStringDefaultsView.columns_hash[colname],
374
409
  "Column name #{colname.inspect} was not found in these columns #{SSTestStringDefaultsView.columns.map(&:name).inspect}"
375
410
  end
@@ -434,12 +469,11 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
434
469
  describe "block writes to a database" do
435
470
  def setup
436
471
  @conn = ActiveRecord::Base.connection
437
- @connection_handler = ActiveRecord::Base.connection_handler
438
472
  end
439
473
 
440
474
  def test_errors_when_an_insert_query_is_called_while_preventing_writes
441
475
  assert_raises(ActiveRecord::ReadOnlyError) do
442
- @connection_handler.while_preventing_writes do
476
+ ActiveRecord::Base.while_preventing_writes do
443
477
  @conn.insert("INSERT INTO [subscribers] ([nick]) VALUES ('aido')")
444
478
  end
445
479
  end
@@ -449,7 +483,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
449
483
  @conn.insert("INSERT INTO [subscribers] ([nick]) VALUES ('aido')")
450
484
 
451
485
  assert_raises(ActiveRecord::ReadOnlyError) do
452
- @connection_handler.while_preventing_writes do
486
+ ActiveRecord::Base.while_preventing_writes do
453
487
  @conn.update("UPDATE [subscribers] SET [subscribers].[name] = 'Aidan' WHERE [subscribers].[nick] = 'aido'")
454
488
  end
455
489
  end
@@ -459,7 +493,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
459
493
  @conn.execute("INSERT INTO [subscribers] ([nick]) VALUES ('aido')")
460
494
 
461
495
  assert_raises(ActiveRecord::ReadOnlyError) do
462
- @connection_handler.while_preventing_writes do
496
+ ActiveRecord::Base.while_preventing_writes do
463
497
  @conn.execute("DELETE FROM [subscribers] WHERE [subscribers].[nick] = 'aido'")
464
498
  end
465
499
  end
@@ -468,7 +502,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
468
502
  def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes
469
503
  @conn.execute("INSERT INTO [subscribers] ([nick]) VALUES ('aido')")
470
504
 
471
- @connection_handler.while_preventing_writes do
505
+ ActiveRecord::Base.while_preventing_writes do
472
506
  assert_equal 1, @conn.execute("SELECT * FROM [subscribers] WHERE [subscribers].[nick] = 'aido'")
473
507
  end
474
508
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cases/helper_sqlserver"
4
+ require "migrations/create_clients_and_change_column_collation"
5
+
6
+ class ChangeColumnCollationTestSqlServer < ActiveRecord::TestCase
7
+ before do
8
+ @old_verbose = ActiveRecord::Migration.verbose
9
+ ActiveRecord::Migration.verbose = false
10
+ CreateClientsAndChangeColumnCollation.new.up
11
+ end
12
+
13
+ after do
14
+ CreateClientsAndChangeColumnCollation.new.down
15
+ ActiveRecord::Migration.verbose = @old_verbose
16
+ end
17
+
18
+ def find_column(table, name)
19
+ table.find { |column| column.name == name }
20
+ end
21
+
22
+ let(:clients_table) { connection.columns("clients") }
23
+ let(:name_column) { find_column(clients_table, "name") }
24
+ let(:code_column) { find_column(clients_table, "code") }
25
+
26
+ it "change column collation to other than default" do
27
+ _(name_column.collation).must_equal "SQL_Latin1_General_CP1_CS_AS"
28
+ end
29
+
30
+ it "change column collation to default" do
31
+ _(code_column.collation).must_equal "SQL_Latin1_General_CP1_CI_AS"
32
+ end
33
+ end
@@ -24,22 +24,35 @@ class UniquenessValidationTest < ActiveRecord::TestCase
24
24
  end
25
25
  end
26
26
 
27
- # Skip the test if database is case-insensitive.
28
- coerce_tests! :test_validate_case_sensitive_uniqueness_by_default
29
- def test_validate_case_sensitive_uniqueness_by_default_coerced
30
- database_collation = connection.select_one("SELECT collation_name FROM sys.databases WHERE name = 'activerecord_unittest'").values.first
31
- skip if database_collation.include?("_CI_")
27
+ # Same as original coerced test except that it handles default SQL Server case-insensitive collation.
28
+ coerce_tests! :test_validate_uniqueness_by_default_database_collation
29
+ def test_validate_uniqueness_by_default_database_collation_coerced
30
+ Topic.validates_uniqueness_of(:author_email_address)
32
31
 
33
- original_test_validate_case_sensitive_uniqueness_by_default_coerced
32
+ topic1 = Topic.new(author_email_address: "david@loudthinking.com")
33
+ topic2 = Topic.new(author_email_address: "David@loudthinking.com")
34
+
35
+ assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
36
+
37
+ assert_not topic1.valid?
38
+ assert_not topic1.save
39
+
40
+ # Case insensitive collation (SQL_Latin1_General_CP1_CI_AS) by default.
41
+ # Should not allow "David" if "david" exists.
42
+ assert_not topic2.valid?
43
+ assert_not topic2.save
44
+
45
+ assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
46
+ assert_equal 1, Topic.where(author_email_address: "David@loudthinking.com").count
34
47
  end
35
48
  end
36
49
 
37
50
  require "models/event"
38
51
  module ActiveRecord
39
52
  class AdapterTest < ActiveRecord::TestCase
40
- # I really don`t think we can support legacy binds.
41
- coerce_tests! :test_select_all_with_legacy_binds
42
- coerce_tests! :test_insert_update_delete_with_legacy_binds
53
+ # Legacy binds are not supported.
54
+ coerce_tests! :test_select_all_insert_update_delete_with_casted_binds
55
+ coerce_tests! :test_select_all_insert_update_delete_with_legacy_binds
43
56
 
44
57
  # As far as I can tell, SQL Server does not support null bytes in strings.
45
58
  coerce_tests! :test_update_prepared_statement
@@ -54,13 +67,63 @@ module ActiveRecord
54
67
  assert_not_nil error.cause
55
68
  end
56
69
  end
70
+ end
71
+ end
72
+
73
+ module ActiveRecord
74
+ class AdapterPreventWritesTest < ActiveRecord::TestCase
75
+ # Fix randomly failing test. The loading of the model's schema was affecting the test.
76
+ coerce_tests! :test_errors_when_an_insert_query_is_called_while_preventing_writes
77
+ def test_errors_when_an_insert_query_is_called_while_preventing_writes_coerced
78
+ Subscriber.send(:load_schema!)
79
+ original_test_errors_when_an_insert_query_is_called_while_preventing_writes
80
+ end
57
81
 
82
+ # Fix randomly failing test. The loading of the model's schema was affecting the test.
83
+ coerce_tests! :test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_containing_read_command_is_called_while_preventing_writes
84
+ def test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_containing_read_command_is_called_while_preventing_writes_coerced
85
+ Subscriber.send(:load_schema!)
86
+ original_test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_containing_read_command_is_called_while_preventing_writes
87
+ end
88
+
89
+ # Fix randomly failing test. The loading of the model's schema was affecting the test.
90
+ coerce_tests! :test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_is_called_while_preventing_writes
91
+ def test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_is_called_while_preventing_writes_coerced
92
+ Subscriber.send(:load_schema!)
93
+ original_test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_is_called_while_preventing_writes
94
+ end
95
+ end
96
+ end
97
+
98
+ module ActiveRecord
99
+ class AdapterPreventWritesLegacyTest < ActiveRecord::TestCase
58
100
  # Fix randomly failing test. The loading of the model's schema was affecting the test.
59
101
  coerce_tests! :test_errors_when_an_insert_query_is_called_while_preventing_writes
60
102
  def test_errors_when_an_insert_query_is_called_while_preventing_writes_coerced
61
103
  Subscriber.send(:load_schema!)
62
104
  original_test_errors_when_an_insert_query_is_called_while_preventing_writes
63
105
  end
106
+
107
+ # Fix randomly failing test. The loading of the model's schema was affecting the test.
108
+ coerce_tests! :test_errors_when_an_insert_query_prefixed_by_a_slash_star_comment_is_called_while_preventing_writes
109
+ def test_errors_when_an_insert_query_prefixed_by_a_slash_star_comment_is_called_while_preventing_writes_coerced
110
+ Subscriber.send(:load_schema!)
111
+ original_test_errors_when_an_insert_query_prefixed_by_a_slash_star_comment_is_called_while_preventing_writes
112
+ end
113
+
114
+ # Fix randomly failing test. The loading of the model's schema was affecting the test.
115
+ coerce_tests! :test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_is_called_while_preventing_writes
116
+ def test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_is_called_while_preventing_writes_coerced
117
+ Subscriber.send(:load_schema!)
118
+ original_test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_is_called_while_preventing_writes
119
+ end
120
+
121
+ # Fix randomly failing test. The loading of the model's schema was affecting the test.
122
+ coerce_tests! :test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_containing_read_command_is_called_while_preventing_writes
123
+ def test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_containing_read_command_is_called_while_preventing_writes_coerced
124
+ Subscriber.send(:load_schema!)
125
+ original_test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_containing_read_command_is_called_while_preventing_writes
126
+ end
64
127
  end
65
128
  end
66
129
 
@@ -176,7 +239,7 @@ class BasicsTest < ActiveRecord::TestCase
176
239
  # SQL Server does not have query for release_savepoint
177
240
  coerce_tests! %r{an empty transaction does not raise if preventing writes}
178
241
  test "an empty transaction does not raise if preventing writes coerced" do
179
- ActiveRecord::Base.connection_handler.while_preventing_writes do
242
+ ActiveRecord::Base.while_preventing_writes do
180
243
  assert_queries(1, ignore_none: true) do
181
244
  Bird.transaction do
182
245
  ActiveRecord::Base.connection.materialize_transactions
@@ -234,6 +297,55 @@ module ActiveRecord
234
297
  coerce_tests! :test_statement_cache_with_find_by
235
298
  coerce_tests! :test_statement_cache_with_in_clause
236
299
  coerce_tests! :test_statement_cache_with_sql_string_literal
300
+
301
+ # Same as original coerced test except prepared statements include `EXEC sp_executesql` wrapper.
302
+ coerce_tests! :test_bind_params_to_sql_with_prepared_statements, :test_bind_params_to_sql_with_unprepared_statements
303
+ def test_bind_params_to_sql_with_prepared_statements_coerced
304
+ assert_bind_params_to_sql_coerced(prepared: true)
305
+ end
306
+
307
+ def test_bind_params_to_sql_with_unprepared_statements_coerced
308
+ @connection.unprepared_statement do
309
+ assert_bind_params_to_sql_coerced(prepared: false)
310
+ end
311
+ end
312
+
313
+ private
314
+
315
+ def assert_bind_params_to_sql_coerced(prepared:)
316
+ table = Author.quoted_table_name
317
+ pk = "#{table}.#{Author.quoted_primary_key}"
318
+
319
+ # prepared_statements: true
320
+ #
321
+ # 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
322
+ #
323
+ # prepared_statements: false
324
+ #
325
+ # SELECT [authors].* FROM [authors] WHERE ([authors].[id] IN (1, 2, 3) OR [authors].[id] IS NULL)
326
+ #
327
+ sql_unprepared = "SELECT #{table}.* FROM #{table} WHERE (#{pk} IN (#{bind_params(1..3)}) OR #{pk} IS NULL)"
328
+ 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"
329
+
330
+ authors = Author.where(id: [1, 2, 3, nil])
331
+ assert_equal sql_unprepared, @connection.to_sql(authors.arel)
332
+ assert_sql(prepared ? sql_prepared : sql_unprepared) { assert_equal 3, authors.length }
333
+
334
+ # prepared_statements: true
335
+ #
336
+ # 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
337
+ #
338
+ # prepared_statements: false
339
+ #
340
+ # SELECT [authors].* FROM [authors] WHERE [authors].[id] IN (1, 2, 3)
341
+ #
342
+ sql_unprepared = "SELECT #{table}.* FROM #{table} WHERE #{pk} IN (#{bind_params(1..3)})"
343
+ 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"
344
+
345
+ authors = Author.where(id: [1, 2, 3, 9223372036854775808])
346
+ assert_equal sql_unprepared, @connection.to_sql(authors.arel)
347
+ assert_sql(prepared ? sql_prepared : sql_unprepared) { assert_equal 3, authors.length }
348
+ end
237
349
  end
238
350
  end
239
351
 
@@ -256,11 +368,112 @@ class CalculationsTest < ActiveRecord::TestCase
256
368
  original_test_offset_is_kept
257
369
  end
258
370
 
259
- # Are decimal, not integer.
371
+ # The SQL Server `AVG()` function for a list of integers returns an integer (not a decimal).
260
372
  coerce_tests! :test_should_return_decimal_average_of_integer_field
261
373
  def test_should_return_decimal_average_of_integer_field_coerced
262
374
  value = Account.average(:id)
263
- assert_equal BigDecimal("3.0").to_s, BigDecimal(value).to_s
375
+ assert_equal 3, value
376
+ end
377
+
378
+ # In SQL Server the `AVG()` function for a list of integers returns an integer so need to cast values as decimals before averaging.
379
+ # Match SQL Server limit implementation.
380
+ coerce_tests! :test_select_avg_with_group_by_as_virtual_attribute_with_sql
381
+ def test_select_avg_with_group_by_as_virtual_attribute_with_sql_coerced
382
+ rails_core = companies(:rails_core)
383
+
384
+ sql = <<~SQL
385
+ SELECT firm_id, AVG(CAST(credit_limit AS DECIMAL)) AS avg_credit_limit
386
+ FROM accounts
387
+ WHERE firm_id = ?
388
+ GROUP BY firm_id
389
+ ORDER BY firm_id
390
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY
391
+ SQL
392
+
393
+ account = Account.find_by_sql([sql, rails_core]).first
394
+
395
+ # id was not selected, so it should be nil
396
+ # (cannot select id because it wasn't used in the GROUP BY clause)
397
+ assert_nil account.id
398
+
399
+ # firm_id was explicitly selected, so it should be present
400
+ assert_equal(rails_core, account.firm)
401
+
402
+ # avg_credit_limit should be present as a virtual attribute
403
+ assert_equal(52.5, account.avg_credit_limit)
404
+ end
405
+
406
+ # In SQL Server the `AVG()` function for a list of integers returns an integer so need to cast values as decimals before averaging.
407
+ # Order column must be in the GROUP clause.
408
+ coerce_tests! :test_select_avg_with_group_by_as_virtual_attribute_with_ar
409
+ def test_select_avg_with_group_by_as_virtual_attribute_with_ar_coerced
410
+ rails_core = companies(:rails_core)
411
+
412
+ account = Account
413
+ .select(:firm_id, "AVG(CAST(credit_limit AS DECIMAL)) AS avg_credit_limit")
414
+ .where(firm: rails_core)
415
+ .group(:firm_id)
416
+ .order(:firm_id)
417
+ .take!
418
+
419
+ # id was not selected, so it should be nil
420
+ # (cannot select id because it wasn't used in the GROUP BY clause)
421
+ assert_nil account.id
422
+
423
+ # firm_id was explicitly selected, so it should be present
424
+ assert_equal(rails_core, account.firm)
425
+
426
+ # avg_credit_limit should be present as a virtual attribute
427
+ assert_equal(52.5, account.avg_credit_limit)
428
+ end
429
+
430
+ # In SQL Server the `AVG()` function for a list of integers returns an integer so need to cast values as decimals before averaging.
431
+ # SELECT columns must be in the GROUP clause.
432
+ # Match SQL Server limit implementation.
433
+ coerce_tests! :test_select_avg_with_joins_and_group_by_as_virtual_attribute_with_sql
434
+ def test_select_avg_with_joins_and_group_by_as_virtual_attribute_with_sql_coerced
435
+ rails_core = companies(:rails_core)
436
+
437
+ sql = <<~SQL
438
+ SELECT companies.*, AVG(CAST(accounts.credit_limit AS DECIMAL)) AS avg_credit_limit
439
+ FROM companies
440
+ INNER JOIN accounts ON companies.id = accounts.firm_id
441
+ WHERE companies.id = ?
442
+ GROUP BY companies.id, companies.type, companies.firm_id, companies.firm_name, companies.name, companies.client_of, companies.rating, companies.account_id, companies.description
443
+ ORDER BY companies.id
444
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY
445
+ SQL
446
+
447
+ firm = DependentFirm.find_by_sql([sql, rails_core]).first
448
+
449
+ # all the DependentFirm attributes should be present
450
+ assert_equal rails_core, firm
451
+ assert_equal rails_core.name, firm.name
452
+
453
+ # avg_credit_limit should be present as a virtual attribute
454
+ assert_equal(52.5, firm.avg_credit_limit)
455
+ end
456
+
457
+
458
+ # In SQL Server the `AVG()` function for a list of integers returns an integer so need to cast values as decimals before averaging.
459
+ # SELECT columns must be in the GROUP clause.
460
+ coerce_tests! :test_select_avg_with_joins_and_group_by_as_virtual_attribute_with_ar
461
+ def test_select_avg_with_joins_and_group_by_as_virtual_attribute_with_ar_coerced
462
+ rails_core = companies(:rails_core)
463
+
464
+ firm = DependentFirm
465
+ .select("companies.*", "AVG(CAST(accounts.credit_limit AS DECIMAL)) AS avg_credit_limit")
466
+ .where(id: rails_core)
467
+ .joins(:account)
468
+ .group(:id, :type, :firm_id, :firm_name, :name, :client_of, :rating, :account_id, :description)
469
+ .take!
470
+
471
+ # all the DependentFirm attributes should be present
472
+ assert_equal rails_core, firm
473
+ assert_equal rails_core.name, firm.name
474
+
475
+ # avg_credit_limit should be present as a virtual attribute
476
+ assert_equal(52.5, firm.avg_credit_limit)
264
477
  end
265
478
 
266
479
  # Match SQL Server limit implementation
@@ -329,14 +542,18 @@ module ActiveRecord
329
542
  coerce_tests! :test_quote_ar_object
330
543
  def test_quote_ar_object_coerced
331
544
  value = DatetimePrimaryKey.new(id: @time)
332
- assert_equal "'02-14-2017 12:34:56.79'", @connection.quote(value)
545
+ assert_deprecated do
546
+ assert_equal "'02-14-2017 12:34:56.79'", @connection.quote(value)
547
+ end
333
548
  end
334
549
 
335
550
  # Use our date format.
336
551
  coerce_tests! :test_type_cast_ar_object
337
552
  def test_type_cast_ar_object_coerced
338
553
  value = DatetimePrimaryKey.new(id: @time)
339
- assert_equal "02-14-2017 12:34:56.79", @connection.type_cast(value)
554
+ assert_deprecated do
555
+ assert_equal "02-14-2017 12:34:56.79", @connection.type_cast(value)
556
+ end
340
557
  end
341
558
  end
342
559
  end
@@ -396,39 +613,6 @@ module ActiveRecord
396
613
  end
397
614
 
398
615
  class MigrationTest < ActiveRecord::TestCase
399
- # We do not have do the DecimalWithoutScale type.
400
- coerce_tests! :test_add_table_with_decimals
401
- def test_add_table_with_decimals_coerced
402
- Person.connection.drop_table :big_numbers rescue nil
403
- assert !BigNumber.table_exists?
404
- GiveMeBigNumbers.up
405
- BigNumber.reset_column_information
406
- assert BigNumber.create(
407
- :bank_balance => 1586.43,
408
- :big_bank_balance => BigDecimal("1000234000567.95"),
409
- :world_population => 6000000000,
410
- :my_house_population => 3,
411
- :value_of_e => BigDecimal("2.7182818284590452353602875")
412
- )
413
- b = BigNumber.first
414
- assert_not_nil b
415
- assert_not_nil b.bank_balance
416
- assert_not_nil b.big_bank_balance
417
- assert_not_nil b.world_population
418
- assert_not_nil b.my_house_population
419
- assert_not_nil b.value_of_e
420
- assert_kind_of BigDecimal, b.world_population
421
- assert_equal "6000000000.0", b.world_population.to_s
422
- assert_kind_of Integer, b.my_house_population
423
- assert_equal 3, b.my_house_population
424
- assert_kind_of BigDecimal, b.bank_balance
425
- assert_equal BigDecimal("1586.43"), b.bank_balance
426
- assert_kind_of BigDecimal, b.big_bank_balance
427
- assert_equal BigDecimal("1000234000567.95"), b.big_bank_balance
428
- GiveMeBigNumbers.down
429
- assert_raise(ActiveRecord::StatementInvalid) { BigNumber.first }
430
- end
431
-
432
616
  # For some reason our tests set Rails.@_env which breaks test env switching.
433
617
  coerce_tests! :test_internal_metadata_stores_environment_when_other_data_exists
434
618
  coerce_tests! :test_internal_metadata_stores_environment
@@ -584,11 +768,6 @@ module ActiveRecord
584
768
  end
585
769
  end
586
770
 
587
- class DatabaseTasksDumpSchemaCacheTest < ActiveRecord::TestCase
588
- # Skip this test with /tmp/my_schema_cache.yml path on Windows.
589
- coerce_tests! :test_dump_schema_cache if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
590
- end
591
-
592
771
  class DatabaseTasksCreateAllTest < ActiveRecord::TestCase
593
772
  # We extend `local_database?` so that common VM IPs can be used.
594
773
  coerce_tests! :test_ignores_remote_databases, :test_warning_for_remote_databases
@@ -653,7 +832,11 @@ class EagerAssociationTest < ActiveRecord::TestCase
653
832
  end
654
833
 
655
834
  require "models/topic"
835
+ require "models/customer"
836
+ require "models/non_primary_key"
656
837
  class FinderTest < ActiveRecord::TestCase
838
+ fixtures :customers, :topics, :authors
839
+
657
840
  # We have implicit ordering, via FETCH.
658
841
  coerce_tests! %r{doesn't have implicit ordering},
659
842
  :test_find_doesnt_have_implicit_ordering
@@ -698,6 +881,87 @@ class FinderTest < ActiveRecord::TestCase
698
881
  end
699
882
  end
700
883
  end
884
+
885
+ # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
886
+ coerce_tests! :test_include_on_unloaded_relation_with_match
887
+ def test_include_on_unloaded_relation_with_match_coerced
888
+ assert_sql(/1 AS one.*FETCH NEXT @2 ROWS ONLY.*@2 = 1/) do
889
+ assert_equal true, Customer.where(name: "David").include?(customers(:david))
890
+ end
891
+ end
892
+
893
+ # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
894
+ coerce_tests! :test_include_on_unloaded_relation_without_match
895
+ def test_include_on_unloaded_relation_without_match_coerced
896
+ assert_sql(/1 AS one.*FETCH NEXT @2 ROWS ONLY.*@2 = 1/) do
897
+ assert_equal false, Customer.where(name: "David").include?(customers(:mary))
898
+ end
899
+ end
900
+
901
+ # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
902
+ coerce_tests! :test_member_on_unloaded_relation_with_match
903
+ def test_member_on_unloaded_relation_with_match_coerced
904
+ assert_sql(/1 AS one.*FETCH NEXT @2 ROWS ONLY.*@2 = 1/) do
905
+ assert_equal true, Customer.where(name: "David").member?(customers(:david))
906
+ end
907
+ end
908
+
909
+ # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
910
+ coerce_tests! :test_member_on_unloaded_relation_without_match
911
+ def test_member_on_unloaded_relation_without_match_coerced
912
+ assert_sql(/1 AS one.*FETCH NEXT @2 ROWS ONLY.*@2 = 1/) do
913
+ assert_equal false, Customer.where(name: "David").member?(customers(:mary))
914
+ end
915
+ end
916
+
917
+ # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
918
+ coerce_tests! :test_implicit_order_column_is_configurable
919
+ def test_implicit_order_column_is_configurable_coerced
920
+ old_implicit_order_column = Topic.implicit_order_column
921
+ Topic.implicit_order_column = "title"
922
+
923
+ assert_equal topics(:fifth), Topic.first
924
+ assert_equal topics(:third), Topic.last
925
+
926
+ c = Topic.connection
927
+ assert_sql(/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) {
928
+ Topic.last
929
+ }
930
+ ensure
931
+ Topic.implicit_order_column = old_implicit_order_column
932
+ end
933
+
934
+ # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
935
+ coerce_tests! :test_implicit_order_set_to_primary_key
936
+ def test_implicit_order_set_to_primary_key_coerced
937
+ old_implicit_order_column = Topic.implicit_order_column
938
+ Topic.implicit_order_column = "id"
939
+
940
+ c = Topic.connection
941
+ assert_sql(/ORDER BY #{Regexp.escape(c.quote_table_name("topics.id"))} DESC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY.*@0 = 1/i) {
942
+ Topic.last
943
+ }
944
+ ensure
945
+ Topic.implicit_order_column = old_implicit_order_column
946
+ end
947
+
948
+ # Check for `FETCH NEXT x ROWS` rather then `LIMIT`.
949
+ coerce_tests! :test_implicit_order_for_model_without_primary_key
950
+ def test_implicit_order_for_model_without_primary_key_coerced
951
+ old_implicit_order_column = NonPrimaryKey.implicit_order_column
952
+ NonPrimaryKey.implicit_order_column = "created_at"
953
+
954
+ c = NonPrimaryKey.connection
955
+
956
+ assert_sql(/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) {
957
+ NonPrimaryKey.last
958
+ }
959
+ ensure
960
+ NonPrimaryKey.implicit_order_column = old_implicit_order_column
961
+ end
962
+
963
+ # SQL Server is unable to use aliased SELECT in the HAVING clause.
964
+ coerce_tests! :test_include_on_unloaded_relation_with_having_referencing_aliased_select
701
965
  end
702
966
 
703
967
  module ActiveRecord
@@ -929,7 +1193,14 @@ class RelationTest < ActiveRecord::TestCase
929
1193
  coerce_tests! :test_reorder_with_first
930
1194
  def test_reorder_with_first_coerced
931
1195
  sql_log = capture_sql do
932
- assert Post.order(:title).reorder(nil).first
1196
+ message = <<~MSG.squish
1197
+ `.reorder(nil)` with `.first` / `.first!` no longer
1198
+ takes non-deterministic result in Rails 6.2.
1199
+ To continue taking non-deterministic result, use `.take` / `.take!` instead.
1200
+ MSG
1201
+ assert_deprecated(message) do
1202
+ assert Post.order(:title).reorder(nil).first
1203
+ end
933
1204
  end
934
1205
  assert sql_log.none? { |sql| /order by [posts].[title]/i.match?(sql) }, "ORDER BY title was used in the query: #{sql_log}"
935
1206
  assert sql_log.all? { |sql| /order by \[posts\]\.\[id\]/i.match?(sql) }, "default ORDER BY ID was not used in the query: #{sql_log}"
@@ -982,6 +1253,31 @@ class RelationTest < ActiveRecord::TestCase
982
1253
  end
983
1254
  end
984
1255
 
1256
+ module ActiveRecord
1257
+ class RelationTest < ActiveRecord::TestCase
1258
+ # Skipping this test. SQL Server doesn't support optimizer hint as comments
1259
+ coerce_tests! :test_relation_with_optimizer_hints_filters_sql_comment_delimiters
1260
+
1261
+ coerce_tests! :test_does_not_duplicate_optimizer_hints_on_merge
1262
+ def test_does_not_duplicate_optimizer_hints_on_merge_coerced
1263
+ escaped_table = Post.connection.quote_table_name("posts")
1264
+ expected = "SELECT #{escaped_table}.* FROM #{escaped_table} OPTION (OMGHINT)"
1265
+ query = Post.optimizer_hints("OMGHINT").merge(Post.optimizer_hints("OMGHINT")).to_sql
1266
+ assert_equal expected, query
1267
+ end
1268
+
1269
+ # Original Rails test fails on Windows CI because the dump file was not being binary read.
1270
+ coerce_tests! :test_marshal_load_legacy_relation
1271
+ def test_marshal_load_legacy_relation_coerced
1272
+ path = File.expand_path(
1273
+ "support/marshal_compatibility_fixtures/legacy_relation.dump",
1274
+ ARTest::SQLServer.root_activerecord_test
1275
+ )
1276
+ assert_equal 11, Marshal.load(File.binread(path)).size
1277
+ end
1278
+ end
1279
+ end
1280
+
985
1281
  require "models/post"
986
1282
  class SanitizeTest < ActiveRecord::TestCase
987
1283
  # Use nvarchar string (N'') in assert
@@ -1225,6 +1521,7 @@ module ActiveRecord
1225
1521
 
1226
1522
  original_test_statement_cache_values_differ
1227
1523
  ensure
1524
+ Book.where(author_id: nil, name: 'my book').delete_all
1228
1525
  Book.connection.add_index(:books, [:author_id, :name], unique: true)
1229
1526
  end
1230
1527
  end
@@ -1233,8 +1530,43 @@ end
1233
1530
  module ActiveRecord
1234
1531
  module ConnectionAdapters
1235
1532
  class SchemaCacheTest < ActiveRecord::TestCase
1533
+ # Tests fail on Windows AppVeyor CI with 'Permission denied' error when renaming file during `File.atomic_write` call.
1534
+ coerce_tests! :test_yaml_dump_and_load, :test_yaml_dump_and_load_with_gzip if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
1535
+
1536
+ # Ruby 2.5 and 2.6 have issues to marshal Time before 1900. 2012.sql has one column with default value 1753
1537
+ coerce_tests! :test_marshal_dump_and_load_with_gzip, :test_marshal_dump_and_load_via_disk
1538
+
1539
+ # Tests fail on Windows AppVeyor CI with 'Permission denied' error when renaming file during `File.atomic_write` call.
1540
+ unless RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
1541
+ def test_marshal_dump_and_load_with_gzip_coerced
1542
+ with_marshable_time_defaults { original_test_marshal_dump_and_load_with_gzip }
1543
+ end
1544
+ def test_marshal_dump_and_load_via_disk_coerced
1545
+ with_marshable_time_defaults { original_test_marshal_dump_and_load_via_disk }
1546
+ end
1547
+ end
1548
+
1236
1549
  private
1237
1550
 
1551
+ def with_marshable_time_defaults
1552
+ # Detect problems
1553
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.7")
1554
+ column = @connection.columns(:sst_datatypes).find { |c| c.name == "datetime" }
1555
+ current_default = column.default if column.default.is_a?(Time) && column.default.year < 1900
1556
+ end
1557
+
1558
+ # Correct problems
1559
+ if current_default.present?
1560
+ @connection.change_column_default(:sst_datatypes, :datetime, current_default.dup.change(year: 1900))
1561
+ end
1562
+
1563
+ # Run original test
1564
+ yield
1565
+ ensure
1566
+ # Revert changes
1567
+ @connection.change_column_default(:sst_datatypes, :datetime, current_default) if current_default.present?
1568
+ end
1569
+
1238
1570
  # We need to give the full path for this to work.
1239
1571
  def schema_dump_path
1240
1572
  File.join ARTest::SQLServer.root_activerecord, "test/assets/schema_dump_5_1.yml"
@@ -1243,23 +1575,26 @@ module ActiveRecord
1243
1575
  end
1244
1576
  end
1245
1577
 
1578
+ require "models/post"
1579
+ require "models/comment"
1246
1580
  class UnsafeRawSqlTest < ActiveRecord::TestCase
1581
+ fixtures :posts
1582
+
1247
1583
  # Use LEN() vs length() function.
1248
1584
  coerce_tests! %r{order: always allows Arel}
1249
1585
  test "order: always allows Arel" do
1250
- ids_depr = with_unsafe_raw_sql_deprecated { Post.order(Arel.sql("len(title)")).pluck(:title) }
1251
- ids_disabled = with_unsafe_raw_sql_disabled { Post.order(Arel.sql("len(title)")).pluck(:title) }
1586
+ titles = Post.order(Arel.sql("len(title)")).pluck(:title)
1252
1587
 
1253
- assert_equal ids_depr, ids_disabled
1588
+ assert_not_empty titles
1254
1589
  end
1255
1590
 
1256
1591
  # Use LEN() vs length() function.
1257
1592
  coerce_tests! %r{pluck: always allows Arel}
1258
1593
  test "pluck: always allows Arel" do
1259
- values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, Arel.sql("len(title)")) }
1260
- values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, Arel.sql("len(title)")) }
1594
+ excepted_values = Post.includes(:comments).pluck(:title).map { |title| [title, title.size] }
1595
+ values = Post.includes(:comments).pluck(:title, Arel.sql("len(title)"))
1261
1596
 
1262
- assert_equal values_depr, values_disabled
1597
+ assert_equal excepted_values, values
1263
1598
  end
1264
1599
 
1265
1600
  # Use LEN() vs length() function.
@@ -1267,11 +1602,73 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase
1267
1602
  test "order: allows valid Array arguments" do
1268
1603
  ids_expected = Post.order(Arel.sql("author_id, len(title)")).pluck(:id)
1269
1604
 
1270
- ids_depr = with_unsafe_raw_sql_deprecated { Post.order(["author_id", "len(title)"]).pluck(:id) }
1271
- ids_disabled = with_unsafe_raw_sql_disabled { Post.order(["author_id", "len(title)"]).pluck(:id) }
1605
+ ids = Post.order(["author_id", "len(title)"]).pluck(:id)
1606
+
1607
+ assert_equal ids_expected, ids
1608
+ end
1609
+
1610
+ test "order: allows string column names that are quoted" do
1611
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1612
+
1613
+ ids = Post.order("[id]").pluck(:id)
1614
+
1615
+ assert_equal ids_expected, ids
1616
+ end
1617
+
1618
+ test "order: allows string column names that are quoted with table" do
1619
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1620
+
1621
+ ids = Post.order("[posts].[id]").pluck(:id)
1622
+
1623
+ assert_equal ids_expected, ids
1624
+ end
1625
+
1626
+ test "order: allows string column names that are quoted with table and user" do
1627
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1628
+
1629
+ ids = Post.order("[dbo].[posts].[id]").pluck(:id)
1272
1630
 
1273
- assert_equal ids_expected, ids_depr
1274
- assert_equal ids_expected, ids_disabled
1631
+ assert_equal ids_expected, ids
1632
+ end
1633
+
1634
+ test "order: allows string column names that are quoted with table, user and database" do
1635
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1636
+
1637
+ ids = Post.order("[activerecord_unittest].[dbo].[posts].[id]").pluck(:id)
1638
+
1639
+ assert_equal ids_expected, ids
1640
+ end
1641
+
1642
+ test "pluck: allows string column name that are quoted" do
1643
+ titles_expected = Post.pluck(Arel.sql("title"))
1644
+
1645
+ titles = Post.pluck("[title]")
1646
+
1647
+ assert_equal titles_expected, titles
1648
+ end
1649
+
1650
+ test "pluck: allows string column name that are quoted with table" do
1651
+ titles_expected = Post.pluck(Arel.sql("title"))
1652
+
1653
+ titles = Post.pluck("[posts].[title]")
1654
+
1655
+ assert_equal titles_expected, titles
1656
+ end
1657
+
1658
+ test "pluck: allows string column name that are quoted with table and user" do
1659
+ titles_expected = Post.pluck(Arel.sql("title"))
1660
+
1661
+ titles = Post.pluck("[dbo].[posts].[title]")
1662
+
1663
+ assert_equal titles_expected, titles
1664
+ end
1665
+
1666
+ test "pluck: allows string column name that are quoted with table, user and database" do
1667
+ titles_expected = Post.pluck(Arel.sql("title"))
1668
+
1669
+ titles = Post.pluck("[activerecord_unittest].[dbo].[posts].[title]")
1670
+
1671
+ assert_equal titles_expected, titles
1275
1672
  end
1276
1673
  end
1277
1674
 
@@ -1313,6 +1710,25 @@ class RelationMergingTest < ActiveRecord::TestCase
1313
1710
  relation = Post.all.merge(Post.order([Arel.sql("title LIKE ?"), "%suffix"]))
1314
1711
  assert_equal ["title LIKE N'%suffix'"], relation.order_values
1315
1712
  end
1713
+
1714
+ # Same as original but change first regexp to match sp_executesql binding syntax
1715
+ coerce_tests! :test_merge_doesnt_duplicate_same_clauses
1716
+ def test_merge_doesnt_duplicate_same_clauses_coerced
1717
+ david, mary, bob = authors(:david, :mary, :bob)
1718
+
1719
+ non_mary_and_bob = Author.where.not(id: [mary, bob])
1720
+
1721
+ author_id = Author.connection.quote_table_name("authors.id")
1722
+ assert_sql(/WHERE #{Regexp.escape(author_id)} NOT IN \((@\d), \g<1>\)'/) do
1723
+ assert_equal [david], non_mary_and_bob.merge(non_mary_and_bob)
1724
+ end
1725
+
1726
+ only_david = Author.where("#{author_id} IN (?)", david)
1727
+
1728
+ assert_sql(/WHERE \(#{Regexp.escape(author_id)} IN \(1\)\)\z/) do
1729
+ assert_equal [david], only_david.merge(only_david)
1730
+ end
1731
+ end
1316
1732
  end
1317
1733
 
1318
1734
  module ActiveRecord
@@ -1333,6 +1749,7 @@ class EnumTest < ActiveRecord::TestCase
1333
1749
 
1334
1750
  send(:'original_enums are distinct per class')
1335
1751
  ensure
1752
+ Book.where(author_id: nil, name: nil).delete_all
1336
1753
  Book.connection.add_index(:books, [:author_id, :name], unique: true)
1337
1754
  end
1338
1755
 
@@ -1343,6 +1760,7 @@ class EnumTest < ActiveRecord::TestCase
1343
1760
 
1344
1761
  send(:'original_creating new objects with enum scopes')
1345
1762
  ensure
1763
+ Book.where(author_id: nil, name: nil).delete_all
1346
1764
  Book.connection.add_index(:books, [:author_id, :name], unique: true)
1347
1765
  end
1348
1766
 
@@ -1353,6 +1771,7 @@ class EnumTest < ActiveRecord::TestCase
1353
1771
 
1354
1772
  send(:'original_enums are inheritable')
1355
1773
  ensure
1774
+ Book.where(author_id: nil, name: nil).delete_all
1356
1775
  Book.connection.add_index(:books, [:author_id, :name], unique: true)
1357
1776
  end
1358
1777
 
@@ -1363,6 +1782,7 @@ class EnumTest < ActiveRecord::TestCase
1363
1782
 
1364
1783
  send(:'original_declare multiple enums at a time')
1365
1784
  ensure
1785
+ Book.where(author_id: nil, name: nil).delete_all
1366
1786
  Book.connection.add_index(:books, [:author_id, :name], unique: true)
1367
1787
  end
1368
1788
  end
@@ -1400,6 +1820,8 @@ end
1400
1820
 
1401
1821
  require "models/citation"
1402
1822
  class EagerLoadingTooManyIdsTest < ActiveRecord::TestCase
1823
+ fixtures :citations
1824
+
1403
1825
  # Original Rails test fails with SQL Server error message "The query processor ran out of internal resources and
1404
1826
  # could not produce a query plan". This error goes away if you change database compatibility level to 110 (SQL 2012)
1405
1827
  # (see https://www.mssqltips.com/sqlservertip/5279/sql-server-error-query-processor-ran-out-of-internal-resources-and-could-not-produce-a-query-plan/).
@@ -1407,14 +1829,14 @@ class EagerLoadingTooManyIdsTest < ActiveRecord::TestCase
1407
1829
  # unprepared statement is used if the number of values exceeds the adapter's `bind_params_length`. The coerced test
1408
1830
  # still does this as there will be 32,768 remaining citation records in the database and the `bind_params_length` of
1409
1831
  # adapter is 2,098.
1410
- coerce_tests! :test_eager_loading_too_may_ids
1411
- def test_eager_loading_too_may_ids_coerced
1832
+ coerce_tests! :test_eager_loading_too_many_ids
1833
+ def test_eager_loading_too_many_ids_coerced
1412
1834
  # Remove excess records.
1413
1835
  Citation.limit(32768).order(id: :desc).delete_all
1414
1836
 
1415
1837
  # Perform test
1416
1838
  citation_count = Citation.count
1417
- assert_sql(/WHERE \(\[citations\]\.\[id\] IN \(0, 1/) do
1839
+ assert_sql(/WHERE \[citations\]\.\[id\] IN \(0, 1/) do
1418
1840
  assert_equal citation_count, Citation.eager_load(:citations).offset(0).size
1419
1841
  end
1420
1842
  end
@@ -1426,6 +1848,14 @@ class LogSubscriberTest < ActiveRecord::TestCase
1426
1848
  def test_vebose_query_logs_coerced
1427
1849
  original_test_vebose_query_logs
1428
1850
  end
1851
+
1852
+ # Bindings logged slightly differently.
1853
+ coerce_tests! :test_where_in_binds_logging_include_attribute_names
1854
+ def test_where_in_binds_logging_include_attribute_names_coerced
1855
+ Developer.where(id: [1, 2, 3, 4, 5]).load
1856
+ wait
1857
+ assert_match(%{@0 = 1, @1 = 2, @2 = 3, @3 = 4, @4 = 5 [["id", nil], ["id", nil], ["id", nil], ["id", nil], ["id", nil]]}, @logger.logged(:debug).last)
1858
+ end
1429
1859
  end
1430
1860
 
1431
1861
  class ActiveRecordSchemaTest < ActiveRecord::TestCase
@@ -1437,22 +1867,125 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
1437
1867
  end
1438
1868
  end
1439
1869
 
1440
- module ActiveRecord
1441
- module ConnectionAdapters
1442
- class ReaperTest < ActiveRecord::TestCase
1443
- # Coerce can be removed if Rails version > 6.0.3
1444
- coerce_tests! :test_connection_pool_starts_reaper_in_fork unless Process.respond_to?(:fork)
1870
+ class ReloadModelsTest < ActiveRecord::TestCase
1871
+ # Skip test on Windows. The number of arguments passed to `IO.popen` in
1872
+ # `activesupport/lib/active_support/testing/isolation.rb` exceeds what Windows can handle.
1873
+ coerce_tests! :test_has_one_with_reload if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
1874
+ end
1875
+
1876
+ require "models/post"
1877
+ class AnnotateTest < ActiveRecord::TestCase
1878
+ # Same as original coerced test except our SQL starts with `EXEC sp_executesql`.
1879
+ # TODO: Remove coerce after Rails 7 (see https://github.com/rails/rails/pull/42027)
1880
+ coerce_tests! :test_annotate_wraps_content_in_an_inline_comment
1881
+ def test_annotate_wraps_content_in_an_inline_comment_coerced
1882
+ quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts")
1883
+
1884
+ assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do
1885
+ posts = Post.select(:id).annotate("foo")
1886
+ assert posts.first
1887
+ end
1888
+ end
1889
+
1890
+ # Same as original coerced test except our SQL starts with `EXEC sp_executesql`.
1891
+ # TODO: Remove coerce after Rails 7 (see https://github.com/rails/rails/pull/42027)
1892
+ coerce_tests! :test_annotate_is_sanitized
1893
+ def test_annotate_is_sanitized_coerced
1894
+ quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts")
1895
+
1896
+ assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do
1897
+ posts = Post.select(:id).annotate("*/foo/*")
1898
+ assert posts.first
1899
+ end
1900
+
1901
+ assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do
1902
+ posts = Post.select(:id).annotate("**//foo//**")
1903
+ assert posts.first
1904
+ end
1905
+
1906
+ assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/ /\* bar \*/}i) do
1907
+ posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
1908
+ assert posts.first
1909
+ end
1910
+
1911
+ assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \+ MAX_EXECUTION_TIME\(1\) \*/}i) do
1912
+ posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
1913
+ assert posts.first
1445
1914
  end
1446
1915
  end
1447
1916
  end
1448
1917
 
1449
- class FixturesTest < ActiveRecord::TestCase
1450
- # Skip test on Windows. Skip can be removed when Rails PR https://github.com/rails/rails/pull/39234 has been merged.
1451
- coerce_tests! :test_binary_in_fixtures if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
1918
+ class MarshalSerializationTest < ActiveRecord::TestCase
1919
+ private
1920
+
1921
+ def marshal_fixture_path(file_name)
1922
+ File.expand_path(
1923
+ "support/marshal_compatibility_fixtures/#{ActiveRecord::Base.connection.adapter_name}/#{file_name}.dump",
1924
+ ARTest::SQLServer.test_root_sqlserver
1925
+ )
1926
+ end
1452
1927
  end
1453
1928
 
1454
- class ReloadModelsTest < ActiveRecord::TestCase
1455
- # Skip test on Windows. The number of arguements passed to `IO.popen` in
1456
- # `activesupport/lib/active_support/testing/isolation.rb` exceeds what Windows can handle.
1457
- coerce_tests! :test_has_one_with_reload if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
1929
+ class NestedThroughAssociationsTest < ActiveRecord::TestCase
1930
+ # Same as original but replace order with "order(:id)" to ensure that assert_includes_and_joins_equal doesn't raise
1931
+ # "A column has been specified more than once in the order by list"
1932
+ # Example: original test generate queries like "ORDER BY authors.id, [authors].[id]". We don't support duplicate columns in the order list
1933
+ coerce_tests! :test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload_via_joins, :test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins
1934
+ def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload_via_joins_coerced
1935
+ # preload table schemas
1936
+ Author.joins(:category_post_comments).first
1937
+
1938
+ assert_includes_and_joins_equal(
1939
+ Author.where("comments.id" => comments(:does_it_hurt).id).order(:id),
1940
+ [authors(:david), authors(:mary)], :category_post_comments
1941
+ )
1942
+ end
1943
+
1944
+ def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins_coerced
1945
+ # preload table schemas
1946
+ Category.joins(:post_comments).first
1947
+
1948
+ assert_includes_and_joins_equal(
1949
+ Category.where("comments.id" => comments(:more_greetings).id).order(:id),
1950
+ [categories(:general), categories(:technology)], :post_comments
1951
+ )
1952
+ end
1953
+ end
1954
+
1955
+ class BasePreventWritesTest < ActiveRecord::TestCase
1956
+ # SQL Server does not have query for release_savepoint
1957
+ coerce_tests! %r{an empty transaction does not raise if preventing writes}
1958
+ test "an empty transaction does not raise if preventing writes coerced" do
1959
+ ActiveRecord::Base.while_preventing_writes do
1960
+ assert_queries(1, ignore_none: true) do
1961
+ Bird.transaction do
1962
+ ActiveRecord::Base.connection.materialize_transactions
1963
+ end
1964
+ end
1965
+ end
1966
+ end
1967
+
1968
+ class BasePreventWritesLegacyTest < ActiveRecord::TestCase
1969
+ # SQL Server does not have query for release_savepoint
1970
+ coerce_tests! %r{an empty transaction does not raise if preventing writes}
1971
+ test "an empty transaction does not raise if preventing writes coerced" do
1972
+ ActiveRecord::Base.connection_handler.while_preventing_writes do
1973
+ assert_queries(1, ignore_none: true) do
1974
+ Bird.transaction do
1975
+ ActiveRecord::Base.connection.materialize_transactions
1976
+ end
1977
+ end
1978
+ end
1979
+ end
1980
+ end
1981
+ end
1982
+
1983
+ class MigratorTest < ActiveRecord::TestCase
1984
+ # Test fails on Windows AppVeyor CI for unknown reason.
1985
+ coerce_tests! :test_migrator_db_has_no_schema_migrations_table if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
1986
+ end
1987
+
1988
+ class MultiDbMigratorTest < ActiveRecord::TestCase
1989
+ # Test fails on Windows AppVeyor CI for unknown reason.
1990
+ coerce_tests! :test_migrator_db_has_no_schema_migrations_table if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
1458
1991
  end