activerecord-sqlserver-adapter 7.0.7 → 7.1.0.beta1

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 +3 -2
  3. data/CHANGELOG.md +2 -94
  4. data/Gemfile +3 -0
  5. data/README.md +16 -11
  6. data/Rakefile +2 -6
  7. data/VERSION +1 -1
  8. data/activerecord-sqlserver-adapter.gemspec +1 -1
  9. data/lib/active_record/connection_adapters/sqlserver/core_ext/abstract_adapter.rb +20 -0
  10. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +42 -0
  11. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +4 -4
  12. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +10 -2
  13. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +15 -3
  14. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -31
  15. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +87 -131
  16. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +5 -5
  17. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +3 -2
  18. data/lib/active_record/connection_adapters/sqlserver/savepoints.rb +24 -0
  19. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +71 -58
  20. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +3 -3
  21. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +6 -0
  22. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +4 -6
  23. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +10 -0
  24. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +81 -118
  25. data/lib/active_record/connection_adapters/sqlserver_column.rb +1 -0
  26. data/lib/active_record/sqlserver_base.rb +1 -10
  27. data/lib/active_record/tasks/sqlserver_database_tasks.rb +5 -2
  28. data/lib/arel/visitors/sqlserver.rb +0 -33
  29. data/test/cases/adapter_test_sqlserver.rb +8 -7
  30. data/test/cases/coerced_tests.rb +558 -248
  31. data/test/cases/column_test_sqlserver.rb +6 -6
  32. data/test/cases/connection_test_sqlserver.rb +3 -6
  33. data/test/cases/disconnected_test_sqlserver.rb +5 -8
  34. data/test/cases/execute_procedure_test_sqlserver.rb +1 -1
  35. data/test/cases/rake_test_sqlserver.rb +1 -1
  36. data/test/cases/schema_dumper_test_sqlserver.rb +2 -2
  37. data/test/cases/view_test_sqlserver.rb +6 -10
  38. data/test/config.yml +1 -2
  39. data/test/support/connection_reflection.rb +2 -8
  40. data/test/support/core_ext/query_cache.rb +7 -1
  41. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
  42. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic.dump +0 -0
  43. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic_associations.dump +0 -0
  44. metadata +15 -9
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "tiny_tds"
3
4
  require "base64"
4
5
  require "active_record"
5
6
  require "arel_sqlserver"
@@ -11,11 +12,13 @@ require "active_record/connection_adapters/sqlserver/core_ext/explain_subscriber
11
12
  require "active_record/connection_adapters/sqlserver/core_ext/attribute_methods"
12
13
  require "active_record/connection_adapters/sqlserver/core_ext/finder_methods"
13
14
  require "active_record/connection_adapters/sqlserver/core_ext/preloader"
15
+ require "active_record/connection_adapters/sqlserver/core_ext/abstract_adapter"
14
16
  require "active_record/connection_adapters/sqlserver/version"
15
17
  require "active_record/connection_adapters/sqlserver/type"
16
18
  require "active_record/connection_adapters/sqlserver/database_limits"
17
19
  require "active_record/connection_adapters/sqlserver/database_statements"
18
20
  require "active_record/connection_adapters/sqlserver/database_tasks"
21
+ require "active_record/connection_adapters/sqlserver/savepoints"
19
22
  require "active_record/connection_adapters/sqlserver/transaction"
20
23
  require "active_record/connection_adapters/sqlserver/errors"
21
24
  require "active_record/connection_adapters/sqlserver/schema_creation"
@@ -39,7 +42,8 @@ module ActiveRecord
39
42
  SQLServer::Showplan,
40
43
  SQLServer::SchemaStatements,
41
44
  SQLServer::DatabaseLimits,
42
- SQLServer::DatabaseTasks
45
+ SQLServer::DatabaseTasks,
46
+ SQLServer::Savepoints
43
47
 
44
48
  ADAPTER_NAME = "SQLServer".freeze
45
49
 
@@ -77,93 +81,38 @@ module ActiveRecord
77
81
  end
78
82
 
79
83
  def new_client(config)
80
- case config[:mode]
81
- when :dblib
82
- require "tiny_tds"
83
- dblib_connect(config)
84
+ TinyTds::Client.new(config)
85
+ rescue TinyTds::Error => error
86
+ if error.message.match(/database .* does not exist/i)
87
+ raise ActiveRecord::NoDatabaseError
84
88
  else
