From d7245f0db6fc7b2f52b13502a144165cce4892ca Mon Sep 17 00:00:00 2001
From: Steve Hull
Date: Mon, 4 Oct 2010 18:11:27 -0700
Subject: [PATCH] Initial Commit
---
.gemspec | 19 +
README.markdown | 4 +
activerecord-sybase-adapter.gemspec | 19 +
.../connection_adapters/sybase_adapter.rb | 664 ++++++++++++++++++
4 files changed, 706 insertions(+)
create mode 100644 .gemspec
create mode 100644 README.markdown
create mode 100644 activerecord-sybase-adapter.gemspec
create mode 100644 lib/active_record/connection_adapters/sybase_adapter.rb
diff --git a/.gemspec b/.gemspec
new file mode 100644
index 0000000..66668f7
--- /dev/null
+++ b/.gemspec
@@ -0,0 +1,19 @@
+# -*- encoding: utf-8 -*-
+lib = File.expand_path('../lib/', __FILE__)
+$:.unshift lib unless $:.include?(lib)
+
+Gem::Specification.new do |s|
+ s.name = "activerecord-sybase-adapter"
+ s.version = 1.0
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["John R. Sheets", "Will Sobel"]
+ s.email = [""]
+ s.homepage = "http://dev.rubyonrails.org/ticket/2030"
+ s.summary = "Make ActiveRecord talk to Sybase"
+ s.description = ""
+
+ s.required_rubygems_version = ">= 1.3.6"
+
+ s.files = Dir.glob("lib/**/*") + %w(README.markdown)
+ s.require_path = 'lib'
+end
diff --git a/README.markdown b/README.markdown
new file mode 100644
index 0000000..56d4c19
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,4 @@
+ActiveRecord Sybase Adapter
+==========================
+
+Flawed though it is, my office still depends on this for our app. At some point, we may be able to retire this completely, but for now, we need a source for this in our Gemfile.
diff --git a/activerecord-sybase-adapter.gemspec b/activerecord-sybase-adapter.gemspec
new file mode 100644
index 0000000..1887355
--- /dev/null
+++ b/activerecord-sybase-adapter.gemspec
@@ -0,0 +1,19 @@
+# -*- encoding: utf-8 -*-
+lib = File.expand_path('../lib/', __FILE__)
+$:.unshift lib unless $:.include?(lib)
+
+Gem::Specification.new do |s|
+ s.name = "activerecord-sybase-adapter"
+ s.version = 1.0
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["?"]
+ s.email = [""]
+ s.homepage = ""
+ s.summary = "Make ActiveRecord talk to Sybase"
+ s.description = ""
+
+ s.required_rubygems_version = ">= 1.3.6"
+
+ s.files = Dir.glob("lib/**/*") + %w(README.markdown)
+ s.require_path = 'lib'
+end
diff --git a/lib/active_record/connection_adapters/sybase_adapter.rb b/lib/active_record/connection_adapters/sybase_adapter.rb
new file mode 100644
index 0000000..5fc12f4
--- /dev/null
+++ b/lib/active_record/connection_adapters/sybase_adapter.rb
@@ -0,0 +1,664 @@
+# 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:
+ #
+ # * :host -- The name of the database server. No default, must be provided.
+ # * :database -- The name of the database. No default, must be provided.
+ # * :username -- Defaults to "sa".
+ # * :password -- 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 = <= 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