Skip to content

Commit 38d8a5f

Browse files
committed
Initial IronRuby ADONET connection mode support baked right in. Removed most &block parameters, no handle/request object yielded anymore. Better abstraction and compliance per the ActiveRecord abstract adapter to not yielding handles for #execute and only for low level #select. Better wrapping of all queries at lowest level in #log so exceptions at anytime can be handled correctly by core AR. Critical for System::Data's command readers. Better abstraction for introspecting on #connection_mode. Added support for running singular test cases via TextMate's Command-R.
1 parent 44dfaee commit 38d8a5f

File tree

10 files changed

+180
-97
lines changed

10 files changed

+180
-97
lines changed

CHANGELOG

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11

22
MASTER
33

4+
* Initial IronRuby ADONET connection mode support baked right in. Removed most &block
5+
parameters, no handle/request object yielded anymore. Better abstraction and compliance
6+
per the ActiveRecord abstract adapter to not yielding handles for #execute and only for
7+
low level #select. Better wrapping of all queries at lowest level in #log so exceptions
8+
at anytime can be handled correctly by core AR. Critical for System::Data's command
9+
readers. Better abstraction for introspecting on #connection_mode. Added support for
10+
running singular test cases via TextMate's Command-R. [Ken Collins]
11+
412
* Force a binary encoding on values coming in and out of those columns for ruby 1.9.
513
Fixes ticket #33 [Jeroen Zwartepoorte]
614

README.rdoc

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ The SQL Server adapter for rails is back for ActiveRecord 2.2 and up! We are cur
66

77
== What's New
88

9-
* Strict ODBC required! No DBI means around 20% faster!
9+
* IronRuby support using ADONET connection mode.
10+
* Direct ODBC mode. No DBI anymore, means around 20% faster!
1011
* Now supports SQL Server 2008 too!
1112
* Fully tested under 1.9!!! Correctly encodes/decodes UTF-8 types in ruby 1.9 too.
1213
* Now supports both rails 2.2 & 2.3!!!
@@ -129,7 +130,7 @@ It is our goal to match the adapter version with each version of rails. However
129130

130131
== Installation
131132

132-
You will need Ruby ODBC. If you are using the adapter under 1.9, then you need at least ruby-odbc version 0.9996. Currently ADO modes are not supported since we dropped the unnecessary DBI dependency and transport layer. This was done so we could incorporate other transports such as ADO.NET (w IronRuby) mode in the future or possibly a straight FreeTDS layer. The sky is the limit now and we have a code that can be accept these optional transports. If you are interested in helping, open a ticket and submit a patch. Or start a conversation on the Google Group.
133+
You will need Ruby ODBC. If you are using the adapter under 1.9, then you need at least ruby-odbc version 0.9996. ODBC is the preferred mode, however if you are using IronRuby you can use the ADONET connection mode which uses native System.Data connection. Other connection modes may be supported, possibly a straight FreeTDS layer. The sky is the limit now and we have a code that can be accept these optional transports. If you are interested in helping, open a ticket and submit a patch. Or start a conversation on the Google Group.
133134

134135
$ gem install activerecord-sqlserver-adapter
135136

@@ -142,6 +143,15 @@ Here are some external links for libraries and/or tutorials on how to install an
142143
* http://www.ch-werner.de/rubyodbc/
143144

144145

146+
== IronRuby ADONET Mode
147+
148+
A few details on this implementation. All that is needed in your database.yml configuration file is "mode: adonet" vs "odbc" and if you are running IronRuby, the connection will be native. No need for ANY DBI middle layer is needed or special extension to this adapter. The adapter is opinionated in regards to IronRuby on types coming out of the DB. For example strings will be String, not System::String and DateTime vs System::Datetime. This is so that we can pass all the ActiveRecord tests. When using the adapter it is best to stick with default Ruby types coming in and out. Currently IronRuby is passing most of the ActiveRecord tests. Here is a list of the ones remaining.
149+
150+
http://gist.github.com/381101
151+
152+
Some are in the adapters realm and some are in Marshalling which is IronRuby core to fix. Feel like helping knock these out, submit a patch.
153+
154+
145155
== Contributing
146156

147157
If you’d like to contribute a feature or bugfix, thanks! To make sure your fix/feature has a high chance of being added, please read the following guidelines. First, ask on the Google list, IRC, or post a ticket on github issues. Second, make sure there are tests! We will not accept any patch that is not tested. Please read the RUNNING_UNIT_TESTS file for the details of how to run the unit tests.

