Skip to content

Commit

Permalink
Initial IronRuby ADONET connection mode support baked right in. Remov…
Browse files Browse the repository at this point in the history
…ed 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.
  • Loading branch information
metaskills committed Apr 27, 2010
1 parent 44dfaee commit 38d8a5f
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 97 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG
@@ -1,6 +1,14 @@

MASTER

* 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. [Ken Collins]

* Force a binary encoding on values coming in and out of those columns for ruby 1.9.
Fixes ticket #33 [Jeroen Zwartepoorte]

Expand Down
14 changes: 12 additions & 2 deletions README.rdoc
Expand Up @@ -6,7 +6,8 @@ The SQL Server adapter for rails is back for ActiveRecord 2.2 and up! We are cur

== What's New

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

== Installation

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.
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.

$ gem install activerecord-sqlserver-adapter

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


== IronRuby ADONET Mode

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.

http://gist.github.com/381101

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.


== Contributing

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.
Expand Down
7 changes: 4 additions & 3 deletions RUNNING_UNIT_TESTS
Expand Up @@ -34,9 +34,10 @@ test databases. Use an empty password for said user.

== Running with Rake

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

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

Expand Down
46 changes: 23 additions & 23 deletions Rakefile
Expand Up @@ -4,38 +4,38 @@ require 'rake/rdoctask'


namespace :sqlserver do

['sqlserver','sqlserver_odbc'].each do |adapter|

Rake::TestTask.new("test_#{adapter}") do |t|
t.libs << "test"
t.libs << "test/connections/native_#{adapter}"
t.libs << "../../../rails/activerecord/test/"
t.test_files = (
Dir.glob("test/cases/**/*_test_sqlserver.rb").sort +
Dir.glob("../../../rails/activerecord/test/**/*_test.rb").sort )
t.verbose = true

namespace :test do

['odbc','adonet'].each do |mode|

Rake::TestTask.new(mode) do |t|
t.libs << "test"
t.libs << "test/connections/native_sqlserver#{mode == 'adonet' ? '' : "_#{mode}"}"
t.libs << "../../../rails/activerecord/test/"
t.test_files = (
Dir.glob("test/cases/**/*_test_sqlserver.rb").sort +
Dir.glob("../../../rails/activerecord/test/**/*_test.rb").sort )
t.verbose = true
end

end

namespace adapter do
task :test => "test_#{adapter}"
desc 'Test with unicode types enabled, uses ODBC mode.'
task :unicode_types do
ENV['ENABLE_DEFAULT_UNICODE_TYPES'] = 'true'
test = Rake::Task['sqlserver:test:odbc']
test.invoke
end

end

desc 'Test with unicode types enabled.'
task :test_unicode_types do
ENV['ENABLE_DEFAULT_UNICODE_TYPES'] = 'true'
test = Rake::Task['sqlserver:test_sqlserver_odbc']
test.invoke

end

end


desc 'Test the default ODBC mode, taks sqlserver:test_sqlserver_odbc.'
desc 'Default runs tests for the adapters ODBC mode.'
task :test do
test = Rake::Task['sqlserver:test_sqlserver_odbc']
test = Rake::Task['sqlserver:test:odbc']
test.invoke
end

181 changes: 125 additions & 56 deletions lib/active_record/connection_adapters/sqlserver_adapter.rb
Expand Up @@ -16,7 +16,9 @@ def self.sqlserver_connection(config) #:nodoc:
require_library_or_gem 'odbc' unless defined?(ODBC)
require 'active_record/connection_adapters/sqlserver_adapter/core_ext/odbc'
raise ArgumentError, 'Missing :dsn configuration.' unless config.has_key?(:dsn)
config = config.slice :dsn, :username, :password
when :adonet
require 'System.Data'
raise ArgumentError, 'Missing :database configuration.' unless config.has_key?(:database)
when :ado
raise NotImplementedError, 'Please use version 2.3.1 of the adapter for ADO connections. Future versions may support ADO.NET.'
raise ArgumentError, 'Missing :database configuration.' unless config.has_key?(:database)
Expand Down Expand Up @@ -166,12 +168,12 @@ class SQLServerAdapter < AbstractAdapter
SUPPORTED_VERSIONS = [2000,2005,2008].freeze
LIMITABLE_TYPES = ['string','integer','float','char','nchar','varchar','nvarchar'].freeze
LOST_CONNECTION_EXCEPTIONS = {
:odbc => ['ODBC::Error'],
:ado => []
:odbc => ['ODBC::Error'],
:adonet => ['TypeError','System::Data::SqlClient::SqlException']
}
LOST_CONNECTION_MESSAGES = {
:odbc => [/link failure/, /server failed/, /connection was already closed/, /invalid handle/i],
:ado => []
:odbc => [/link failure/, /server failed/, /connection was already closed/, /invalid handle/i],
:adonet => [/current state is closed/, /network-related/]
}

cattr_accessor :native_text_database_type, :native_binary_database_type, :native_string_database_type,
Expand Down Expand Up @@ -318,7 +320,7 @@ def quoted_utf8_value(value)

# REFERENTIAL INTEGRITY ====================================#

def disable_referential_integrity(&block)
def disable_referential_integrity
do_execute "EXEC sp_MSforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'"
yield
ensure
Expand All @@ -341,34 +343,14 @@ def reconnect!
end

def disconnect!
raw_connection.disconnect rescue nil
end

def raw_connection_run(sql)
with_auto_reconnect do
case connection_mode
when :odbc
block_given? ? raw_connection.run_block(sql) { |handle| yield(handle) } : raw_connection.run(sql)
else :ado

end
end
end

def raw_connection_do(sql)
case connection_mode
when :odbc
raw_connection.do(sql)
else :ado