85
- raise ArgumentError, "Unknown connection mode in #{config.inspect}."
89
+ raise
86
90
  end
87
91
  end
88
92
 
89
- def dblib_connect(config)
90
- TinyTds::Client.new(
91
- dataserver: config[:dataserver],
92
- host: config[:host],
93
- port: config[:port],
94
- username: config[:username],
95
- password: config[:password],
96
- database: config[:database],
97
- tds_version: config[:tds_version] || "7.3",
98
- appname: config_appname(config),
99
- login_timeout: config_login_timeout(config),
100
- timeout: config_timeout(config),
101
- encoding: config_encoding(config),
102
- azure: config[:azure],
103
- contained: config[:contained]
104
- ).tap do |client|
105
- if config[:azure]
106
- client.execute("SET ANSI_NULLS ON").do
107
- client.execute("SET ANSI_NULL_DFLT_ON ON").do
108
- client.execute("SET ANSI_PADDING ON").do
109
- client.execute("SET ANSI_WARNINGS ON").do
110
- else
111
- client.execute("SET ANSI_DEFAULTS ON").do
112
- end
113
- client.execute("SET QUOTED_IDENTIFIER ON").do
114
- client.execute("SET CURSOR_CLOSE_ON_COMMIT OFF").do
115
- client.execute("SET IMPLICIT_TRANSACTIONS OFF").do
116
- client.execute("SET TEXTSIZE 2147483647").do
117
- client.execute("SET CONCAT_NULL_YIELDS_NULL ON").do
118
- end
119
- rescue TinyTds::Error => e
120
- raise ActiveRecord::NoDatabaseError if e.message.match(/database .* does not exist/i)
121
- raise e
122
- end
123
-
124
- def config_appname(config)
125
- if instance_methods.include?(:configure_application_name)
126
- ActiveSupport::Deprecation.warn <<~MSG.squish
127
- Configuring the application name used by TinyTDS by overriding the
128
- `ActiveRecord::ConnectionAdapters::SQLServerAdapter#configure_application_name`
129
- instance method is no longer supported. The application name should configured
130
- using the `appname` setting in the `database.yml` file instead. Consult the
131
- README for further information."
132
- MSG
133
- end
134
-
135
- config[:appname] || rails_application_name
136
- end
137
-
138
93
  def rails_application_name
139
94
  Rails.application.class.name.split("::").first
140
95
  rescue
141
96
  nil # Might not be in a Rails context so we fallback to `nil`.
142
97
  end
98
+ end
143
99
 
144
- def config_login_timeout(config)
145
- config[:login_timeout].present? ? config[:login_timeout].to_i : nil
146
- end
147
-
148
- def config_timeout(config)
149
- config[:timeout].present? ? config[:timeout].to_i / 1000 : nil
150
- end
100
+ def initialize(...)
101
+ super
151
102
 
152
- def config_encoding(config)
153
- config[:encoding].present? ? config[:encoding] : nil
154
- end
155
- end
103
+ @config[:tds_version] = "7.3" unless @config[:tds_version]
104
+ @config[:appname] = self.class.rails_application_name unless @config[:appname]
105
+ @config[:login_timeout] = @config[:login_timeout].present? ? @config[:login_timeout].to_i : nil
106
+ @config[:timeout] = @config[:timeout].present? ? @config[:timeout].to_i / 1000 : nil
107
+ @config[:encoding] = @config[:encoding].present? ? @config[:encoding] : nil
156
108
 
157
- def initialize(connection, logger, _connection_options, config)
158
- super(connection, logger, config)
159
- @connection_options = config
160
- perform_connection_configuration
109
+ @connection_parameters ||= @config
161
110
  end
162
111
 
163
112
  # === Abstract Adapter ========================================== #
164
113
 
165
114
  def arel_visitor
166
- Arel::Visitors::SQLServer.new self
115
+ Arel::Visitors::SQLServer.new(self)
167
116
  end
168
117
 
169
118
  def valid_type?(type)
@@ -171,13 +120,7 @@ module ActiveRecord
171
120
  end
172
121
 
173
122
  def schema_creation
174
- SQLServer::SchemaCreation.new self
175
- end
176
-
177
- def self.database_exists?(config)
178
- !!ActiveRecord::Base.sqlserver_connection(config)
179
- rescue ActiveRecord::NoDatabaseError
180
- false
123
+ SQLServer::SchemaCreation.new(self)
181
124
  end
