activerecord-sqlserver-adapter 6.1.2.1 → 7.2.4

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +30 -0
  3. data/.devcontainer/boot.sh +22 -0
  4. data/.devcontainer/devcontainer.json +38 -0
  5. data/.devcontainer/docker-compose.yml +42 -0
  6. data/.github/workflows/ci.yml +7 -4
  7. data/.gitignore +3 -1
  8. data/CHANGELOG.md +19 -42
  9. data/Dockerfile.ci +3 -3
  10. data/Gemfile +6 -1
  11. data/MIT-LICENSE +1 -1
  12. data/README.md +113 -27
  13. data/RUNNING_UNIT_TESTS.md +27 -14
  14. data/Rakefile +2 -6
  15. data/VERSION +1 -1
  16. data/activerecord-sqlserver-adapter.gemspec +3 -3
  17. data/appveyor.yml +4 -6
  18. data/docker-compose.ci.yml +2 -1
  19. data/lib/active_record/connection_adapters/sqlserver/core_ext/abstract_adapter.rb +20 -0
  20. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +6 -4
  21. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +5 -23
  22. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +10 -7
  23. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +2 -0
  24. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +12 -2
  25. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +24 -16
  26. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -31
  27. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +143 -155
  28. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +5 -5
  29. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +57 -56
  30. data/lib/active_record/connection_adapters/sqlserver/savepoints.rb +26 -0
  31. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +14 -12
  32. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +11 -0
  33. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +213 -57
  34. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +3 -3
  35. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +13 -2
  36. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +4 -6
  37. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +19 -1
  38. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +1 -1
  39. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +1 -1
  40. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +1 -1
  41. data/lib/active_record/connection_adapters/sqlserver/utils.rb +21 -10
  42. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +187 -187
  43. data/lib/active_record/connection_adapters/sqlserver_column.rb +1 -0
  44. data/lib/active_record/tasks/sqlserver_database_tasks.rb +42 -33
  45. data/lib/arel/visitors/sqlserver.rb +77 -34
  46. data/test/cases/active_schema_test_sqlserver.rb +127 -0
  47. data/test/cases/adapter_test_sqlserver.rb +114 -26
  48. data/test/cases/coerced_tests.rb +1121 -340
  49. data/test/cases/column_test_sqlserver.rb +67 -64
  50. data/test/cases/connection_test_sqlserver.rb +3 -6
  51. data/test/cases/dbconsole.rb +19 -0
  52. data/test/cases/disconnected_test_sqlserver.rb +8 -5
  53. data/test/cases/eager_load_too_many_ids_test_sqlserver.rb +18 -0
  54. data/test/cases/enum_test_sqlserver.rb +49 -0
  55. data/test/cases/execute_procedure_test_sqlserver.rb +9 -5
  56. data/test/cases/fetch_test_sqlserver.rb +19 -0
  57. data/test/cases/helper_sqlserver.rb +11 -5
  58. data/test/cases/index_test_sqlserver.rb +8 -6
  59. data/test/cases/json_test_sqlserver.rb +1 -1
  60. data/test/cases/lateral_test_sqlserver.rb +2 -2
  61. data/test/cases/migration_test_sqlserver.rb +19 -1
  62. data/test/cases/optimizer_hints_test_sqlserver.rb +21 -12
  63. data/test/cases/pessimistic_locking_test_sqlserver.rb +8 -7
  64. data/test/cases/primary_keys_test_sqlserver.rb +2 -2
  65. data/test/cases/rake_test_sqlserver.rb +10 -5
  66. data/test/cases/schema_dumper_test_sqlserver.rb +155 -109
  67. data/test/cases/schema_test_sqlserver.rb +64 -1
  68. data/test/cases/showplan_test_sqlserver.rb +7 -7
  69. data/test/cases/specific_schema_test_sqlserver.rb +17 -13
  70. data/test/cases/transaction_test_sqlserver.rb +13 -8
  71. data/test/cases/trigger_test_sqlserver.rb +20 -0
  72. data/test/cases/utils_test_sqlserver.rb +2 -2
  73. data/test/cases/uuid_test_sqlserver.rb +8 -0
  74. data/test/cases/view_test_sqlserver.rb +58 -0
  75. data/test/config.yml +1 -2
  76. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +1 -1
  77. data/test/models/sqlserver/alien.rb +5 -0
  78. data/test/models/sqlserver/table_with_spaces.rb +5 -0
  79. data/test/models/sqlserver/trigger.rb +8 -0
  80. data/test/schema/sqlserver_specific_schema.rb +54 -6
  81. data/test/support/coerceable_test_sqlserver.rb +4 -4
  82. data/test/support/connection_reflection.rb +3 -9
  83. data/test/support/core_ext/query_cache.rb +7 -1
  84. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic.dump +0 -0
  85. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
  86. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic.dump +0 -0
  87. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic_associations.dump +0 -0
  88. data/test/support/query_assertions.rb +49 -0
  89. data/test/support/rake_helpers.rb +3 -1
  90. data/test/support/table_definition_sqlserver.rb +24 -0
  91. data/test/support/test_in_memory_oltp.rb +2 -2
  92. metadata +41 -17
  93. data/lib/active_record/sqlserver_base.rb +0 -18
  94. data/test/cases/scratchpad_test_sqlserver.rb +0 -8
  95. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic.dump +0 -0
  96. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic_associations.dump +0 -0
  97. data/test/support/sql_counter_sqlserver.rb +0 -29
