Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added Sybase database adapter that relies on the Sybase Open Client b…

…indings (see http://raa.ruby-lang.org/project/sybase-ctlib) (closes #3765) [John Sheets]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3735 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit ec4dd572e777db80f7dc74c2093ba2ea951fedec 1 parent 6a3f4c9
David Heinemeier Hansson dhh authored
Showing with 599 additions and 0 deletions.
  1. +599 −0 activerecord/lib/active_record/connection_adapters/sybase_adapter.rb
599 activerecord/lib/active_record/connection_adapters/sybase_adapter.rb
View
@@ -0,0 +1,599 @@
+# sybase_adaptor.rb
+# Author: John Sheets <dev@metacasa.net>
+# Date: 01 Mar 2006
+#
+# Based on code from Will Sobel (http://dev.rubyonrails.org/ticket/2030)
+
+require 'active_record/connection_adapters/abstract_adapter'
+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, 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:
+ # * Does not support DATE SQL column types; use DATETIME instead.
+ # * Date columns on HABTM join tables are returned as String, not Time.
+ # * Insertions are potentially broken for :polymorphic join tables
+ # * BLOB column access not yet fully supported
+ #
+ class SybaseAdapter < AbstractAdapter # :nodoc:
+ class ColumnWithIdentity < Column
+ attr_reader :identity, :primary
+
+ def initialize(name, default, sql_type = nil, nullable = nil, identity = nil, primary = nil)
+ super(name, default, sql_type, nullable)
+ @default = type_cast(default)
+ @identity = identity
+ @primary = primary
+ end
+
+ def simplified_type(field_type)
+ case field_type
+ when /int|bigint|smallint|tinyint/i then :integer
+ when /float|double|decimal|money|numeric|real|smallmoney/i then :float
+ 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, logger = nil)
+ super(connection, logger)
+ context = connection.context
+ context.init(logger)
+ @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 },
+ :datetime => { :name => "datetime" },
+ :timestamp => { :name => "timestamp" },
+ :time => { :name => "time" },
+ :date => { :name => "datetime" },
+ :binary => { :name => "image"},
+ :boolean => { :name => "tinyint", :limit => 1 }
+ }
+ 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!"
+ #@connection.close rescue nil
+ end
+
+ # Check for a limit statement and parse out the limit and
+ # offset if specified. Remove the limit from the sql statement
+ # and call select.
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ # Remove limit clause from statement. This will almost always
+ # contain LIMIT 1 from the caller. set the rowcount to 1 before
+ # calling select.
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ table_structure(table_name).inject([]) do |columns, column|
+ name, default, type, nullable, identity, primary = column
+ columns << ColumnWithIdentity.new(name, default, type, nullable, identity, primary)
+ columns
+ end
+ end
+
+ def insert(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
+ execute 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
+ execute(sql, name)
+ ident = select_one("SELECT @@IDENTITY AS last_id")["last_id"]
+ id_value || ident
+ end
+ ensure
+ if ii_enabled
+ begin
+ execute 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)
+ log(sql, name) do
+ @connection.context.reset
+ @connection.set_rowcount(@limit || 0)
+ @limit = @offset = nil
+ @connection.sql_norow(sql)
+ if @connection.cmd_fail? or @connection.context.failed?
+ raise "SQL Command Failed for #{name}: #{sql}\nMessage: #{@connection.context.message}"
+ end
+ end
+ # Return rows affected
+ @connection.results[0].row_count
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction() execute "BEGIN TRAN" end
+ def commit_db_transaction() execute "COMMIT TRAN" end
+ def rollback_db_transaction() execute "ROLLBACK TRAN" end
+
+ def tables(name = nil)
+ tables = []
+ select("select name from sysobjects where type='U'", name).each do |row|
+ tables << row['name']
+ end
+ tables
+ end
+
+ def indexes(table_name, name = nil)
+ indexes = []
+ select("exec sp_helpindex #{table_name}", name).each do |index|
+ unique = index["index_description"] =~ /unique/
+ primary = index["index_description"] =~ /^clustered/
+ if !primary
+ cols = index["index_keys"].split(", ").each { |col| col.strip! }
+ indexes << IndexDefinition.new(table_name, index["index_name"], unique, cols)
+ end
+ end
+ indexes
+ end
+
+ def remove_index(table_name, options = {})
+ # Override for different Sybase SQL syntax.
+ execute "DROP INDEX #{table_name}.#{index_name(table_name, options)}"
+ end
+
+ def quoted_true
+ "1"
+ end
+
+ def quoted_false
+ "0"
+ end
+
+ def quote(value, column = nil)
+ case value
+ when String
+ if column && column.type == :binary
+ "#{quote_string(column.class.string_to_binary(value))}"
+ elsif value =~ /^[+-]?[0-9]+$/o
+ value
+ else
+ "'#{quote_string(value)}'"
+ end
+ when NilClass then "NULL"
+ when TrueClass then '1'
+ when FalseClass then '0'
+ when Float, Fixnum, Bignum then value.to_s
+ when Date then "'#{value.to_s}'"
+ when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
+ else "'#{quote_string(value.to_yaml)}'"
+ end
+ end
+
+ def quote_column(type, value)
+ case type
+ when :boolean
+ case value
+ when String then value =~ /^[ty]/o ? 1 : 0
+ when true then 1
+ when false then 0
+ else value.to_i
+ end
+ when :integer then value.to_i
+ when :float then value.to_f
+ when :text, :string, :enum
+ case value
+ when String, Symbol, Fixnum, Float, Bignum, TrueClass, FalseClass
+ "'#{quote_string(value.to_s)}'"
+ else
+ "'#{quote_string(value.to_yaml)}'"
+ end
+ when :date, :datetime, :time
+ case value
+ when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
+ when Date then "'#{value.to_s}'"
+ else "'#{quote_string(value)}'"
+ end
+ else "'#{quote_string(value.to_yaml)}'"
+ end
+ end
+
+ def quote_string(s)
+ s.gsub(/'/, "''") # ' (for ruby-mode)
+ end
+
+ def quote_column_name(name)
+ "[#{name}]"
+ end
+
+ def add_limit_offset!(sql, options) # :nodoc:
+ @limit = options[:limit]
+ @offset = options[:offset]
+ if !normal_select?
+ # 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
+
+ private
+ # 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
+
+ def normal_select?
+ # 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.
+ has_limit = !@limit.nil?
+ has_offset = !@offset.nil? && @offset > 0
+ !has_limit || !has_offset
+ end
+
+ def zero_limit?
+ !@limit.nil? && @limit == 0
+ end
+
+ # Select limit number of rows starting at optional offset.
+ def select(sql, name = nil)
+ @connection.context.reset
+ log(sql, name) do
+ if normal_select?
+ # If limit is not explicitly set, return all results.
+ @logger.debug "Setting row count to (#{@limit})" if @logger
+
+ # Run a normal select
+ @connection.set_rowcount(@limit || 0)
+ @connection.sql(sql)
+ else
+ # Select into a temp table and prune results
+ @logger.debug "Selecting #{@limit + (@offset || 0)} or fewer rows into #artemp" if @logger
+ @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
+
+ rows = []
+ if @connection.context.failed? or @connection.cmd_fail?
+ raise StatementInvalid, "SQL Command Failed for #{name}: #{sql}\nMessage: #{@connection.context.message}"
+ else
+ results = @connection.top_row_result
+ if results && results.rows.length > 0
+ fields = fixup_column_names(results.columns)
+ results.rows.each do |row|
+ hashed_row = {}
+ row.zip(fields) { |cell, column| hashed_row[column] = cell }
+ rows << hashed_row
+ end
+ end
+ end
+ @connection.sql_norow("drop table #artemp") if !normal_select?
+ @limit = @offset = nil
+ return rows
+ end
+
+ def enable_identity_insert(table_name, enable = true)
+ if has_identity_column(table_name)
+ "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
+ end
+ 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)
+ @table_columns = {} unless @table_columns
+ @table_columns[table_name] = columns(table_name) if @table_columns[table_name] == nil
+ @table_columns[table_name].each do |col|
+ return col.name if col.identity
+ end
+
+ return nil
+ end
+
+ def query_contains_identity_column(sql, col)
+ sql =~ /\[#{col}\]/
+ end
+
+ # Remove trailing _ from names.
+ def fixup_column_names(columns)
+ columns.map { |column| column.sub(/_$/, '') }
+ end
+
+ def table_structure(table_name)
+ 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 col.cdefault *= def.id
+ AND obj.type = 'U' AND obj.name = '#{table_name}' ORDER BY col.colid
+SQLTEXT
+ log(sql, "Get Column Info ") do
+ @connection.set_rowcount(0)
+ @connection.sql(sql)
+ end
+ if @connection.context.failed?
+ raise "SQL Command for table_structure for #{table_name} failed\nMessage: #{@connection.context.message}"
+ elsif !@connection.cmd_fail?
+ columns = []
+ results = @connection.top_row_result
+ results.rows.each do |row|
+ name, type, prec, scale, length, status, sysstat2, default = row
+ type = normalize_type(type, prec, scale, length)
+ default_value = nil
+ name.sub!(/_$/o, '')
+ 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
+
+ columns << [name, default_value, type, nullable, identity, primary]
+ end
+ columns
+ else
+ nil
+ end
+ end
+
+ def normalize_type(field_type, prec, scale, length)
+ if field_type =~ /numeric/i and (scale.nil? or scale == 0)
+ type = 'int'
+ elsif field_type =~ /money/i
+ type = 'numeric'
+ else
+ type = field_type
+ end
+ size = ''
+ if prec
+ size = "(#{prec})"
+ elsif length
+ size = "(#{length})"
+ end
+ return type + size
+ end
+
+ def default_value(value)
+ 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
+ values.each do |fixture|
+ allow_identity_inserts table_name, true
+ @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
+ allow_identity_inserts table_name, false
+ end
+ end
+
+ def allow_identity_inserts(table_name, enable)
+ @connection.execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" rescue nil
+ end
+
+end
Please sign in to comment.
Something went wrong with that request. Please try again.