activerecord-sqlserver-adapter 6.0.2 → 6.1.2.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -56
  3. data/README.md +28 -11
  4. data/VERSION +1 -1
  5. data/activerecord-sqlserver-adapter.gemspec +1 -1
  6. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +2 -0
  7. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +5 -10
  8. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +9 -2
  9. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +2 -0
  10. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +2 -0
  11. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -4
  12. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +27 -15
  13. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +4 -3
  14. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +22 -1
  15. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +9 -3
  16. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +8 -6
  17. data/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +36 -7
  18. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +0 -1
  19. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +2 -2
  20. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +2 -1
  21. data/lib/active_record/connection_adapters/sqlserver/utils.rb +1 -1
  22. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +100 -70
  23. data/lib/active_record/connection_adapters/sqlserver_column.rb +75 -19
  24. data/lib/active_record/sqlserver_base.rb +9 -15
  25. data/lib/active_record/tasks/sqlserver_database_tasks.rb +17 -14
  26. data/lib/arel/visitors/sqlserver.rb +74 -29
  27. data/test/cases/adapter_test_sqlserver.rb +27 -17
  28. data/test/cases/change_column_collation_test_sqlserver.rb +33 -0
  29. data/test/cases/coerced_tests.rb +544 -77
  30. data/test/cases/column_test_sqlserver.rb +4 -0
  31. data/test/cases/disconnected_test_sqlserver.rb +39 -0
  32. data/test/cases/execute_procedure_test_sqlserver.rb +9 -0
  33. data/test/cases/fetch_test_sqlserver.rb +18 -0
  34. data/test/cases/in_clause_test_sqlserver.rb +27 -0
  35. data/test/cases/migration_test_sqlserver.rb +7 -0
  36. data/test/cases/order_test_sqlserver.rb +7 -0
  37. data/test/cases/primary_keys_test_sqlserver.rb +103 -0
  38. data/test/cases/rake_test_sqlserver.rb +38 -2
  39. data/test/cases/schema_dumper_test_sqlserver.rb +9 -0
  40. data/test/migrations/create_clients_and_change_column_collation.rb +19 -0
  41. data/test/models/sqlserver/composite_pk.rb +9 -0
  42. data/test/models/sqlserver/sst_string_collation.rb +3 -0
  43. data/test/schema/sqlserver_specific_schema.rb +25 -0
  44. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic.dump +0 -0
  45. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic_associations.dump +0 -0
  46. data/test/support/sql_counter_sqlserver.rb +14 -12
  47. metadata +23 -8
  48. data/lib/active_record/connection_adapters/sqlserver/core_ext/query_methods.rb +0 -28
@@ -3,9 +3,13 @@
3
3
  module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module SQLServer
6
- class SchemaCreation < AbstractAdapter::SchemaCreation
6
+ class SchemaCreation < SchemaCreation
7
7
  private
8
8
 
9
+ def supports_index_using?
10
+ false
11
+ end
12
+
9
13
  def visit_TableDefinition(o)
10
14
  if_not_exists = o.if_not_exists
11
15
 
@@ -29,11 +33,28 @@ module ActiveRecord
29
33
  sql
30
34
  end
31
35
 
36
+ def visit_CreateIndexDefinition(o)
37
+ if_not_exists = o.if_not_exists
38
+
39
+ o.if_not_exists = false
40
+
41
+ sql = super
42
+
43
+ if if_not_exists
44
+ sql = "IF NOT EXISTS (SELECT name FROM sysindexes WHERE name = '#{o.index.name}') #{sql}"
45
+ end
46
+
47
+ sql
48
+ end
49
+
32
50
  def add_column_options!(sql, options)
33
51
  sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
34
52
  if options[:null] == false
35
53
  sql << " NOT NULL"
36
54
  end
55
+ if options[:collation].present?
56
+ sql << " COLLATE #{options[:collation]}"
57
+ end
37
58
  if options[:is_identity] == true
38
59
  sql << " IDENTITY(1,1)"
39
60
  end