RUNNING_UNIT_TESTS

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ test databases. Use an empty password for said user.
3434

3535
== Running with Rake
3636

37-
The easiest way to run the unit tests is through Rake. Either run "rake test_sqlserver"
38-
or "rake test_sqlserver_odbc". For more information, checkout the full array
39-
of rake tasks with "rake -T"
37+
The easiest way to run the unit tests is through Rake. Either run "rake test" which
38+
defaults to ODBC mode or being mode specific using either "rake sqlserver:test:adonet"
39+
or "rake sqlserver:test:odbc". For more information, checkout the full array of rake tasks
40+
with "rake -T"
4041

4142
Rake can be found at http://rake.rubyforge.org
4243

Rakefile

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,38 @@ require 'rake/rdoctask'
44

55

66
namespace :sqlserver do
7-
8-
['sqlserver','sqlserver_odbc'].each do |adapter|
9-
10-
Rake::TestTask.new("test_#{adapter}") do |t|
11-
t.libs << "test"
12-
t.libs << "test/connections/native_#{adapter}"
13-
t.libs << "../../../rails/activerecord/test/"
14-
t.test_files = (
15-
Dir.glob("test/cases/**/*_test_sqlserver.rb").sort +
16-
Dir.glob("../../../rails/activerecord/test/**/*_test.rb").sort )
17-
t.verbose = true
7+
8+
namespace :test do
9+
10+
['odbc','adonet'].each do |mode|
11+
12+
Rake::TestTask.new(mode) do |t|
13+
t.libs << "test"
14+
t.libs << "test/connections/native_sqlserver#{mode == 'adonet' ? '' : "_#{mode}"}"
15+
t.libs << "../../../rails/activerecord/test/"
16+
t.test_files = (
17+
Dir.glob("test/cases/**/*_test_sqlserver.rb").sort +
18+
Dir.glob("../../../rails/activerecord/test/**/*_test.rb").sort )
19+
t.verbose = true
20+
end
21+
1822
end
1923

20-
namespace adapter do
21-
task :test => "test_#{adapter}"
24+
desc 'Test with unicode types enabled, uses ODBC mode.'
25+
task :unicode_types do
26+
ENV['ENABLE_DEFAULT_UNICODE_TYPES'] = 'true'
27+
test = Rake::Task['sqlserver:test:odbc']
28+
test.invoke
2229
end
23-
24-
end
25-
26-
desc 'Test with unicode types enabled.'
27-
task :test_unicode_types do
28-
ENV['ENABLE_DEFAULT_UNICODE_TYPES'] = 'true'
29-
test = Rake::Task['sqlserver:test_sqlserver_odbc']
30-
test.invoke
30+
3131
end
3232

3333
end
3434

3535

36-
desc 'Test the default ODBC mode, taks sqlserver:test_sqlserver_odbc.'
36+
desc 'Default runs tests for the adapters ODBC mode.'
3737
task :test do
38-
test = Rake::Task['sqlserver:test_sqlserver_odbc']
38+
test = Rake::Task['sqlserver:test:odbc']
3939
test.invoke
4040
end
4141

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 125 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ def self.sqlserver_connection(config) #:nodoc:
1616
require_library_or_gem 'odbc' unless defined?(ODBC)
1717
require 'active_record/connection_adapters/sqlserver_adapter/core_ext/odbc'
1818
raise ArgumentError, 'Missing :dsn configuration.' unless config.has_key?(:dsn)
19-
config = config.slice :dsn, :username, :password
19+
when :adonet
20+
require 'System.Data'
21+
raise ArgumentError, 'Missing :database configuration.' unless config.has_key?(:database)
2022
when :ado
2123
raise NotImplementedError, 'Please use version 2.3.1 of the adapter for ADO connections. Future versions may support ADO.NET.'
2224
raise ArgumentError, 'Missing :database configuration.' unless config.has_key?(:database)
@@ -166,12 +168,12 @@ class SQLServerAdapter < AbstractAdapter
166168
SUPPORTED_VERSIONS = [2000,2005,2008].freeze
167169
LIMITABLE_TYPES = ['string','integer','float','char','nchar','varchar','nvarchar'].freeze
168170
LOST_CONNECTION_EXCEPTIONS = {
169-
:odbc => ['ODBC::Error'],
170-
:ado => []
171+
:odbc => ['ODBC::Error'],
172+
:adonet => ['TypeError','System::Data::SqlClient::SqlException']
171173
}
172174
LOST_CONNECTION_MESSAGES = {
173-
:odbc => [/link failure/, /server failed/, /connection was already closed/, /invalid handle/i],
174-
:ado => []
175+
:odbc => [/link failure/, /server failed/, /connection was already closed/, /invalid handle/i],
176+
:adonet => [/current state is closed/, /network-related/]
175177
}
176178

