Permalink
Browse files

Much better support for Microsoft SQL Server

This substantial commit provides much better support for Microsoft
SQL Server.  Specific MSSQL subadapters were added for the ado, odbc,
and jdbc adapters.  Mostly the subadapters involve getting insert
working properly to return the last inserted id.

While making these changes I've noticed that the ado adapter has huge
problems, enough so that I wouldn't recommend that anyone use it.  It
doesn't use a stable native connection, which means it can't work
correctly with transactions.  It requires a pretty ugly hack to get
insert to return the id inserted.

Transactions on ado now unconditionally yield nil.  I thought about
them raising an exception instead, but that would make the ado
adapter not work well with models (without fiddling).  It's possible
the behavior will be changed in the future.

As bad as the ado adapter is now, it's still much better than before.
Before, the ado adapter would run all queries twice when fetching
rows, and if you did any nonidempotent actions inside the SQL, you'd
have problems (as I found out when I used the ugly hack to get
insert to return the id inserted).

The ado and odbc adapters now catch the native exceptions and raise
Sequel::DatabaseError exceptions.  Also, the behavior to handle
blank identifiers has been standardized.  Sequel will now assume an
identifier of 'untitled' if a blank identifier is given.

The shared MSSQL adapter now supports Database#tables and
Database#schema, using the INFORMATION_SCHEMA views (very similarly
to what was used in Sequel 2.0).  Now, it also supports add_column,
rename_column, set_columns_type, set_column_null, and
set_column_default.

The shared MSSQL's schema method doesn't include primary key info, so
some of the model logic changed so that it doesn't try to set
no primary key unless all schema hashes include a primary key entry.

The shared MSSQL adapter now uses the datetime type instead of the
timestamp type for generic datetimes, and uses bit and image for
boolean and file types.  It uses 0 and 1 for false and true, and
no longer attempts to use IS TRUE.

The odbc adapter's literal_time method has been fixed.

