activerecord-sqlserver-adapter 7.0.6 → 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 -3
  3. data/CHANGELOG.md +2 -88
  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 +29 -6
  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 +14 -8
@@ -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