@@ -15,7 +15,7 @@ module ActiveRecord
15
15
  private
16
16
 
17
17
  def explicit_primary_key_default?(column)
18
- column.is_primary? && !column.is_identity?
18
+ column.type == :integer && !column.is_identity?
19
19
  end
20
20
 
21
21
  def schema_limit(column)
@@ -27,11 +27,17 @@ module ActiveRecord
27
27
  def schema_collation(column)
28
28
  return unless column.collation
29
29
 
30
- column.collation if column.collation != @connection.collation
30
+ # use inspect to ensure collation is dumped as string. Without this it's dumped as
31
+ # a constant ('collation: SQL_Latin1_General_CP1_CI_AS')
32
+ collation = column.collation.inspect
33
+ # use inspect to ensure string comparison
34
+ default_collation = @connection.collation.inspect
35
+
36
+ collation if collation != default_collation
31
37
  end
32
38
 
33
39
  def default_primary_key?(column)
34
- super && column.is_primary? && column.is_identity?
40
+ super && column.is_identity?
35
41
  end
36
42
  end
37
43
  end
@@ -27,7 +27,7 @@ module ActiveRecord
27
27
  end
28
28
  end
29
29
  if options[:if_exists] && @version_year < 2016
30
- execute "IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = #{quote(table_name)}) DROP TABLE #{quote_table_name(table_name)}"
30
+ execute "IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = #{quote(table_name)}) DROP TABLE #{quote_table_name(table_name)}", "SCHEMA"
31
31
  else
32
32
  super
33
33
  end
@@ -51,7 +51,7 @@ module ActiveRecord
51
51
  index[:index_keys].split(",").each do |column|
52
52
  column.strip!
53
53
 
54
- if column.ends_with?("(-)")
54
+ if column.end_with?("(-)")
55
55
  column.gsub! "(-)", ""
56
56
  orders[column] = :desc
57
57
  end
@@ -84,7 +84,7 @@ module ActiveRecord
84
84
  end
85
85
 
86
86
  def new_column(name, default, sql_type_metadata, null, default_function = nil, collation = nil, comment = nil, sqlserver_options = {})
87
- SQLServerColumn.new(
87
+ SQLServer::Column.new(
88
88
  name,
89
89
  default,
90
90
  sql_type_metadata,
@@ -130,8 +130,9 @@ module ActiveRecord
130
130
  rename_table_indexes(table_name, new_name)
131
131
  end
132
132
 
133
- def remove_column(table_name, column_name, type = nil, options = {})
133
+ def remove_column(table_name, column_name, type = nil, **options)
134
134
  raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_name.is_a? Array
135
+ return if options[:if_exists] == true && !column_exists?(table_name, column_name)
135
136
 
136
137
  remove_check_constraints(table_name, column_name)
137
138
  remove_default_constraint(table_name, column_name)
@@ -156,6 +157,7 @@ module ActiveRecord
156
157
  end
157
158
  sql_commands << "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(options[:default], column_object)} WHERE #{quote_column_name(column_name)} IS NULL" if !options[:null].nil? && options[:null] == false && !options[:default].nil?
158
159
  alter_command = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, limit: options[:limit], precision: options[:precision], scale: options[:scale])}"
160
+ alter_command += " COLLATE #{options[:collation]}" if options[:collation].present?
159
161
  alter_command += " NOT NULL" if !options[:null].nil? && options[:null] == false
160
162
  sql_commands << alter_command
161
163
  if without_constraints
@@ -190,7 +192,7 @@ module ActiveRecord
190
192
  end
191
193
 
192
194
  def rename_index(table_name, old_name, new_name)
193
- raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" if new_name.length > allowed_index_name_length
195
+ raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters" if new_name.length > index_name_length
194
196
 
195
197
  identifier = SQLServer::Utils.extract_identifiers("#{table_name}.#{old_name}")
196
198
  execute_procedure :sp_rename, identifier.quoted, new_name, "INDEX"
@@ -330,7 +332,7 @@ module ActiveRecord
330
332
  def initialize_native_database_types