In order to ease the connection to MSSQL servers with instances
using a connection string, Sequel now will unescape URL parts. So the
following now works:

  Sequel.connect(ado:///db?host=server%5cinstance)

The ado adapter specs were removed, because the ado adapter itself
doesn't really have any specific behavior that should be tested.  Now
that Sequel has the generic integration tests, those should be used
instead.  I removed the spec_ado rake task. and replaced it with a
spec_firebird rake task.

Here's the results for integration testing on MSSQL with each
adapter:

* ado: 115 examples, 42 failures
* jdbc: 117 examples, 22 failures
* odbc: 115 examples, 19 failures

Many of the remaining failures are due to the fact that some tests
try to insert values into an autoincrementing primary key field,
which MSSQL doesn't allow.  Those tests should be refactored unless
they are explicitly testing that feature.
  • Loading branch information...
1 parent e4d905f commit f44d7916a62d9eacf3b58f9f366c1dccbc0c1f94 @jeremyevans jeremyevans committed Jul 7, 2009
View
@@ -168,7 +168,7 @@ begin
t.spec_opts = spec_opts.call
end
- %w'postgres sqlite mysql informix oracle ado'.each do |adapter|
+ %w'postgres sqlite mysql informix oracle firebird'.each do |adapter|
desc "Run #{adapter} specs without coverage"
Spec::Rake::SpecTask.new("spec_#{adapter}") do |t|
t.spec_files = ["spec/adapters/#{adapter}_spec.rb"] + Dir["spec/integration/*_test.rb"]
View
@@ -1,15 +1,7 @@
require 'win32ole'
module Sequel
- # The ADO adapter provides connectivity to ADO databases in Windows. ADO
- # databases can be opened using a URL with the ado schema:
- #
- # DB = Sequel.connect('ado://mydb')
- #
- # or using the Sequel.ado method:
- #
- # DB = Sequel.ado('mydb')
- #
+ # The ADO adapter provides connectivity to ADO databases in Windows.
module ADO
class Database < Sequel::Database
set_adapter_scheme :ado
@@ -19,20 +11,19 @@ def initialize(opts)
opts[:driver] ||= 'SQL Server'
case opts[:driver]
when 'SQL Server'
- Sequel.require 'adapters/shared/mssql'
- extend Sequel::MSSQL::DatabaseMethods
+ Sequel.require 'adapters/ado/mssql'
+ extend Sequel::ADO::MSSQL::DatabaseMethods
end
end
# Connect to the database. In addition to the usual database options,
- # the following option has effect:
+ # the following options have an effect:
#
# * :command_timeout - Sets the time in seconds to wait while attempting
- # to execute a command before cancelling the attempt and generating
- # an error. Specifically, it sets the ADO CommandTimeout property.
- # If this property is not set, the default of 30 seconds is used.
+ # to execute a command before cancelling the attempt and generating
+ # an error. Specifically, it sets the ADO CommandTimeout property.
+ # If this property is not set, the default of 30 seconds is used.
# * :provider - Sets the Provider of this ADO connection (for example, "SQLOLEDB")
-
def connect(server)
opts = server_opts(server)
s = "driver=#{opts[:driver]};server=#{opts[:host]};database=#{opts[:database]}#{";uid=#{opts[:user]};pwd=#{opts[:password]}" if opts[:user]}"
@@ -50,14 +41,28 @@ def dataset(opts = nil)
def execute(sql, opts={})
log_info(sql)
synchronize(opts[:server]) do |conn|
- r = conn.Execute(sql)
- yield(r) if block_given?
- r
+ begin
+ r = conn.Execute(sql)
+ yield(r) if block_given?
+ rescue ::WIN32OLERuntimeError => e
+ raise_error(e)
+ end
end
+ nil
+ end
+ alias do execute
+
+ # The ADO adapter doesn't support transactions, since it appears not to
+ # use a single native connection for each connection in the pool
+ def transaction(opts={})
+ yield nil
end
- alias_method :do, :execute
private
+
+ def connection_pool_default_options
+ super.merge(:pool_convert_exceptions=>false)
+ end
def disconnect_connection(conn)
conn.Close
@@ -67,25 +72,8 @@ def disconnect_connection(conn)
class Dataset < Sequel::Dataset
def fetch_rows(sql)
execute(sql) do |s|
- @columns = s.Fields.extend(Enumerable).map do |column|
- name = column.Name.empty? ? '(no column name)' : column.Name
- output_identifier(name)
- end
-
- unless s.eof
- s.moveFirst
- s.getRows.transpose.each {|r| yield hash_row(r)}
- end
- end
- self
- end
-
- private
-
- def hash_row(row)
- @columns.inject({}) do |m, c|
- m[c] = row.shift
- m
+ @columns = cols = s.Fields.extend(Enumerable).map{|column| output_identifier(column.Name)}
+ s.getRows.transpose.each{|r| yield cols.inject({}){|m,c| m[c] = r.shift; m}}
end
end
end
@@ -0,0 +1,30 @@
+Sequel.require 'adapters/shared/mssql'
+
+module Sequel
+ module ADO
+ # Database and Dataset instance methods for MSSQL specific
+ # support via ADO.
+ module MSSQL
+ module DatabaseMethods
+ include Sequel::MSSQL::DatabaseMethods
+
+ # Return instance of Sequel::ADO::MSSQL::Dataset with the given opts.
+ def dataset(opts=nil)
+ Sequel::ADO::MSSQL::Dataset.new(self, opts)
+ end
+ end
+
+ class Dataset < ADO::Dataset
+ include Sequel::MSSQL::DatasetMethods
+
+ # Use a nasty hack of multiple SQL statements in the same call and
+ # having the last one return the most recently inserted id. This
+ # is necessary as ADO doesn't provide a consistent native connection.
+ def insert(values={})
+ return super if @opts[:sql]
+ with_sql("SET NOCOUNT ON; #{insert_sql(values)}; SELECT SCOPE_IDENTITY()").single_value
+ end
+ end
+ end
+ end
+end
@@ -58,8 +58,8 @@ module JavaSQL
Java::oracle.jdbc.driver.OracleDriver
end,
:sqlserver=>proc do |db|
- Sequel.require 'adapters/shared/mssql'
- db.extend(Sequel::MSSQL::DatabaseMethods)
+ Sequel.require 'adapters/jdbc/mssql'
+ db.extend(Sequel::JDBC::MSSQL::DatabaseMethods)
com.microsoft.sqlserver.jdbc.SQLServerDriver
end,
:h2=>proc do |db|
@@ -0,0 +1,38 @@
+Sequel.require 'adapters/shared/mssql'
+
+module Sequel
+ module JDBC
+ # Database and Dataset instance methods for MSSQL specific
+ # support via JDBC.
+ module MSSQL
+ # Database instance methods for MSSQL databases accessed via JDBC.
+ module DatabaseMethods
+ include Sequel::MSSQL::DatabaseMethods
+
+ # Return instance of Sequel::JDBC::MSSQL::Dataset with the given opts.
+ def dataset(opts=nil)
+ Sequel::JDBC::MSSQL::Dataset.new(self, opts)
+ end
+
+ private
+
+ # Get the last inserted id using SCOPE_IDENTITY().
+ def last_insert_id(conn, opts={})
+ stmt = conn.createStatement
+ begin
+ rs = stmt.executeQuery('SELECT SCOPE_IDENTITY()')
+ rs.next
+ rs.getInt(1)
+ ensure
+ stmt.close
+ end
+ end
+ end
+
+ # Dataset class for MSSQL datasets accessed via JDBC.
+ class Dataset < JDBC::Dataset
+ include Sequel::MSSQL::DatasetMethods
+ end
+ end
+ end
+end
@@ -12,8 +12,8 @@ def initialize(opts)
super(opts)
case opts[:db_type]
when 'mssql'
- Sequel.require 'adapters/shared/mssql'
- extend Sequel::MSSQL::DatabaseMethods
+ Sequel.require 'adapters/odbc/mssql'
+ extend Sequel::ODBC::MSSQL::DatabaseMethods
when 'progress'
Sequel.require 'adapters/shared/progress'
extend Sequel::Progress::DatabaseMethods
@@ -50,6 +50,8 @@ def execute(sql, opts={})
begin
r = conn.run(sql)
yield(r) if block_given?
+ rescue ::ODBC::Error => e
+ raise_error(e)
ensure
r.drop if r
end
@@ -59,12 +61,22 @@ def execute(sql, opts={})
def execute_dui(sql, opts={})
log_info(sql)
- synchronize(opts[:server]){|conn| conn.do(sql)}
+ synchronize(opts[:server]) do |conn|
+ begin
+ conn.do(sql)
+ rescue ::ODBC::Error => e
+ raise_error(e)
+ end
+ end
end
- alias_method :do, :execute_dui
+ alias do execute_dui
private
+ def connection_pool_default_options
+ super.merge(:pool_convert_exceptions=>false)
+ end
+
def connection_execute_method
:do
end
@@ -81,19 +93,19 @@ class Dataset < Sequel::Dataset
ODBC_TIMESTAMP_AFTER_SECONDS =
ODBC_TIMESTAMP_FORMAT.index( '%S' ).succ - ODBC_TIMESTAMP_FORMAT.length
ODBC_DATE_FORMAT = "{d '%Y-%m-%d'}".freeze
- UNTITLED_COLUMN = 'untitled_%d'.freeze
def fetch_rows(sql, &block)
execute(sql) do |s|
- untitled_count = 0
- @columns = s.columns(true).map do |c|
- if (n = c.name).empty?
- n = UNTITLED_COLUMN % (untitled_count += 1)
+ i = -1
+ cols = s.columns(true).map{|c| [output_identifier(c.name), i+=1]}
+ @columns = cols.map{|c| c.at(0)}
+ if rows = s.fetch_all
+ rows.each do |row|
+ hash = {}
+ cols.each{|n,i| hash[n] = convert_odbc_value(row[i])}
+ yield hash
end
- output_identifier(n)
end
- rows = s.fetch_all
- rows.each {|row| yield hash_row(row)} if rows
end
self
end
@@ -119,15 +131,7 @@ def convert_odbc_value(v)
v
end
end
-
- def hash_row(row)
- hash = {}
- row.each_with_index do |v, idx|
- hash[@columns[idx]] = convert_odbc_value(v)
- end
- hash
- end
-
+
def literal_date(v)
v.strftime(ODBC_DATE_FORMAT)
end
@@ -149,7 +153,7 @@ def literal_true
def literal_time(v)
formatted = v.strftime(ODBC_TIMESTAMP_FORMAT)
- formatted.insert(ODBC_TIMESTAMP_AFTER_SECONDS, ".#{(v.usec.to_f/1000).round}") if usec >= 1000
+ formatted.insert(ODBC_TIMESTAMP_AFTER_SECONDS, ".#{(v.usec.to_f/1000).round}") if v.usec >= 1000
formatted
end
end
@@ -0,0 +1,44 @@
+Sequel.require 'adapters/shared/mssql'
+
+module Sequel
+ module ODBC
+ # Database and Dataset instance methods for MSSQL specific
+ # support via ODBC.
+ module MSSQL
+ module DatabaseMethods
+ include Sequel::MSSQL::DatabaseMethods
+ LAST_INSERT_ID_SQL='SELECT SCOPE_IDENTITY()'
+
+ # Return an instance of Sequel::ODBC::MSSQL::Dataset with the given opts.
+ def dataset(opts=nil)
+ Sequel::ODBC::MSSQL::Dataset.new(self, opts)
+ end
+
+ # Return the last inserted identity value.
+ def execute_insert(sql, opts={})
+ log_info(sql)
+ synchronize(opts[:server]) do |conn|
+ begin
+ conn.do(sql)
+ log_info(LAST_INSERT_ID_SQL)
+ begin
+ s = conn.run(LAST_INSERT_ID_SQL)
+ if (rows = s.fetch_all) and (row = rows.first)
+ Integer(row.first)
+ end
+ ensure
+ s.drop if s
+ end
+ rescue ::ODBC::Error => e
+ raise_error(e)
+ end
+ end
+ end
+ end
+
+ class Dataset < ODBC::Dataset
+ include Sequel::MSSQL::DatasetMethods
+ end
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit f44d791

Please sign in to comment.