activerecord-sqlserver-adapter 7.0.7 → 7.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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)