raw_connection.disconnect rescue nil
else :adonet
raw_connection.close rescue nil
end
end

def finish_statement_handle(handle)
handle.drop if handle && handle.respond_to?(:drop) && !handle.finished?
handle
end

# DATABASE STATEMENTS ======================================#

def user_options
Expand Down Expand Up @@ -399,13 +381,12 @@ def select_rows(sql, name = nil)
raw_select(sql,name).first.last
end

def execute(sql, name = nil, &block)
def execute(sql, name = nil, skip_logging = false)
if table_name = query_requires_identity_insert?(sql)
handle = with_identity_insert_enabled(table_name) { raw_execute(sql,name,&block) }
with_identity_insert_enabled(table_name) { do_execute(sql,name) }
else
handle = raw_execute(sql,name,&block)
do_execute(sql,name)
end
finish_statement_handle(handle)
end

def execute_procedure(proc_name, *variables)
Expand Down Expand Up @@ -782,8 +763,19 @@ def connect
@connection = case connection_mode
when :odbc
ODBC.connect config[:dsn], config[:username], config[:password]
when :ado

when :adonet
System::Data::SqlClient::SqlConnection.new.tap do |connection|
connection.connection_string = System::Data::SqlClient::SqlConnectionStringBuilder.new.tap do |cs|
cs.user_i_d = config[:username] if config[:username]
cs.password = config[:password] if config[:password]
cs.integrated_security = true if config[:integrated_security] == 'true'
cs.add 'Server', config[:host].to_clr_string
cs.initial_catalog = config[:database]
cs.multiple_active_result_sets = false
cs.pooling = false
end.to_s
connection.open
end
end
rescue
raise unless @auto_connecting
Expand Down Expand Up @@ -829,6 +821,37 @@ def auto_reconnected?
@auto_connecting = false
end

def raw_connection_run(sql)
with_auto_reconnect do
case connection_mode
when :odbc
block_given? ? raw_connection.run_block(sql) { |handle| yield(handle) } : raw_connection.run(sql)
else :adonet
raw_connection.create_command.tap{ |cmd| cmd.command_text = sql }.execute_reader
end
end
end

def raw_connection_do(sql)
case connection_mode
when :odbc
raw_connection.do(sql)
else :adonet
raw_connection.create_command.tap{ |cmd| cmd.command_text = sql }.execute_non_query
end
end

def finish_statement_handle(handle)
case connection_mode
when :odbc
handle.drop if handle && handle.respond_to?(:drop) && !handle.finished?
when :adonet
handle.close if handle && handle.respond_to?(:close) && !handle.is_closed
handle.dispose if handle && handle.respond_to?(:dispose)
end
handle
end

# DATABASE STATEMENTS ======================================

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

def raw_execute(sql, name = nil, &block)
log(sql,name) { raw_connection_run(sql) }
end

def do_execute(sql,name=nil)
log(sql, name || 'EXECUTE') do
with_auto_reconnect { raw_connection_do(sql) }
end
end

def raw_select(sql, name = nil)
handle = raw_execute(sql,name)
fields_and_row_sets = []
loop do
fields = handle.columns(true).map{|c|c.name}
results = handle_as_array(handle)
rows = results.inject([]) do |rows,row|
row.each_with_index do |value, i|
if value.is_a? ODBC::TimeStamp
row[i] = value.to_sqlserver_string
end
log(sql,name) do
begin
handle = raw_connection_run(sql)
loop do
fields_and_rows = case connection_mode
when :odbc
handle_to_fields_and_rows_odbc(handle)
when :adonet
handle_to_fields_and_rows_adonet(handle)
end
fields_and_row_sets << fields_and_rows
break unless handle_more_results?(handle)
end
rows << row
ensure
finish_statement_handle(handle)
end
fields_and_row_sets << [fields,rows]
finish_statement_handle(handle) && break unless handle.more_results
end
fields_and_row_sets
end

def handle_as_array(handle)
array = handle.inject([]) do |rows,row|
rows << row.inject([]){ |values,value| values << value }
def handle_more_results?(handle)
case connection_mode
when :odbc
handle.more_results
when :adonet
handle.next_result
end
end

def handle_to_fields_and_rows_odbc(handle)
fields = handle.columns(true).map { |c| c.name }
results = handle.inject([]) do |rows,row|
rows << row.inject([]) { |values,value| values << value }
end
rows = results.inject([]) do |rows,row|
row.each_with_index do |value, i|
if value.is_a? ODBC::TimeStamp
row[i] = value.to_sqlserver_string
end
end
rows << row
end
[fields,rows]
end

def handle_to_fields_and_rows_adonet(handle)
if handle.has_rows
fields = []
rows = []
fields_named = false
while handle.read
row = []
handle.visible_field_count.times do |row_index|
value = handle.get_value(row_index)
value = if value.is_a? System::String
value.to_s
elsif value.is_a? System::DBNull
nil
elsif value.is_a? System::DateTime
value.to_string("yyyy-MM-dd HH:MM:ss.fff").to_s
else
value
end
row << value
fields << handle.get_name(row_index).to_s unless fields_named
end
rows << row
fields_named = true
end
else
fields, rows = [], []
end
array
[fields,rows]
end

def add_limit_offset_for_association_limiting!(sql, options)
Expand Down Expand Up @@ -945,7 +1014,7 @@ def default_name(table_name, column_name)

# IDENTITY INSERTS =========================================#

def with_identity_insert_enabled(table_name, &block)
def with_identity_insert_enabled(table_name)
table_name = quote_table_name(table_name_or_views_table_name(table_name))
set_identity_insert(table_name, true)
yield
Expand Down

0 comments on commit 38d8a5f

Please sign in to comment.