@@ -18,9 +18,6 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
18
18
  it "has basic and non-sensitive information in the adapters inspect method" do
19
19
  string = connection.inspect
20
20
  _(string).must_match %r{ActiveRecord::ConnectionAdapters::SQLServerAdapter}
21
- _(string).must_match %r{version\: \d.\d}
22
- _(string).must_match %r{mode: dblib}
23
- _(string).must_match %r{azure: (true|false)}
24
21
  _(string).wont_match %r{host}
25
22
  _(string).wont_match %r{password}
26
23
  _(string).wont_match %r{username}
@@ -32,7 +29,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
32
29
  end
33
30
 
34
31
  it "raises invalid statement error for bad SQL" do
35
- assert_raise(ActiveRecord::StatementInvalid) { Topic.connection.update("UPDATE XXX") }
32
+ assert_raise(ActiveRecord::StatementInvalid) { Topic.lease_connection.update("UPDATE XXX") }
36
33
  end
37
34
 
38
35
  it "is has our adapter_name" do
@@ -62,8 +59,8 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
62
59
  end
63
60
 
64
61
  it "test table existence across database schemas" do
65
- arunit_connection = Topic.connection
66
- arunit2_connection = College.connection
62
+ arunit_connection = Topic.lease_connection
63
+ arunit2_connection = College.lease_connection
67
64
 
68
65
  arunit_database = arunit_connection.pool.db_config.database
69
66
  arunit2_database = arunit2_connection.pool.db_config.database
@@ -102,16 +99,17 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
102
99
  it "test bad connection" do
103
100
  assert_raise ActiveRecord::NoDatabaseError do
104
101
  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
102
+ configuration = db_config.configuration_hash.merge(database: "nonexistent_activerecord_unittest")
103
+ connection = ActiveRecord::ConnectionAdapters::SQLServerAdapter.new(configuration)
104
+ connection.exec_query("SELECT 1")
107
105
  end
108
106
  end
109
107
 
110
108
  it "test database exists returns false if database does not exist" do
111
109
  db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
112
- configuration = db_config.configuration_hash.merge(database: "inexistent_activerecord_unittest")
110
+ configuration = db_config.configuration_hash.merge(database: "nonexistent_activerecord_unittest")
113
111
  assert_not ActiveRecord::ConnectionAdapters::SQLServerAdapter.database_exists?(configuration),
