activerecord-sqlserver-adapter 6.1.0.0 → 7.0.0.0.rc1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +4 -1
  3. data/CHANGELOG.md +12 -23
  4. data/Gemfile +1 -0
  5. data/MIT-LICENSE +1 -1
  6. data/README.md +31 -16
  7. data/VERSION +1 -1
  8. data/activerecord-sqlserver-adapter.gemspec +2 -2
  9. data/appveyor.yml +4 -6
  10. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +2 -0
  11. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +5 -1
  12. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +2 -0
  13. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +2 -0
  14. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +7 -13
  15. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +15 -6
  16. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +4 -5
  17. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +28 -9
  18. data/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +14 -5
  19. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +3 -1
  20. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +3 -2
  21. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +1 -1
  22. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +1 -1
  23. data/lib/active_record/connection_adapters/sqlserver/utils.rb +16 -1
  24. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +99 -76
  25. data/lib/active_record/connection_adapters/sqlserver_column.rb +74 -35
  26. data/lib/arel/visitors/sqlserver.rb +17 -2
  27. data/test/cases/adapter_test_sqlserver.rb +10 -2
  28. data/test/cases/coerced_tests.rb +314 -85
  29. data/test/cases/column_test_sqlserver.rb +62 -58
  30. data/test/cases/eager_load_too_many_ids_test_sqlserver.rb +18 -0
  31. data/test/cases/fetch_test_sqlserver.rb +18 -0
  32. data/test/cases/rake_test_sqlserver.rb +36 -0
  33. data/test/cases/schema_dumper_test_sqlserver.rb +2 -2
  34. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +1 -1
  35. data/test/models/sqlserver/composite_pk.rb +9 -0
  36. data/test/schema/sqlserver_specific_schema.rb +18 -0
  37. data/test/support/coerceable_test_sqlserver.rb +4 -4
  38. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic.dump +0 -0
  39. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
  40. data/test/support/rake_helpers.rb +3 -1
  41. metadata +18 -15
  42. data/lib/active_record/connection_adapters/sqlserver/core_ext/query_methods.rb +0 -28
  43. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic.dump +0 -0
  44. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic_associations.dump +0 -0
@@ -10,7 +10,6 @@ require "active_record/connection_adapters/sqlserver/core_ext/explain"
10
10
  require "active_record/connection_adapters/sqlserver/core_ext/explain_subscriber"
11
11
  require "active_record/connection_adapters/sqlserver/core_ext/attribute_methods"
12
12
  require "active_record/connection_adapters/sqlserver/core_ext/finder_methods"
13
- require "active_record/connection_adapters/sqlserver/core_ext/query_methods"
14
13
  require "active_record/connection_adapters/sqlserver/core_ext/preloader"
15
14
  require "active_record/connection_adapters/sqlserver/version"
16
15
  require "active_record/connection_adapters/sqlserver/type"
@@ -106,7 +105,23 @@ module ActiveRecord
106
105
  end
107
106
 
108
107
  def config_appname(config)
109
- config[:appname] || configure_application_name || Rails.application.class.name.split("::").first rescue nil
108
+ if instance_methods.include?(:configure_application_name)
109
+ ActiveSupport::Deprecation.warn <<~MSG.squish
110
+ Configuring the application name used by TinyTDS by overriding the
111
+ `ActiveRecord::ConnectionAdapters::SQLServerAdapter#configure_application_name`
112
+ instance method is no longer supported. The application name should configured
113
+ using the `appname` setting in the `database.yml` file instead. Consult the
114
+ README for further information."
115
+ MSG
116
+ end
117
+
118
+ config[:appname] || rails_application_name
119
+ end
120
+
121
+ def rails_application_name
122
+ Rails.application.class.name.split("::").first
123
+ rescue
124
+ nil # Might not be in a Rails context so we fallback to `nil`.
110
125
  end
111
126
 
112
127
  def config_login_timeout(config)
@@ -362,97 +377,107 @@ module ActiveRecord
362
377
  version_year
363
378
  end
364
379
 