182
125
 
183
126
  def supports_ddl_transactions?
@@ -228,12 +171,8 @@ module ActiveRecord
228
171
  true
229
172
  end
230
173
 
231
- def supports_check_constraints?
232
- true
233
- end
234
-
235
174
  def supports_json?
236
- @version_year >= 2016
175
+ version_year >= 2016
237
176
  end
238
177
 
239
178
  def supports_comments?
@@ -252,12 +191,16 @@ module ActiveRecord
252
191
  true
253
192
  end
254
193
 
194
+ def supports_common_table_expressions?
195
+ true
196
+ end
197
+
255
198
  def supports_lazy_transactions?
256
199
  true
257
200
  end
258
201
 
259
202
  def supports_in_memory_oltp?
260
- @version_year >= 2014
203
+ version_year >= 2014
261
204
  end
262
205
 
263
206
  def supports_insert_returning?
@@ -276,50 +219,52 @@ module ActiveRecord
276
219
  false
277
220
  end
278
221
 
222
+ def return_value_after_insert?(column) # :nodoc:
223
+ column.is_primary? || column.is_identity?
224
+ end
225
+
279
226
  def disable_referential_integrity
280
227
  tables = tables_with_referential_integrity
281
- tables.each { |t| do_execute "ALTER TABLE #{quote_table_name(t)} NOCHECK CONSTRAINT ALL" }
228
+ tables.each { |t| execute "ALTER TABLE #{quote_table_name(t)} NOCHECK CONSTRAINT ALL" }
282
229
  yield
283
230
  ensure
284
- tables.each { |t| do_execute "ALTER TABLE #{quote_table_name(t)} CHECK CONSTRAINT ALL" }
231
+ tables.each { |t| execute "ALTER TABLE #{quote_table_name(t)} CHECK CONSTRAINT ALL" }
285
232
  end
286
233
 
287
234
  # === Abstract Adapter (Connection Management) ================== #
288
235
 
289
236
  def active?
290
- return false unless @connection
291
-
292
- raw_connection_do "SELECT 1"
293
- true
237
+ @raw_connection&.active?
294
238
  rescue *connection_errors
295
239
  false
296
240
  end
297
241
 
298
- def reconnect!
299
- super
300
- disconnect!
242
+ def reconnect
243
+ @raw_connection&.close rescue nil
244
+ @raw_connection = nil
245
+ @spid = nil
246
+ @collation = nil
247
+
301
248
  connect
302
249
  end
303
250
 
304
251
  def disconnect!
305
252
  super
306
- case @connection_options[:mode]
307
- when :dblib
308
- @connection.close rescue nil
309
- end
310
- @connection = nil
253
+
254
+ @raw_connection&.close rescue nil
255
+ @raw_connection = nil
311
256
  @spid = nil
312
257
  @collation = nil
313
258
  end
314
259
 
315
- def clear_cache!
260
+ def clear_cache!(...)
316
261
  @view_information = nil
317
262
  super
318
263
  end
319
264
 
320
265
  def reset!
321
266
  reset_transaction
322
- do_execute "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION"
267
+ execute "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION"
323
268
  end
324
269
 
325
270
  # === Abstract Adapter (Misc Support) =========================== #
@@ -360,7 +305,7 @@ module ActiveRecord
360
305
  end
361
306
 
362
307
  def database_prefix
363
- @connection_options[:database_prefix]
308
+ @connection_parameters[:database_prefix]
364
309
  end
365
310
 
366
311
  def database_prefix_identifier(name)
@@ -376,7 +321,7 @@ module ActiveRecord
376
321
  end
377
322
 
378
323
  def inspect
379
- "#<#{self.class} version: #{version}, mode: #{@connection_options[:mode]}, azure: #{sqlserver_azure?.inspect}>"
324
+ "#<#{self.class} version: #{version}, azure: #{sqlserver_azure?.inspect}>"
380
325
  end
381
326
 
382
327
  def combine_bind_parameters(from_clause: [], join_clause: [], where_clause: [], having_clause: [], limit: nil, offset: nil)
@@ -390,6 +335,12 @@ module ActiveRecord
390
335
  version_year
391
336
  end
392
337
 
338
+ def check_version # :nodoc:
339
+ if schema_cache.database_version < 2012
340
+ raise "Your version of SQL Server (#{database_version}) is too old. SQL Server Active Record supports 2012 or higher."
341
+ end
342
+ end
343
+
393
344
  class << self