114
- "expected database to not exist"
112
+ "expected database #{configuration[:database]} to not exist"
115
113
  end
116
114
 
117
115
  it "test database exists returns true when the database exists" do
@@ -193,17 +191,31 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
193
191
  @identity_insert_sql_unquoted = "INSERT INTO funny_jokes (id, name) VALUES(420, 'Knock knock')"
194
192
  @identity_insert_sql_unordered = "INSERT INTO [funny_jokes] ([name],[id]) VALUES('Knock knock',420)"
195
193
  @identity_insert_sql_sp = "EXEC sp_executesql N'INSERT INTO [funny_jokes] ([id],[name]) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Knock knock'"
196
- @identity_insert_sql_unquoted_sp = "EXEC sp_executesql N'INSERT INTO [funny_jokes] (id, name) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Knock knock'"
194
+ @identity_insert_sql_unquoted_sp = "EXEC sp_executesql N'INSERT INTO funny_jokes (id, name) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Knock knock'"
197
195
  @identity_insert_sql_unordered_sp = "EXEC sp_executesql N'INSERT INTO [funny_jokes] ([name],[id]) VALUES (@0, @1)', N'@0 nvarchar(255), @1 int', @0 = N'Knock knock', @1 = 420"
196
+
197
+ @identity_insert_sql_non_dbo = "INSERT INTO [test].[aliens] ([id],[name]) VALUES(420,'Mork')"
198
+ @identity_insert_sql_non_dbo_unquoted = "INSERT INTO test.aliens ([id],[name]) VALUES(420,'Mork')"
199
+ @identity_insert_sql_non_dbo_unordered = "INSERT INTO [test].[aliens] ([name],[id]) VALUES('Mork',420)"
200
+ @identity_insert_sql_non_dbo_sp = "EXEC sp_executesql N'INSERT INTO [test].[aliens] ([id],[name]) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Mork'"
201
+ @identity_insert_sql_non_dbo_unquoted_sp = "EXEC sp_executesql N'INSERT INTO test.aliens (id, name) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Mork'"
202
+ @identity_insert_sql_non_dbo_unordered_sp = "EXEC sp_executesql N'INSERT INTO [test].[aliens] ([name],[id]) VALUES (@0, @1)', N'@0 nvarchar(255), @1 int', @0 = N'Mork', @1 = 420"
198
203
  end
199
204
 
200
205
  it "return quoted table_name to #query_requires_identity_insert? when INSERT sql contains id column" do
201
- assert_equal "funny_jokes", connection.send(:query_requires_identity_insert?, @identity_insert_sql)
202
- assert_equal "funny_jokes", connection.send(:query_requires_identity_insert?, @identity_insert_sql_unquoted)
203
- assert_equal "funny_jokes", connection.send(:query_requires_identity_insert?, @identity_insert_sql_unordered)
204
- assert_equal "funny_jokes", connection.send(:query_requires_identity_insert?, @identity_insert_sql_sp)
205
- assert_equal "funny_jokes", connection.send(:query_requires_identity_insert?, @identity_insert_sql_unquoted_sp)
206
- assert_equal "funny_jokes", connection.send(:query_requires_identity_insert?, @identity_insert_sql_unordered_sp)
206
+ assert_equal "[funny_jokes]", connection.send(:query_requires_identity_insert?, @identity_insert_sql)
207
+ assert_equal "[funny_jokes]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_unquoted)
208
+ assert_equal "[funny_jokes]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_unordered)
209
+ assert_equal "[funny_jokes]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_sp)
210
+ assert_equal "[funny_jokes]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_unquoted_sp)
211
+ assert_equal "[funny_jokes]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_unordered_sp)
212
+
213
+ assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo)
214
+ assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unquoted)
215
+ assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unordered)
216
+ assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_sp)
217
+ assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unquoted_sp)
218
+ assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unordered_sp)
207
219
  end
208
220
 
209
221
  it "return false to #query_requires_identity_insert? for normal SQL" do
@@ -270,29 +282,31 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
270
282
  end