331
333
  {
332
334
  primary_key: "bigint NOT NULL IDENTITY(1,1) PRIMARY KEY",
333
- primary_key_nonclustered: "int NOT NULL IDENTITY(1,1) PRIMARY KEY NONCLUSTERED",
335
+ primary_key_nonclustered: "bigint NOT NULL IDENTITY(1,1) PRIMARY KEY NONCLUSTERED",
334
336
  integer: { name: "int", limit: 4 },
335
337
  bigint: { name: "bigint" },
336
338
  boolean: { name: "bit" },
@@ -3,16 +3,45 @@
3
3
  module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module SQLServer
6
- class SqlTypeMetadata < ActiveRecord::ConnectionAdapters::SqlTypeMetadata
7
- def initialize(**kwargs)
8
- @sqlserver_options = kwargs.extract!(:sqlserver_options)
9
- super(**kwargs)
6
+ class TypeMetadata < DelegateClass(SqlTypeMetadata)
7
+ undef to_yaml if method_defined?(:to_yaml)
8
+
9
+ include Deduplicable
10
+
11
+ attr_reader :is_identity, :is_primary, :table_name, :ordinal_position
12
+
13
+ def initialize(type_metadata, is_identity: nil, is_primary: nil, table_name: nil, ordinal_position: nil)
14
+ super(type_metadata)
15
+ @is_identity = is_identity
16
+ @is_primary = is_primary
17
+ @table_name = table_name
18
+ @ordinal_position = ordinal_position
19
+ end
20
+
21
+ def ==(other)
22
+ other.is_a?(TypeMetadata) &&
23
+ __getobj__ == other.__getobj__ &&
24
+ is_identity == other.is_identity &&
25
+ is_primary == other.is_primary &&
26
+ table_name == other.table_name &&
27
+ ordinal_position == other.ordinal_position
28
+ end
29
+ alias eql? ==
30
+
31
+ def hash
32
+ TypeMetadata.hash ^
33
+ __getobj__.hash ^
34
+ is_identity.hash ^
35
+ is_primary.hash ^
36
+ table_name.hash ^
37
+ ordinal_position.hash
10
38
  end
11
39
 
12
- protected
40
+ private
13
41
 
14
- def attributes_for_hash
15
- super + [@sqlserver_options]
42
+ def deduplicated
43
+ __setobj__(__getobj__.deduplicate)
44
+ super
16
45
  end
17
46
  end
18
47
  end
@@ -9,7 +9,6 @@ module ActiveRecord
9
9
  options[:is_identity] = true unless options.key?(:default)
10
10
  elsif type == :uuid
11
11
  options[:default] = options.fetch(:default, "NEWID()")
12
- options[:primary_key] = true
13
12
  end
14
13
  super
15
14
  end
@@ -31,9 +31,9 @@ module ActiveRecord
31
31
  module SQLServerRealTransaction
32
32
  attr_reader :starting_isolation_level
33
33
 
34
- def initialize(connection, options, **args)
34
+ def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false)
35
35
  @connection = connection
36
- @starting_isolation_level = current_isolation_level if options[:isolation]
36
+ @starting_isolation_level = current_isolation_level if isolation
37
37
  super
38
38
  end
39
39
 
@@ -10,7 +10,8 @@ module ActiveRecord
10
10
  end
11
11
 
12
12
  def serialize(value)
13
- return unless value.present?
13
+ value = super
14
+ return value unless value.acts_like?(:date)
14
15
 
15
16
  date = super(value).to_s(:_sqlserver_dateformat)
16
17
  Data.new date, self
@@ -92,7 +92,7 @@ module ActiveRecord
92
92
  @schema = @parts.first
93
93
  end
94
94
  rest = scanner.rest
95
- rest = rest.starts_with?(".") ? rest[1..-1] : rest[0..-1]
95
+ rest = rest.start_with?(".") ? rest[1..-1] : rest[0..-1]
96
96
  @object = unquote(rest)
97
97
  @parts << @object
98
98
  end
