--- C:\ruby\lib\ruby\gems\1.8\gems\activerecord-sqlserver-adapter-1.0.0.9250\lib\active_record\connection_adapters\sqlserver_adapter.rb Tue Jul 29 20:06:46 2008
+++ C:\ruby\lib\ruby\gems\1.8\gems\activerecord-sqlserver-adapter-1.0.0.9250\lib\active_record\connection_adapters\Export of sqlserver_adapter.rb Tue Jul 29 20:02:08 2008
@@ -1,6 +1,5 @@
require 'active_record/connection_adapters/abstract_adapter'
-require 'base64'
require 'bigdecimal'
require 'bigdecimal/util'
@@ -42,7 +41,11 @@
raise ArgumentError, "Missing Database. Argument ':database' must be set in order for this adapter to work." unless config.has_key?(:database)
database = config[:database]
host = config[:host] ? config[:host].to_s : 'localhost'
- driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User ID=#{username};Password=#{password};"
+ unless config[:trusted_connection]
+ driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User Id=#{username};Password=#{password};"
+ else
+ driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};Trusted_Connection=Yes;"
+ end
end
conn = DBI.connect(driver_url, username, password)
conn["AutoCommit"] = autocommit
@@ -60,12 +63,11 @@
@is_special = sql_type =~ /text|ntext|image/i
# TODO: check ok to remove @scale = scale_value
# SQL Server only supports limits on *char and float types
- @limit = nil unless @type == :float or @type == :string
+ @limit = nil unless @type == :string
end
def simplified_type(field_type)
case field_type
- when /real/i then :float
when /money/i then :decimal
when /image/i then :binary
when /bit/i then :boolean
@@ -79,13 +81,11 @@
case type
when :datetime then cast_to_datetime(value)
when :timestamp then cast_to_time(value)
- when :time then cast_to_time(value)
- when :date then cast_to_datetime(value)
when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or value.to_s == '1'
else super
end
end
-
+
def cast_to_time(value)
return value if value.is_a?(Time)
time_array = ParseDate.parsedate(value)
@@ -105,7 +105,6 @@
if value.is_a?(DateTime)
return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec)
- #return DateTime.new(value.year, value.mon, value.day, value.hour, value.min, value.sec)
end
return cast_to_time(value) if value.is_a?(Date) or value.is_a?(String) rescue nil
@@ -114,23 +113,37 @@
# TODO: Find less hack way to convert DateTime objects into Times
- def self.string_to_time(value)
- if value.is_a?(DateTime)
- return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec)
- else
- super
+# def self.string_to_time(value)
+# if value.is_a?(DateTime)
+# return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec)
+# else
+# super
+# end
+# end
+
+ # These methods will only allow the adapter to insert binary data with a length of 7K or less
+ # because of a SQL Server statement length policy.
+ def self.string_to_binary(value)
+ value.gsub(/(\r|\n|\0|\x1a)/) do
+ case $1
+ when "\r" then "%00"
+ when "\n" then "%01"
+ when "\0" then "%02"
+ when "\x1a" then "%03"
+ end
end
end
- # These methods will only allow the adapter to insert binary data with a length of 7K or less
- # because of a SQL Server statement length policy.
- def self.string_to_binary(value)
- Base64.encode64(value)
- end
-
- def self.binary_to_string(value)
- Base64.decode64(value)
- end
+ def self.binary_to_string(value)
+ value.gsub(/(%00|%01|%02|%03)/) do
+ case $1
+ when "%00" then "\r"
+ when "%01" then "\n"
+ when "%02\0" then "\0"
+ when "%03" then "\x1a"
+ end
+ end
+ end
end
# In ADO mode, this adapter will ONLY work on Windows systems,
@@ -157,13 +170,11 @@
# * :mode -- ADO or ODBC. Defaults to ADO.
# * :username -- Defaults to sa.
# * :password -- Defaults to empty string.
- # * :windows_auth -- Defaults to "User ID=#{username};Password=#{password}"
#
# ADO specific options:
#
# * :host -- Defaults to localhost.
# * :database -- The name of the database. No default, must be provided.
- # * :windows_auth -- Use windows authentication instead of username/password.
#
# ODBC specific options:
#
@@ -186,15 +197,15 @@
{
:primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
+ :text => { :name => "varchar", :limit => "MAX"},
:integer => { :name => "int" },
- :float => { :name => "float", :limit => 8 },
+ :float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
- :time => { :name => "datetime" },
- :date => { :name => "datetime" },
- :binary => { :name => "image"},
+ :time => { :name => "time" },
+ :date => { :name => "date" },
+ :binary => { :name => "varchar", :limit => "MAX"},
:boolean => { :name => "bit"}
}
end
@@ -244,7 +255,7 @@
@connection.disconnect rescue nil
end
- def select_rows(sql, name = nil)
+ def select_rows(sql, name = nil)
rows = []
repair_special_columns(sql)
log(sql, name) do
@@ -261,26 +272,34 @@
end
end
rows
- end
-
- def columns(table_name, name = nil)
+ end
+
+ def columns(table_name, name = nil)
return [] if table_name.blank?
table_name = table_name.to_s if table_name.is_a?(Symbol)
table_name = table_name.split('.')[-1] unless table_name.nil?
table_name = table_name.gsub(/[\[\]]/, '')
sql = %Q{
- SELECT
- cols.COLUMN_NAME as ColName,
- cols.COLUMN_DEFAULT as DefaultValue,
- cols.NUMERIC_SCALE as numeric_scale,
- cols.NUMERIC_PRECISION as numeric_precision,
- cols.DATA_TYPE as ColType,
- cols.IS_NULLABLE As IsNullable,
- COL_LENGTH(cols.TABLE_NAME, cols.COLUMN_NAME) as Length,
- COLUMNPROPERTY(OBJECT_ID(cols.TABLE_NAME), cols.COLUMN_NAME, 'IsIdentity') as IsIdentity,
- cols.NUMERIC_SCALE as Scale
- FROM INFORMATION_SCHEMA.COLUMNS cols
- WHERE cols.TABLE_NAME = '#{table_name}'
+SELECT
+clmns.name AS ColName,
+object_definition(clmns.default_object_id) as DefaultValue,
+CAST(clmns.scale AS int) AS numeric_scale,
+CAST(clmns.precision AS int) AS numeric_precision,
+usrt.name AS ColType,
+case clmns.is_nullable when 0 then 'NO' else 'YES' end AS IsNullable,
+CAST(CASE WHEN baset.name IN (N'nchar', N'nvarchar') AND clmns.max_length <> -1 THEN
+clmns.max_length/2 ELSE clmns.max_length END AS int) AS Length,
+clmns.is_identity as IsIdentity
+FROM
+sys.tables AS tbl
+INNER JOIN sys.all_columns AS clmns ON clmns.object_id=tbl.object_id
+LEFT OUTER JOIN sys.types AS usrt ON usrt.user_type_id = clmns.user_type_id
+LEFT OUTER JOIN sys.types AS baset ON baset.user_type_id = clmns.system_type_id and
+baset.user_type_id = baset.system_type_id
+WHERE
+(tbl.name=N'#{table_name}' )
+ORDER BY
+clmns.column_id ASC
}
# Comment out if you want to have the Columns select statment logged.
# Personally, I think it adds unnecessary bloat to the log.
@@ -289,7 +308,7 @@
#result = @connection.select_all(sql)
columns = []
result.each do |field|
- default = field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null/i ? nil : field[:DefaultValue]
+ default = field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null|NULL/ ? nil : field[:DefaultValue]
if field[:ColType] =~ /numeric|decimal/i
type = "#{field[:ColType]}(#{field[:numeric_precision]},#{field[:numeric_scale]})"
else
@@ -301,28 +320,19 @@
end
columns
end
-
- def empty_insert_statement(table_name)
- "INSERT INTO #{table_name} DEFAULT VALUES"
- end
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
- super || select_value("SELECT @@IDENTITY AS Ident")
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
+ execute(sql, name)
+ id_value || select_one("SELECT scope_identity() AS Ident")["Ident"]
end
- def update_sql(sql, name = nil)
- autoCommiting = @connection["AutoCommit"]
- begin
- begin_db_transaction if autoCommiting
- execute(sql, name)
- affectedRows = select_value("SELECT @@ROWCOUNT AS AffectedRows")
- commit_db_transaction if autoCommiting
- affectedRows
- rescue
- rollback_db_transaction if autoCommiting
- raise
- end
+ def update(sql, name = nil)
+ execute(sql, name) do |handle|
+ handle.rows
+ end || select_one("SELECT @@ROWCOUNT AS AffectedRows")["AffectedRows"]
end
+
+ alias_method :delete, :update
def execute(sql, name = nil)
if sql =~ /^\s*INSERT/i && (table_name = query_requires_identity_insert?(sql))
@@ -366,14 +376,9 @@
case value
when TrueClass then '1'
when FalseClass then '0'
- else
- if value.acts_like?(:time)
- "'#{value.strftime("%Y%m%d %H:%M:%S")}'"
- elsif value.acts_like?(:date)
- "'#{value.strftime("%Y%m%d")}'"
- else
- super
- end
+ when Time, DateTime then "'#{value.strftime("%Y%m%d %H:%M:%S")}'"
+ when Date then "'#{value.strftime("%Y%m%d")}'"
+ else super
end
end
@@ -381,49 +386,27 @@
string.gsub(/\'/, "''")
end
- def quote_column_name(name)
- "[#{name}]"
+ def quoted_true
+ "1"
end
- def add_limit_offset!(sql, options)
- if options[:limit] and options[:offset]
- total_rows = @connection.select_all("SELECT count(*) as TotalRows from (#{sql.gsub(/\bSELECT(\s+DISTINCT)?\b/i, "SELECT#{$1} TOP 1000000000")}) tally")[0][:TotalRows].to_i
- if (options[:limit] + options[:offset]) >= total_rows
- options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
- end
- sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]} ")
- sql << ") AS tmp1"
- if options[:order]
- order = options[:order].split(',').map do |field|
- parts = field.split(" ")
- tc = parts[0]
- if sql =~ /\.\[/ and tc =~ /\./ # if column quoting used in query
- tc.gsub!(/\./, '\\.\\[')
- tc << '\\]'
- end
- if sql =~ /#{tc} AS (t\d_r\d\d?)/
- parts[0] = $1
- elsif parts[0] =~ /\w+\.(\w+)/
- parts[0] = $1
- end
- parts.join(' ')
- end.join(', ')
- sql << " ORDER BY #{change_order_direction(order)}) AS tmp2 ORDER BY #{order}"
- else
- sql << " ) AS tmp2"
- end
- elsif sql !~ /^\s*SELECT (@@|COUNT\()/i
- sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i) do
- "SELECT#{$1} TOP #{options[:limit]}"
- end unless options[:limit].nil?
- end
+ def quoted_false
+ "0"
end
- def add_lock!(sql, options)
- @logger.info "Warning: SQLServer :lock option '#{options[:lock].inspect}' not supported" if @logger && options.has_key?(:lock)
- sql
+ def quote_column_name(name)
+ "[#{name}]"
end
+ def add_limit_offset!(sql, options)
+ if options[:limit] && options[:offset] && options[:offset] > 0
+ sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i) {"SELECT#{$1} TOP #{options[:limit] + options[:offset]} "}
+ sql.sub!(/ FROM /i, " INTO #limit_offset_temp -- limit => #{options[:limit]} offset => #{options[:offset]} \n FROM ")
+ elsif options[:limit] && (sql !~ /^\s*SELECT (@@|COUNT\()/i)
+ sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i) {"SELECT#{$1} TOP #{options[:limit]} "}
+ end
+ end
+
def recreate_database(name)
drop_database(name)
create_database(name)
@@ -443,9 +426,9 @@
def tables(name = nil)
execute("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'", name) do |sth|
- result = sth.inject([]) do |tables, field|
+ sth.inject([]) do |tables, field|
table_name = field[0]
- tables << table_name unless table_name == 'dtproperties'
+ tables << table_name unless table_name == 'dtproperties'
tables
end
end
@@ -454,28 +437,34 @@
def indexes(table_name, name = nil)
ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = false
indexes = []
- execute("EXEC sp_helpindex '#{table_name}'", name) do |handle|
- if handle.column_info.any?
- handle.each do |index|
- unique = index[1] =~ /unique/
- primary = index[1] =~ /primary key/
- if !primary
- indexes << IndexDefinition.new(table_name, index[0], unique, index[2].split(", ").map {|e| e.gsub('(-)','')})
- end
+ execute("EXEC sp_helpindex '#{table_name}'", name) do |sth|
+ sth.each do |index|
+ unique = index[1] =~ /unique/
+ primary = index[1] =~ /primary key/
+ if !primary
+ indexes << IndexDefinition.new(table_name, index[0], unique, index[2].split(", "))
end
end
end
indexes
- ensure
- ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = true
+ ensure
+ ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = true
end
+
+ def add_order_by_for_association_limiting!(sql, options)
+ # Just skip ORDER BY clause. I dont know better solution for DISTINCT plus ORDER BY.
+ # And this doesnt cause to much problem..
+ return sql
+ end
def rename_table(name, new_name)
execute "EXEC sp_rename '#{name}', '#{new_name}'"
end
-
+
+ # Adds a new column to the named table.
+ # See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
- add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(add_column_sql, options)
# TODO: Add support to mimic date columns, using constraints to mark them as such in the database
# add_column_sql << " CONSTRAINT ck__#{table_name}__#{column_name}__date_only CHECK ( CONVERT(CHAR(12), #{quote_column_name(column_name)}, 14)='00:00:00:000' )" if type == :date
@@ -487,27 +476,20 @@
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
- sql = "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- sql << " NOT NULL" if options[:null] == false
- sql_commands = [sql]
+ sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"]
if options_include_default?(options)
remove_default_constraint(table_name, column_name)
- sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(options[:default], options[:column])} FOR #{quote_column_name(column_name)}"
+ sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(options[:default], options[:column])} FOR #{column_name}"
end
sql_commands.each {|c|
execute(c)
}
end
- def change_column_default(table_name, column_name, default)
- remove_default_constraint(table_name, column_name)
- execute "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(default, column_name)} FOR #{quote_column_name(column_name)}"
- end
-
def remove_column(table_name, column_name)
remove_check_constraints(table_name, column_name)
remove_default_constraint(table_name, column_name)
- execute "ALTER TABLE [#{table_name}] DROP COLUMN #{quote_column_name(column_name)}"
+ execute "ALTER TABLE [#{table_name}] DROP COLUMN [#{column_name}]"
end
def remove_default_constraint(table_name, column_name)
@@ -533,7 +515,23 @@
private
def select(sql, name = nil)
repair_special_columns(sql)
-
+ if match = query_has_limit_and_offset?(sql)
+ matched, limit, offset = *match
+ execute(sql)
+ # SET ROWCOUNT n causes all statements to only affect n rows, which we use
+ # to delete offset rows from the temporary table
+ execute("SET ROWCOUNT #{offset}")
+ execute("DELETE from #limit_offset_temp")
+ execute("SET ROWCOUNT 0")
+ result = execute_select("SELECT * FROM #limit_offset_temp")
+ execute("DROP TABLE #limit_offset_temp")
+ result
+ else
+ execute_select(sql)
+ end
+ end
+
+ def execute_select(sql)
result = []
execute(sql) do |handle|
handle.each do |row|
@@ -550,6 +548,10 @@
result
end
+ def query_has_limit_and_offset?(sql)
+ match = sql.match(/#limit_offset_temp -- limit => (\d+) offset => (\d+)/)
+ end
+
# Turns IDENTITY_INSERT ON for table during execution of the block
# N.B. This sets the state of IDENTITY_INSERT to OFF after the
# block has been executed without regard to its previous state