Skip to content

Commit 4842d2e

Browse files
authored
Fixed adapter to pass SQL Server only tests (#1081)
1 parent bfd77e5 commit 4842d2e

14 files changed

+178
-235
lines changed

README.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,16 @@ end
9090

9191
#### Configure Connection
9292

93-
We currently conform to an unpublished and non-standard AbstractAdapter interface to configure connections made to the database. To do so, just implement the `configure_connection` method in an initializer like so. In this case below we are setting the `TEXTSIZE` to 64 megabytes.
93+
The adapter conforms to the AbstractAdapter interface to configure connections. If you require additional connection
94+
configuration then implement the `configure_connection` method in an initializer like so. In the following
95+
example we are setting the `TEXTSIZE` to 64 megabytes.
9496

9597
```ruby
9698
module ActiveRecord
9799
module ConnectionAdapters
98100
class SQLServerAdapter < AbstractAdapter
99101
def configure_connection
102+
super
100103
raw_connection_do "SET TEXTSIZE #{64.megabytes}"
101104
end
102105
end
@@ -171,7 +174,8 @@ To then connect the application to your SQL Server instance edit the `config/dat
171174

172175
## Installation
173176

174-
The adapter has no strict gem dependencies outside of `ActiveRecord`. You will have to pick a connection mode, the default is dblib which uses the `TinyTDS` gem. Just bundle the gem and the adapter will use it.
177+
The adapter has no strict gem dependencies outside of `ActiveRecord` and
178+
[TinyTDS](https://github.com/rails-sqlserver/tiny_tds).
175179

176180
```ruby
177181
gem 'activerecord-sqlserver-adapter'

Rakefile

-6
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,8 @@ namespace :test do
1919
t.verbose = false
2020
end
2121
end
22-
23-
task "dblib:env" do
24-
ENV["ARCONN_MODE"] = "dblib"
25-
end
2622
end
2723

28-
task "test:dblib" => "test:dblib:env"
29-
3024
namespace :profile do
3125
["dblib"].each do |mode|
3226
namespace mode.to_sym do

lib/active_record/connection_adapters/sqlserver/database_statements.rb

+77-72
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,58 @@ def write_query?(sql) # :nodoc:
1313
!READ_QUERY.match?(sql.b)
1414
end
1515

16-
def execute(sql, name = nil)
16+
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
17+
result = nil
1718
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
2119

22-
materialize_transactions
23-
mark_transaction_written_if_write(sql)
20+
log(sql, name, async: async) do
21+
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
22+
handle = if id_insert_table_name = query_requires_identity_insert?(sql)
23+
with_identity_insert_enabled(id_insert_table_name) { _execute(sql, conn) }
24+
else
25+
_execute(sql, conn)
26+
end
2427

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)
28+
result = handle.do
29+
end
2930
end
31+
32+
result
3033
end
3134

3235
def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false)
36+
result = nil
3337
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
3738

38-
materialize_transactions
39+
check_if_write_query(sql)
3940
mark_transaction_written_if_write(sql)
4041

41-
sp_executesql(sql, name, binds, prepare: prepare, async: async)
42+
unless without_prepared_statement?(binds)
43+
types, params = sp_executesql_types_and_parameters(binds)
44+
sql = sp_executesql_sql(sql, types, params, name)
45+
end
46+
47+
log(sql, name, binds, async: async) do
48+
with_raw_connection do |conn|
49+
begin
50+
options = { ar_result: true }
51+
52+
handle = _execute(sql, conn)
53+
result = handle_to_names_and_values(handle, options)
54+
ensure
55+
finish_statement_handle(handle)
56+
end
57+
end
58+
end
59+
60+
result
4261
end
4362

4463
def exec_insert(sql, name = nil, binds = [], pk = nil, _sequence_name = nil, returning: nil)
4564
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, returning) }
65+
with_identity_insert_enabled(id_insert_table_name) { super }
4766
else
48-
super(sql, name, binds, pk, returning)
67+
super
4968
end
5069
end
5170

@@ -60,7 +79,7 @@ def exec_update(sql, name, binds)
6079
end
6180

6281
def begin_db_transaction
63-
do_execute "BEGIN TRANSACTION", "TRANSACTION"
82+
execute "BEGIN TRANSACTION", "TRANSACTION"
6483
end
6584

6685
def transaction_isolation_levels
@@ -73,25 +92,25 @@ def begin_isolated_db_transaction(isolation)
7392
end
7493

7594
def set_transaction_isolation_level(isolation_level)
76-
do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}", "TRANSACTION"
95+
execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}", "TRANSACTION"
7796
end
7897

7998
def commit_db_transaction
80-
do_execute "COMMIT TRANSACTION", "TRANSACTION"
99+
execute "COMMIT TRANSACTION", "TRANSACTION"
81100
end
82101

83102
def exec_rollback_db_transaction
84-
do_execute "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION", "TRANSACTION"
103+
execute "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION", "TRANSACTION"
85104
end
86105

87106
include Savepoints
88107

89108
def create_savepoint(name = current_savepoint_name)
90-
do_execute "SAVE TRANSACTION #{name}", "TRANSACTION"
109+
execute "SAVE TRANSACTION #{name}", "TRANSACTION"
91110
end
92111

93112
def exec_rollback_to_savepoint(name = current_savepoint_name)
94-
do_execute "ROLLBACK TRANSACTION #{name}", "TRANSACTION"
113+
execute "ROLLBACK TRANSACTION #{name}", "TRANSACTION"
95114
end
96115

97116
def release_savepoint(name = current_savepoint_name)
@@ -164,27 +183,27 @@ def build_insert_sql(insert) # :nodoc:
164183
# === SQLServer Specific ======================================== #
165184

166185
def execute_procedure(proc_name, *variables)
167-
materialize_transactions
168-
169186
vars = if variables.any? && variables.first.is_a?(Hash)
170187
variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
171188
else
172189
variables.map { |v| quote(v) }
173190
end.join(", ")
174191
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) }
192+
193+
log(sql, "Execute Procedure") do
194+
with_raw_connection do |conn|
195+
result = _execute(sql, conn)
180196
options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }
197+
181198
result.each(options) do |row|
182199
r = row.with_indifferent_access
183200
yield(r) if block_given?
184201
end
202+
185203
result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
186204
end
187205
end
206+
188207
end
189208

190209
def with_identity_insert_enabled(table_name)
@@ -198,8 +217,8 @@ def with_identity_insert_enabled(table_name)
198217
def use_database(database = nil)
199218
return if sqlserver_azure?
200219

201-
name = SQLServer::Utils.extract_identifiers(database || @connection_options[:database]).quoted
202-
do_execute "USE #{name}" unless name.blank?
220+
name = SQLServer::Utils.extract_identifiers(database || @connection_parameters[:database]).quoted
221+
execute "USE #{name}" unless name.blank?
203222
end
204223

205224
def user_options
@@ -270,19 +289,22 @@ def sql_for_insert(sql, pk, binds, _returning)
270289
end
271290

272291
sql = if pk && use_output_inserted? && !database_prefix_remote_server?
273-
quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted
274292
table_name ||= get_table_name(sql)
275293
exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
276294

277295
if exclude_output_inserted
296+
quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted
297+
278298
id_sql_type = exclude_output_inserted.is_a?(TrueClass) ? "bigint" : exclude_output_inserted
279299
<<~SQL.squish
280300
DECLARE @ssaIdInsertTable table (#{quoted_pk} #{id_sql_type});
281301
#{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT INSERTED.#{quoted_pk} INTO @ssaIdInsertTable"}
282302
SELECT CAST(#{quoted_pk} AS #{id_sql_type}) FROM @ssaIdInsertTable
283303
SQL
284304
else
285-
sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT INSERTED.#{quoted_pk}"
305+
inserted_keys = Array(pk).map { |primary_key| " INSERTED.#{SQLServer::Utils.extract_identifiers(primary_key).quoted}" }
306+
307+
sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT" + inserted_keys.join(",")
286308
end
287309
else
288310
"#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
@@ -293,29 +315,22 @@ def sql_for_insert(sql, pk, binds, _returning)
293315
# === SQLServer Specific ======================================== #
294316

295317
def set_identity_insert(table_name, enable = true)
296-
do_execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
318+
execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
297319
rescue Exception
298320
raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
299321
end
300322

301323
# === SQLServer Specific (Executing) ============================ #
302324

303-
def do_execute(sql, name = "SQL")
304-
connect if @connection.nil?
305-
306-
materialize_transactions
307-
mark_transaction_written_if_write(sql)
308-
309-
log(sql, name) { raw_connection_do(sql) }
310-
end
311-
312325
# TODO: Adapter should be refactored to use `with_raw_connection` to translate exceptions.
313326
def sp_executesql(sql, name, binds, options = {})
314327
options[:ar_result] = true if options[:fetch] != :rows
328+
315329
unless without_prepared_statement?(binds)
316330
types, params = sp_executesql_types_and_parameters(binds)
317331
sql = sp_executesql_sql(sql, types, params, name)
318332
end
333+
319334
raw_select sql, name, binds, options
320335
rescue => original_exception
321336
translated_exception = translate_exception_class(original_exception, sql, binds)
@@ -373,11 +388,8 @@ def sp_executesql_sql(sql, types, params, name)
373388
end
374389

375390
def raw_connection_do(sql)
376-
case @connection_options[:mode]
377-
when :dblib
378-
result = ensure_established_connection! { dblib_execute(sql) }
379-
result.do
380-
end
391+
result = ensure_established_connection! { dblib_execute(sql) }
392+
result.do
381393
ensure
382394
@update_sql = false
383395
end
@@ -436,45 +448,38 @@ def _raw_select(sql, options = {})
436448
end
437449

438450
def raw_connection_run(sql)
439-
case @connection_options[:mode]
440-
when :dblib
441-
ensure_established_connection! { dblib_execute(sql) }
442-
end
443-
end
444-
445-
def handle_more_results?(handle)
446-
case @connection_options[:mode]
447-
when :dblib
448-
end
451+
ensure_established_connection! { dblib_execute(sql) }
449452
end
450453

451454
def handle_to_names_and_values(handle, options = {})
452-
case @connection_options[:mode]
453-
when :dblib
454-
handle_to_names_and_values_dblib(handle, options)
455-
end
456-
end
457-
458-
def handle_to_names_and_values_dblib(handle, options = {})
459455
query_options = {}.tap do |qo|
460456
qo[:timezone] = ActiveRecord.default_timezone || :utc
461457
qo[:as] = (options[:ar_result] || options[:fetch] == :rows) ? :array : :hash
462458
end
463459
results = handle.each(query_options)
464460
columns = lowercase_schema_reflection ? handle.fields.map { |c| c.downcase } : handle.fields
461+
465462
options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
466463
end
467464

468465
def finish_statement_handle(handle)
469-
case @connection_options[:mode]
470-
when :dblib
471-
handle.cancel if handle
472-
end
466+
handle.cancel if handle
473467
handle
474468
end
475469

470+
# TODO: Rename
471+
def _execute(sql, conn)
472+
conn.execute(sql).tap do |result|
473+
# TinyTDS returns false instead of raising an exception if connection fails.
474+
# Getting around this by raising an exception ourselves while this PR
475+
# https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
476+
raise TinyTds::Error, "failed to execute statement" if result.is_a?(FalseClass)
477+
end
478+
end
479+
480+
# TODO: Remove
476481
def dblib_execute(sql)
477-
@connection.execute(sql).tap do |result|
482+
@raw_connection.execute(sql).tap do |result|
478483
# TinyTDS returns false instead of raising an exception if connection fails.
479484
# Getting around this by raising an exception ourselves while this PR
480485
# https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
@@ -483,7 +488,7 @@ def dblib_execute(sql)
483488
end
484489

485490
def ensure_established_connection!
486-
raise TinyTds::Error, 'SQL Server client is not connected' unless @connection
491+
raise TinyTds::Error, 'SQL Server client is not connected' unless @raw_connection
487492

488493
yield
489494
end

lib/active_record/connection_adapters/sqlserver/database_tasks.rb

+5-5
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ def create_database(database, options = {})
88
name = SQLServer::Utils.extract_identifiers(database)
99
db_options = create_database_options(options)
1010
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}"
1212
end
1313

1414
def drop_database(database)
1515
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}"
1818
end
1919

2020
def current_database
@@ -33,7 +33,7 @@ def collation
3333

3434
def create_database_options(options = {})
3535
keys = [:collate]
36-
copts = @connection_options
36+
copts = @connection_parameters
3737
options = {
3838
collate: copts[:collation]
3939
}.merge(options.symbolize_keys).select { |_, v|
@@ -46,7 +46,7 @@ def create_database_options(options = {})
4646

4747
def create_database_edition_options(options = {})
4848
keys = [:maxsize, :edition, :service_objective]
49-
copts = @connection_options
49+
copts = @connection_parameters
5050
edition_options = {
5151
maxsize: copts[:azure_maxsize],
5252
edition: copts[:azure_edition],

0 commit comments

Comments
 (0)