@@ -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"
@@ -59,13 +58,89 @@ module ActiveRecord
59
58
  self.use_output_inserted = true
60
59
  self.exclude_output_inserted_table_names = Concurrent::Map.new { false }
61
60
 
62
- def initialize(connection, logger = nil, config = {})
61
+ class << self
62
+ def new_client(config)
63
+ case config[:mode]
64
+ when :dblib
65
+ require "tiny_tds"
66
+ dblib_connect(config)
67
+ else
68
+ raise ArgumentError, "Unknown connection mode in #{config.inspect}."
69
+ end
70
+ end
71
+
72
+ def dblib_connect(config)
73
+ TinyTds::Client.new(
74
+ dataserver: config[:dataserver],
75
+ host: config[:host],
76
+ port: config[:port],
77
+ username: config[:username],
78
+ password: config[:password],
79
+ database: config[:database],
80
+ tds_version: config[:tds_version] || "7.3",
81
+ appname: config_appname(config),
82
+ login_timeout: config_login_timeout(config),
83
+ timeout: config_timeout(config),
84
+ encoding: config_encoding(config),
85
+ azure: config[:azure],
86
+ contained: config[:contained]
87
+ ).tap do |client|
88
+ if config[:azure]
89
+ client.execute("SET ANSI_NULLS ON").do
90
+ client.execute("SET ANSI_NULL_DFLT_ON ON").do
91
+ client.execute("SET ANSI_PADDING ON").do
92
+ client.execute("SET ANSI_WARNINGS ON").do
93
+ else
94
+ client.execute("SET ANSI_DEFAULTS ON").do
95
+ end
96
+ client.execute("SET QUOTED_IDENTIFIER ON").do
97
+ client.execute("SET CURSOR_CLOSE_ON_COMMIT OFF").do
98
+ client.execute("SET IMPLICIT_TRANSACTIONS OFF").do
99
+ client.execute("SET TEXTSIZE 2147483647").do
100
+ client.execute("SET CONCAT_NULL_YIELDS_NULL ON").do
101
+ end
102
+ rescue TinyTds::Error => e
103
+ raise ActiveRecord::NoDatabaseError if e.message.match(/database .* does not exist/i)
104
+ raise e
105
+ end
106
+
107
+ def config_appname(config)
108
+ if self.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
+ return nil if Rails.application.nil?
123
+
124
+ Rails.application.class.name.split("::").first
125
+ end
126
+
127
+ def config_login_timeout(config)
128
+ config[:login_timeout].present? ? config[:login_timeout].to_i : nil
129
+ end
130
+
131
+ def config_timeout(config)
132
+ config[:timeout].present? ? config[:timeout].to_i / 1000 : nil
133
+ end
134
+
135
+ def config_encoding(config)
136
+ config[:encoding].present? ? config[:encoding] : nil
137
+ end
138
+ end
139
+
140
+ def initialize(connection, logger, _connection_options, config)
63
141
  super(connection, logger, config)
64
- # Our Responsibility
65
142
  @connection_options = config
66
- connect
67
- initialize_dateformatter
68
- use_database
143
+ configure_connection
69
144
  end
70
145
 
71
146
  # === Abstract Adapter ========================================== #
@@ -226,6 +301,14 @@ module ActiveRecord
226
301
  do_execute "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION"
227
302
  end
228
303
 
304
+ def configure_connection
305
+ @spid = _raw_select("SELECT @@SPID", fetch: :rows).first.first
306
+ @version_year = version_year
307
+
308
+ initialize_dateformatter
309
+ use_database
310
+ end
311
+
229
312
  # === Abstract Adapter (Misc Support) =========================== #
230
313
 
231
314
  def tables_with_referential_integrity
@@ -375,7 +458,9 @@ module ActiveRecord
375
458
 
376
459
  def translate_exception(e, message:, sql:, binds:)
377
460
  case message
