activerecord-sqlserver-adapter 7.0.5.1 → 7.1.0.rc1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -3
  3. data/.gitignore +3 -1
  4. data/CHANGELOG.md +38 -83
  5. data/Gemfile +3 -0
  6. data/README.md +16 -11
  7. data/RUNNING_UNIT_TESTS.md +24 -10
  8. data/Rakefile +2 -6
  9. data/VERSION +1 -1
  10. data/activerecord-sqlserver-adapter.gemspec +1 -1
  11. data/lib/active_record/connection_adapters/sqlserver/core_ext/abstract_adapter.rb +20 -0
  12. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +29 -6
  13. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +4 -4
  14. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +10 -2
  15. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +15 -3
  16. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -31
  17. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +86 -133
  18. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +5 -5
  19. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +3 -2
  20. data/lib/active_record/connection_adapters/sqlserver/savepoints.rb +24 -0
  21. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +68 -29
  22. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +3 -3
  23. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +6 -0
  24. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +4 -6
  25. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +10 -0
  26. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +81 -114
  27. data/lib/active_record/connection_adapters/sqlserver_column.rb +1 -0
  28. data/lib/active_record/sqlserver_base.rb +1 -10
  29. data/lib/active_record/tasks/sqlserver_database_tasks.rb +5 -2
  30. data/lib/arel/visitors/sqlserver.rb +0 -33
  31. data/test/cases/adapter_test_sqlserver.rb +8 -7
  32. data/test/cases/coerced_tests.rb +526 -235
  33. data/test/cases/column_test_sqlserver.rb +6 -6
  34. data/test/cases/connection_test_sqlserver.rb +3 -6
  35. data/test/cases/disconnected_test_sqlserver.rb +5 -8
  36. data/test/cases/execute_procedure_test_sqlserver.rb +1 -1
  37. data/test/cases/rake_test_sqlserver.rb +1 -1
  38. data/test/cases/schema_dumper_test_sqlserver.rb +2 -2
  39. data/test/cases/transaction_test_sqlserver.rb +13 -8
  40. data/test/config.yml +1 -2
  41. data/test/support/connection_reflection.rb +2 -8
  42. data/test/support/core_ext/query_cache.rb +7 -1
  43. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
  44. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic.dump +0 -0
  45. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic_associations.dump +0 -0
  46. data/test/support/sql_counter_sqlserver.rb +0 -15
  47. 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,54 @@ 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
+ if id_insert_table_name = query_requires_identity_insert?(sql)
47
+ with_identity_insert_enabled(id_insert_table_name, conn) do
48
+ result = internal_exec_sql_query(sql, conn)
49
+ end
50
+ else
51
+ result = internal_exec_sql_query(sql, conn)
52
+ end
53
+ end
49
54
  end
55
+
56
+ result
57
+ end
58
+
59
+ def internal_exec_sql_query(sql, conn)
60
+ handle = internal_raw_execute(sql, conn)
61
+ handle_to_names_and_values(handle, ar_result: true)
62
+ ensure
63
+ finish_statement_handle(handle)
50
64
  end
51
65
 
52
66
  def exec_delete(sql, name, binds)
@@ -60,7 +74,7 @@ module ActiveRecord
60
74
  end
61
75
 
62
76
  def begin_db_transaction
63
- do_execute "BEGIN TRANSACTION", "TRANSACTION"
77
+ internal_execute("BEGIN TRANSACTION", "TRANSACTION", allow_retry: true, materialize_transactions: false)
64
78
  end
65
79
 
66
80
  def transaction_isolation_levels
@@ -68,33 +82,20 @@ module ActiveRecord
68
82
  end
69
83
 
70
84
  def begin_isolated_db_transaction(isolation)
71
- set_transaction_isolation_level transaction_isolation_levels.fetch(isolation)
85
+ set_transaction_isolation_level(transaction_isolation_levels.fetch(isolation))
72
86
  begin_db_transaction
73
87
  end
74
88
 
75
89
  def set_transaction_isolation_level(isolation_level)
76
- do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}", "TRANSACTION"
90
+ internal_execute("SET TRANSACTION ISOLATION LEVEL #{isolation_level}", "TRANSACTION", allow_retry: true, materialize_transactions: false)
77
91
  end