177179
cattr_accessor :native_text_database_type, :native_binary_database_type, :native_string_database_type,
@@ -318,7 +320,7 @@ def quoted_utf8_value(value)
318320

319321
# REFERENTIAL INTEGRITY ====================================#
320322

321-
def disable_referential_integrity(&block)
323+
def disable_referential_integrity
322324
do_execute "EXEC sp_MSforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'"
323325
yield
324326
ensure
@@ -341,34 +343,14 @@ def reconnect!
341343
end
342344

343345
def disconnect!
344-
raw_connection.disconnect rescue nil
345-
end
346-
347-
def raw_connection_run(sql)
348-
with_auto_reconnect do
349-
case connection_mode
350-
when :odbc
351-
block_given? ? raw_connection.run_block(sql) { |handle| yield(handle) } : raw_connection.run(sql)
352-
else :ado
353-
354-
end
355-
end
356-
end
357-
358-
def raw_connection_do(sql)
359346
case connection_mode
360347
when :odbc
361-
raw_connection.do(sql)
362-
else :ado
363-
348+
raw_connection.disconnect rescue nil
349+
else :adonet
350+
raw_connection.close rescue nil
364351
end
365352
end
366353

367-
def finish_statement_handle(handle)
368-
handle.drop if handle && handle.respond_to?(:drop) && !handle.finished?
369-
handle
370-
end
371-
372354
# DATABASE STATEMENTS ======================================#
373355

374356
def user_options
@@ -399,13 +381,12 @@ def select_rows(sql, name = nil)
399381
raw_select(sql,name).first.last
400382
end
401383

402-
def execute(sql, name = nil, &block)
384+
def execute(sql, name = nil, skip_logging = false)
403385
if table_name = query_requires_identity_insert?(sql)
404-
handle = with_identity_insert_enabled(table_name) { raw_execute(sql,name,&block) }
386+
with_identity_insert_enabled(table_name) { do_execute(sql,name) }
405387
else
406-
handle = raw_execute(sql,name,&block)
388+
do_execute(sql,name)
407389
end
408-
finish_statement_handle(handle)
409390
end
410391

411392
def execute_procedure(proc_name, *variables)
@@ -782,8 +763,19 @@ def connect
782763
@connection = case connection_mode
783764
when :odbc
784765
ODBC.connect config[:dsn], config[:username], config[:password]
785-
when :ado
786-
766+
when :adonet
767+
System::Data::SqlClient::SqlConnection.new.tap do |connection|
768+
connection.connection_string = System::Data::SqlClient::SqlConnectionStringBuilder.new.tap do |cs|
769+
cs.user_i_d = config[:username] if config[:username]
770+
cs.password = config[:password] if config[:password]
771+
cs.integrated_security = true if config[:integrated_security] == 'true'
772+
cs.add 'Server', config[:host].to_clr_string
773+
cs.initial_catalog = config[:database]
774+
cs.multiple_active_result_sets = false
775+
cs.pooling = false
776+
end.to_s
777+
connection.open
778+
end
787779
end
788780
rescue
789781
raise unless @auto_connecting
@@ -829,6 +821,37 @@ def auto_reconnected?
829821
@auto_connecting = false
830822
end
831823

824+
def raw_connection_run(sql)
825+
with_auto_reconnect do
826+
case connection_mode
827+
when :odbc
828+
block_given? ? raw_connection.run_block(sql) { |handle| yield(handle) } : raw_connection.run(sql)
829+
else :adonet
830+
raw_connection.create_command.tap{ |cmd| cmd.command_text = sql }.execute_reader
831+
end
832+
end
833+
end
834+
835+
def raw_connection_do(sql)
836+
case connection_mode
837+
when :odbc
838+
raw_connection.do(sql)
839+
else :adonet
840+
raw_connection.create_command.tap{ |cmd| cmd.command_text = sql }.execute_non_query
841+
end
842+
end
843+
844+
def finish_statement_handle(handle)
845+
case connection_mode
846+
when :odbc
847+
handle.drop if handle && handle.respond_to?(:drop) && !handle.finished?
848+
when :adonet
849+
handle.close if handle && handle.respond_to?(:close) && !handle.is_closed
850+
handle.dispose if handle && handle.respond_to?(:dispose)
851+
end
852+
handle
853+
end
854+
832855
# DATABASE STATEMENTS ======================================
833856

