sqlserver_adapter.rb.diff

Diff from official version - Chris Taylor, 2008-07-29 21:43

Download (19.4 KB)

View differences:

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 1
require 'active_record/connection_adapters/abstract_adapter'
2 2

  
3
require 'base64'
4 3
require 'bigdecimal'
5 4
require 'bigdecimal/util'
6 5

  
......
42 41
        raise ArgumentError, "Missing Database. Argument ':database' must be set in order for this adapter to work." unless config.has_key?(:database)
43 42
        database  = config[:database]
44 43
        host      = config[:host] ? config[:host].to_s : 'localhost'
45
        driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User ID=#{username};Password=#{password};"
44
        unless config[:trusted_connection]
45
          driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User Id=#{username};Password=#{password};"
46
        else
47
          driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};Trusted_Connection=Yes;"
48
        end
46 49
      end
47 50
      conn      = DBI.connect(driver_url, username, password)
48 51
      conn["AutoCommit"] = autocommit
......
60 63
        @is_special = sql_type =~ /text|ntext|image/i
61 64
        # TODO: check ok to remove @scale = scale_value
62 65
        # SQL Server only supports limits on *char and float types
63
        @limit = nil unless @type == :float or @type == :string
66
        @limit = nil unless @type == :string
64 67
      end
65 68

  
66 69
      def simplified_type(field_type)
67 70
        case field_type
68
          when /real/i              then :float
69 71
          when /money/i             then :decimal
70 72
          when /image/i             then :binary
71 73
          when /bit/i               then :boolean
......
79 81
        case type
80 82
        when :datetime  then cast_to_datetime(value)
81 83
        when :timestamp then cast_to_time(value)
82
        when :time      then cast_to_time(value)
83
        when :date      then cast_to_datetime(value)
84 84
        when :boolean   then value == true or (value =~ /^t(rue)?$/i) == 0 or value.to_s == '1'
85 85
        else super
86 86
        end
87 87
      end
88
      
88
 
89 89
      def cast_to_time(value)
90 90
        return value if value.is_a?(Time)
91 91
        time_array = ParseDate.parsedate(value)
......
105 105
   
106 106
        if value.is_a?(DateTime)
107 107
          return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec)
108
          #return DateTime.new(value.year, value.mon, value.day, value.hour, value.min, value.sec)
109 108
        end
110 109
        
111 110
        return cast_to_time(value) if value.is_a?(Date) or value.is_a?(String) rescue nil
......
114 113
      
115 114
      # TODO: Find less hack way to convert DateTime objects into Times
116 115
      
117
      def self.string_to_time(value)
118
        if value.is_a?(DateTime)
119
          return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec)
120
        else
121
          super
116
#      def self.string_to_time(value)
117
#        if value.is_a?(DateTime)
118
#          return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec)
119
#        else
120
#          super
121
#        end
122
#      end
123

  
124
      # These methods will only allow the adapter to insert binary data with a length of 7K or less
125
      # because of a SQL Server statement length policy.
126
      def self.string_to_binary(value)
127
        value.gsub(/(\r|\n|\0|\x1a)/) do
128
          case $1
129
            when "\r"   then  "%00"
130
            when "\n"   then  "%01"
131
            when "\0"   then  "%02"
132
            when "\x1a" then  "%03"
133
          end
122 134
        end
123 135
      end
124 136

  
125
      # These methods will only allow the adapter to insert binary data with a length of 7K or less
126
      # because of a SQL Server statement length policy.
127
      def self.string_to_binary(value) 
128
 	      Base64.encode64(value) 
129
      end 
130
 	       
131
 	    def self.binary_to_string(value) 
132
 	      Base64.decode64(value) 
133
 	    end 
137
      def self.binary_to_string(value)
138
        value.gsub(/(%00|%01|%02|%03)/) do
139
          case $1
140
            when "%00"    then  "\r"
141
            when "%01"    then  "\n"
142
            when "%02\0"  then  "\0"
143
            when "%03"    then  "\x1a"
144
          end
145
        end
146
      end
134 147
    end
135 148

  
136 149
    # In ADO mode, this adapter will ONLY work on Windows systems, 
......
157 170
    # * <tt>:mode</tt>      -- ADO or ODBC. Defaults to ADO.
158 171
    # * <tt>:username</tt>  -- Defaults to sa.
159 172
    # * <tt>:password</tt>  -- Defaults to empty string.
160
    # * <tt>:windows_auth</tt> -- Defaults to "User ID=#{username};Password=#{password}"
161 173
    #
162 174
    # ADO specific options:
163 175
    #
164 176
    # * <tt>:host</tt>      -- Defaults to localhost.