365
- protected
366
-
367
- # === Abstract Adapter (Misc Support) =========================== #
368
-
369
- def initialize_type_map(m = type_map)
370
- m.register_type %r{.*}, SQLServer::Type::UnicodeString.new
371
-
372
- # Exact Numerics
373
- register_class_with_limit m, "bigint(8)", SQLServer::Type::BigInteger
374
- m.alias_type "bigint", "bigint(8)"
375
- register_class_with_limit m, "int(4)", SQLServer::Type::Integer
376
- m.alias_type "integer", "int(4)"
377
- m.alias_type "int", "int(4)"
378
- register_class_with_limit m, "smallint(2)", SQLServer::Type::SmallInteger
379
- m.alias_type "smallint", "smallint(2)"
380
- register_class_with_limit m, "tinyint(1)", SQLServer::Type::TinyInteger
381
- m.alias_type "tinyint", "tinyint(1)"
382
- m.register_type "bit", SQLServer::Type::Boolean.new
383
- m.register_type %r{\Adecimal}i do |sql_type|
384
- scale = extract_scale(sql_type)
385
- precision = extract_precision(sql_type)
386
- if scale == 0
387
- SQLServer::Type::DecimalWithoutScale.new(precision: precision)
388
- else
389
- SQLServer::Type::Decimal.new(precision: precision, scale: scale)
380
+ class << self
381
+ protected
382
+
383
+ def initialize_type_map(m)
384
+ m.register_type %r{.*}, SQLServer::Type::UnicodeString.new
385
+
386
+ # Exact Numerics
387
+ register_class_with_limit m, "bigint(8)", SQLServer::Type::BigInteger
388
+ m.alias_type "bigint", "bigint(8)"
389
+ register_class_with_limit m, "int(4)", SQLServer::Type::Integer
390
+ m.alias_type "integer", "int(4)"
391
+ m.alias_type "int", "int(4)"
392
+ register_class_with_limit m, "smallint(2)", SQLServer::Type::SmallInteger
393
+ m.alias_type "smallint", "smallint(2)"
394
+ register_class_with_limit m, "tinyint(1)", SQLServer::Type::TinyInteger
395
+ m.alias_type "tinyint", "tinyint(1)"
396
+ m.register_type "bit", SQLServer::Type::Boolean.new
397
+ m.register_type %r{\Adecimal}i do |sql_type|
398
+ scale = extract_scale(sql_type)
399
+ precision = extract_precision(sql_type)
400
+ if scale == 0
401
+ SQLServer::Type::DecimalWithoutScale.new(precision: precision)
402
+ else
403
+ SQLServer::Type::Decimal.new(precision: precision, scale: scale)
404
+ end
390
405
  end
391
- end
392
- m.alias_type %r{\Anumeric}i, "decimal"
393
- m.register_type "money", SQLServer::Type::Money.new
394
- m.register_type "smallmoney", SQLServer::Type::SmallMoney.new
395
-
396
- # Approximate Numerics
397
- m.register_type "float", SQLServer::Type::Float.new
398
- m.register_type "real", SQLServer::Type::Real.new
399
-
400
- # Date and Time
401
- m.register_type "date", SQLServer::Type::Date.new
402
- m.register_type %r{\Adatetime} do |sql_type|
403
- precision = extract_precision(sql_type)
404
- if precision
405
- SQLServer::Type::DateTime2.new precision: precision
406
- else
407
- SQLServer::Type::DateTime.new
406
+ m.alias_type %r{\Anumeric}i, "decimal"
407
+ m.register_type "money", SQLServer::Type::Money.new
408
+ m.register_type "smallmoney", SQLServer::Type::SmallMoney.new
409
+
410
+ # Approximate Numerics
411
+ m.register_type "float", SQLServer::Type::Float.new
412
+ m.register_type "real", SQLServer::Type::Real.new
413
+
414
+ # Date and Time
415
+ m.register_type "date", SQLServer::Type::Date.new
416
+ m.register_type %r{\Adatetime} do |sql_type|
417
+ precision = extract_precision(sql_type)
418
+ if precision
419
+ SQLServer::Type::DateTime2.new precision: precision
420
+ else
421
+ SQLServer::Type::DateTime.new
422
+ end
408
423
  end