394
345
  protected
395
346
 
@@ -517,7 +468,7 @@ module ActiveRecord
517
468
  # === SQLServer Specific (Connection Management) ================ #
518
469
 
519
470
  def connection_errors
520
- @connection_errors ||= [].tap do |errors|
471
+ @raw_connection_errors ||= [].tap do |errors|
521
472
  errors << TinyTds::Error if defined?(TinyTds::Error)
522
473
  end
523
474
  end
@@ -538,32 +489,44 @@ module ActiveRecord
538
489
  end
539
490
 
540
491
  def version_year
541
- return 2016 if sqlserver_version =~ /vNext/
542
-
543
- /SQL Server (\d+)/.match(sqlserver_version).to_a.last.to_s.to_i
544
- rescue StandardError
545
- 2016
492
+ @version_year ||= begin
493
+ if sqlserver_version =~ /vNext/
494
+ 2016
495
+ else
496
+ /SQL Server (\d+)/.match(sqlserver_version).to_a.last.to_s.to_i
497
+ end
498
+ rescue StandardError
499
+ 2016
500
+ end
546
501
  end
547
502
 
548
503
  def sqlserver_version
549
- @sqlserver_version ||= _raw_select("SELECT @@version", fetch: :rows).first.first.to_s
504
+ @sqlserver_version ||= _raw_select("SELECT @@version", @raw_connection, fetch: :rows).first.first.to_s
550
505
  end
551
506
 
552
507
  private
553
508
 
554
509
  def connect
555
- @connection = self.class.new_client(@connection_options)
556
- perform_connection_configuration
510
+ @raw_connection = self.class.new_client(@connection_parameters)
557
511
  end
558
512
 
559
- def perform_connection_configuration
560
- configure_connection_defaults
561
- configure_connection if self.respond_to?(:configure_connection)
562
- end
513
+ def configure_connection
514
+ if @config[:azure]
515
+ @raw_connection.execute("SET ANSI_NULLS ON").do
516
+ @raw_connection.execute("SET ANSI_NULL_DFLT_ON ON").do
517
+ @raw_connection.execute("SET ANSI_PADDING ON").do
518
+ @raw_connection.execute("SET ANSI_WARNINGS ON").do
519
+ else
520
+ @raw_connection.execute("SET ANSI_DEFAULTS ON").do
521
+ end
522
+
523
+ @raw_connection.execute("SET QUOTED_IDENTIFIER ON").do
524
+ @raw_connection.execute("SET CURSOR_CLOSE_ON_COMMIT OFF").do
525
+ @raw_connection.execute("SET IMPLICIT_TRANSACTIONS OFF").do
526
+ @raw_connection.execute("SET TEXTSIZE 2147483647").do
527
+ @raw_connection.execute("SET CONCAT_NULL_YIELDS_NULL ON").do
563
528
 
564
- def configure_connection_defaults
565
- @spid = _raw_select("SELECT @@SPID", fetch: :rows).first.first
566
- @version_year = version_year
529
+ @spid = _raw_select("SELECT @@SPID", @raw_connection, fetch: :rows).first.first
567
530
 
568
531
  initialize_dateformatter
569
532
  use_database
@@ -17,6 +17,7 @@ module ActiveRecord
17
17
  def is_identity?
18
18
  is_identity
19
19
  end
20
+ alias_method :auto_incremented_by_db?, :is_identity?
20
21
 
21
22
  def is_primary?
22
23
  is_primary
@@ -7,16 +7,7 @@ module ActiveRecord
7
7
  end
8
8
 
9
9
  def sqlserver_connection(config) #:nodoc:
10
- config = config.symbolize_keys
11
- config.reverse_merge!(mode: :dblib)
12
- config[:mode] = config[:mode].to_s.downcase.underscore.to_sym
13
-
14
- sqlserver_adapter_class.new(
15
- sqlserver_adapter_class.new_client(config),
16
- logger,
17
- nil,
18
- config
19
- )
10
+ sqlserver_adapter_class.new(config)
20
11
  end
21
12
  end
22
13
  end
@@ -10,8 +10,7 @@ module ActiveRecord
10
10
  class SQLServerDatabaseTasks
11
11
  DEFAULT_COLLATION = "SQL_Latin1_General_CP1_CI_AS"
12
12
 