165 177
    # * <tt>:database</tt>  -- The name of the database. No default, must be provided.
166
    # * <tt>:windows_auth</tt> -- Use windows authentication instead of username/password.
167 178
    #
168 179
    # ODBC specific options:                   
169 180
    #
......
186 197
        {
187 198
          :primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
188 199
          :string      => { :name => "varchar", :limit => 255  },
189
          :text        => { :name => "text" },
200
          :text        => { :name => "varchar", :limit => "MAX"},
190 201
          :integer     => { :name => "int" },
191
          :float       => { :name => "float", :limit => 8 },
202
          :float       => { :name => "float" },
192 203
          :decimal     => { :name => "decimal" },
193 204
          :datetime    => { :name => "datetime" },
194 205
          :timestamp   => { :name => "datetime" },
195
          :time        => { :name => "datetime" },
196
          :date        => { :name => "datetime" },
197
          :binary      => { :name => "image"},
206
          :time        => { :name => "time" },
207
          :date        => { :name => "date" },
208
          :binary      => { :name => "varchar", :limit => "MAX"},
198 209
          :boolean     => { :name => "bit"}
199 210
        }
200 211
      end
......
244 255
        @connection.disconnect rescue nil
245 256
      end
246 257

  
247
      def select_rows(sql, name = nil)
258
     def select_rows(sql, name = nil)
248 259
        rows = []
249 260
        repair_special_columns(sql)
250 261
        log(sql, name) do
......
261 272
          end
262 273
        end
263 274
        rows
264
      end
265

  
266
      def columns(table_name, name = nil)
275
      end      
276
            
277
      def columns(table_name, name = nil)        
267 278
        return [] if table_name.blank?
268 279
        table_name = table_name.to_s if table_name.is_a?(Symbol)
269 280
        table_name = table_name.split('.')[-1] unless table_name.nil?
270 281
        table_name = table_name.gsub(/[\[\]]/, '')
271 282
        sql = %Q{
272
          SELECT 
273
            cols.COLUMN_NAME as ColName,  
274
            cols.COLUMN_DEFAULT as DefaultValue,
275
            cols.NUMERIC_SCALE as numeric_scale,
276
            cols.NUMERIC_PRECISION as numeric_precision, 
277
            cols.DATA_TYPE as ColType, 
278
            cols.IS_NULLABLE As IsNullable,  
279
            COL_LENGTH(cols.TABLE_NAME, cols.COLUMN_NAME) as Length,  
280
            COLUMNPROPERTY(OBJECT_ID(cols.TABLE_NAME), cols.COLUMN_NAME, 'IsIdentity') as IsIdentity,  
281
            cols.NUMERIC_SCALE as Scale 
282
          FROM INFORMATION_SCHEMA.COLUMNS cols 
283
          WHERE cols.TABLE_NAME = '#{table_name}'   
283
SELECT
284
clmns.name AS ColName,
285
object_definition(clmns.default_object_id) as DefaultValue,
286
CAST(clmns.scale AS int) AS numeric_scale,
287
CAST(clmns.precision AS int) AS numeric_precision,
288
usrt.name AS ColType,
289
case clmns.is_nullable when 0 then 'NO' else 'YES' end AS IsNullable,
290
CAST(CASE WHEN baset.name IN (N'nchar', N'nvarchar') AND clmns.max_length <> -1 THEN 
291
clmns.max_length/2 ELSE clmns.max_length END AS int) AS Length,
292
clmns.is_identity as IsIdentity
293
FROM
294
sys.tables AS tbl
295
INNER JOIN sys.all_columns AS clmns ON clmns.object_id=tbl.object_id
296
LEFT OUTER JOIN sys.types AS usrt ON usrt.user_type_id = clmns.user_type_id
297
LEFT OUTER JOIN sys.types AS baset ON baset.user_type_id = clmns.system_type_id and 
298
baset.user_type_id = baset.system_type_id
299
WHERE
300
(tbl.name=N'#{table_name}' )
301
ORDER BY
302
clmns.column_id ASC
284 303
        }
285 304
        # Comment out if you want to have the Columns select statment logged.
286 305
        # Personally, I think it adds unnecessary bloat to the log. 
......
289 308
        #result = @connection.select_all(sql)
290 309
        columns = []
291 310
        result.each do |field|
292
          default = field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null/i ? nil : field[:DefaultValue]
311
          default = field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null|NULL/ ? nil : field[:DefaultValue]
293 312
          if field[:ColType] =~ /numeric|decimal/i
294 313
            type = "#{field[:ColType]}(#{field[:numeric_precision]},#{field[:numeric_scale]})"
295 314
          else
......
301 320
        end
302 321
        columns
303 322
      end
304
      
305
      def empty_insert_statement(table_name)