78
92
 
79
93
  def commit_db_transaction
80
- do_execute "COMMIT TRANSACTION", "TRANSACTION"
94
+ internal_execute("COMMIT TRANSACTION", "TRANSACTION", allow_retry: false, materialize_transactions: true)
81
95
  end
82
96
 
83
97
  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)
98
+ internal_execute("IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION", "TRANSACTION", allow_retry: false, materialize_transactions: true)
98
99
  end
99
100
 
100
101
  def case_sensitive_comparison(attribute, value)
@@ -164,42 +165,42 @@ module ActiveRecord
164
165
  # === SQLServer Specific ======================================== #
165
166
 
166
167
  def execute_procedure(proc_name, *variables)
167
- materialize_transactions
168
-
169
168
  vars = if variables.any? && variables.first.is_a?(Hash)
170
169
  variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
171
170
  else
172
171
  variables.map { |v| quote(v) }
173
172
  end.join(", ")
174
173
  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) }
174
+
175
+ log(sql, "Execute Procedure") do
176
+ with_raw_connection do |conn|
177
+ result = internal_raw_execute(sql, conn)
180
178
  options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }
179
+
181
180
  result.each(options) do |row|
182
181
  r = row.with_indifferent_access
183
182
  yield(r) if block_given?
184
183
  end
184
+
185
185
  result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
186
186
  end
187
187
  end
188
+
188
189
  end
189
190
 
190
- def with_identity_insert_enabled(table_name)
191
+ def with_identity_insert_enabled(table_name, conn)
191
192
  table_name = quote_table_name(table_name)
192
- set_identity_insert(table_name, true)
193
+ set_identity_insert(table_name, conn, true)
193
194
  yield
194
195
  ensure
195
- set_identity_insert(table_name, false)
196
+ set_identity_insert(table_name, conn, false)
196
197
  end
197
198
 
198
199
  def use_database(database = nil)
199
200
  return if sqlserver_azure?
200
201
 
201
- name = SQLServer::Utils.extract_identifiers(database || @connection_options[:database]).quoted
202
- do_execute "USE #{name}" unless name.blank?
202
+ name = SQLServer::Utils.extract_identifiers(database || @connection_parameters[:database]).quoted
203
+ execute("USE #{name}", "SCHEMA") unless name.blank?
203
204
  end
204
205
 
205
206
  def user_options
@@ -263,18 +264,19 @@ module ActiveRecord
263
264
 
264
265
  protected
265
266
 
266
- def sql_for_insert(sql, pk, binds)
267
+ def sql_for_insert(sql, pk, binds, returning)
267
268
  if pk.nil?
268
269
  table_name = query_requires_identity_insert?(sql)
269
270
  pk = primary_key(table_name)
270
271
  end
271
272
 
272
273
  sql = if pk && use_output_inserted? && !database_prefix_remote_server?
273
- quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted
274
274
  table_name ||= get_table_name(sql)
275
275
  exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
276
276
 
277
277
  if exclude_output_inserted
278
+ quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted
279
+
278
280
  id_sql_type = exclude_output_inserted.is_a?(TrueClass) ? "bigint" : exclude_output_inserted
279
281
  <<~SQL.squish
