Skip to content

Commit 7863ebf

Browse files
committed
Ruby ODBC compatibility.
1 parent 3febac9 commit 7863ebf

File tree

8 files changed

+54
-104
lines changed

8 files changed

+54
-104
lines changed

lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,13 @@ module Sqlserver
44
module CoreExt
55
module ODBC
66

7-
module TimeStamp
8-
9-
def to_sqlserver_string
10-
date, time, nanoseconds = to_s.split(' ')
11-
"#{date} #{time}.#{sprintf("%03d",nanoseconds.to_i/1000000)}"
12-
end
13-
14-
end
15-
167
module Statement
178

189
def finished?
1910
begin
2011
connected?
2112
false
22-
rescue *Database.parent_modules_error_exceptions
13+
rescue ::ODBC::Error
2314
true
2415
end
2516
end
@@ -28,14 +19,6 @@ def finished?
2819

2920
module Database
3021

31-
def self.parent_modules
32-
@parent_module ||= ['ODBC','ODBC_UTF8','ODBC_NONE'].map{ |odbc_ns| odbc_ns.constantize rescue nil }.compact
33-
end
34-
35-
def self.parent_modules_error_exceptions
36-
@parent_modules_error_exceptions ||= parent_modules.map { |odbc_ns| "::#{odbc_ns}::Error".constantize }
37-
end
38-
3922
def run_block(*args)
4023
yield sth = run(*args)
4124
sth.drop
@@ -49,9 +32,7 @@ def run_block(*args)
4932
end
5033
end
5134

52-
['ODBC','ODBC_UTF8','ODBC_NONE'].map{ |odbc_ns| odbc_ns.constantize rescue nil }.compact.each do |ns|
53-
ns::TimeStamp.send :include, ActiveRecord::ConnectionAdapters::Sqlserver::CoreExt::ODBC::TimeStamp
54-
ns::Statement.send :include, ActiveRecord::ConnectionAdapters::Sqlserver::CoreExt::ODBC::Statement
55-
ns::Database.send :include, ActiveRecord::ConnectionAdapters::Sqlserver::CoreExt::ODBC::Database
56-
end
35+
36+
ODBC::Statement.send :include, ActiveRecord::ConnectionAdapters::Sqlserver::CoreExt::ODBC::Statement
37+
ODBC::Database.send :include, ActiveRecord::ConnectionAdapters::Sqlserver::CoreExt::ODBC::Database
5738

lib/active_record/connection_adapters/sqlserver/database_statements.rb

Lines changed: 25 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ def execute(sql, name = nil)
1616
end
1717

1818
def exec_query(sql, name = 'SQL', binds = [])
19-
return raw_select(sql, name, binds, :ar_result => true) if binds.empty?
2019
if id_insert_table_name = query_requires_identity_insert?(sql)
21-
with_identity_insert_enabled(id_insert_table_name) { do_exec_query(sql, name, binds) }
20+
with_identity_insert_enabled(id_insert_table_name) do
21+
binds.empty? ? raw_select(sql, name, binds, :ar_result => true) : do_exec_query(sql, name, binds)
22+
end
2223
else
23-
do_exec_query(sql, name, binds)
24+
binds.empty? ? raw_select(sql, name, binds, :ar_result => true) : do_exec_query(sql, name, binds)
2425
end
2526
end
2627

2728
def exec_insert(sql, name, binds)
28-
sql = "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
2929
exec_query(sql, name, binds)
3030
end
3131

@@ -208,13 +208,7 @@ def select(sql, name = nil, binds = [])
208208
end
209209

210210
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
211-
@insert_sql = true
212-
case @connection_options[:mode]
213-
when :dblib
214-
execute(sql, name) || id_value
215-
else
216-
super || select_value("SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident")
217-
end
211+
super || select_value("SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident")
218212
end
219213

220214
def update_sql(sql, name = nil)
@@ -228,6 +222,15 @@ def update_sql(sql, name = nil)
228222
end
229223
end
230224

225+
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
226+
sql = "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident" unless binds.empty?
227+
super
228+
end
229+
230+
def last_inserted_id(result)
231+
super || select_value("SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident")
232+
end
233+
231234
# === SQLServer Specific ======================================== #
232235

233236
def valid_isolation_levels
@@ -272,14 +275,13 @@ def do_exec_query(sql, name, binds)
272275
def raw_connection_do(sql)
273276
case @connection_options[:mode]
274277
when :dblib
275-
@insert_sql ? @connection.execute(sql).insert : @connection.execute(sql).do
278+
@connection.execute(sql).do
276279
when :odbc
277280
@connection.do(sql)
278281
else :adonet
279282
@connection.create_command.tap{ |cmd| cmd.command_text = sql }.execute_non_query
280283
end
281284
ensure
282-
@insert_sql = false
283285
@update_sql = false
284286
end
285287

@@ -341,49 +343,18 @@ def handle_to_names_and_values_dblib(handle, options={})
341343