306
        "INSERT INTO #{table_name} DEFAULT VALUES"
307
      end      
308 323

  
309
      def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
310
        super || select_value("SELECT @@IDENTITY AS Ident")
324
      def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
325
        execute(sql, name)
326
        id_value || select_one("SELECT scope_identity() AS Ident")["Ident"]
311 327
      end
312 328

  
313
      def update_sql(sql, name = nil)          
314
        autoCommiting = @connection["AutoCommit"]
315
        begin
316
          begin_db_transaction if autoCommiting
317
          execute(sql, name)
318
          affectedRows = select_value("SELECT @@ROWCOUNT AS AffectedRows")
319
          commit_db_transaction if autoCommiting
320
          affectedRows
321
        rescue
322
          rollback_db_transaction if autoCommiting
323
          raise
324
        end                    
329
      def update(sql, name = nil)
330
        execute(sql, name) do |handle|
331
          handle.rows
332
        end || select_one("SELECT @@ROWCOUNT AS AffectedRows")["AffectedRows"]        
325 333
      end
334
      
335
      alias_method :delete, :update
326 336

  
327 337
      def execute(sql, name = nil)
328 338
        if sql =~ /^\s*INSERT/i && (table_name = query_requires_identity_insert?(sql))
......
366 376
        case value
367 377
          when TrueClass             then '1'
368 378
          when FalseClass            then '0'
369
          else
370
            if value.acts_like?(:time)
371
              "'#{value.strftime("%Y%m%d %H:%M:%S")}'"
372
            elsif value.acts_like?(:date)
373
              "'#{value.strftime("%Y%m%d")}'"
374
            else
375
              super
376
            end
379
          when Time, DateTime        then "'#{value.strftime("%Y%m%d %H:%M:%S")}'"
380
          when Date                  then "'#{value.strftime("%Y%m%d")}'"
381
          else                       super
377 382
        end
378 383
      end
379 384

  
......
381 386
        string.gsub(/\'/, "''")
382 387
      end
383 388

  
384
      def quote_column_name(name)
385
        "[#{name}]"
389
      def quoted_true
390
        "1"
386 391
      end
387 392

  
388
      def add_limit_offset!(sql, options)
389
        if options[:limit] and options[:offset]
390
          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
391
          if (options[:limit] + options[:offset]) >= total_rows
392
            options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
393
          end
394
          sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]} ")
395
          sql << ") AS tmp1"
396
          if options[:order]
397
            order = options[:order].split(',').map do |field|
398
              parts = field.split(" ")
399
              tc = parts[0]
400
              if sql =~ /\.\[/ and tc =~ /\./ # if column quoting used in query
401
                tc.gsub!(/\./, '\\.\\[')
402
                tc << '\\]'
403
              end
404
              if sql =~ /#{tc} AS (t\d_r\d\d?)/
405
                parts[0] = $1
406
              elsif parts[0] =~ /\w+\.(\w+)/
407
                parts[0] = $1
408
              end
409
              parts.join(' ')
410
            end.join(', ')
411
            sql << " ORDER BY #{change_order_direction(order)}) AS tmp2 ORDER BY #{order}"
412
          else
413
            sql << " ) AS tmp2"
414
          end
415
        elsif sql !~ /^\s*SELECT (@@|COUNT\()/i
416
          sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i) do
417
            "SELECT#{$1} TOP #{options[:limit]}"
418
          end unless options[:limit].nil?
419
        end
393
      def quoted_false
394
        "0"
420 395
      end
421 396

  
422
      def add_lock!(sql, options)
423
        @logger.info "Warning: SQLServer :lock option '#{options[:lock].inspect}' not supported" if @logger && options.has_key?(:lock)
424
        sql
397
      def quote_column_name(name)
398
        "[#{name}]"
425 399
      end
426 400

  
401
    def add_limit_offset!(sql, options)
402
      if options[:limit] && options[:offset]  && options[:offset] > 0 
403
        sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i) {"SELECT#{$1} TOP #{options[:limit] + options[:offset]} "} 
404
        sql.sub!(/ FROM /i, " INTO #limit_offset_temp -- limit => #{options[:limit]} offset => #{options[:offset]} \n FROM ") 
405
      elsif options[:limit] && (sql !~ /^\s*SELECT (@@|COUNT\()/i) 
406
        sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i) {"SELECT#{$1} TOP #{options[:limit]} "}       
407
      end
408
    end
409
    
427 410
      def recreate_database(name)
428 411
        drop_database(name)
429 412
        create_database(name)
......
443 426

  
444 427
      def tables(name = nil)
445 428
        execute("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'", name) do |sth|
446
          result = sth.inject([]) do |tables, field|