280
282
  DECLARE @ssaIdInsertTable table (#{quoted_pk} #{id_sql_type});
@@ -282,40 +284,32 @@ module ActiveRecord
282
284
  SELECT CAST(#{quoted_pk} AS #{id_sql_type}) FROM @ssaIdInsertTable
283
285
  SQL
284
286
  else
285
- sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT INSERTED.#{quoted_pk}"
287
+ returning_columns = returning || Array(pk)
288
+
289
+ if returning_columns.any?
290
+ returning_columns_statements = returning_columns.map { |c| " INSERTED.#{SQLServer::Utils.extract_identifiers(c).quoted}" }
291
+ sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT" + returning_columns_statements.join(",")
292
+ else
293
+ sql
294
+ end
286
295
  end
287
296
  else
288
297
  "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
289
298
  end
290
- super
299
+
300
+ [sql, binds]
291
301
  end
292
302
 
293
303
  # === SQLServer Specific ======================================== #
294
304
 
295
- def set_identity_insert(table_name, enable = true)
296
- do_execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
305
+ def set_identity_insert(table_name, conn, enable)
306
+ internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true)
297
307
  rescue Exception
298
308
  raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
299
309
  end
300
310
 
301
311
  # === SQLServer Specific (Executing) ============================ #
302
312
 
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
313
  def sp_executesql_types_and_parameters(binds)
320
314
  types, params = [], []
321
315
  binds.each_with_index do |attr, index|
@@ -328,6 +322,7 @@ module ActiveRecord
328
322
  end
329
323
 
330
324
  def sp_executesql_sql_type(attr)
325
+ return "nvarchar(max)".freeze if attr.is_a?(Symbol)
331
326
  return attr.type.sqlserver_type if attr.type.respond_to?(:sqlserver_type)
332
327
 
333
328
  case value = attr.value_for_database
@@ -339,6 +334,8 @@ module ActiveRecord
339
334
  end
340
335
 
341
336
  def sp_executesql_sql_param(attr)
337
+ return quote(attr) if attr.is_a?(Symbol)
338
+
342
339
  case value = attr.value_for_database
343
340
  when Type::Binary::Data,
344
341
  ActiveRecord::Type::SQLServer::Data
@@ -363,16 +360,6 @@ module ActiveRecord
363
360
  sql.freeze
364
361
  end
365
362
 
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
363
  # === SQLServer Specific (Identity Inserts) ===================== #
377
364
 
378
365
  def use_output_inserted?
@@ -392,10 +379,6 @@ module ActiveRecord
392
379
  self.class.exclude_output_inserted_table_names[table_name]
393
380
  end
394
381
 
395
- def exec_insert_requires_identity?(sql, _pk, _binds)
396
- query_requires_identity_insert?(sql)
397
- end
398
-
399
382
  def query_requires_identity_insert?(sql)
400
383
  return false unless insert_sql?(sql)
401
384
 
@@ -415,68 +398,38 @@ module ActiveRecord
415
398
 
416
399
  # === SQLServer Specific (Selecting) ============================ #
417
400
 
418
- def raw_select(sql, name = "SQL", binds = [], options = {})
419
- log(sql, name, binds, async: options[:async]) { _raw_select(sql, options) }
420
- end
421
-
422
- def _raw_select(sql, options = {})
423
- handle = raw_connection_run(sql)
424
- handle_to_names_and_values(handle, options)
401
+ def _raw_select(sql, conn)
402
+ handle = internal_raw_execute(sql, conn)
403
+ handle_to_names_and_values(handle, fetch: :rows)
425
404
  ensure
426
405
  finish_statement_handle(handle)
427
406
  end
428
407
 
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
408
  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
409
  query_options = {}.tap do |qo|
451
410
  qo[:timezone] = ActiveRecord.default_timezone || :utc
452
411
  qo[:as] = (options[:ar_result] || options[:fetch] == :rows) ? :array : :hash
453
412
  end
454
413
  results = handle.each(query_options)
455
414
  columns = lowercase_schema_reflection ? handle.fields.map { |c| c.downcase } : handle.fields
415
+
456
416
  options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
457
417
  end
458
418
 
459
419
  def finish_statement_handle(handle)
460
- case @connection_options[:mode]
461
- when :dblib
462
- handle.cancel if handle
463
- end
420
+ handle.cancel if handle
464
421
  handle
465
422
  end
466
423
 
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)
424
+ # TinyTDS returns false instead of raising an exception if connection fails.
425
+ # Getting around this by raising an exception ourselves while PR
426
+ # https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
427
+ def internal_raw_execute(sql, conn, perform_do: false)
428
+ result = conn.execute(sql).tap do |_result|
429
+ raise TinyTds::Error, "failed to execute statement" if _result.is_a?(FalseClass)
473
430
  end
474
- end
475
431
 
476
- def ensure_established_connection!
477
- raise TinyTds::Error, 'SQL Server client is not connected' unless @connection
478
-
479
- yield
432
+ perform_do ? result.do : result
480
433
  end
481
434
  end
482
435
  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