342344
# TODO [Rails31] Use options[:ar_result]
343345
def handle_to_names_and_values_odbc(handle, options={})
344-
@connection.use_utc = ActiveRecord::Base.default_timezone == :utc if @connection_supports_native_types
345-
case options[:fetch]
346-
when :all, :one
347-
if @connection_supports_native_types
348-
if options[:fetch] == :all
349-
handle.each_hash || []
350-
else
351-
row = handle.fetch_hash
352-
rows = row ? [row] : [[]]
353-
end
354-
else
355-
rows = if options[:fetch] == :all
356-
handle.fetch_all || []
357-
else
358-
row = handle.fetch
359-
row ? [row] : [[]]
360-
end
361-
names = handle.columns(true).map{ |c| c.name }
362-
names_and_values = []
363-
rows.each do |row|
364-
h = {}
365-
i = 0
366-
while i < row.size
367-
v = row[i]
368-
h[names[i]] = v.respond_to?(:to_sqlserver_string) ? v.to_sqlserver_string : v
369-
i += 1
370-
end
371-
names_and_values << h
372-
end
373-
names_and_values
374-
end
375-
when :rows
346+
@connection.use_utc = ActiveRecord::Base.default_timezone == :utc
347+
if options[:ar_result]
348+
columns = handle.columns(true).map { |c| c.name }
376349
rows = handle.fetch_all || []
377-
return rows if @connection_supports_native_types
378-
rows.each do |row|
379-
i = 0
380-
while i < row.size
381-
v = row[i]
382-
row[i] = v.to_sqlserver_string if v.respond_to?(:to_sqlserver_string)
383-
i += 1
384-
end
350+
ActiveRecord::Result.new(columns, rows)
351+
else
352+
case options[:fetch]
353+
when :all
354+
handle.each_hash || []
355+
when :rows
356+
handle.fetch_all || []
385357
end
386-
rows
387358
end
388359
end
389360

lib/active_record/connection_adapters/sqlserver/errors.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module Errors
99

1010
LOST_CONNECTION_EXCEPTIONS = {
1111
:dblib => ['TinyTds::Error'],
12-
:odbc => ['ODBC::Error','ODBC_UTF8::Error','ODBC_NONE::Error'],
12+
:odbc => ['ODBC::Error'],
1313
:adonet => ['TypeError','System::Data::SqlClient::SqlException']
1414
}.freeze
1515

lib/active_record/connection_adapters/sqlserver/schema_statements.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,9 @@ def unqualify_db_name(table_name)
278278
end
279279