424
+ m.register_type %r{\Adatetimeoffset}i do |sql_type|
425
+ precision = extract_precision(sql_type)
426
+ SQLServer::Type::DateTimeOffset.new precision: precision
427
+ end
428
+ m.register_type "smalldatetime", SQLServer::Type::SmallDateTime.new
429
+ m.register_type %r{\Atime}i do |sql_type|
430
+ precision = extract_precision(sql_type) || DEFAULT_TIME_PRECISION
431
+ SQLServer::Type::Time.new precision: precision
432
+ end
433
+
434
+ # Character Strings
435
+ register_class_with_limit m, %r{\Achar}i, SQLServer::Type::Char
436
+ register_class_with_limit m, %r{\Avarchar}i, SQLServer::Type::Varchar
437
+ m.register_type "varchar(max)", SQLServer::Type::VarcharMax.new
438
+ m.register_type "text", SQLServer::Type::Text.new
439
+
440
+ # Unicode Character Strings
441
+ register_class_with_limit m, %r{\Anchar}i, SQLServer::Type::UnicodeChar
442
+ register_class_with_limit m, %r{\Anvarchar}i, SQLServer::Type::UnicodeVarchar
443
+ m.alias_type "string", "nvarchar(4000)"
444
+ m.register_type "nvarchar(max)", SQLServer::Type::UnicodeVarcharMax.new
445
+ m.register_type "nvarchar(max)", SQLServer::Type::UnicodeVarcharMax.new
446
+ m.register_type "ntext", SQLServer::Type::UnicodeText.new
447
+
448
+ # Binary Strings
449
+ register_class_with_limit m, %r{\Abinary}i, SQLServer::Type::Binary
450
+ register_class_with_limit m, %r{\Avarbinary}i, SQLServer::Type::Varbinary
451
+ m.register_type "varbinary(max)", SQLServer::Type::VarbinaryMax.new
452
+
453
+ # Other Data Types
454
+ m.register_type "uniqueidentifier", SQLServer::Type::Uuid.new
455
+ m.register_type "timestamp", SQLServer::Type::Timestamp.new
409
456
  end
410
- m.register_type %r{\Adatetimeoffset}i do |sql_type|
411
- precision = extract_precision(sql_type)
412
- SQLServer::Type::DateTimeOffset.new precision: precision
413
- end
414
- m.register_type "smalldatetime", SQLServer::Type::SmallDateTime.new
415
- m.register_type %r{\Atime}i do |sql_type|
416
- precision = extract_precision(sql_type) || DEFAULT_TIME_PRECISION
417
- SQLServer::Type::Time.new precision: precision
418
- end
457
+ end
419
458
 
420
- # Character Strings
421
- register_class_with_limit m, %r{\Achar}i, SQLServer::Type::Char
422
- register_class_with_limit m, %r{\Avarchar}i, SQLServer::Type::Varchar
423
- m.register_type "varchar(max)", SQLServer::Type::VarcharMax.new
424
- m.register_type "text", SQLServer::Type::Text.new
459
+ TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
425
460
 
426
- # Unicode Character Strings
427
- register_class_with_limit m, %r{\Anchar}i, SQLServer::Type::UnicodeChar
428
- register_class_with_limit m, %r{\Anvarchar}i, SQLServer::Type::UnicodeVarchar
429
- m.alias_type "string", "nvarchar(4000)"
430
- m.register_type "nvarchar(max)", SQLServer::Type::UnicodeVarcharMax.new
431
- m.register_type "nvarchar(max)", SQLServer::Type::UnicodeVarcharMax.new
432
- m.register_type "ntext", SQLServer::Type::UnicodeText.new
461
+ protected
433
462
 
434
- # Binary Strings
435
- register_class_with_limit m, %r{\Abinary}i, SQLServer::Type::Binary
436
- register_class_with_limit m, %r{\Avarbinary}i, SQLServer::Type::Varbinary
437
- m.register_type "varbinary(max)", SQLServer::Type::VarbinaryMax.new
463
+ # === Abstract Adapter (Misc Support) =========================== #
438
464
 
439
- # Other Data Types
440
- m.register_type "uniqueidentifier", SQLServer::Type::Uuid.new
441
- m.register_type "timestamp", SQLServer::Type::Timestamp.new
465
+ def type_map
466
+ TYPE_MAP
442
467
  end
443
468
 
444
469
  def translate_exception(e, message:, sql:, binds:)
445
470
  case message
446
471
  when /(SQL Server client is not connected)|(failed to execute statement)/i
447
472
  ConnectionNotEstablished.new(message)
448
- when /(cannot insert duplicate key .* with unique index) | (violation of unique key constraint)/i
473
+ when /(cannot insert duplicate key .* with unique index) | (violation of (unique|primary) key constraint)/i
449
474
  RecordNotUnique.new(message, sql: sql, binds: binds)
