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
@@ -4,7 +4,7 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module SQLServer
6
6
  module DatabaseStatements
7
- READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :dbcc, :explain, :save, :select, :set, :rollback, :waitfor) # :nodoc:
7
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :dbcc, :explain, :save, :select, :set, :rollback, :waitfor, :use) # :nodoc:
8
8
  private_constant :READ_QUERY
9
9
 
10
10
  def write_query?(sql) # :nodoc:
@@ -13,40 +13,56 @@ module ActiveRecord
13
13
  !READ_QUERY.match?(sql.b)
14
14
  end
15
15
 
16
- def execute(sql, name = nil)
17
- sql = transform_query(sql)
18
- if preventing_writes? && write_query?(sql)
19
- raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
20
- end
16
+ def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
17
+ result = nil
21
18
 
22
- materialize_transactions
23
- mark_transaction_written_if_write(sql)
24
-
25
- if id_insert_table_name = query_requires_identity_insert?(sql)
26
- with_identity_insert_enabled(id_insert_table_name) { do_execute(sql, name) }
27
- else
28
- do_execute(sql, name)
19
+ log(sql, name, async: async) do
20
+ with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
21
+ result = if id_insert_table_name = query_requires_identity_insert?(sql)
22
+ with_identity_insert_enabled(id_insert_table_name, conn) { internal_raw_execute(sql, conn, perform_do: true) }
23
+ else
24
+ internal_raw_execute(sql, conn, perform_do: true)
25
+ end
26
+ end
29
27
  end
28
+
29
+ result
30
30
  end
31
31
 
32
- def exec_query(sql, name = "SQL", binds = [], prepare: false, async: false)
32
+ def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false)
33
+ result = nil
33
34
  sql = transform_query(sql)
34
- if preventing_writes? && write_query?(sql)
35
- raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
36
- end
37
35
 
38
- materialize_transactions
36
+ check_if_write_query(sql)
39
37
  mark_transaction_written_if_write(sql)
40
38
 
41
- sp_executesql(sql, name, binds, prepare: prepare, async: async)
42
- end
39
+ unless without_prepared_statement?(binds)
40
+ types, params = sp_executesql_types_and_parameters(binds)
41
+ sql = sp_executesql_sql(sql, types, params, name)
42
+ end
43
43
 
44
- def exec_insert(sql, name = nil, binds = [], pk = nil, _sequence_name = nil)
45
- if id_insert_table_name = exec_insert_requires_identity?(sql, pk, binds)
46
- with_identity_insert_enabled(id_insert_table_name) { super(sql, name, binds, pk) }
47
- else
48
- super(sql, name, binds, pk)
44
+ log(sql, name, binds, async: async) do
45
+ with_raw_connection do |conn|
46
+ begin
47
+ options = { ar_result: true }
48
+
49
+ # TODO: Look into refactoring this.
50
+ if id_insert_table_name = query_requires_identity_insert?(sql)
51
+ with_identity_insert_enabled(id_insert_table_name, conn) do
52
+ handle = internal_raw_execute(sql, conn)
53
+ result = handle_to_names_and_values(handle, options)
54
+ end
55
+ else
56
+ handle = internal_raw_execute(sql, conn)
57
+ result = handle_to_names_and_values(handle, options)
58
+ end
59
+ ensure
60
+ finish_statement_handle(handle)
61
+ end
62
+ end
49
63
  end
64
+
65
+ result
50
66
  end
51
67
 
52
68
  def exec_delete(sql, name, binds)
@@ -60,7 +76,7 @@ module ActiveRecord
60
76
  end
61
77
 
62
78
  def begin_db_transaction
63
- do_execute "BEGIN TRANSACTION", "TRANSACTION"
79
+ internal_execute("BEGIN TRANSACTION", "TRANSACTION", allow_retry: true, materialize_transactions: false)
64
80
  end
65
81
 
66
82
  def transaction_isolation_levels
@@ -68,33 +84,20 @@ module ActiveRecord
68
84
  end
69
85
 
70
86
  def begin_isolated_db_transaction(isolation)
71
- set_transaction_isolation_level transaction_isolation_levels.fetch(isolation)
87
+ set_transaction_isolation_level(transaction_isolation_levels.fetch(isolation))
72
88
  begin_db_transaction
73
89
  end
74
90
 
75
91
  def set_transaction_isolation_level(isolation_level)
76
- do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}", "TRANSACTION"
92
+ internal_execute("SET TRANSACTION ISOLATION LEVEL #{isolation_level}", "TRANSACTION", allow_retry: true, materialize_transactions: false)
77
93
  end
78
94
 
79
95
  def commit_db_transaction
