diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/sybase_adapter.rb')
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/sybase_adapter.rb | 664 |
1 files changed, 0 insertions, 664 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/sybase_adapter.rb b/activerecord/lib/active_record/connection_adapters/sybase_adapter.rb deleted file mode 100644 index 5fc12f4b77..0000000000 --- a/activerecord/lib/active_record/connection_adapters/sybase_adapter.rb +++ /dev/null @@ -1,664 +0,0 @@ -# sybase_adapter.rb -# Author: John R. Sheets -# -# 01 Mar 2006: Initial version. Based on code from Will Sobel -# (http://dev.rubyonrails.org/ticket/2030) -# -# 17 Mar 2006: Added support for migrations; fixed issues with :boolean columns. -# -# 13 Apr 2006: Improved column type support to properly handle dates and user-defined -# types; fixed quoting of integer columns. -# -# 05 Jan 2007: Updated for Rails 1.2 release: -# restricted Fixtures#insert_fixtures monkeypatch to Sybase adapter; -# removed SQL type precision from TEXT type to fix broken -# ActiveRecordStore (jburks, #6878); refactored select() to use execute(); -# fixed leaked exception for no-op change_column(); removed verbose SQL dump -# from columns(); added missing scale parameter in normalize_type(). - -require 'active_record/connection_adapters/abstract_adapter' - -begin -require 'sybsql' - -module ActiveRecord - class Base - # Establishes a connection to the database that's used by all Active Record objects - def self.sybase_connection(config) # :nodoc: - config = config.symbolize_keys - - username = config[:username] ? config[:username].to_s : 'sa' - password = config[:password] ? config[:password].to_s : '' - - if config.has_key?(:host) - host = config[:host] - else - raise ArgumentError, "No database server name specified. Missing argument: host." - end - - if config.has_key?(:database) - database = config[:database] - else - raise ArgumentError, "No database specified. Missing argument: database." - end - - ConnectionAdapters::SybaseAdapter.new( - SybSQL.new({'S' => host, 'U' => username, 'P' => password}, - ConnectionAdapters::SybaseAdapterContext), database, config, logger) - end - end # class Base - - module ConnectionAdapters - - # ActiveRecord connection adapter for Sybase Open Client bindings - # (see http://raa.ruby-lang.org/project/sybase-ctlib). - # - # Options: - # - # * <tt>:host</tt> -- The name of the database server. No default, must be provided. - # * <tt>:database</tt> -- The name of the database. No default, must be provided. - # * <tt>:username</tt> -- Defaults to "sa". - # * <tt>:password</tt> -- Defaults to empty string. - # - # Usage Notes: - # - # * The sybase-ctlib bindings do not support the DATE SQL column type; use DATETIME instead. - # * Table and column names are limited to 30 chars in Sybase 12.5 - # * :binary columns not yet supported - # * :boolean columns use the BIT SQL type, which does not allow nulls or - # indexes. If a DEFAULT is not specified for ALTER TABLE commands, the - # column will be declared with DEFAULT 0 (false). - # - # Migrations: - # - # The Sybase adapter supports migrations, but for ALTER TABLE commands to - # work, the database must have the database option 'select into' set to - # 'true' with sp_dboption (see below). The sp_helpdb command lists the current - # options for all databases. - # - # 1> use mydb - # 2> go - # 1> master..sp_dboption mydb, "select into", true - # 2> go - # 1> checkpoint - # 2> go - class SybaseAdapter < AbstractAdapter # :nodoc: - class ColumnWithIdentity < Column - attr_reader :identity - - def initialize(name, default, sql_type = nil, nullable = nil, identity = nil, primary = nil) - super(name, default, sql_type, nullable) - @default, @identity, @primary = type_cast(default), identity, primary - end - - def simplified_type(field_type) - case field_type - when /int|bigint|smallint|tinyint/i then :integer - when /float|double|real/i then :float - when /decimal|money|numeric|smallmoney/i then :decimal - when /text|ntext/i then :text - when /binary|image|varbinary/i then :binary - when /char|nchar|nvarchar|string|varchar/i then :string - when /bit/i then :boolean - when /datetime|smalldatetime/i then :datetime - else super - end - end - - def self.string_to_binary(value) - "0x#{value.unpack("H*")[0]}" - end - - def self.binary_to_string(value) - # FIXME: sybase-ctlib uses separate sql method for binary columns. - value - end - end # class ColumnWithIdentity - - # Sybase adapter - def initialize(connection, database, config = {}, logger = nil) - super(connection, logger) - context = connection.context - context.init(logger) - @config = config - @numconvert = config.has_key?(:numconvert) ? config[:numconvert] : true - @limit = @offset = 0 - unless connection.sql_norow("USE #{database}") - raise "Cannot USE #{database}" - end - end - - def native_database_types - { - :primary_key => "numeric(9,0) IDENTITY PRIMARY KEY", - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "int" }, - :float => { :name => "float", :limit => 8 }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :timestamp => { :name => "timestamp" }, - :time => { :name => "time" }, - :date => { :name => "datetime" }, - :binary => { :name => "image"}, - :boolean => { :name => "bit" } - } - end - - def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: - return super unless type.to_s == 'integer' - if !limit.nil? && limit < 4 - 'smallint' - else - 'integer' - end - end - - def adapter_name - 'Sybase' - end - - def active? - !(@connection.connection.nil? || @connection.connection_dead?) - end - - def disconnect! - @connection.close rescue nil - end - - def reconnect! - raise "Sybase Connection Adapter does not yet support reconnect!" - # disconnect! - # connect! # Not yet implemented - end - - def table_alias_length - 30 - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - begin - table_name = get_table_name(sql) - col = get_identity_column(table_name) - ii_enabled = false - - if col != nil - if query_contains_identity_column(sql, col) - begin - enable_identity_insert(table_name, true) - ii_enabled = true - rescue Exception => e - raise ActiveRecordError, "IDENTITY_INSERT could not be turned ON" - end - end - end - - log(sql, name) do - super || select_value("SELECT @@IDENTITY AS last_id") - end - ensure - if ii_enabled - begin - enable_identity_insert(table_name, false) - rescue Exception => e - raise ActiveRecordError, "IDENTITY_INSERT could not be turned OFF" - end - end - end - end - - def execute(sql, name = nil) - raw_execute(sql, name) - @connection.results[0].row_count - end - - def begin_db_transaction() raw_execute "BEGIN TRAN" end - def commit_db_transaction() raw_execute "COMMIT TRAN" end - def rollback_db_transaction() raw_execute "ROLLBACK TRAN" end - - def current_database - select_value("select DB_NAME() as name") - end - - def tables(name = nil) - select("select name from sysobjects where type='U'", name).map { |row| row['name'] } - end - - def indexes(table_name, name = nil) - select("exec sp_helpindex #{table_name}", name).map do |index| - unique = index["index_description"] =~ /unique/ - primary = index["index_description"] =~ /^clustered/ - if !primary - cols = index["index_keys"].split(", ").each { |col| col.strip! } - IndexDefinition.new(table_name, index["index_name"], unique, cols) - end - end.compact - end - - def columns(table_name, name = nil) - sql = <<SQLTEXT -SELECT col.name AS name, type.name AS type, col.prec, col.scale, - col.length, col.status, obj.sysstat2, def.text - FROM sysobjects obj, syscolumns col, systypes type, syscomments def - WHERE obj.id = col.id AND col.usertype = type.usertype AND type.name != 'timestamp' - AND col.cdefault *= def.id AND obj.type = 'U' AND obj.name = '#{table_name}' ORDER BY col.colid -SQLTEXT - @logger.debug "Get Column Info for table '#{table_name}'" if @logger - @connection.set_rowcount(0) - @connection.sql(sql) - - raise "SQL Command for table_structure for #{table_name} failed\nMessage: #{@connection.context.message}" if @connection.context.failed? - return nil if @connection.cmd_fail? - - @connection.top_row_result.rows.map do |row| - name, type, prec, scale, length, status, sysstat2, default = row - name.sub!(/_$/o, '') - type = normalize_type(type, prec, scale, length) - default_value = nil - if default =~ /DEFAULT\s+(.+)/o - default_value = $1.strip - default_value = default_value[1...-1] if default_value =~ /^['"]/o - end - nullable = (status & 8) == 8 - identity = status >= 128 - primary = (sysstat2 & 8) == 8 - ColumnWithIdentity.new(name, default_value, type, nullable, identity, primary) - end - end - - def quoted_true - "1" - end - - def quoted_false - "0" - end - - def quote(value, column = nil) - return value.quoted_id if value.respond_to?(:quoted_id) - - case value - when String - if column && column.type == :binary && column.class.respond_to?(:string_to_binary) - "#{quote_string(column.class.string_to_binary(value))}" - elsif @numconvert && force_numeric?(column) && value =~ /^[+-]?[0-9]+$/o - value - else - "'#{quote_string(value)}'" - end - when NilClass then (column && column.type == :boolean) ? '0' : "NULL" - when TrueClass then '1' - when FalseClass then '0' - when Float, Fixnum, Bignum then force_numeric?(column) ? value.to_s : "'#{value.to_s}'" - else - if value.acts_like?(:time) - "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'" - else - super - end - end - end - - # True if column is explicitly declared non-numeric, or - # if column is nil (not specified). - def force_numeric?(column) - (column.nil? || [:integer, :float, :decimal].include?(column.type)) - end - - def quote_string(s) - s.gsub(/'/, "''") # ' (for ruby-mode) - end - - def quote_column_name(name) - # If column name is close to max length, skip the quotes, since they - # seem to count as part of the length. - ((name.to_s.length + 2) <= table_alias_length) ? "[#{name}]" : name.to_s - end - - def add_limit_offset!(sql, options) # :nodoc: - @limit = options[:limit] - @offset = options[:offset] - if use_temp_table? - # Use temp table to hack offset with Sybase - sql.sub!(/ FROM /i, ' INTO #artemp FROM ') - elsif zero_limit? - # "SET ROWCOUNT 0" turns off limits, so we have - # to use a cheap trick. - if sql =~ /WHERE/i - sql.sub!(/WHERE/i, 'WHERE 1 = 2 AND ') - elsif sql =~ /ORDER\s+BY/i - sql.sub!(/ORDER\s+BY/i, 'WHERE 1 = 2 ORDER BY') - else - sql << 'WHERE 1 = 2' - end - end - end - - def add_lock!(sql, options) #:nodoc: - @logger.info "Warning: Sybase :lock option '#{options[:lock].inspect}' not supported" if @logger && options.has_key?(:lock) - sql - end - - def supports_migrations? #:nodoc: - true - end - - def rename_table(name, new_name) - execute "EXEC sp_rename '#{name}', '#{new_name}'" - end - - def rename_column(table, column, new_column_name) - execute "EXEC sp_rename '#{table}.#{column}', '#{new_column_name}'" - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - begin - execute "ALTER TABLE #{table_name} MODIFY #{column_name} #{type_to_sql(type, options[:limit])}" - rescue StatementInvalid => e - # Swallow exception and reset context if no-op. - raise e unless e.message =~ /no columns to drop, add or modify/ - @connection.context.reset - end - - if options.has_key?(:default) - remove_default_constraint(table_name, column_name) - execute "ALTER TABLE #{table_name} REPLACE #{column_name} DEFAULT #{quote options[:default]}" - end - end - - def remove_column(table_name, column_name) - remove_default_constraint(table_name, column_name) - execute "ALTER TABLE #{table_name} DROP #{column_name}" - end - - def remove_default_constraint(table_name, column_name) - sql = "select def.name from sysobjects def, syscolumns col, sysobjects tab where col.cdefault = def.id and col.name = '#{column_name}' and tab.name = '#{table_name}' and col.id = tab.id" - select(sql).each do |constraint| - execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["name"]}" - end - end - - def remove_index(table_name, options = {}) - execute "DROP INDEX #{table_name}.#{index_name(table_name, options)}" - end - - def add_column_options!(sql, options) #:nodoc: - sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options) - - if check_null_for_column?(options[:column], sql) - sql << (options[:null] == false ? " NOT NULL" : " NULL") - end - sql - end - - def enable_identity_insert(table_name, enable = true) - if has_identity_column(table_name) - execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" - end - end - - private - def check_null_for_column?(col, sql) - # Sybase columns are NOT NULL by default, so explicitly set NULL - # if :null option is omitted. Disallow NULLs for boolean. - type = col.nil? ? "" : col[:type] - - # Ignore :null if a primary key - return false if type =~ /PRIMARY KEY/i - - # Ignore :null if a :boolean or BIT column - if (sql =~ /\s+bit(\s+DEFAULT)?/i) || type == :boolean - # If no default clause found on a boolean column, add one. - sql << " DEFAULT 0" if $1.nil? - return false - end - true - end - - # Return the last value of the identity global value. - def last_insert_id - @connection.sql("SELECT @@IDENTITY") - unless @connection.cmd_fail? - id = @connection.top_row_result.rows.first.first - if id - id = id.to_i - id = nil if id == 0 - end - else - id = nil - end - id - end - - def affected_rows(name = nil) - @connection.sql("SELECT @@ROWCOUNT") - unless @connection.cmd_fail? - count = @connection.top_row_result.rows.first.first - count = count.to_i if count - else - 0 - end - end - - # If limit is not set at all, we can ignore offset; - # if limit *is* set but offset is zero, use normal select - # with simple SET ROWCOUNT. Thus, only use the temp table - # if limit is set and offset > 0. - def use_temp_table? - !@limit.nil? && !@offset.nil? && @offset > 0 - end - - def zero_limit? - !@limit.nil? && @limit == 0 - end - - def raw_execute(sql, name = nil) - log(sql, name) do - @connection.context.reset - @logger.debug "Setting row count to (#{@limit})" if @logger && @limit - @connection.set_rowcount(@limit || 0) - if sql =~ /^\s*SELECT/i - @connection.sql(sql) - else - @connection.sql_norow(sql) - end - @limit = @offset = nil - if @connection.cmd_fail? or @connection.context.failed? - raise "SQL Command Failed for #{name}: #{sql}\nMessage: #{@connection.context.message}" - end - end - end - - # Select limit number of rows starting at optional offset. - def select(sql, name = nil) - if !use_temp_table? - execute(sql, name) - else - log(sql, name) do - # Select into a temp table and prune results - @logger.debug "Selecting #{@limit + (@offset || 0)} or fewer rows into #artemp" if @logger - @connection.context.reset - @connection.set_rowcount(@limit + (@offset || 0)) - @connection.sql_norow(sql) # Select into temp table - @logger.debug "Deleting #{@offset || 0} or fewer rows from #artemp" if @logger - @connection.set_rowcount(@offset || 0) - @connection.sql_norow("delete from #artemp") # Delete leading rows - @connection.set_rowcount(0) - @connection.sql("select * from #artemp") # Return the rest - end - end - - raise StatementInvalid, "SQL Command Failed for #{name}: #{sql}\nMessage: #{@connection.context.message}" if @connection.context.failed? or @connection.cmd_fail? - - rows = [] - results = @connection.top_row_result - if results && results.rows.length > 0 - fields = results.columns.map { |column| column.sub(/_$/, '') } - results.rows.each do |row| - hashed_row = {} - row.zip(fields) { |cell, column| hashed_row[column] = cell } - rows << hashed_row - end - end - @connection.sql_norow("drop table #artemp") if use_temp_table? - @limit = @offset = nil - rows - end - - def get_table_name(sql) - if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i - $1 - elsif sql =~ /from\s+([^\(\s]+)\s*/i - $1 - else - nil - end - end - - def has_identity_column(table_name) - !get_identity_column(table_name).nil? - end - - def get_identity_column(table_name) - @id_columns ||= {} - if !@id_columns.has_key?(table_name) - @logger.debug "Looking up identity column for table '#{table_name}'" if @logger - col = columns(table_name).detect { |col| col.identity } - @id_columns[table_name] = col.nil? ? nil : col.name - end - @id_columns[table_name] - end - - def query_contains_identity_column(sql, col) - sql =~ /\[#{col}\]/ - end - - # Resolve all user-defined types (udt) to their fundamental types. - def resolve_type(field_type) - (@udts ||= {})[field_type] ||= select_one("sp_help #{field_type}")["Storage_type"].strip - end - - def normalize_type(field_type, prec, scale, length) - has_scale = (!scale.nil? && scale > 0) - type = if field_type =~ /numeric/i and !has_scale - 'int' - elsif field_type =~ /money/i - 'numeric' - else - resolve_type(field_type.strip) - end - - spec = if prec - has_scale ? "(#{prec},#{scale})" : "(#{prec})" - elsif length && !(type =~ /date|time|text/) - "(#{length})" - else - '' - end - "#{type}#{spec}" - end - end # class SybaseAdapter - - class SybaseAdapterContext < SybSQLContext - DEADLOCK = 1205 - attr_reader :message - - def init(logger = nil) - @deadlocked = false - @failed = false - @logger = logger - @message = nil - end - - def srvmsgCB(con, msg) - # Do not log change of context messages. - if msg['severity'] == 10 or msg['severity'] == 0 - return true - end - - if msg['msgnumber'] == DEADLOCK - @deadlocked = true - else - @logger.info "SQL Command failed!" if @logger - @failed = true - end - - if @logger - @logger.error "** SybSQLContext Server Message: **" - @logger.error " Message number #{msg['msgnumber']} Severity #{msg['severity']} State #{msg['state']} Line #{msg['line']}" - @logger.error " Server #{msg['srvname']}" - @logger.error " Procedure #{msg['proc']}" - @logger.error " Message String: #{msg['text']}" - end - - @message = msg['text'] - - true - end - - def deadlocked? - @deadlocked - end - - def failed? - @failed - end - - def reset - @deadlocked = false - @failed = false - @message = nil - end - - def cltmsgCB(con, msg) - return true unless ( msg.kind_of?(Hash) ) - unless ( msg[ "severity" ] ) then - return true - end - - if @logger - @logger.error "** SybSQLContext Client-Message: **" - @logger.error " Message number: LAYER=#{msg[ 'layer' ]} ORIGIN=#{msg[ 'origin' ]} SEVERITY=#{msg[ 'severity' ]} NUMBER=#{msg[ 'number' ]}" - @logger.error " Message String: #{msg['msgstring']}" - @logger.error " OS Error: #{msg['osstring']}" - - @message = msg['msgstring'] - end - - @failed = true - - # Not retry , CS_CV_RETRY_FAIL( probability TimeOut ) - if( msg[ 'severity' ] == "RETRY_FAIL" ) then - @timeout_p = true - return false - end - - return true - end - end # class SybaseAdapterContext - - end # module ConnectionAdapters -end # module ActiveRecord - - -# Allow identity inserts for fixtures. -require "active_record/fixtures" -class Fixtures - alias :original_insert_fixtures :insert_fixtures - - def insert_fixtures - if @connection.instance_of?(ActiveRecord::ConnectionAdapters::SybaseAdapter) - values.each do |fixture| - @connection.enable_identity_insert(table_name, true) - @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert' - @connection.enable_identity_insert(table_name, false) - end - else - original_insert_fixtures - end - end -end - -rescue LoadError => cannot_require_sybase - # Couldn't load sybase adapter -end |