Sybase Adapter type conversion cleanup [dev@metacasa.net]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4270 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
f106e2c5c8
commit
9a5b91a329
@ -1,5 +1,7 @@
|
|||||||
*SVN*
|
*SVN*
|
||||||
|
|
||||||
|
* Sybase Adapter type conversion cleanup. Closes #4736. [dev@metacasa.net]
|
||||||
|
|
||||||
* Fix bug where calculations with long alias names return null. [Rick]
|
* Fix bug where calculations with long alias names return null. [Rick]
|
||||||
|
|
||||||
* Raise error when trying to add to a has_many :through association. Use the Join Model instead. [Rick]
|
* Raise error when trying to add to a has_many :through association. Use the Join Model instead. [Rick]
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
# sybase_adaptor.rb
|
# sybase_adaptor.rb
|
||||||
# Author: John Sheets <dev@metacasa.net>
|
# Author: John Sheets <dev@metacasa.net>
|
||||||
# Date: 01 Mar 2006
|
#
|
||||||
|
# 01 Mar 2006: Initial version.
|
||||||
|
# 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.
|
||||||
#
|
#
|
||||||
# Based on code from Will Sobel (http://dev.rubyonrails.org/ticket/2030)
|
# Based on code from Will Sobel (http://dev.rubyonrails.org/ticket/2030)
|
||||||
#
|
#
|
||||||
# 17 Mar 2006: Added support for migrations; fixed issues with :boolean columns.
|
|
||||||
#
|
|
||||||
|
|
||||||
require 'active_record/connection_adapters/abstract_adapter'
|
require 'active_record/connection_adapters/abstract_adapter'
|
||||||
|
|
||||||
@ -35,7 +37,7 @@ def self.sybase_connection(config) # :nodoc:
|
|||||||
|
|
||||||
ConnectionAdapters::SybaseAdapter.new(
|
ConnectionAdapters::SybaseAdapter.new(
|
||||||
SybSQL.new({'S' => host, 'U' => username, 'P' => password},
|
SybSQL.new({'S' => host, 'U' => username, 'P' => password},
|
||||||
ConnectionAdapters::SybaseAdapterContext), database, logger)
|
ConnectionAdapters::SybaseAdapterContext), database, config, logger)
|
||||||
end
|
end
|
||||||
end # class Base
|
end # class Base
|
||||||
|
|
||||||
@ -48,7 +50,7 @@ module ConnectionAdapters
|
|||||||
#
|
#
|
||||||
# * <tt>:host</tt> -- The name of the database server. No default, must be provided.
|
# * <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>:database</tt> -- The name of the database. No default, must be provided.
|
||||||
# * <tt>:username</tt> -- Defaults to sa.
|
# * <tt>:username</tt> -- Defaults to "sa".
|
||||||
# * <tt>:password</tt> -- Defaults to empty string.
|
# * <tt>:password</tt> -- Defaults to empty string.
|
||||||
#
|
#
|
||||||
# Usage Notes:
|
# Usage Notes:
|
||||||
@ -95,6 +97,16 @@ def simplified_type(field_type)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.string_to_time(string)
|
||||||
|
return string unless string.is_a?(String)
|
||||||
|
|
||||||
|
# Since Sybase doesn't handle DATE or TIME, handle it here.
|
||||||
|
# Populate nil year/month/day with string_to_dummy_time() values.
|
||||||
|
time_array = ParseDate.parsedate(string)[0..5]
|
||||||
|
time_array[0] ||= 2000; time_array[1] ||= 1; time_array[2] ||= 1;
|
||||||
|
Time.send(Base.default_timezone, *time_array) rescue nil
|
||||||
|
end
|
||||||
|
|
||||||
def self.string_to_binary(value)
|
def self.string_to_binary(value)
|
||||||
"0x#{value.unpack("H*")[0]}"
|
"0x#{value.unpack("H*")[0]}"
|
||||||
end
|
end
|
||||||
@ -106,10 +118,12 @@ def self.binary_to_string(value)
|
|||||||
end # class ColumnWithIdentity
|
end # class ColumnWithIdentity
|
||||||
|
|
||||||
# Sybase adapter
|
# Sybase adapter
|
||||||
def initialize(connection, database, logger = nil)
|
def initialize(connection, database, config = {}, logger = nil)
|
||||||
super(connection, logger)
|
super(connection, logger)
|
||||||
context = connection.context
|
context = connection.context
|
||||||
context.init(logger)
|
context.init(logger)
|
||||||
|
@config = config
|
||||||
|
@numconvert = config.has_key?(:numconvert) ? config[:numconvert] : true
|
||||||
@limit = @offset = 0
|
@limit = @offset = 0
|
||||||
unless connection.sql_norow("USE #{database}")
|
unless connection.sql_norow("USE #{database}")
|
||||||
raise "Cannot USE #{database}"
|
raise "Cannot USE #{database}"
|
||||||
@ -154,16 +168,10 @@ def table_alias_length
|
|||||||
30
|
30
|
||||||
end
|
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)
|
def select_all(sql, name = nil)
|
||||||
select(sql, name)
|
select(sql, name)
|
||||||
end
|
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)
|
def select_one(sql, name = nil)
|
||||||
result = select(sql, name)
|
result = select(sql, name)
|
||||||
result.nil? ? nil : result.first
|
result.nil? ? nil : result.first
|
||||||
@ -186,7 +194,7 @@ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
|||||||
if col != nil
|
if col != nil
|
||||||
if query_contains_identity_column(sql, col)
|
if query_contains_identity_column(sql, col)
|
||||||
begin
|
begin
|
||||||
execute enable_identity_insert(table_name, true)
|
enable_identity_insert(table_name, true)
|
||||||
ii_enabled = true
|
ii_enabled = true
|
||||||
rescue Exception => e
|
rescue Exception => e
|
||||||
raise ActiveRecordError, "IDENTITY_INSERT could not be turned ON"
|
raise ActiveRecordError, "IDENTITY_INSERT could not be turned ON"
|
||||||
@ -202,7 +210,7 @@ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
|||||||
ensure
|
ensure
|
||||||
if ii_enabled
|
if ii_enabled
|
||||||
begin
|
begin
|
||||||
execute enable_identity_insert(table_name, false)
|
enable_identity_insert(table_name, false)
|
||||||
rescue Exception => e
|
rescue Exception => e
|
||||||
raise ActiveRecordError, "IDENTITY_INSERT could not be turned OFF"
|
raise ActiveRecordError, "IDENTITY_INSERT could not be turned OFF"
|
||||||
end
|
end
|
||||||
@ -231,6 +239,10 @@ def begin_db_transaction() execute "BEGIN TRAN" end
|
|||||||
def commit_db_transaction() execute "COMMIT TRAN" end
|
def commit_db_transaction() execute "COMMIT TRAN" end
|
||||||
def rollback_db_transaction() execute "ROLLBACK TRAN" end
|
def rollback_db_transaction() execute "ROLLBACK TRAN" end
|
||||||
|
|
||||||
|
def current_database
|
||||||
|
select_one("select DB_NAME() as name")["name"]
|
||||||
|
end
|
||||||
|
|
||||||
def tables(name = nil)
|
def tables(name = nil)
|
||||||
tables = []
|
tables = []
|
||||||
select("select name from sysobjects where type='U'", name).each do |row|
|
select("select name from sysobjects where type='U'", name).each do |row|
|
||||||
@ -265,7 +277,7 @@ def quote(value, column = nil)
|
|||||||
when String
|
when String
|
||||||
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
||||||
"#{quote_string(column.class.string_to_binary(value))}"
|
"#{quote_string(column.class.string_to_binary(value))}"
|
||||||
elsif value =~ /^[+-]?[0-9]+$/o
|
elsif @numconvert && force_numeric?(column) && value =~ /^[+-]?[0-9]+$/o
|
||||||
value
|
value
|
||||||
else
|
else
|
||||||
"'#{quote_string(value)}'"
|
"'#{quote_string(value)}'"
|
||||||
@ -273,39 +285,18 @@ def quote(value, column = nil)
|
|||||||
when NilClass then (column && column.type == :boolean) ? '0' : "NULL"
|
when NilClass then (column && column.type == :boolean) ? '0' : "NULL"
|
||||||
when TrueClass then '1'
|
when TrueClass then '1'
|
||||||
when FalseClass then '0'
|
when FalseClass then '0'
|
||||||
when Float, Fixnum, Bignum then value.to_s
|
when Float, Fixnum, Bignum
|
||||||
|
force_numeric?(column) ? value.to_s : "'#{value.to_s}'"
|
||||||
when Date then "'#{value.to_s}'"
|
when Date then "'#{value.to_s}'"
|
||||||
when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
|
when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
|
||||||
else "'#{quote_string(value.to_yaml)}'"
|
else "'#{quote_string(value.to_yaml)}'"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def quote_column(type, value)
|
# True if column is explicitly declared non-numeric, or
|
||||||
case type
|
# if column is nil (not specified).
|
||||||
when :boolean
|
def force_numeric?(column)
|
||||||
case value
|
(column.nil? || [:integer, :float].include?(column.type))
|
||||||
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
|
end
|
||||||
|
|
||||||
def quote_string(s)
|
def quote_string(s)
|
||||||
@ -313,7 +304,9 @@ def quote_string(s)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def quote_column_name(name)
|
def quote_column_name(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
|
end
|
||||||
|
|
||||||
def add_limit_offset!(sql, options) # :nodoc:
|
def add_limit_offset!(sql, options) # :nodoc:
|
||||||
@ -348,12 +341,17 @@ def rename_column(table, column, new_column_name)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
||||||
sql_commands = ["ALTER TABLE #{table_name} MODIFY #{column_name} #{type_to_sql(type, options[:limit])}"]
|
begin
|
||||||
|
execute "ALTER TABLE #{table_name} MODIFY #{column_name} #{type_to_sql(type, options[:limit])}"
|
||||||
|
rescue StatementInvalid => e
|
||||||
|
# Swallow exception if no-op.
|
||||||
|
raise e unless e.message =~ /no columns to drop, add or modify/
|
||||||
|
end
|
||||||
|
|
||||||
if options[:default]
|
if options[:default]
|
||||||
remove_default_constraint(table_name, column_name)
|
remove_default_constraint(table_name, column_name)
|
||||||
sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{options[:default]} FOR #{column_name}"
|
execute "ALTER TABLE #{table_name} REPLACE #{column_name} DEFAULT #{options[:default]}"
|
||||||
end
|
end
|
||||||
sql_commands.each { |c| execute(c) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_column(table_name, column_name)
|
def remove_column(table_name, column_name)
|
||||||
@ -381,6 +379,12 @@ def add_column_options!(sql, options) #:nodoc:
|
|||||||
sql
|
sql
|
||||||
end
|
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
|
private
|
||||||
def check_null_for_column?(col, sql)
|
def check_null_for_column?(col, sql)
|
||||||
# Sybase columns are NOT NULL by default, so explicitly set NULL
|
# Sybase columns are NOT NULL by default, so explicitly set NULL
|
||||||
@ -426,7 +430,7 @@ def affected_rows(name = nil)
|
|||||||
|
|
||||||
def normal_select?
|
def normal_select?
|
||||||
# If limit is not set at all, we can ignore offset;
|
# If limit is not set at all, we can ignore offset;
|
||||||
# If limit *is* set but offset is zero, use normal select
|
# if limit *is* set but offset is zero, use normal select
|
||||||
# with simple SET ROWCOUNT. Thus, only use the temp table
|
# with simple SET ROWCOUNT. Thus, only use the temp table
|
||||||
# if limit is set and offset > 0.
|
# if limit is set and offset > 0.
|
||||||
has_limit = !@limit.nil?
|
has_limit = !@limit.nil?
|
||||||
@ -468,7 +472,7 @@ def select(sql, name = nil)
|
|||||||
else
|
else
|
||||||
results = @connection.top_row_result
|
results = @connection.top_row_result
|
||||||
if results && results.rows.length > 0
|
if results && results.rows.length > 0
|
||||||
fields = fixup_column_names(results.columns)
|
fields = results.columns.map { |column| column.sub(/_$/, '') }
|
||||||
results.rows.each do |row|
|
results.rows.each do |row|
|
||||||
hashed_row = {}
|
hashed_row = {}
|
||||||
row.zip(fields) { |cell, column| hashed_row[column] = cell }
|
row.zip(fields) { |cell, column| hashed_row[column] = cell }
|
||||||
@ -481,12 +485,6 @@ def select(sql, name = nil)
|
|||||||
return rows
|
return rows
|
||||||
end
|
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)
|
def get_table_name(sql)
|
||||||
if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
|
if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
|
||||||
$1
|
$1
|
||||||
@ -502,24 +500,18 @@ def has_identity_column(table_name)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def get_identity_column(table_name)
|
def get_identity_column(table_name)
|
||||||
@table_columns = {} unless @table_columns
|
@table_columns ||= {}
|
||||||
@table_columns[table_name] = columns(table_name) if @table_columns[table_name] == nil
|
@table_columns[table_name] ||= columns(table_name)
|
||||||
@table_columns[table_name].each do |col|
|
@table_columns[table_name].each do |col|
|
||||||
return col.name if col.identity
|
return col.name if col.identity
|
||||||
end
|
end
|
||||||
|
nil
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def query_contains_identity_column(sql, col)
|
def query_contains_identity_column(sql, col)
|
||||||
sql =~ /\[#{col}\]/
|
sql =~ /\[#{col}\]/
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove trailing _ from names.
|
|
||||||
def fixup_column_names(columns)
|
|
||||||
columns.map { |column| column.sub(/_$/, '') }
|
|
||||||
end
|
|
||||||
|
|
||||||
def table_structure(table_name)
|
def table_structure(table_name)
|
||||||
sql = <<SQLTEXT
|
sql = <<SQLTEXT
|
||||||
SELECT col.name AS name, type.name AS type, col.prec, col.scale, col.length,
|
SELECT col.name AS name, type.name AS type, col.prec, col.scale, col.length,
|
||||||
@ -558,18 +550,23 @@ def table_structure(table_name)
|
|||||||
end
|
end
|
||||||
end
|
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)
|
def normalize_type(field_type, prec, scale, length)
|
||||||
if field_type =~ /numeric/i and (scale.nil? or scale == 0)
|
if field_type =~ /numeric/i and (scale.nil? or scale == 0)
|
||||||
type = 'int'
|
type = 'int'
|
||||||
elsif field_type =~ /money/i
|
elsif field_type =~ /money/i
|
||||||
type = 'numeric'
|
type = 'numeric'
|
||||||
else
|
else
|
||||||
type = field_type
|
type = resolve_type(field_type.strip)
|
||||||
end
|
end
|
||||||
size = ''
|
size = ''
|
||||||
if prec
|
if prec
|
||||||
size = "(#{prec})"
|
size = "(#{prec})"
|
||||||
elsif length
|
elsif length && !(type =~ /date|time/)
|
||||||
size = "(#{length})"
|
size = "(#{length})"
|
||||||
end
|
end
|
||||||
return type + size
|
return type + size
|
||||||
@ -668,17 +665,13 @@ class Fixtures
|
|||||||
|
|
||||||
def insert_fixtures
|
def insert_fixtures
|
||||||
values.each do |fixture|
|
values.each do |fixture|
|
||||||
allow_identity_inserts table_name, true
|
@connection.enable_identity_insert(table_name, true)
|
||||||
@connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
|
@connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
|
||||||
allow_identity_inserts table_name, false
|
@connection.enable_identity_insert(table_name, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def allow_identity_inserts(table_name, enable)
|
|
||||||
@connection.execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" rescue nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue LoadError => cannot_require_sybase
|
rescue LoadError => cannot_require_sybase
|
||||||
# Couldn't load sybase adapter
|
# Couldn't load sybase adapter
|
||||||
end
|
end
|
||||||
|
@ -26,7 +26,7 @@ CREATE TABLE topics (
|
|||||||
author_name varchar(255) NULL,
|
author_name varchar(255) NULL,
|
||||||
author_email_address varchar(255) NULL,
|
author_email_address varchar(255) NULL,
|
||||||
written_on datetime NULL,
|
written_on datetime NULL,
|
||||||
bonus_time time NULL,
|
bonus_time datetime NULL,
|
||||||
last_read datetime NULL,
|
last_read datetime NULL,
|
||||||
content varchar(255) NULL,
|
content varchar(255) NULL,
|
||||||
approved bit default 1,
|
approved bit default 1,
|
||||||
@ -118,7 +118,7 @@ CREATE TABLE mixins (
|
|||||||
|
|
||||||
CREATE TABLE people (
|
CREATE TABLE people (
|
||||||
id numeric(9,0) IDENTITY PRIMARY KEY,
|
id numeric(9,0) IDENTITY PRIMARY KEY,
|
||||||
first_name varchar(40) NOT NULL,
|
first_name varchar(40) NULL,
|
||||||
lock_version int DEFAULT 0
|
lock_version int DEFAULT 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user