80
- do_execute "COMMIT TRANSACTION", "TRANSACTION"
96
+ internal_execute("COMMIT TRANSACTION", "TRANSACTION", allow_retry: false, materialize_transactions: true)
81
97
  end
82
98
 
83
99
  def exec_rollback_db_transaction
84
- do_execute "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION", "TRANSACTION"
85
- end
86
-
87
- include Savepoints
88
-
89
- def create_savepoint(name = current_savepoint_name)
90
- do_execute "SAVE TRANSACTION #{name}", "TRANSACTION"
91
- end
92
-
93
- def exec_rollback_to_savepoint(name = current_savepoint_name)
94
- do_execute "ROLLBACK TRANSACTION #{name}", "TRANSACTION"
95
- end
96
-
97
- def release_savepoint(name = current_savepoint_name)
100
+ internal_execute("IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION", "TRANSACTION", allow_retry: false, materialize_transactions: true)
98
101
  end
99
102
 
100
103
  def case_sensitive_comparison(attribute, value)
@@ -164,42 +167,42 @@ module ActiveRecord
164
167
  # === SQLServer Specific ======================================== #
165
168
 
166
169
  def execute_procedure(proc_name, *variables)
167
- materialize_transactions
168
-
169
170
  vars = if variables.any? && variables.first.is_a?(Hash)
170
171
  variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
171
172
  else
172
173
  variables.map { |v| quote(v) }
173
174
  end.join(", ")
174
175
  sql = "EXEC #{proc_name} #{vars}".strip
175
- name = "Execute Procedure"
176
- log(sql, name) do
177
- case @connection_options[:mode]
178
- when :dblib
179
- result = ensure_established_connection! { dblib_execute(sql) }
176
+
177
+ log(sql, "Execute Procedure") do
178
+ with_raw_connection do |conn|
179
+ result = internal_raw_execute(sql, conn)
180
180
  options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }
181
+
181
182
  result.each(options) do |row|
182
183
  r = row.with_indifferent_access
183
184
  yield(r) if block_given?
184
185
  end
186
+
185
187
  result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
186
188
  end
187
189
  end
190
+
188
191
  end
189
192
 
190
- def with_identity_insert_enabled(table_name)
193
+ def with_identity_insert_enabled(table_name, conn)
191
194
  table_name = quote_table_name(table_name)
192
- set_identity_insert(table_name, true)
195
+ set_identity_insert(table_name, conn, true)
193
196
  yield
194
197
  ensure
195
- set_identity_insert(table_name, false)
198
+ set_identity_insert(table_name, conn, false)
196
199
  end
197
200
 
198
201
  def use_database(database = nil)
199
202
  return if sqlserver_azure?
200
203
 
201
- name = SQLServer::Utils.extract_identifiers(database || @connection_options[:database]).quoted
202
- do_execute "USE #{name}" unless name.blank?
204
+ name = SQLServer::Utils.extract_identifiers(database || @connection_parameters[:database]).quoted
205
+ execute("USE #{name}", "SCHEMA") unless name.blank?
203
206
  end
204
207
 
205
208
  def user_options
@@ -263,18 +266,19 @@ module ActiveRecord
263
266
 
264
267
  protected
265
268
 
266
- def sql_for_insert(sql, pk, binds)
269
+ def sql_for_insert(sql, pk, binds, returning)
267
270
  if pk.nil?
268
271
  table_name = query_requires_identity_insert?(sql)
269
272
  pk = primary_key(table_name)
270
273
  end
271
274
 
272
275
  sql = if pk && use_output_inserted? && !database_prefix_remote_server?
273
- quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted
274
276
  table_name ||= get_table_name(sql)
275
277
  exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
276
278
 
277
279
  if exclude_output_inserted
280
+ quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted
281
+
278
282
  id_sql_type = exclude_output_inserted.is_a?(TrueClass) ? "bigint" : exclude_output_inserted
279
283
  <<~SQL.squish