450
475
  when /(conflicted with the foreign key constraint) | (The DELETE statement conflicted with the REFERENCE constraint)/i
451
476
  InvalidForeignKey.new(message, sql: sql, binds: binds)
452
477
  when /has been chosen as the deadlock victim/i
453
478
  DeadlockVictim.new(message, sql: sql, binds: binds)
454
479
  when /database .* does not exist/i
455
- NoDatabaseError.new(message, sql: sql, binds: binds)
480
+ NoDatabaseError.new(message)
456
481
  when /data would be truncated/
457
482
  ValueTooLong.new(message, sql: sql, binds: binds)
458
483
  when /connection timed out/
@@ -484,8 +509,6 @@ module ActiveRecord
484
509
  end
485
510
  end
486
511
 
487
- def configure_application_name; end
488
-
489
512
  def initialize_dateformatter
490
513
  @database_dateformat = user_options_dateformat
491
514
  a, b, c = @database_dateformat.each_char.to_a
@@ -2,48 +2,87 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module ConnectionAdapters
5
- class SQLServerColumn < Column
6
- def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **sqlserver_options)
7
- @sqlserver_options = sqlserver_options
8
- super
9
- end
5
+ module SQLServer
6
+ class Column < ConnectionAdapters::Column
7
+ delegate :is_identity, :is_primary, :table_name, :ordinal_position, to: :sql_type_metadata
10
8
 
11
- def is_identity?
12
- @sqlserver_options[:is_identity]
13
- end
9
+ def initialize(*, is_identity: nil, is_primary: nil, table_name: nil, ordinal_position: nil, **)
10
+ super
11
+ @is_identity = is_identity
12
+ @is_primary = is_primary
13
+ @table_name = table_name
14
+ @ordinal_position = ordinal_position
15
+ end
14
16
 
15
- def is_primary?
16
- @sqlserver_options[:is_primary]
17
- end
17
+ def is_identity?
18
+ is_identity
19
+ end
18
20
 
19
- def table_name
20
- @sqlserver_options[:table_name]
21
- end
21
+ def is_primary?
22
+ is_primary
23
+ end
22
24
 
23
- def is_utf8?
24
- sql_type =~ /nvarchar|ntext|nchar/i
25
- end
25
+ def is_utf8?
26
+ sql_type =~ /nvarchar|ntext|nchar/i
27
+ end
26
28
 
27
- def case_sensitive?
28
- collation && collation.match(/_CS/)
29
- end
29
+ def case_sensitive?
30
+ collation && collation.match(/_CS/)
31
+ end
30
32
 
31
- private
32
-
33
- # In the Rails version of this method there is an assumption that the `default` value will always be a
34
- # `String` class, which must be true for the MySQL/PostgreSQL/SQLite adapters. However, in the SQL Server
35
- # adapter the `default` value can also be Boolean/Date/Time/etc. Changed the implementation of this method
36
- # to handle non-String `default` objects.
37
- def deduplicated
38
- @name = -name
39
- @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata
40
- @default = (default.is_a?(String) ? -default : default.dup.freeze) if default
41
- @default_function = -default_function if default_function
42
- @collation = -collation if collation
43
- @comment = -comment if comment
44
-
45
- freeze
33
+ def init_with(coder)
34
+ @is_identity = coder["is_identity"]
35
+ @is_primary = coder["is_primary"]
36
+ @table_name = coder["table_name"]
37
+ @ordinal_position = coder["ordinal_position"]
38
+ super
39
+ end
40
+
41
+ def encode_with(coder)
42
+ coder["is_identity"] = @is_identity
43
+ coder["is_primary"] = @is_primary
44
+ coder["table_name"] = @table_name
45
+ coder["ordinal_position"] = @ordinal_position
46
+ super
47
+ end
48
+
49
+ def ==(other)
50
+ other.is_a?(Column) &&
51
+ super &&
52
+ is_identity? == other.is_identity? &&
53
+ is_primary? == other.is_primary? &&
54
+ table_name == other.table_name &&
55
+ ordinal_position == other.ordinal_position
56
+ end
57
+ alias :eql? :==
58
+
59
+ def hash
60
+ Column.hash ^
61
+ super.hash ^
62
+ is_identity?.hash ^
63
+ is_primary?.hash ^
64
+ table_name.hash ^
65
+ ordinal_position.hash
66
+ end
67
+
68
+ private
69
+
70
+ # In the Rails version of this method there is an assumption that the `default` value will always be a
71
+ # `String` class, which must be true for the MySQL/PostgreSQL/SQLite adapters. However, in the SQL Server
72
+ # adapter the `default` value can also be Boolean/Date/Time/etc. Changed the implementation of this method
73
+ # to handle non-String `default` objects.
74
+ def deduplicated
75
+ @name = -name
76
+ @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata
77
+ @default = (default.is_a?(String) ? -default : default.dup.freeze) if default
78
+ @default_function = -default_function if default_function
79
+ @collation = -collation if collation
80
+ @comment = -comment if comment
81
+ freeze
82
+ end
46
83
  end