378
- when /(cannot insert duplicate key .* with unique index) | (violation of unique key constraint)/i
461
+ when /(SQL Server client is not connected)|(failed to execute statement)/i
462
+ ConnectionNotEstablished.new(message)
463
+ when /(cannot insert duplicate key .* with unique index) | (violation of (unique|primary) key constraint)/i
379
464
  RecordNotUnique.new(message, sql: sql, binds: binds)
380
465
  when /(conflicted with the foreign key constraint) | (The DELETE statement conflicted with the REFERENCE constraint)/i
381
466
  InvalidForeignKey.new(message, sql: sql, binds: binds)
@@ -408,78 +493,16 @@ module ActiveRecord
408
493
 
409
494
  # === SQLServer Specific (Connection Management) ================ #
410
495
 
411
- def connect
412
- config = @connection_options
413
- @connection = case config[:mode]
414
- when :dblib
415
- dblib_connect(config)
416
- end
417
- @spid = _raw_select("SELECT @@SPID", fetch: :rows).first.first
418
- @version_year = version_year
419
- configure_connection
420
- end
421
-
422
496
  def connection_errors
423
497
  @connection_errors ||= [].tap do |errors|
424
498
  errors << TinyTds::Error if defined?(TinyTds::Error)
425
499
  end
426
500
  end
427
501
 
428
- def dblib_connect(config)
429
- TinyTds::Client.new(
430
- dataserver: config[:dataserver],
431
- host: config[:host],
432
- port: config[:port],
433
- username: config[:username],
434
- password: config[:password],
435
- database: config[:database],
436
- tds_version: config[:tds_version] || "7.3",
437
- appname: config_appname(config),
438
- login_timeout: config_login_timeout(config),
439
- timeout: config_timeout(config),
440
- encoding: config_encoding(config),
441
- azure: config[:azure],
442
- contained: config[:contained]
443
- ).tap do |client|
444
- if config[:azure]
445
- client.execute("SET ANSI_NULLS ON").do
446
- client.execute("SET ANSI_NULL_DFLT_ON ON").do
447
- client.execute("SET ANSI_PADDING ON").do
448
- client.execute("SET ANSI_WARNINGS ON").do
449
- else
450
- client.execute("SET ANSI_DEFAULTS ON").do
451
- end
452
- client.execute("SET QUOTED_IDENTIFIER ON").do
453
- client.execute("SET CURSOR_CLOSE_ON_COMMIT OFF").do
454
- client.execute("SET IMPLICIT_TRANSACTIONS OFF").do
455
- client.execute("SET TEXTSIZE 2147483647").do
456
- client.execute("SET CONCAT_NULL_YIELDS_NULL ON").do
457
- end
458
- end
459
-
460
- def config_appname(config)
461
- config[:appname] || configure_application_name || Rails.application.class.name.split("::").first rescue nil
462
- end
463
-
464
- def config_login_timeout(config)
465
- config[:login_timeout].present? ? config[:login_timeout].to_i : nil
466
- end
467
-
468
- def config_timeout(config)
469
- config[:timeout].present? ? config[:timeout].to_i / 1000 : nil
470
- end
471
-
472
- def config_encoding(config)
473
- config[:encoding].present? ? config[:encoding] : nil
474
- end
475
-
476
- def configure_connection; end
477
-
478
- def configure_application_name; end
479
-
480
502
  def initialize_dateformatter
481
503
  @database_dateformat = user_options_dateformat
482
504
  a, b, c = @database_dateformat.each_char.to_a
505
+
483
506
  [a, b, c].each { |f| f.upcase! if f == "y" }
484
507
  dateformat = "%#{a}-%#{b}-%#{c}"
485
508
  ::Date::DATE_FORMATS[:_sqlserver_dateformat] = dateformat
@@ -502,6 +525,13 @@ module ActiveRecord
502
525
  def sqlserver_version
503
526
  @sqlserver_version ||= _raw_select("SELECT @@version", fetch: :rows).first.first.to_s
504
527
  end
528
+
529
+ private
530
+
531
+ def connect
532
+ @connection = self.class.new_client(@connection_options)
533
+ configure_connection
534
+ end
505
535
  end
506
536
  end
507
537
  end