13
- delegate :connection, :establish_connection, :clear_active_connections!,
14
- to: ActiveRecord::Base
13
+ delegate :connection, :establish_connection, to: ActiveRecord::Base
15
14
 
16
15
  def self.using_database_configurations?
17
16
  true
@@ -53,6 +52,10 @@ module ActiveRecord
53
52
  create true
54
53
  end
55
54
 
55
+ def clear_active_connections!
56
+ ActiveRecord::Base.connection_handler.clear_active_connections!
57
+ end
58
+
56
59
  def structure_dump(filename, extra_flags)
57
60
  server_arg = "-S #{Shellwords.escape(configuration_hash[:host])}"
58
61
  server_arg += ":#{Shellwords.escape(configuration_hash[:port])}" if configuration_hash[:port]
@@ -64,39 +64,6 @@ module Arel
64
64
  super
65
65
  end
66
66
 
67
- def visit_Arel_Nodes_HomogeneousIn(o, collector)
68
- collector.preparable = false
69
-
70
- collector << quote_table_name(o.table_name) << "." << quote_column_name(o.column_name)
71
-
72
- if o.type == :in
73
- collector << " IN ("
74
- else
75
- collector << " NOT IN ("
76
- end
77
-
78
- values = o.casted_values
79
-
80
- if values.empty?
81
- collector << @connection.quote(nil)
82
- elsif @connection.prepared_statements
83
- # Monkey-patch start. Add query attribute bindings rather than just values.
84
- column_name = o.column_name
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)
88
- attrs = values.map { |value| ActiveRecord::Relation::QueryAttribute.new(column_name, value, column_type) }
89
-
90
- collector.add_binds(attrs, &bind_block)
91
- # Monkey-patch end.
92
- else
93
- collector.add_binds(values, &bind_block)
94
- end
95
-
96
- collector << ")"
97
- collector
98
- end
99
-
100
67
  def visit_Arel_Nodes_SelectStatement(o, collector)
101
68
  @select_statement = o
102
69
  distinct_One_As_One_Is_So_Not_Fetch o
@@ -19,7 +19,6 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
19
19
  string = connection.inspect
20
20
  _(string).must_match %r{ActiveRecord::ConnectionAdapters::SQLServerAdapter}
21
21
  _(string).must_match %r{version\: \d.\d}
22
- _(string).must_match %r{mode: dblib}
23
22
  _(string).must_match %r{azure: (true|false)}
24
23
  _(string).wont_match %r{host}
25
24
  _(string).wont_match %r{password}
@@ -102,16 +101,18 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
102
101
  it "test bad connection" do
103
102
  assert_raise ActiveRecord::NoDatabaseError do
104
103
  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
104
+ configuration = db_config.configuration_hash.merge(database: "nonexistent_activerecord_unittest")
105
+
106
+ connection = ActiveRecord::Base.sqlserver_connection configuration
107
+ connection.exec_query("SELECT 1")
107
108
  end
108
109
  end
109
110
 
110
111
  it "test database exists returns false if database does not exist" do
111
112
  db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
112
- configuration = db_config.configuration_hash.merge(database: "inexistent_activerecord_unittest")
113
+ configuration = db_config.configuration_hash.merge(database: "nonexistent_activerecord_unittest")
113
114
  assert_not ActiveRecord::ConnectionAdapters::SQLServerAdapter.database_exists?(configuration),
114
- "expected database to not exist"
115
+ "expected database #{configuration[:database]} to not exist"
115
116
  end
116
117
 
117
118
  it "test database exists returns true when the database exists" do
@@ -306,7 +307,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
306
307
  end
307
308
 
308
309
  describe "database statements" do
309
- it "run the database consistency checker useroptions command" do
310
+ it "run the database consistency checker 'user_options' command" do
310
311
  skip "on azure" if connection_sqlserver_azure?
311
312
  keys = [:textsize, :language, :isolation_level, :dateformat]
312
313
  user_options = connection.user_options
@@ -345,7 +346,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
345
346
  assert_equal "tinyint", connection.type_to_sql(:integer, limit: 1)
346
347
  end
347
348
 
348
- it "create bigints when limit is greateer than 4" do
349
+ it "create bigints when limit is greater than 4" do
349
350
  assert_equal "bigint", connection.type_to_sql(:integer, limit: 5)
350
351
  assert_equal "bigint", connection.type_to_sql(:integer, limit: 6)
351
352
  assert_equal "bigint", connection.type_to_sql(:integer, limit: 7)