271
283
 
272
284
  it "NOT ALLOW by default the deletion of a referenced parent" do
273
- SSTestHasPk.connection.disable_referential_integrity {}
285
+ SSTestHasPk.lease_connection.disable_referential_integrity {}
274
286
  assert_raise(ActiveRecord::StatementInvalid) { @parent.destroy }
275
287
  end
276
288
 
277
289
  it "ALLOW deletion of referenced parent using #disable_referential_integrity block" do
278
- SSTestHasPk.connection.disable_referential_integrity { @parent.destroy }
290
+ assert_difference("SSTestHasPk.count", -1) do
291
+ SSTestHasPk.lease_connection.disable_referential_integrity { @parent.destroy }
292
+ end
279
293
  end
280
294
 
281
295
  it "again NOT ALLOW deletion of referenced parent after #disable_referential_integrity block" do
282
296
  assert_raise(ActiveRecord::StatementInvalid) do
283
- SSTestHasPk.connection.disable_referential_integrity {}
297
+ SSTestHasPk.lease_connection.disable_referential_integrity {}
284
298
  @parent.destroy
285
299
  end
286
300
  end
287
301
 
288
302
  it "not disable referential integrity for the same table twice" do
289
- tables = SSTestHasPk.connection.tables_with_referential_integrity
303
+ tables = SSTestHasPk.lease_connection.tables_with_referential_integrity
290
304
  assert_equal tables.size, tables.uniq.size
291
305
  end
292
306
  end
293
307
 
294
308
  describe "database statements" do
295
- it "run the database consistency checker useroptions command" do
309
+ it "run the database consistency checker 'user_options' command" do
296
310
  skip "on azure" if connection_sqlserver_azure?
297
311
  keys = [:textsize, :language, :isolation_level, :dateformat]
298
312
  user_options = connection.user_options
@@ -331,7 +345,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
331
345
  assert_equal "tinyint", connection.type_to_sql(:integer, limit: 1)
332
346
  end
333
347
 
334
- it "create bigints when limit is greateer than 4" do
348
+ it "create bigints when limit is greater than 4" do
335
349
  assert_equal "bigint", connection.type_to_sql(:integer, limit: 5)
336
350
  assert_equal "bigint", connection.type_to_sql(:integer, limit: 6)
337
351
  assert_equal "bigint", connection.type_to_sql(:integer, limit: 7)
@@ -373,11 +387,28 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
373
387
  assert_match(/CREATE VIEW sst_customers_view/, view_info["VIEW_DEFINITION"])
374
388
  end
375
389
 
390
+ it "allows connection#view_information to work with qualified object names" do
391
+ view_info = connection.send(:view_information, "[activerecord_unittest].[dbo].[sst_customers_view]")
392
+ assert_equal("sst_customers_view", view_info["TABLE_NAME"])
393
+ assert_match(/CREATE VIEW sst_customers_view/, view_info["VIEW_DEFINITION"])
394
+ end
395
+
396
+ it "allows connection#view_information to work across databases when using qualified object names" do
397
+ # College is defined in activerecord_unittest2 database.
398
+ view_info = College.lease_connection.send(:view_information, "[activerecord_unittest].[dbo].[sst_customers_view]")
399
+ assert_equal("sst_customers_view", view_info["TABLE_NAME"])
400
+ assert_match(/CREATE VIEW sst_customers_view/, view_info["VIEW_DEFINITION"])
401
+ end
402
+
376
403
  it "allow the connection#view_table_name method to return true table_name for the view" do
377
404
  assert_equal "customers", connection.send(:view_table_name, "sst_customers_view")
378
405
  assert_equal "topics", connection.send(:view_table_name, "topics"), "No view here, the same table name should come back."
379
406
  end
380
407
 