834857
def select(sql, name = nil, ignore_special_columns = false)
@@ -864,41 +887,87 @@ def info_schema_query
864887
log_info_schema_queries ? yield : ActiveRecord::Base.silence{ yield }
865888
end
866889

867-
def raw_execute(sql, name = nil, &block)
868-
log(sql,name) { raw_connection_run(sql) }
869-
end
870-
871890
def do_execute(sql,name=nil)
872891
log(sql, name || 'EXECUTE') do
873892
with_auto_reconnect { raw_connection_do(sql) }
874893
end
875894
end
876895

877896
def raw_select(sql, name = nil)
878-
handle = raw_execute(sql,name)
879897
fields_and_row_sets = []
880-
loop do
881-
fields = handle.columns(true).map{|c|c.name}
882-
results = handle_as_array(handle)
883-
rows = results.inject([]) do |rows,row|
884-
row.each_with_index do |value, i|
885-
if value.is_a? ODBC::TimeStamp
886-
row[i] = value.to_sqlserver_string
887-
end
898+
log(sql,name) do
899+
begin
900+
handle = raw_connection_run(sql)
901+
loop do
902+
fields_and_rows = case connection_mode
903+
when :odbc
904+
handle_to_fields_and_rows_odbc(handle)
905+
when :adonet
906+
handle_to_fields_and_rows_adonet(handle)
907+
end
908+
fields_and_row_sets << fields_and_rows
909+
break unless handle_more_results?(handle)
888910
end
889-
rows << row
911+
ensure
912+
finish_statement_handle(handle)
890913
end
891-
fields_and_row_sets << [fields,rows]
892-
finish_statement_handle(handle) && break unless handle.more_results
893914
end
894915
fields_and_row_sets
895916
end
896917

897-
def handle_as_array(handle)
898-
array = handle.inject([]) do |rows,row|
899-
rows << row.inject([]){ |values,value| values << value }
918+
def handle_more_results?(handle)
919+
case connection_mode
920+
when :odbc
921+
handle.more_results
922+
when :adonet
923+
handle.next_result
924+
end
925+
end
926+
927+
def handle_to_fields_and_rows_odbc(handle)
928+
fields = handle.columns(true).map { |c| c.name }
929+
results = handle.inject([]) do |rows,row|
930+
rows << row.inject([]) { |values,value| values << value }
931+
end
932+
rows = results.inject([]) do |rows,row|
933+
row.each_with_index do |value, i|
934+
if value.is_a? ODBC::TimeStamp
935+
row[i] = value.to_sqlserver_string
936+
end
937+
end
938+
rows << row
939+
end
940+
[fields,rows]
941+
end
942+
943+
def handle_to_fields_and_rows_adonet(handle)
944+
if handle.has_rows
945+
fields = []
946+
rows = []
947+
fields_named = false
948+
while handle.read
949+
row = []
950+
handle.visible_field_count.times do |row_index|
951+
value = handle.get_value(row_index)
952+
value = if value.is_a? System::String
953+
value.to_s
954+
elsif value.is_a? System::DBNull
955+
nil
956+
elsif value.is_a? System::DateTime
957+
value.to_string("yyyy-MM-dd HH:MM:ss.fff").to_s
958+
else
959+
value
960+
end
961+
row << value
962+
fields << handle.get_name(row_index).to_s unless fields_named
963+
end
964+
rows << row
965+
fields_named = true
966+
end
967+
else
968+
fields, rows = [], []
900969
end
901-
array
970+
[fields,rows]
902971
end
903972

904973
def add_limit_offset_for_association_limiting!(sql, options)
@@ -945,7 +1014,7 @@ def default_name(table_name, column_name)
9451014

9461015
# IDENTITY INSERTS =========================================#
9471016

948-
def with_identity_insert_enabled(table_name, &block)
1017+
def with_identity_insert_enabled(table_name)
9491018
table_name = quote_table_name(table_name_or_views_table_name(table_name))
9501019
set_identity_insert(table_name, true)
9511020
yield

0 commit comments

Comments
 (0)