280280
def get_table_name(sql)
281-
if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
282-
$1 || $2
283-
elsif sql =~ /from\s+([^\(\s]+)\s*/i
281+
if sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)\s+INTO\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
282+
$2 || $3
283+
elsif sql =~ /FROM\s+([^\(\s]+)\s*/i
284284
$1
285285
else
286286
nil
@@ -366,14 +366,14 @@ def query_requires_identity_insert?(sql)
366366
if insert_sql?(sql)
367367
table_name = get_table_name(sql)
368368
id_column = identity_column(table_name)
369-
id_column && sql =~ /^\s*INSERT[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? quote_table_name(table_name) : false
369+
id_column && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? quote_table_name(table_name) : false
370370
else
371371
false
372372
end
373373
end
374374

375375
def insert_sql?(sql)
376-
!(sql =~ /^\s*INSERT/i).nil?
376+
!(sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)/i).nil?
377377
end
378378

379379
def with_identity_insert_enabled(table_name)

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,7 @@ def self.sqlserver_connection(config) #:nodoc:
2525
warn("TinyTds v0.4.3 or higher required. Using #{TinyTds::VERSION}") unless TinyTds::Client.instance_methods.map(&:to_s).include?("active?")
2626
when :odbc
2727
raise ArgumentError, 'Missing :dsn configuration.' unless config.has_key?(:dsn)
28-
if RUBY_VERSION < '1.9'
29-
require_library_or_gem 'odbc'
30-
else
31-
begin
32-
# TODO: [ODBC] Change this to 'odbc_utf8'
33-
require_library_or_gem 'odbc'
34-
rescue LoadError
35-
require_library_or_gem 'odbc'
36-
end
37-
end unless ['::ODBC','::ODBC_UTF8','::ODBC_NONE'].any? { |odbc_ns| odbc_ns.constantize rescue nil }
28+
require_library_or_gem 'odbc'
3829
require 'active_record/connection_adapters/sqlserver/core_ext/odbc'
3930
when :adonet
4031
require 'System.Data'
@@ -401,20 +392,20 @@ def connect
401392
end
402393
end
403394
when :odbc
404-
odbc = ['::ODBC','::ODBC_UTF8','::ODBC_NONE'].detect{ |odbc_ns| odbc_ns.constantize rescue nil }.constantize
405395
if config[:dsn].include?(';')
406-
driver = odbc::Driver.new.tap do |d|
396+
driver = ODBC::Driver.new.tap do |d|
407397
d.name = config[:dsn_name] || 'Driver1'
408398
d.attrs = config[:dsn].split(';').map{ |atr| atr.split('=') }.reject{ |kv| kv.size != 2 }.inject({}){ |h,kv| k,v = kv ; h[k] = v ; h }
409399
end
410-
odbc::Database.new.drvconnect(driver)
400+
ODBC::Database.new.drvconnect(driver)
411401
else
412-
odbc.connect config[:dsn], config[:username], config[:password]
413-
end.tap do |c|
414-
if c.respond_to?(:use_time)
402+
ODBC.connect config[:dsn], config[:username], config[:password]
403+
end.tap do |c|
404+
begin
415405
c.use_time = true
416406
c.use_utc = ActiveRecord::Base.default_timezone == :utc
417-
@connection_supports_native_types = true
407+
rescue Exception => e
408+
warn "Ruby ODBC v0.99992 or higher is required."
418409
end
419410
end
420411
when :adonet

test/cases/adapter_test_sqlserver.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def setup
115115

116116
should 'return true to #insert_sql? for inserts only' do
117117
assert @connection.send(:insert_sql?,'INSERT...')
118+
assert @connection.send(:insert_sql?, "EXEC sp_executesql N'INSERT INTO [fk_test_has_fks] ([fk_id]) VALUES (@0); SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident', N'@0 int', @0 = 0")
118119
assert !@connection.send(:insert_sql?,'UPDATE...')
119120
assert !@connection.send(:insert_sql?,'SELECT...')
120121
end
@@ -238,12 +239,18 @@ def setup
238239
@identity_insert_sql = "INSERT INTO [funny_jokes] ([id],[name]) VALUES(420,'Knock knock')"
239240
@identity_insert_sql_unquoted = "INSERT INTO funny_jokes (id, name) VALUES(420, 'Knock knock')"
240241
@identity_insert_sql_unordered = "INSERT INTO [funny_jokes] ([name],[id]) VALUES('Knock knock',420)"
242+
@identity_insert_sql_sp = "EXEC sp_executesql N'INSERT INTO [funny_jokes] ([id],[name]) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Knock knock'"
243+
@identity_insert_sql_unquoted_sp = "EXEC sp_executesql N'INSERT INTO [funny_jokes] (id, name) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Knock knock'"
244+
@identity_insert_sql_unordered_sp = "EXEC sp_executesql N'INSERT INTO [funny_jokes] ([name],[id]) VALUES (@0, @1)', N'@0 nvarchar(255), @1 int', @0 = N'Knock knock', @1 = 420"
241245
end
242246

243247
should 'return quoted table_name to #query_requires_identity_insert? when INSERT sql contains id column' do
244248
assert_equal '[funny_jokes]', @connection.send(:query_requires_identity_insert?,@identity_insert_sql)
245249
assert_equal '[funny_jokes]', @connection.send(:query_requires_identity_insert?,@identity_insert_sql_unquoted)
246250
assert_equal '[funny_jokes]', @connection.send(:query_requires_identity_insert?,@identity_insert_sql_unordered)
251+
assert_equal '[funny_jokes]', @connection.send(:query_requires_identity_insert?,@identity_insert_sql_sp)
252+
assert_equal '[funny_jokes]', @connection.send(:query_requires_identity_insert?,@identity_insert_sql_unquoted_sp)
253+
assert_equal '[funny_jokes]', @connection.send(:query_requires_identity_insert?,@identity_insert_sql_unordered_sp)
247254
end
248255

249256
should 'return false to #query_requires_identity_insert? for normal SQL' do
@@ -306,9 +313,10 @@ def setup
306313

307314
end
308315

309-
context 'When disableing referential integrity' do
316+
context 'When disabling referential integrity' do
310317

311318
setup do
319+
@connection.disable_referential_integrity { FkTestHasPk.delete_all; FkTestHasFk.delete_all }
312320
@parent = FkTestHasPk.create!
313321
@member = FkTestHasFk.create!(:fk_id => @parent.id)
314322
end

test/cases/column_test_sqlserver.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ def setup
8888

8989
end
9090

91-
9291
context 'For all national/unicode columns' do
9392

9493
setup do

test/cases/connection_test_sqlserver.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ def setup
6868

6969
should 'insert with identity closes statement' do
7070
assert_all_odbc_statements_used_are_closed do
71-
@connection.insert("INSERT INTO accounts ([id], [firm_id],[credit_limit]) values (999, 1, 50)")
71+
@connection.exec_insert "INSERT INTO accounts ([id],[firm_id],[credit_limit]) VALUES (999, 1, 50)", "SQL", []
7272
end
7373
end
7474

7575
should 'insert without identity closes statement' do
7676
assert_all_odbc_statements_used_are_closed do
77-
@connection.insert("INSERT INTO accounts ([firm_id],[credit_limit]) values (1, 50)")
77+
@connection.exec_insert "INSERT INTO accounts ([firm_id],[credit_limit]) VALUES (1, 50)", "SQL", []
7878
end
7979
end
8080

0 commit comments

Comments
 (0)