408
+ it "allow the connection#view_table_name method to return true table_name for the view for other connections" do
409
+ assert_equal "customers", College.lease_connection.send(:view_table_name, "[activerecord_unittest].[dbo].[sst_customers_view]")
410
+ assert_equal "topics", College.lease_connection.send(:view_table_name, "topics"), "No view here, the same table name should come back."
411
+ end
381
412
  # With same column names
382
413
 
383
414
  it "have matching column objects" do
@@ -402,7 +433,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
402
433
  end
403
434
 
404
435
  it "respond true to data_source_exists?" do
405
- assert SSTestCustomersView.connection.data_source_exists?(SSTestCustomersView.table_name)
436
+ assert SSTestCustomersView.lease_connection.data_source_exists?(SSTestCustomersView.table_name)
406
437
  end
407
438
 
408
439
  # With aliased column names
@@ -430,7 +461,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
430
461
  end
431
462
 
432
463
  it "respond true to data_source_exists?" do
433
- assert SSTestStringDefaultsView.connection.data_source_exists?(SSTestStringDefaultsView.table_name)
464
+ assert SSTestStringDefaultsView.lease_connection.data_source_exists?(SSTestStringDefaultsView.table_name)
434
465
  end
435
466
 
436
467
  # That have more than 4000 chars for their defintion
@@ -476,7 +507,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
476
507
 
477
508
  describe "block writes to a database" do
478
509
  def setup
479
- @conn = ActiveRecord::Base.connection
510
+ @conn = ActiveRecord::Base.lease_connection
480
511
  end
481
512
 
482
513
  def test_errors_when_an_insert_query_is_called_while_preventing_writes
@@ -515,4 +546,61 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
515
546
  end
516
547
  end
517
548
  end
549
+
550
+ describe 'table is in non-dbo schema' do
551
+ it "records can be created successfully" do
552
+ assert_difference("Alien.count", 1) do
553
+ Alien.create!(name: 'Trisolarans')
554
+ end
555
+ end
556
+
557
+ it 'records can be inserted using SQL' do
558
+ assert_difference("Alien.count", 2) do
559
+ Alien.lease_connection.exec_insert("insert into [test].[aliens] (id, name) VALUES(1, 'Trisolarans'), (2, 'Xenomorph')")
560
+ end
561
+ end
562
+ end
563
+
564
+ describe 'table names contains spaces' do
565
+ it 'records can be created successfully' do
566
+ assert_difference("TableWithSpaces.count", 1) do
567
+ TableWithSpaces.create!(name: 'Bob')
568
+ end
569
+ end
570
+ end
571
+
572
+ describe "exec_insert" do
573
+ it 'values clause should be case-insensitive' do
574
+ assert_difference("Post.count", 4) do
575
+ first_insert = connection.exec_insert("INSERT INTO [posts] ([id],[title],[body]) VALUES(100, 'Title', 'Body'), (102, 'Title', 'Body')")
576
+ second_insert = connection.exec_insert("INSERT INTO [posts] ([id],[title],[body]) values(113, 'Body', 'Body'), (114, 'Body', 'Body')")
577
+
578
+ assert_equal first_insert.rows.map(&:first), [100, 102]
579
+ assert_equal second_insert.rows.map(&:first), [113, 114]
580
+ end
581
+ end
582
+ end
583
+
584
+ describe "placeholder conditions" do
585
+ it 'using time placeholder' do
586
+ assert_equal Task.where("starting < ?", Time.now).count, 1
587
+ end
588
+
589
+ it 'using date placeholder' do
590
+ assert_equal Task.where("starting < ?", Date.today).count, 1
591
+ end
592
+
593
+ it 'using date-time placeholder' do
594
+ assert_equal Task.where("starting < ?", DateTime.current).count, 1
595
+ end
596
+ end
597
+
598
+ describe "distinct select query" do
599
+ it "generated SQL does not contain unnecessary alias projection" do
600
+ sqls = capture_sql do
601
+ Post.includes(:comments).joins(:comments).first
602
+ end
603
+ assert_no_match(/AS alias_0/, sqls.first)
604
+ end
605
+ end
518
606
  end