84
+
85
+ SQLServerColumn = SQLServer::Column
47
86
  end
48
87
  end
49
88
  end
@@ -83,6 +83,8 @@ module Arel
83
83
  # Monkey-patch start. Add query attribute bindings rather than just values.
84
84
  column_name = o.column_name
85
85
  column_type = o.attribute.relation.type_for_attribute(o.column_name)
86
+ # Use cast_type on encrypted attributes. Don't encrypt them again
87
+ column_type = column_type.cast_type if column_type.is_a?(ActiveRecord::Encryption::EncryptedAttributeType)
86
88
  attrs = values.map { |value| ActiveRecord::Relation::QueryAttribute.new(column_name, value, column_type) }
87
89
 
88
90
  collector.add_binds(attrs, &bind_block)
@@ -296,8 +298,21 @@ module Arel
296
298
  def primary_Key_From_Table(t)
297
299
  return unless t
298
300
 
299
- column_name = @connection.schema_cache.primary_keys(t.name) ||
300
- @connection.schema_cache.columns_hash(t.name).first.try(:second).try(:name)
301
+ primary_keys = @connection.schema_cache.primary_keys(t.name)
302
+ column_name = nil
303
+
304
+ case primary_keys
305
+ when NilClass
306
+ column_name = @connection.schema_cache.columns_hash(t.name).first.try(:second).try(:name)
307
+ when String
308
+ column_name = primary_keys
309
+ when Array
310
+ candidate_columns = @connection.schema_cache.columns_hash(t.name).slice(*primary_keys).values
311
+ candidate_column = candidate_columns.find(&:is_identity?)
312
+ candidate_column ||= candidate_columns.first
313
+ column_name = candidate_column.try(:name)
314
+ end
315
+
301
316
  column_name ? t[column_name] : nil
302
317
  end
303
318
 
@@ -120,6 +120,14 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
120
120
  "expected database #{db_config.database} to exist"
121
121
  end
122
122
 
123
+ it "test primary key violation" do
124
+ Post.create!(id: 0, title: 'Setup', body: 'Create post with primary key of zero')
125
+
126
+ assert_raise ActiveRecord::RecordNotUnique do
127
+ Post.create!(id: 0, title: 'Test', body: 'Try to create another post with primary key of zero')
128
+ end
129
+ end
130
+
123
131
  describe "with different language" do
124
132
  before do
125
133
  @default_language = connection.user_options_language
@@ -377,7 +385,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
377
385
  assert !SSTestCustomersView.columns.blank?
378
386
  assert_equal columns.size, SSTestCustomersView.columns.size
379
387
  columns.each do |colname|
380
- assert_instance_of ActiveRecord::ConnectionAdapters::SQLServerColumn,
388
+ assert_instance_of ActiveRecord::ConnectionAdapters::SQLServer::Column,
381
389
  SSTestCustomersView.columns_hash[colname],
382
390
  "Column name #{colname.inspect} was not found in these columns #{SSTestCustomersView.columns.map(&:name).inspect}"
383
391
  end
@@ -404,7 +412,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
404
412
  assert !SSTestStringDefaultsView.columns.blank?
405
413
  assert_equal columns.size, SSTestStringDefaultsView.columns.size
406
414
  columns.each do |colname|
407
- assert_instance_of ActiveRecord::ConnectionAdapters::SQLServerColumn,
415
+ assert_instance_of ActiveRecord::ConnectionAdapters::SQLServer::Column,
408
416
  SSTestStringDefaultsView.columns_hash[colname],
409
417
  "Column name #{colname.inspect} was not found in these columns #{SSTestStringDefaultsView.columns.map(&:name).inspect}"
410
418
  end