280
284
  DECLARE @ssaIdInsertTable table (#{quoted_pk} #{id_sql_type});
@@ -282,40 +286,32 @@ module ActiveRecord
282
286
  SELECT CAST(#{quoted_pk} AS #{id_sql_type}) FROM @ssaIdInsertTable
283
287
  SQL
284
288
  else
285
- sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT INSERTED.#{quoted_pk}"
289
+ returning_columns = returning || Array(pk)
290
+
291
+ if returning_columns.any?
292
+ returning_columns_statements = returning_columns.map { |c| " INSERTED.#{SQLServer::Utils.extract_identifiers(c).quoted}" }
293
+ sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT" + returning_columns_statements.join(",")
294
+ else
295
+ sql
296
+ end
286
297
  end
287
298
  else
288
299
  "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
289
300
  end
290
- super
301
+
302
+ [sql, binds]
291
303
  end
292
304
 
293
305
  # === SQLServer Specific ======================================== #
294
306
 
295
- def set_identity_insert(table_name, enable = true)
296
- do_execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
307
+ def set_identity_insert(table_name, conn, enable)
308
+ internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true)
297
309
  rescue Exception
298
310
  raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
299
311
  end
300
312
 
301
313
  # === SQLServer Specific (Executing) ============================ #
302
314
 
303
- def do_execute(sql, name = "SQL")
304
- materialize_transactions
305
- mark_transaction_written_if_write(sql)
306
-
307
- log(sql, name) { raw_connection_do(sql) }
308
- end
309
-
310
- def sp_executesql(sql, name, binds, options = {})
311
- options[:ar_result] = true if options[:fetch] != :rows
312
- unless without_prepared_statement?(binds)
313
- types, params = sp_executesql_types_and_parameters(binds)
314
- sql = sp_executesql_sql(sql, types, params, name)
315
- end
316
- raw_select sql, name, binds, options
317
- end
318
-
319
315
  def sp_executesql_types_and_parameters(binds)
320
316
  types, params = [], []
321
317
  binds.each_with_index do |attr, index|
@@ -328,6 +324,7 @@ module ActiveRecord
328
324
  end
329
325
 
330
326
  def sp_executesql_sql_type(attr)
327
+ return "nvarchar(max)".freeze if attr.is_a?(Symbol)
331
328
  return attr.type.sqlserver_type if attr.type.respond_to?(:sqlserver_type)
332
329
 
333
330
  case value = attr.value_for_database
@@ -339,6 +336,8 @@ module ActiveRecord
339
336
  end
340
337
 
341
338
  def sp_executesql_sql_param(attr)
339
+ return quote(attr) if attr.is_a?(Symbol)
340
+
342
341
  case value = attr.value_for_database
343
342
  when Type::Binary::Data,
344
343
  ActiveRecord::Type::SQLServer::Data
@@ -363,16 +362,6 @@ module ActiveRecord
363
362
  sql.freeze
364
363
  end
365
364
 
366
- def raw_connection_do(sql)
367
- case @connection_options[:mode]
368
- when :dblib
369
- result = ensure_established_connection! { dblib_execute(sql) }
370
- result.do
371
- end
372
- ensure
373
- @update_sql = false
374
- end
375
-
376
365
  # === SQLServer Specific (Identity Inserts) ===================== #
377
366
 
378
367
  def use_output_inserted?
@@ -392,10 +381,6 @@ module ActiveRecord
392
381
  self.class.exclude_output_inserted_table_names[table_name]
393
382
  end
394
383
 
395
- def exec_insert_requires_identity?(sql, _pk, _binds)
396
- query_requires_identity_insert?(sql)
397
- end
398
-
399
384
  def query_requires_identity_insert?(sql)
400
385
  return false unless insert_sql?(sql)
401
386
 
@@ -415,68 +400,39 @@ module ActiveRecord
415
400
 
416
401
  # === SQLServer Specific (Selecting) ============================ #
417
402
 
418
- def raw_select(sql, name = "SQL", binds = [], options = {})
419
- log(sql, name, binds, async: options[:async]) { _raw_select(sql, options) }
420
- end
403
+ def _raw_select(sql, conn, options = {})
404
+ handle = internal_raw_execute(sql, conn)
421
405
 
422
- def _raw_select(sql, options = {})
423
- handle = raw_connection_run(sql)
424
406
  handle_to_names_and_values(handle, options)
425
407
  ensure
426
408
  finish_statement_handle(handle)
427
409
  end
428
410
 
429
- def raw_connection_run(sql)
430
- case @connection_options[:mode]
431
- when :dblib
432
- ensure_established_connection! { dblib_execute(sql) }
433
- end
434
- end
435
-
436
- def handle_more_results?(handle)
437
- case @connection_options[:mode]
438
- when :dblib
439
- end
440
- end
441
-
442
411
  def handle_to_names_and_values(handle, options = {})
443
- case @connection_options[:mode]
444
- when :dblib
445
- handle_to_names_and_values_dblib(handle, options)
446
- end
447
- end
448
-
449
- def handle_to_names_and_values_dblib(handle, options = {})
450
412
  query_options = {}.tap do |qo|
451
413
  qo[:timezone] = ActiveRecord.default_timezone || :utc
452
414
  qo[:as] = (options[:ar_result] || options[:fetch] == :rows) ? :array : :hash
453
415
  end
454
416
  results = handle.each(query_options)
455
417
  columns = lowercase_schema_reflection ? handle.fields.map { |c| c.downcase } : handle.fields
418
+
456
419
  options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
457
420
  end
458
421
 
459
422
  def finish_statement_handle(handle)
460
- case @connection_options[:mode]
461
- when :dblib
462
- handle.cancel if handle
463
- end
423
+ handle.cancel if handle
464
424
  handle
465
425
  end
466
426
 
467
- def dblib_execute(sql)
468
- @connection.execute(sql).tap do |result|
469
- # TinyTDS returns false instead of raising an exception if connection fails.
470
- # Getting around this by raising an exception ourselves while this PR
471
- # https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
472
- raise TinyTds::Error, "failed to execute statement" if result.is_a?(FalseClass)
427
+ # TinyTDS returns false instead of raising an exception if connection fails.
428
+ # Getting around this by raising an exception ourselves while PR
429
+ # https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
430
+ def internal_raw_execute(sql, conn, perform_do: false)
431
+ result = conn.execute(sql).tap do |_result|
432
+ raise TinyTds::Error, "failed to execute statement" if _result.is_a?(FalseClass)
473
433
  end
474
- end
475
434
 
476
- def ensure_established_connection!
477
- raise TinyTds::Error, 'SQL Server client is not connected' unless @connection
478
-
479
- yield
435
+ perform_do ? result.do : result
480
436
  end
481
437
  end
482
438
  end
@@ -8,13 +8,13 @@ module ActiveRecord
8
8
  name = SQLServer::Utils.extract_identifiers(database)
9
9
  db_options = create_database_options(options)
10
10
  edition_options = create_database_edition_options(options)
11
- do_execute "CREATE DATABASE #{name} #{db_options} #{edition_options}"
11
+ execute "CREATE DATABASE #{name} #{db_options} #{edition_options}"
12
12
  end
13
13
 
14
14
  def drop_database(database)
15
15
  name = SQLServer::Utils.extract_identifiers(database)
16
- do_execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
17
- do_execute "DROP DATABASE #{name}"
16
+ execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
17
+ execute "DROP DATABASE #{name}"
18
18
  end
19
19
 
20
20
  def current_database
@@ -33,7 +33,7 @@ module ActiveRecord
33
33
 
34
34
  def create_database_options(options = {})
35
35
  keys = [:collate]
36
- copts = @connection_options
36
+ copts = @connection_parameters
37
37
  options = {
38
38
  collate: copts[:collation]
39
39
  }.merge(options.symbolize_keys).select { |_, v|
@@ -46,7 +46,7 @@ module ActiveRecord
46
46
 
47
47
  def create_database_edition_options(options = {})
48
48
  keys = [:maxsize, :edition, :service_objective]
49
- copts = @connection_options
49
+ copts = @connection_parameters
50
50
  edition_options = {
51
51
  maxsize: copts[:azure_maxsize],
52
52
  edition: copts[:azure_edition],
@@ -85,7 +85,7 @@ module ActiveRecord
85
85
  (
86
86
  (?:
87
87
  # [database_name].[database_owner].[table_name].[column_name] | function(one or no argument)
88
- ((?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\])) | \w+\((?:|\g<2>)\)
88
+ ((?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\]) | \w+\((?:|\g<2>)\))
89
89
  )
90
90
  (?:\s+AS\s+(?:\w+|\[\w+\]))?
91
91
  )
@@ -98,8 +98,9 @@ module ActiveRecord
98
98
  (
99
99
  (?:
100
100
  # [database_name].[database_owner].[table_name].[column_name] | function(one or no argument)
101
- ((?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\])) | \w+\((?:|\g<2>)\)
101
+ ((?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\]) | \w+\((?:|\g<2>)\))
102
102
  )
103
+ (?:\s+COLLATE\s+\w+)?
103
104
  (?:\s+ASC|\s+DESC)?
104
105
  (?:\s+NULLS\s+(?:FIRST|LAST))?
105
106
  )
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module SQLServer
6
+ module Savepoints
7
+ def current_savepoint_name
8
+ current_transaction.savepoint_name
9
+ end
10
+
11
+ def create_savepoint(name = current_savepoint_name)
12
+ internal_execute("SAVE TRANSACTION #{name}", "TRANSACTION")
13
+ end
14
+
15
+ def exec_rollback_to_savepoint(name = current_savepoint_name)
16
+ internal_execute("ROLLBACK TRANSACTION #{name}", "TRANSACTION")
17
+ end
18
+
19
+ def release_savepoint(_name)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end