429
          sth.inject([]) do |tables, field|
447 430
            table_name = field[0]
448
            tables << table_name unless table_name == 'dtproperties'            
431
            tables << table_name unless table_name == 'dtproperties'
449 432
            tables
450 433
          end
451 434
        end
......
454 437
      def indexes(table_name, name = nil)
455 438
        ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = false
456 439
        indexes = []        
457
        execute("EXEC sp_helpindex '#{table_name}'", name) do |handle|
458
          if handle.column_info.any?
459
            handle.each do |index| 
460
              unique = index[1] =~ /unique/
461
              primary = index[1] =~ /primary key/
462
              if !primary
463
                indexes << IndexDefinition.new(table_name, index[0], unique, index[2].split(", ").map {|e| e.gsub('(-)','')})
464
              end
440
        execute("EXEC sp_helpindex '#{table_name}'", name) do |sth|
441
          sth.each do |index| 
442
            unique = index[1] =~ /unique/
443
            primary = index[1] =~ /primary key/
444
            if !primary
445
              indexes << IndexDefinition.new(table_name, index[0], unique, index[2].split(", "))
465 446
            end
466 447
          end
467 448
        end
468 449
        indexes
469
      ensure
470
        ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = true
450
        ensure
451
          ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = true
471 452
      end
453

  
454
      def add_order_by_for_association_limiting!(sql, options)
455
        # Just skip ORDER BY clause. I dont know better solution for DISTINCT plus ORDER BY.
456
        # And this doesnt cause to much problem..
457
        return sql
458
      end
472 459
            
473 460
      def rename_table(name, new_name)
474 461
        execute "EXEC sp_rename '#{name}', '#{new_name}'"
475 462
      end
476

  
463
      
464
      # Adds a new column to the named table.
465
      # See TableDefinition#column for details of the options you can use.
477 466
      def add_column(table_name, column_name, type, options = {})
478
        add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"      
467
        add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
479 468
        add_column_options!(add_column_sql, options)
480 469
        # TODO: Add support to mimic date columns, using constraints to mark them as such in the database
481 470
        # 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 476
      end
488 477
      
489 478
      def change_column(table_name, column_name, type, options = {}) #:nodoc:
490
        sql = "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
491
        sql << " NOT NULL" if options[:null] == false
492
        sql_commands = [sql]        
479
        sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"]
493 480
        if options_include_default?(options)
494 481
          remove_default_constraint(table_name, column_name)
495
          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)}"
482
          sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(options[:default], options[:column])} FOR #{column_name}"
496 483
        end
497 484
        sql_commands.each {|c|
498 485
          execute(c)
499 486
        }
500 487
      end
501 488
      
502
      def change_column_default(table_name, column_name, default)
503
        remove_default_constraint(table_name, column_name)
504
        execute "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(default, column_name)} FOR #{quote_column_name(column_name)}"  
505
      end
506
      
507 489
      def remove_column(table_name, column_name)
508 490
        remove_check_constraints(table_name, column_name)
509 491
        remove_default_constraint(table_name, column_name)
510
        execute "ALTER TABLE [#{table_name}] DROP COLUMN #{quote_column_name(column_name)}"
492
        execute "ALTER TABLE [#{table_name}] DROP COLUMN [#{column_name}]"
511 493
      end
512 494
      
513 495
      def remove_default_constraint(table_name, column_name)
......
533 515
      private 
534 516
        def select(sql, name = nil)
535 517
          repair_special_columns(sql)
536

  
518
          if match = query_has_limit_and_offset?(sql) 
519
            matched, limit, offset = *match 
520
            execute(sql) 
521
            # SET ROWCOUNT n causes all statements to only affect n rows, which we use 
522
            # to delete offset rows from the temporary table 
523
            execute("SET ROWCOUNT #{offset}") 
524
            execute("DELETE from #limit_offset_temp") 
525
            execute("SET ROWCOUNT 0") 
526
            result = execute_select("SELECT * FROM #limit_offset_temp") 
527
            execute("DROP TABLE #limit_offset_temp") 
528
            result 
529
          else 
530
            execute_select(sql) 
531
          end 
532
        end 
533
        
534
        def execute_select(sql)
537 535
          result = []          
538 536
          execute(sql) do |handle|
539 537
            handle.each do |row|
......
550 548
          result
551 549
        end
552 550

  
551
        def query_has_limit_and_offset?(sql) 
552
          match = sql.match(/#limit_offset_temp -- limit => (\d+) offset => (\d+)/) 
553
        end 
554
      
553 555
        # Turns IDENTITY_INSERT ON for table during execution of the block
554 556
        # N.B. This sets the state of IDENTITY_INSERT to OFF after the
555 557
        # block has been executed without regard to its previous state