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