diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 81553ae..0000000 --- a/Rakefile +++ /dev/null @@ -1,122 +0,0 @@ -#==================================================== -# -# Copyright 2008-2010 iAnywhere Solutions, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# -# See the License for the specific language governing permissions and -# limitations under the License. -# -# While not a requirement of the license, if you do modify this file, we -# would appreciate hearing about it. Please email sqlany_interfaces@sybase.com -# -# -#==================================================== - -require 'rake/clean' -require 'rake/rdoctask' -require 'rubygems' -require 'rubygems/builder' - -PACKAGE_NAME = "activerecord-sqlanywhere-adapter" -ARCH=Config::CONFIG['arch'] - -pkg_version = "1.0.0" - -spec = Gem::Specification.new do |spec| - spec.authors = ["Eric Farrar"] - spec.email = 'eric.farrar@ianywhere.com' - spec.name = 'activerecord-sqlanywhere-adapter' - spec.summary = 'ActiveRecord driver for SQL Anywhere' - spec.description = <<-EOF - ActiveRecord driver for SQL Anywhere - EOF - spec.version = pkg_version - spec.has_rdoc = true - spec.rubyforge_project = 'sqlanywhere' - spec.homepage = 'http://sqlanywhere.rubyforge.org' - spec.files = Dir['lib/**/*.rb'] + Dir['test/**/*'] - spec.required_ruby_version = '>= 1.9.2' - spec.require_paths = ['lib'] - spec.add_dependency('sqlanywhere', '>= 0.1.5') - spec.add_dependency('activerecord', '>= 3.0.3') - spec.rdoc_options << '--title' << 'ActiveRecord Driver for SQL Anywhere' << - '--main' << 'README' << - '--line-numbers' - spec.extra_rdoc_files = ['README', 'CHANGELOG', 'LICENSE'] -end - -desc "Build the gem" -task :gem => ["activerecord-sqlanywhere-adapter-#{pkg_version}.gem"] do -end - -file "activerecord-sqlanywhere-adapter-#{pkg_version}.gem" => ['lib/active_record/connection_adapters/sqlanywhere_adapter.rb', 'README', 'Rakefile', 'lib/arel/visitors/sqlanywhere.rb'] do - Gem::Builder.new(spec).build -end - - -desc "Install the gem" -task :install => ["activerecord-sqlanywhere-adapter-#{pkg_version}.gem"] do - sh "gem install activerecord-sqlanywhere-adapter-#{pkg_version}.gem" -end - -desc "Build distributables (src zip, src tar.gz, gem)" -task :dist do |t| - puts "Cleaning Build Environment..." - Rake.application['clobber'].invoke - system "rake clobber" - - files = Dir.glob('*') - - puts "Creating #{File.join('build', PACKAGE_NAME)}-#{pkg_version} directory..." - FileUtils.mkdir_p "#{File.join('build', PACKAGE_NAME)}-#{pkg_version}" - - puts "Copying files to #{File.join('build', PACKAGE_NAME)}-#{pkg_version}..." - FileUtils.cp_r files, "#{File.join('build', PACKAGE_NAME)}-#{pkg_version}" - - if( ARCH =~ /win32/ ) then - system "attrib -R #{File.join('build', PACKAGE_NAME)}-#{pkg_version} /S" - else - system "find #{File.join('build', PACKAGE_NAME)}-#{pkg_version} -type d -exec chmod 755 {} \\;" - system "find #{File.join('build', PACKAGE_NAME)}-#{pkg_version} -type f -exec chmod 644 {} \\;" - end - - if( ARCH =~ /win32/ ) then - puts "Creating #{File.join('build', PACKAGE_NAME)}-#{pkg_version}.zip..." - system "cd build && zip -q -r #{PACKAGE_NAME}-#{pkg_version}.zip #{PACKAGE_NAME}-#{pkg_version}" - else - puts "Creating #{File.join('build', PACKAGE_NAME)}-#{pkg_version}.tar..." - system "tar cf #{File.join('build', PACKAGE_NAME)}-#{pkg_version}.tar -C build #{PACKAGE_NAME}-#{pkg_version}" - - puts "GZipping to create #{File.join('build', PACKAGE_NAME, PACKAGE_NAME)}-#{pkg_version}.tar.gz..." - system "gzip #{File.join('build', PACKAGE_NAME)}-#{pkg_version}.tar" - end - - puts "Building GEM distributable..." - Rake.application['gem'].invoke - - puts "Copying GEM to #{File.join('build', PACKAGE_NAME)}-#{pkg_version}.gem..." - FileUtils.cp "#{PACKAGE_NAME}-#{pkg_version}.gem", "build" -end - -Rake::RDocTask.new do |rd| - rd.title = "ActiveRecord Driver for SQL Anywhere" - rd.main = "README" - rd.rdoc_files.include('README', 'CHANGELOG', 'LICENSE', 'lib/**/*.rb') -end - -desc "Publish the RDOCs on RubyForge" -task :publish_rdoc => ["html/index.html"] do - system "pscp -r html/* efarrar@rubyforge.org:/var/www/gforge-projects/sqlanywhere/activerecord-sqlanywhere-adapter" -end - -CLOBBER.include("activerecord-sqlanywhere-adapter-#{pkg_version}.gem", "build/**/*", "build") diff --git a/activerecord-sqlanywhere-adapter.gemspec b/activerecord-sqlanywhere-adapter.gemspec deleted file mode 100755 index b358617..0000000 --- a/activerecord-sqlanywhere-adapter.gemspec +++ /dev/null @@ -1,42 +0,0 @@ -Gem::Specification.new do |s| - s.name = %q{activerecord-sqlanywhere-adapter} - s.version = "1.0.1" - - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = [%q{Eric Farar}] - s.description = %q{ActiveRecord driver for SQL Anywhere} - s.email = %q{eric.farrar@ianywhere.com} - s.files = [ - "CHANGELOG", - "LICENSE", - "README", - "Rakefile", - "test/connection.rb", - "lib/active_record/connection_adapters/sqlanywhere_adapter.rb", - "lib/arel/visitors/sqlanywhere.rb", - "lib/active_record/connection_adapters/sqlanywhere.rake", - "lib/activerecord-sqlanywhere-adapter.rb" - - ] - s.homepage = %q{http://sqlanywhere.rubyforge.org} - s.licenses = [%q{Apache License Version 2.0}] - s.require_paths = [%q{lib}] - s.rubygems_version = %q{>= 1.8.8} - s.summary = %q{ActiveRecord driver for SQL Anywhere} - - if s.respond_to? :specification_version then - s.specification_version = 3 - - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, [">= 0.1.5"]) - s.add_runtime_dependency(%q, [">= 3.0.3"]) - else - s.add_dependency(%q, [">= 0.1.5"]) - s.add_dependency(%q, [">= 3.0.3"]) - end - else - s.add_dependency(%q, [">= 0.1.5"]) - s.add_dependency(%q, [">= 3.0.3"]) - end -end - diff --git a/activerecord-sqlanywhere-jdbc-in4systems-adapter-1.0.13-java.gem b/activerecord-sqlanywhere-jdbc-in4systems-adapter-1.0.13-java.gem new file mode 100644 index 0000000..22d8fa5 Binary files /dev/null and b/activerecord-sqlanywhere-jdbc-in4systems-adapter-1.0.13-java.gem differ diff --git a/activerecord-sqlanywhere-jdbc-in4systems-adapter.gemspec b/activerecord-sqlanywhere-jdbc-in4systems-adapter.gemspec new file mode 100755 index 0000000..1496825 --- /dev/null +++ b/activerecord-sqlanywhere-jdbc-in4systems-adapter.gemspec @@ -0,0 +1,27 @@ +Gem::Specification.new do |s| + s.name = %q{activerecord-sqlanywhere-jdbc-in4systems-adapter} + s.version = "1.0.13" + + s.authors = ['Eric Farar', 'Sri Kalai'] + s.description = %q{ActiveRecord JDBC driver for SQL Anywhere customized for in4systems} + s.email = %q{support@in4systems.com} + s.files = [ + "CHANGELOG", + "LICENSE", + "README", + "test/connection.rb", + "lib/active_record/connection_adapters/sqlanywhere_jdbc_in4systems_adapter.rb", + "lib/arel/visitors/sqlanywhere.rb", + ] + s.executables = [] + s.test_files = [] + s.has_rdoc = false + s.homepage = 'https://github.com/in4systems/activerecord-sqlanywhere-adapter' + s.licenses = [%q{Apache License Version 2.0}] + s.require_paths = [%q{lib}] + s.summary = %q{ActiveRecord driver for SQL Anywhere} + s.platform = 'java' + s.add_dependency 'activerecord-jdbc-adapter' + s.add_dependency 'activerecord', '>= 3.0.3' +end + diff --git a/lib/active_record/connection_adapters/sqlanywhere.rake b/lib/active_record/connection_adapters/sqlanywhere.rake deleted file mode 100755 index 95befe9..0000000 --- a/lib/active_record/connection_adapters/sqlanywhere.rake +++ /dev/null @@ -1,48 +0,0 @@ -# Taken from https://github.com/rsim/oracle-enhanced/blob/master/lib/active_record/connection_adapters/oracle_enhanced.rake - -# implementation idea taken from JDBC adapter -# added possibility to execute previously defined task (passed as argument to task block) -def redefine_task(*args, &block) - task_name = Hash === args.first ? args.first.keys[0] : args.first - existing_task = Rake.application.lookup task_name - existing_actions = nil - if existing_task - class << existing_task; public :instance_variable_set, :instance_variable_get; end - existing_task.instance_variable_set "@prerequisites", FileList[] - existing_actions = existing_task.instance_variable_get "@actions" - existing_task.instance_variable_set "@actions", [] - end - task(*args) do - block.call(existing_actions) - end - end - - -# https://github.com/rsim/oracle-enhanced/blob/master/lib/active_record/connection_adapters/oracle_enhanced.rake - -if defined?(drop_database) == 'method' - def drop_database_with_sqlanywhere(config) - if config['adapter'] == 'sqlanywhere' - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection.purge_database - else - drop_database_without_sqlanywhere(config) - end - end - alias :drop_database_without_sqlanywhere :drop_database - alias :drop_database :drop_database_with_sqlanywhere -end - -namespace :db do - namespace :test do - redefine_task :purge => :environment do |existing_actions| - abcs = ActiveRecord::Base.configurations - if abcs['test']['adapter'] == 'sqlanywhere' - ActiveRecord::Base.establish_connection(:test) - ActiveRecord::Base.connection.purge_database - else - Array(existing_actions).each{|action| action.call} - end - end - end -end \ No newline at end of file diff --git a/lib/active_record/connection_adapters/sqlanywhere_adapter.rb b/lib/active_record/connection_adapters/sqlanywhere_adapter.rb deleted file mode 100755 index 667f0ce..0000000 --- a/lib/active_record/connection_adapters/sqlanywhere_adapter.rb +++ /dev/null @@ -1,757 +0,0 @@ -#==================================================== -# -# Copyright 2008-2010 iAnywhere Solutions, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# -# See the License for the specific language governing permissions and -# limitations under the License. -# -# While not a requirement of the license, if you do modify this file, we -# would appreciate hearing about it. Please email sqlany_interfaces@sybase.com -# -# -#==================================================== - -require 'active_record/connection_adapters/abstract_adapter' -require 'arel/visitors/sqlanywhere.rb' - -# Singleton class to hold a valid instance of the SQLAnywhereInterface across all connections -class SA - include Singleton - attr_accessor :api - - def initialize - require 'sqlanywhere' unless defined? SQLAnywhere - @api = SQLAnywhere::SQLAnywhereInterface.new() - raise LoadError, "Could not load SQLAnywhere DBCAPI library" if SQLAnywhere::API.sqlany_initialize_interface(@api) == 0 - raise LoadError, "Could not initialize SQLAnywhere DBCAPI library" if @api.sqlany_init() == 0 - end -end - -module ActiveRecord - class Base - DEFAULT_CONFIG = { :username => 'dba', :password => 'sql' } - # Main connection function to SQL Anywhere - # Connection Adapter takes four parameters: - # * :database (required, no default). Corresponds to "DatabaseName=" in connection string - # * :server (optional, defaults to :databse). Corresponds to "ServerName=" in connection string - # * :username (optional, default to 'dba') - # * :password (optional, deafult to 'sql') - # * :encoding (optional, defaults to charset of OS) - # * :commlinks (optional). Corresponds to "CommLinks=" in connection string - # * :connection_name (optional). Corresponds to "ConnectionName=" in connection string - - def self.sqlanywhere_connection(config) - - if config[:connection_string] - connection_string = config[:connection_string] - else - config = DEFAULT_CONFIG.merge(config) - - raise ArgumentError, "No database name was given. Please add a :database option." unless config.has_key?(:database) - - connection_string = "ServerName=#{(config[:server] || config[:database])};DatabaseName=#{config[:database]};UserID=#{config[:username]};Password=#{config[:password]};" - connection_string += "CommLinks=#{config[:commlinks]};" unless config[:commlinks].nil? - connection_string += "ConnectionName=#{config[:connection_name]};" unless config[:connection_name].nil? - connection_string += "CharSet=#{config[:encoding]};" unless config[:encoding].nil? - connection_string += "Idle=0" # Prevent the server from disconnecting us if we're idle for >240mins (by default) - end - - db = SA.instance.api.sqlany_new_connection() - - ConnectionAdapters::SQLAnywhereAdapter.new(db, logger, connection_string) - end - end - - module ConnectionAdapters - class SQLAnywhereException < StandardError - attr_reader :errno - attr_reader :sql - - def initialize(message, errno, sql) - super(message) - @errno = errno - @sql = sql - end - end - - class SQLAnywhereColumn < Column - private - # Overridden to handle SQL Anywhere integer, varchar, binary, and timestamp types - def simplified_type(field_type) - return :boolean if field_type =~ /tinyint/i - return :boolean if field_type =~ /bit/i - return :text if field_type =~ /long varchar/i - return :string if field_type =~ /varchar/i - return :binary if field_type =~ /long binary/i - return :datetime if field_type =~ /timestamp/i - return :integer if field_type =~ /smallint|bigint/i - super - end - - def extract_limit(sql_type) - case sql_type - when /^tinyint/i - 1 - when /^smallint/i - 2 - when /^integer/i - 4 - when /^bigint/i - 8 - else super - end - end - - protected - # Handles the encoding of a binary object into SQL Anywhere - # SQL Anywhere requires that binary values be encoded as \xHH, where HH is a hexadecimal number - # This function encodes the binary string in this format - def self.string_to_binary(value) - "\\x" + value.unpack("H*")[0].scan(/../).join("\\x") - end - - def self.binary_to_string(value) - value.gsub(/\\x[0-9]{2}/) { |byte| byte[2..3].hex } - end - - # Should override the time column values. - # Sybase doesn't like the time zones. - - end - - class SQLAnywhereAdapter < AbstractAdapter - def initialize( connection, logger, connection_string = "") #:nodoc: - super(connection, logger) - @auto_commit = true - @affected_rows = 0 - @connection_string = connection_string - @visitor = Arel::Visitors::SQLAnywhere.new self - connect! - end - - def self.visitor_for(pool) - config = pool.spec.config - - if config.fetch(:prepared_statements) {true} - Arel::Visitors::SQLAnywhere.new pool - else - BindSubstitution.new pool - end - end - - def adapter_name #:nodoc: - 'SQLAnywhere' - end - - def supports_migrations? #:nodoc: - true - end - - def requires_reloading? - true - end - - def active? - # The liveness variable is used a low-cost "no-op" to test liveness - SA.instance.api.sqlany_execute_immediate(@connection, "SET liveness = 1") == 1 - rescue - false - end - - def disconnect! - result = SA.instance.api.sqlany_disconnect( @connection ) - super - end - - def reconnect! - disconnect! - connect! - end - - def supports_count_distinct? #:nodoc: - true - end - - def supports_autoincrement? #:nodoc: - true - end - - # Maps native ActiveRecord/Ruby types into SQLAnywhere types - # TINYINTs are treated as the default boolean value - # ActiveRecord allows NULLs in boolean columns, and the SQL Anywhere BIT type does not - # As a result, TINYINT must be used. All TINYINT columns will be assumed to be boolean and - # should not be used as single-byte integer columns. This restriction is similar to other ActiveRecord database drivers - def native_database_types #:nodoc: - { - :primary_key => 'INTEGER PRIMARY KEY DEFAULT AUTOINCREMENT NOT NULL', - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "long varchar" }, - :integer => { :name => "integer", :limit => 4 }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "long binary" }, - :boolean => { :name => "tinyint", :limit => 1} - } - end - - # QUOTING ================================================== - - # Applies quotations around column names in generated queries - def quote_column_name(name) #:nodoc: - %Q("#{name}") - end - - # Handles special quoting of binary columns. Binary columns will be treated as strings inside of ActiveRecord. - # ActiveRecord requires that any strings it inserts into databases must escape the backslash (\). - # Since in the binary case, the (\x) is significant to SQL Anywhere, it cannot be escaped. - def quote(value, column = nil) - case value - when String, ActiveSupport::Multibyte::Chars - value_S = value.to_s - if column && column.type == :binary && column.class.respond_to?(:string_to_binary) - "'#{column.class.string_to_binary(value_S)}'" - else - super(value, column) - end - else - super(value, column) - end - end - - def quoted_true - '1' - end - - def quoted_false - '0' - end - - - # This function (distinct) is based on the Oracle Enhacned ActiveRecord driver maintained by Raimonds Simanovskis (2010) - # (https://github.com/rsim/oracle-enhanced) - def distinct(columns, order_by) #:nodoc: - return "DISTINCT #{columns}" if order_by.blank? - - # construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using - # FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT - order_columns = if order_by.is_a?(String) - order_by.split(',').map { |s| s.strip }.reject(&:blank?) - else # in latest ActiveRecord versions order_by is already Array - order_by - end - order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i| - # remove any ASC/DESC modifiers - value = c =~ /^(.+)\s+(ASC|DESC)\s*$/i ? $1 : c - "FIRST_VALUE(#{value}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__" - end - sql = "DISTINCT #{columns}, " - sql << order_columns * ", " - end - - # The database execution function - def execute(sql, name = nil) #:nodoc: - if name == :skip_logging - r = SA.instance.api.sqlany_execute_immediate(@connection, sql) - sqlanywhere_error_test(sql) if r==0 - else - log(sql, name) { execute(sql, :skip_logging) } - end - end - - def sqlanywhere_error_test(sql = '') - error_code, error_message = SA.instance.api.sqlany_error(@connection) - if error_code != 0 - sqlanywhere_error(error_code, error_message, sql) - end - end - - def sqlanywhere_error(code, message, sql) - raise SQLAnywhereException.new(message, code, sql) - end - - def translate_exception(exception, message) - return super unless exception.respond_to?(:errno) - case exception.errno - when -143 - if exception.sql !~ /^SELECT/i then - raise ActiveRecord::ActiveRecordError.new(message) - else - super - end - when -194 - raise InvalidForeignKey.new(message, exception) - when -196 - raise RecordNotUnique.new(message, exception) - when -183 - raise ArgumentError, message - else - super - end - end - - # The database update function. - def update_sql(sql, name = nil) - execute( sql, name ) - return @affected_rows - end - - # The database delete function. - def delete_sql(sql, name = nil) #:nodoc: - execute( sql, name ) - return @affected_rows - end - - # The database insert function. - # ActiveRecord requires that insert_sql returns the primary key of the row just inserted. In most cases, this can be accomplished - # by immediatly querying the @@identity property. If the @@identity property is 0, then passed id_value is used - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - execute(sql, name) - - retval = last_inserted_id(nil) - retval = id_value if retval == 0 - return retval - end - - def exec_delete(sql, name = 'SQL', binds = []) - exec_query(sql, name, binds) - @affected_rows - end - alias :exec_update :exec_delete - - def last_inserted_id(result) - identity = SA.instance.api.sqlany_execute_direct(@connection, 'SELECT @@identity') - raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if identity.nil? - SA.instance.api.sqlany_fetch_next(identity) - retval = SA.instance.api.sqlany_get_column(identity, 0)[1] - SA.instance.api.sqlany_free_stmt(identity) - - return retval - end - - # Returns a query as an array of arrays - def select_rows(sql, name = nil) - rs = SA.instance.api.sqlany_execute_direct(@connection, sql) - raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if rs.nil? - record = [] - while SA.instance.api.sqlany_fetch_next(rs) == 1 - max_cols = SA.instance.api.sqlany_num_cols(rs) - result = Array.new(max_cols) - max_cols.times do |cols| - result[cols] = SA.instance.api.sqlany_get_column(rs, cols)[1] - end - record << result - end - SA.instance.api.sqlany_free_stmt(rs) - return record - end - - def begin_db_transaction #:nodoc: - @auto_commit = false; - end - - def commit_db_transaction #:nodoc: - SA.instance.api.sqlany_commit(@connection) - @auto_commit = true; - end - - def rollback_db_transaction #:nodoc: - SA.instance.api.sqlany_rollback(@connection) - @auto_commit = true; - end - - def add_lock!(sql, options) #:nodoc: - sql - end - - # SQL Anywhere does not support sizing of integers based on the sytax INTEGER(size). Integer sizes - # must be captured when generating the SQL and replaced with the appropriate size. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: - type = type.to_sym - if native = native_database_types[type] - if type == :integer - case limit - when 1 - column_type_sql = 'tinyint' - when 2 - column_type_sql = 'smallint' - when 3..4 - column_type_sql = 'integer' - when 5..8 - column_type_sql = 'bigint' - else - column_type_sql = 'integer' - end - column_type_sql - elsif type == :string and !limit.nil? - "varchar (#{limit})" - elsif type == :boolean - column_type_sql = 'tinyint' - else - super(type, limit, precision, scale) - end - else - super(type, limit, precision, scale) - end - end - - # Do not return SYS-owned or DBO-owned tables or RS_systabgroup-owned - def tables(name = nil) #:nodoc: - sql = "SELECT table_name FROM SYS.SYSTABLE WHERE creator NOT IN (0,3,5)" - select(sql, name).map { |row| row["table_name"] } - end - - def columns(table_name, name = nil) #:nodoc: - table_structure(table_name).map do |field| - SQLAnywhereColumn.new(field['name'], field['default'], field['domain'], (field['nulls'] == 1)) - end - end - - def indexes(table_name, name = nil) #:nodoc: - if @major_version <= 11 # the sql doesn't work in older databases. - return [] - end - sql = "SELECT DISTINCT index_name, \"unique\" FROM SYS.SYSTABLE INNER JOIN SYS.SYSIDXCOL ON SYS.SYSTABLE.table_id = SYS.SYSIDXCOL.table_id INNER JOIN SYS.SYSIDX ON SYS.SYSTABLE.table_id = SYS.SYSIDX.table_id AND SYS.SYSIDXCOL.index_id = SYS.SYSIDX.index_id WHERE table_name = '#{table_name}' AND index_category > 2" - select(sql, name).map do |row| - index = IndexDefinition.new(table_name, row['index_name']) - index.unique = row['unique'] == 1 - sql = "SELECT column_name FROM SYS.SYSIDX INNER JOIN SYS.SYSIDXCOL ON SYS.SYSIDXCOL.table_id = SYS.SYSIDX.table_id AND SYS.SYSIDXCOL.index_id = SYS.SYSIDX.index_id INNER JOIN SYS.SYSCOLUMN ON SYS.SYSCOLUMN.table_id = SYS.SYSIDXCOL.table_id AND SYS.SYSCOLUMN.column_id = SYS.SYSIDXCOL.column_id WHERE index_name = '#{row['index_name']}'" - index.columns = select(sql).map { |col| col['column_name'] } - index - end - end - - def primary_key(table_name) #:nodoc: - sql = "SELECT cname from SYS.SYSCOLUMNS where tname = '#{table_name}' and in_primary_key = 'Y'" - rs = exec_query(sql) - if !rs.nil? and !rs.first.nil? - rs.first['cname'] - else - nil - end - end - - def remove_index(table_name, options={}) #:nodoc: - execute "DROP INDEX #{quote_table_name(table_name)}.#{quote_column_name(index_name(table_name, options))}" - end - - def rename_table(name, new_name) - execute "ALTER TABLE #{quote_table_name(name)} RENAME #{quote_table_name(new_name)}" - end - - def change_column_default(table_name, column_name, default) #:nodoc: - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} DEFAULT #{quote(default)}" - end - - def change_column_null(table_name, column_name, null, default = nil) - unless null || default.nil? - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") - end - execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? '' : 'NOT'} NULL") - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - add_column_sql << ' NULL' if options[:null] - execute(add_column_sql) - end - - def rename_column(table_name, column_name, new_column_name) #:nodoc: - if column_name.downcase == new_column_name.downcase - whine = "if_the_only_change_is_case_sqlanywhere_doesnt_rename_the_column" - rename_column table_name, column_name, "#{new_column_name}#{whine}" - rename_column table_name, "#{new_column_name}#{whine}", new_column_name - else - execute "ALTER TABLE #{quote_table_name(table_name)} RENAME #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" - end - end - - def remove_column(table_name, *column_names) - column_names = column_names.flatten - column_names.zip(columns_for_remove(table_name, *column_names)).each do |unquoted_column_name, column_name| - sql = <<-SQL - SELECT "index_name" FROM SYS.SYSTAB join SYS.SYSTABCOL join SYS.SYSIDXCOL join SYS.SYSIDX - WHERE "column_name" = '#{unquoted_column_name}' AND "table_name" = '#{table_name}' - SQL - select(sql, nil).each do |row| - execute "DROP INDEX \"#{table_name}\".\"#{row['index_name']}\"" - end - execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" - end - end - - - def purge_database - tables.each do |table_name| - drop_table(table_name) - end - end - - protected - def select(sql, name = nil, binds = []) #:nodoc: - exec_query(sql, name, binds).to_a - end - - # ActiveRecord uses the OFFSET/LIMIT keywords at the end of query to limit the number of items in the result set. - # This syntax is NOT supported by SQL Anywhere. In previous versions of this adapter this adapter simply - # overrode the add_limit_offset function and added the appropriate TOP/START AT keywords to the start of the query. - # However, this will not work for cases where add_limit_offset is being used in a subquery since add_limit_offset - # is called with the WHERE clause. - # - # As a result, the following function must be called before every SELECT statement against the database. It - # recursivly walks through all subqueries in the SQL statment and replaces the instances of OFFSET/LIMIT with the - # corresponding TOP/START AT. It was my intent to do the entire thing using regular expressions, but it would seem - # that it is not possible given that it must count levels of nested brackets. - def modify_limit_offset(sql) - modified_sql = "" - subquery_sql = "" - in_single_quote = false - in_double_quote = false - nesting_level = 0 - if sql =~ /(OFFSET|LIMIT)/xmi then - if sql =~ /\(/ then - sql.split(//).each_with_index do |x, i| - case x[0] - when 40 # left brace - ( - modified_sql << x if nesting_level == 0 - subquery_sql << x if nesting_level > 0 - nesting_level = nesting_level + 1 unless in_double_quote || in_single_quote - when 41 # right brace - ) - nesting_level = nesting_level - 1 unless in_double_quote || in_single_quote - if nesting_level == 0 and !in_double_quote and !in_single_quote then - modified_sql << modify_limit_offset(subquery_sql) - subquery_sql = "" - end - modified_sql << x if nesting_level == 0 - subquery_sql << x if nesting_level > 0 - when 39 # single quote - ' - in_single_quote = in_single_quote ^ true unless in_double_quote - modified_sql << x if nesting_level == 0 - subquery_sql << x if nesting_level > 0 - when 34 # double quote - " - in_double_quote = in_double_quote ^ true unless in_single_quote - modified_sql << x if nesting_level == 0 - subquery_sql << x if nesting_level > 0 - else - modified_sql << x if nesting_level == 0 - subquery_sql << x if nesting_level > 0 - end - raise ActiveRecord::StatementInvalid.new("Braces do not match: #{sql}") if nesting_level < 0 - end - else - modified_sql = sql - end - raise ActiveRecord::StatementInvalid.new("Quotes do not match: #{sql}") if in_double_quote or in_single_quote - return "" if modified_sql.nil? - select_components = modified_sql.scan(/\ASELECT\s+(DISTINCT)?(.*?)(?:\s+LIMIT\s+(.*?))?(?:\s+OFFSET\s+(.*?))?\Z/xmi) - return modified_sql if select_components[0].nil? - final_sql = "SELECT #{select_components[0][0]} " - final_sql << "TOP #{select_components[0][2].nil? ? 1000000 : select_components[0][2]} " - final_sql << "START AT #{(select_components[0][3].to_i + 1).to_s} " unless select_components[0][3].nil? - final_sql << "#{select_components[0][1]}" - return final_sql - else - return sql - end - end - - # Queries the structure of a table including the columns names, defaults, type, and nullability - # ActiveRecord uses the type to parse scale and precision information out of the types. As a result, - # chars, varchars, binary, nchars, nvarchars must all be returned in the form type(width) - # numeric and decimal must be returned in the form type(width, scale) - # Nullability is returned as 0 (no nulls allowed) or 1 (nulls allowed) - # Alos, ActiveRecord expects an autoincrement column to have default value of NULL - - def table_structure(table_name) - sql = <<-SQL -SELECT SYS.SYSCOLUMN.column_name AS name, - if left("default",1)='''' then substring("default", 2, length("default")-2) // remove the surrounding quotes - else NULLIF(SYS.SYSCOLUMN."default", 'autoincrement') - endif AS "default", - IF SYS.SYSCOLUMN.domain_id IN (7,8,9,11,33,34,35,3,27) THEN - IF SYS.SYSCOLUMN.domain_id IN (3,27) THEN - SYS.SYSDOMAIN.domain_name || '(' || SYS.SYSCOLUMN.width || ',' || SYS.SYSCOLUMN.scale || ')' - ELSE - SYS.SYSDOMAIN.domain_name || '(' || SYS.SYSCOLUMN.width || ')' - ENDIF - ELSE - SYS.SYSDOMAIN.domain_name - ENDIF AS domain, - IF SYS.SYSCOLUMN.nulls = 'Y' THEN 1 ELSE 0 ENDIF AS nulls -FROM - SYS.SYSCOLUMN - INNER JOIN SYS.SYSTABLE ON SYS.SYSCOLUMN.table_id = SYS.SYSTABLE.table_id - INNER JOIN SYS.SYSDOMAIN ON SYS.SYSCOLUMN.domain_id = SYS.SYSDOMAIN.domain_id -WHERE - table_name = '#{table_name}' -SQL - structure = exec_query(sql, :skip_logging) - raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure == false - structure - end - - # Required to prevent DEFAULT NULL being added to primary keys - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) - end - - private - - def connect! - result = SA.instance.api.sqlany_connect(@connection, @connection_string) - if result == 1 then - set_connection_options - else - error = SA.instance.api.sqlany_error(@connection) - raise ActiveRecord::ActiveRecordError.new("#{error}: Cannot Establish Connection") - end - version = exec_query('select @@version').rows[0][0] - @major_version = /^\d+/.match(version).to_s.to_i - end - - def set_connection_options - SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION non_keywords = 'LOGIN'") rescue nil - SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION timestamp_format = 'YYYY-MM-DD HH:NN:SS'") rescue nil - #SA.instance.api.sqlany_execute_immediate(@connection, "SET OPTION reserved_keywords = 'LIMIT'") rescue nil - # The liveness variable is used a low-cost "no-op" to test liveness - SA.instance.api.sqlany_execute_immediate(@connection, "CREATE VARIABLE liveness INT") rescue nil - end - - def exec_query(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - stmt = SA.instance.api.sqlany_prepare(@connection, sql) - - if stmt.nil? - sqlanywhere_error_test(sql) - end - - for i in 0...binds.length - bind_type = binds[i][0].type - bind_value = binds[i][1] - result, bind_param = SA.instance.api.sqlany_describe_bind_param(stmt, i) - sqlanywhere_error_test(sql) if result==0 - - bind_param.set_direction(1) # https://github.com/sqlanywhere/sqlanywhere/blob/master/ext/sacapi.h#L175 - if bind_value.nil? - bind_param.set_value(nil) - elsif bind_type == :datetime - bind_param.set_value(bind_value.to_datetime.to_s :db) - elsif bind_type == :boolean - bind_param.set_value(bind_value ? 1 : 0) - elsif bind_type == :decimal - bind_param.set_value(bind_value.to_s) - elsif bind_type == :date - bind_param.set_value(bind_value.to_s) - else - bind_param.set_value(bind_value) - end - result = SA.instance.api.sqlany_bind_param(stmt, i, bind_param) - sqlanywhere_error_test(sql) if result==0 - - end - - if SA.instance.api.sqlany_execute(stmt) == 0 - sqlanywhere_error_test(sql) - end - - fields = [] - native_types = [] - - num_cols = SA.instance.api.sqlany_num_cols(stmt) - sqlanywhere_error_test(sql) if num_cols == -1 - - for i in 0...num_cols - result, col_num, name, ruby_type, native_type, precision, scale, max_size, nullable = SA.instance.api.sqlany_get_column_info(stmt, i) - sqlanywhere_error_test(sql) if result==0 - fields << name - native_types << native_type - end - rows = [] - while SA.instance.api.sqlany_fetch_next(stmt) == 1 - row = [] - for i in 0...num_cols - r, value = SA.instance.api.sqlany_get_column(stmt, i) - row << native_type_to_ruby_type(native_types[i], value) - end - rows << row - end - SA.instance.api.sqlany_free_stmt(stmt) - - if @auto_commit - result = SA.instance.api.sqlany_commit(@connection) - sqlanywhere_error_test(sql) if result==0 - end - return ActiveRecord::Result.new(fields, rows) - end - end - - def query(sql) - return if sql.nil? - #sql = modify_limit_offset(sql) - - # ActiveRecord allows a query to return TOP 0. SQL Anywhere requires that the TOP value is a positive integer. - return Array.new() if sql =~ /TOP 0/i - - # Executes the query, iterates through the results, and builds an array of hashes. - rs = SA.instance.api.sqlany_execute_direct(@connection, sql) - if rs.nil? - result, errstr = SA.instance.api.sqlany_error(@connection) - raise SQLAnywhereException.new(errstr, result, sql) - end - - record = [] - if( SA.instance.api.sqlany_num_cols(rs) > 0 ) - while SA.instance.api.sqlany_fetch_next(rs) == 1 - max_cols = SA.instance.api.sqlany_num_cols(rs) - result = Hash.new() - max_cols.times do |cols| - col_content=SA.instance.api.sqlany_get_column(rs, cols)[1] - if !col_content.nil? && col_content.is_a?(String) - puts ":encoding missing in database.yml" if ActiveRecord::Base.configurations[Rails.env]['encoding'].nil? - col_content = col_content.force_encoding(ActiveRecord::Base.configurations[Rails.env]['encoding']) - end - result[SA.instance.api.sqlany_get_column_info(rs, cols)[2]] = col_content - end - record << result - end - @affected_rows = 0 - else - @affected_rows = SA.instance.api.sqlany_affected_rows(rs) - end - SA.instance.api.sqlany_free_stmt(rs) - - SA.instance.api.sqlany_commit(@connection) if @auto_commit - return record - end - - # convert sqlany type to ruby type - # the types are taken from here - # http://dcx.sybase.com/1101/en/dbprogramming_en11/pg-c-api-native-type-enum.html - def native_type_to_ruby_type(native_type, value) - return nil if value.nil? - case native_type - when 484 # DT_DECIMAL (also and more importantly numeric) - BigDecimal.new(value) - else - value - end - end - end - end -end - diff --git a/lib/active_record/connection_adapters/sqlanywhere_jdbc_in4systems_adapter.rb b/lib/active_record/connection_adapters/sqlanywhere_jdbc_in4systems_adapter.rb new file mode 100755 index 0000000..15e95dc --- /dev/null +++ b/lib/active_record/connection_adapters/sqlanywhere_jdbc_in4systems_adapter.rb @@ -0,0 +1,492 @@ +#encoding: utf-8 +#==================================================== +# +# Copyright 2008-2010 iAnywhere Solutions, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# See the License for the specific language governing permissions and +# limitations under the License. +# +# While not a requirement of the license, if you do modify this file, we +# would appreciate hearing about it. Please email sqlany_interfaces@sybase.com +# +# +#==================================================== +#require 'active_record' +require 'activerecord-jdbc-adapter' + +require 'active_record/connection_adapters/abstract_adapter' +require 'arel/visitors/sqlanywhere.rb' +require 'pathname' + +module ActiveRecord + class Base + DEFAULT_CONFIG = { :username => 'dba', :password => 'sql' } + # Main connection function to SQL Anywhere + # Connection Adapter takes four parameters: + # * :database (required, no default). Corresponds to "DatabaseName=" in connection string + # * :server (optional, defaults to :databse). Corresponds to "ServerName=" in connection string + # * :username (optional, default to 'dba') + # * :password (optional, deafult to 'sql') + # * :encoding (optional, defaults to charset of OS) + # * :commlinks (optional). Corresponds to "CommLinks=" in connection string + # * :connection_name (optional). Corresponds to "ConnectionName=" in connection string + + def self.sqlanywhere_jdbc_in4systems_connection(config) + + if config[:connection_string] + connection_string = config[:connection_string] + else + config = DEFAULT_CONFIG.merge(config) + + raise ArgumentError, "No database name was given. Please add a :database option." unless config.has_key?(:database) + + connection_string = "ServerName=#{(config[:server] || config[:database])};DatabaseName=#{config[:database]};UserID=#{config[:username]};Password=#{config[:password]};" + connection_string += "CommLinks=#{config[:commlinks]};" unless config[:commlinks].nil? + connection_string += "ConnectionName=#{config[:connection_name]};" unless config[:connection_name].nil? + connection_string += "CharSet=#{config[:encoding]};" unless config[:encoding].nil? + connection_string += "Idle=0" # Prevent the server from disconnecting us if we're idle for >240mins (by default) + end + + url = 'jdbc:sqlanywhere:' + connection_string + + if ENV['SQLANY16'] + $CLASSPATH << 'sajdbc4.jar' + $CLASSPATH << Pathname.new(ENV['SQLANY16']).join('java').join('sajdbc4.jar').to_s + driver = 'sybase.jdbc4.sqlanywhere.IDriver' + elsif ENV['SQLANY12'] + $CLASSPATH << 'sajdbc4.jar' + $CLASSPATH << Pathname.new(ENV['SQLANY12']).join('java').join('sajdbc4.jar').to_s + driver = 'sybase.jdbc4.sqlanywhere.IDriver' + elsif ENV['SQLANY11'] + $CLASSPATH << 'sajdbc.jar' + $CLASSPATH << Pathname.new(ENV['SQLANY11']).join('java').join('sajdbc.jar').to_s + driver = 'sybase.jdbc.sqlanywhere.IDriver' + else + raise "Cannot find SqlAnywhere installation directory" + end + + conn = ActiveRecord::Base.jdbc_connection({adapter: 'jdbc', driver: driver, url: url}) + + ConnectionAdapters::SQLAnywhereJdbcIn4systemsAdapter.new( conn, logger, connection_string) + end + end + + module ConnectionAdapters + class JdbcTypeConverter + AR_TO_JDBC_TYPES[:text] << lambda {|r| r['type_name'] =~ /^long varchar$/i} + end + + class SQLAnywhereException < StandardError + attr_reader :errno + attr_reader :sql + + def initialize(message, errno, sql) + super(message) + @errno = errno + @sql = sql + end + end + + class SQLAnywhereColumn < Column + private + # Overridden to handle SQL Anywhere integer, varchar, binary, and timestamp types + def simplified_type(field_type) + return :boolean if field_type =~ /tinyint/i + return :boolean if field_type =~ /bit/i + return :text if field_type =~ /long varchar/i + return :string if field_type =~ /varchar/i + return :binary if field_type =~ /long binary/i + return :datetime if field_type =~ /timestamp/i + return :integer if field_type =~ /smallint|bigint/i + return :text if field_type =~ /xml/i + return :integer if field_type =~ /uniqueidentifier/i + super + end + + def extract_limit(sql_type) + case sql_type + when /^tinyint/i + 1 + when /^smallint/i + 2 + when /^integer/i + 4 + when /^bigint/i + 8 + else super + end + end + + protected + # Handles the encoding of a binary object into SQL Anywhere + # SQL Anywhere requires that binary values be encoded as \xHH, where HH is a hexadecimal number + # This function encodes the binary string in this format + def self.string_to_binary(value) + "\\x" + value.unpack("H*")[0].scan(/../).join("\\x") + end + + def self.binary_to_string(value) + value.gsub(/\\x[0-9]{2}/) { |byte| byte[2..3].hex } + end + + # Should override the time column values. + # Sybase doesn't like the time zones. + + end + + class SQLAnywhereJdbcIn4systemsAdapter < AbstractAdapter + delegate :select, :select_rows, :execute, to: :conn + + attr_reader :conn + def initialize( conn, logger, connection_string = "") #:nodoc: + super + @visitor = Arel::Visitors::SQLAnywhere.new self + @conn = conn + end + + def self.visitor_for(pool) + config = pool.spec.config + + if config.fetch(:prepared_statements) {true} + Arel::Visitors::SQLAnywhere.new pool + else + BindSubstitution.new pool + end + end + + def adapter_name #:nodoc: + 'SQLAnywhere' + end + + def supports_migrations? #:nodoc: + true + end + + def requires_reloading? + true + end + + def active? + # The liveness variable is used a low-cost "no-op" to test liveness + SA.instance.api.sqlany_execute_immediate(@connection, "SET liveness = 1") == 1 + rescue + false + end + + def supports_count_distinct? #:nodoc: + true + end + + def supports_autoincrement? #:nodoc: + true + end + + # Maps native ActiveRecord/Ruby types into SQLAnywhere types + # TINYINTs are treated as the default boolean value + # ActiveRecord allows NULLs in boolean columns, and the SQL Anywhere BIT type does not + # As a result, TINYINT must be used. All TINYINT columns will be assumed to be boolean and + # should not be used as single-byte integer columns. This restriction is similar to other ActiveRecord database drivers + def native_database_types #:nodoc: + { + :primary_key => 'INTEGER PRIMARY KEY DEFAULT AUTOINCREMENT NOT NULL', + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "long varchar" }, + :integer => { :name => "integer", :limit => 4 }, + :float => { :name => "float" }, + :decimal => { :name => "decimal" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "binary" }, + :boolean => { :name => "tinyint", :limit => 1} + } + end + + # QUOTING ================================================== + + # Applies quotations around column names in generated queries + def quote_column_name(name) #:nodoc: + %Q("#{name}") + end + + def quote_table_name(name) + owner, table = name.to_s.split('.', 2) + if table == nil + table = owner + owner = :dba + end + "#{quote_column_name(owner)}.#{quote_column_name(table)}" + end + + def quote_table_alias_name(name) + quote_column_name name + end + + + # Handles special quoting of binary columns. Binary columns will be treated as strings inside of ActiveRecord. + # ActiveRecord requires that any strings it inserts into databases must escape the backslash (\). + # Since in the binary case, the (\x) is significant to SQL Anywhere, it cannot be escaped. + def quote(value, column = nil) + case value + when String, ActiveSupport::Multibyte::Chars + value_S = value.to_s + if column && column.type == :binary && column.class.respond_to?(:string_to_binary) + "'#{column.class.string_to_binary(value_S)}'" + else + super(value, column) + end + else + super(value, column) + end + end + + def quoted_true + '1' + end + + def quoted_false + '0' + end + + # SQL Anywhere does not support sizing of integers based on the sytax INTEGER(size). Integer sizes + # must be captured when generating the SQL and replaced with the appropriate size. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: + type = type.to_sym + if native = native_database_types[type] + if type == :integer + case limit + when 1 + column_type_sql = 'tinyint' + when 2 + column_type_sql = 'smallint' + when 3..4 + column_type_sql = 'integer' + when 5..8 + column_type_sql = 'bigint' + else + column_type_sql = 'integer' + end + column_type_sql + elsif type == :string and !limit.nil? + "varchar (#{limit})" + elsif type == :boolean + column_type_sql = 'tinyint' + else + super(type, limit, precision, scale) + end + else + super(type, limit, precision, scale) + end + end + + def viewed_tables(name = nil) + list_of_tables(['view'], name) + end + + def base_tables(name = nil) + list_of_tables(['base'], name) + end + + # Do not return SYS-owned or DBO-owned tables or RS_systabgroup-owned + def tables(name = nil) #:nodoc: + list_of_tables(['base', 'view']) + end + + def columns(table_name, name = nil) #:nodoc: + table_structure(table_name).map do |field| + default = field['default'] + if default == nil # Nil is the usual case + elsif default.starts_with?("'") # If string, remove first and last quotes and the last \n character + default = default[1..-2] + elsif default =~ /^-?\d+(\.\d+)?$/ # If a number string, leave as it is + else # Otherwise, it's probably something (LAST USER, CURRENT TIMESTAMP, etc) that wouldn't work in Rails, so return nil + default = nil + end + SQLAnywhereColumn.new(field['name'], default, field['domain'], (field['nulls'] == 1)) + end + end + + def indexes(table_name, name = nil) #:nodoc: + sql = "SELECT DISTINCT index_name, \"unique\" FROM SYS.SYSTABLE INNER JOIN SYS.SYSIDXCOL ON SYS.SYSTABLE.table_id = SYS.SYSIDXCOL.table_id INNER JOIN SYS.SYSIDX ON SYS.SYSTABLE.table_id = SYS.SYSIDX.table_id AND SYS.SYSIDXCOL.index_id = SYS.SYSIDX.index_id WHERE table_name = '#{table_name}' AND index_category > 2" + select(sql, name).map do |row| + index = IndexDefinition.new(table_name, row['index_name']) + index.unique = row['unique'] == 1 + sql = "SELECT column_name FROM SYS.SYSIDX INNER JOIN SYS.SYSIDXCOL ON SYS.SYSIDXCOL.table_id = SYS.SYSIDX.table_id AND SYS.SYSIDXCOL.index_id = SYS.SYSIDX.index_id INNER JOIN SYS.SYSCOLUMN ON SYS.SYSCOLUMN.table_id = SYS.SYSIDXCOL.table_id AND SYS.SYSCOLUMN.column_id = SYS.SYSIDXCOL.column_id WHERE index_name = '#{row['index_name']}'" + index.columns = select(sql).map { |col| col['column_name'] } + index + end + end + + def primary_key(table_name) #:nodoc: + sql = "SELECT cname from SYS.SYSCOLUMNS where tname = '#{table_name}' and in_primary_key = 'Y'" + rs = exec_query(sql) + if !rs.nil? and !rs.first.nil? + rs.first['cname'] + else + nil + end + end + + def remove_index(table_name, options={}) #:nodoc: + execute "DROP INDEX #{quote_table_name(table_name)}.#{quote_column_name(index_name(table_name, options))}" + end + + def rename_table(name, new_name) + execute "ALTER TABLE #{quote_table_name(name)} RENAME #{quote_table_name(new_name)}" + end + + def change_column_default(table_name, column_name, default) #:nodoc: + execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} DEFAULT #{quote(default)}" + end + + def change_column_null(table_name, column_name, null, default = nil) + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? '' : 'NOT'} NULL") + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + add_column_sql << ' NULL' if options[:null] + execute(add_column_sql) + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + if column_name.downcase == new_column_name.downcase + whine = "if_the_only_change_is_case_sqlanywhere_doesnt_rename_the_column" + rename_column table_name, column_name, "#{new_column_name}#{whine}" + rename_column table_name, "#{new_column_name}#{whine}", new_column_name + else + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" + end + end + + def remove_column(table_name, *column_names) + column_names = column_names.flatten + column_names.zip(columns_for_remove(table_name, *column_names)).each do |unquoted_column_name, column_name| + sql = <<-SQL + SELECT "index_name" FROM SYS.SYSTAB join SYS.SYSTABCOL join SYS.SYSIDXCOL join SYS.SYSIDX + WHERE "column_name" = '#{unquoted_column_name}' AND "table_name" = '#{table_name}' + SQL + select(sql, nil).each do |row| + execute "DROP INDEX \"#{table_name}\".\"#{row['index_name']}\"" + end + execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" + end + end + + def exec_query(sql, name = 'SQL', binds = []) + binds.map! do |column, value| + type = column.type + if value != nil + case type + when :boolean + [column, value ? 1 : 0] + else + [column, value] + end + else + [column, value] + end + end + conn.exec_query(sql, name, binds) + end + + def last_inserted_id(result) + select_value('SELECT @@identity') + end + + def select_user(cache=true) + @user_id = (cache && @user_id) || select_value('SELECT USER') + end + + # Set the database user to be user_id. + # If a block is given, then after running the block, the previous user_id is restored. + def set_user(user_id) + previous_user_id = select_user(true) + if previous_user_id != user_id + execute("SETUSER #{quote_column_name(user_id)}") + @user_id = user_id + end + if block_given? + begin + yield + ensure + set_user(previous_user_id) + end + end + end + + protected + + def list_of_tables(types, name = nil) + sql = "SELECT table_name FROM SYS.SYSTABLE WHERE table_type in (#{types.map{|t| quote(t)}.join(', ')}) and creator NOT IN (0,3,5)" + select(sql, name).map { |row| row["table_name"] } + end + + # Queries the structure of a table including the columns names, defaults, type, and nullability + # ActiveRecord uses the type to parse scale and precision information out of the types. As a result, + # chars, varchars, binary, nchars, nvarchars must all be returned in the form type(width) + # numeric and decimal must be returned in the form type(width, scale) + # Nullability is returned as 0 (no nulls allowed) or 1 (nulls allowed) + # Alos, ActiveRecord expects an autoincrement column to have default value of NULL + + def table_structure(table_name) + sql = <<-SQL +SELECT SYS.SYSCOLUMN.column_name AS name, + "default" AS "default", + IF SYS.SYSCOLUMN.domain_id IN (7,8,9,11,33,34,35,3,27) THEN + IF SYS.SYSCOLUMN.domain_id IN (3,27) THEN + SYS.SYSDOMAIN.domain_name || '(' || SYS.SYSCOLUMN.width || ',' || SYS.SYSCOLUMN.scale || ')' + ELSE + SYS.SYSDOMAIN.domain_name || '(' || SYS.SYSCOLUMN.width || ')' + ENDIF + ELSE + SYS.SYSDOMAIN.domain_name + ENDIF AS domain, + IF SYS.SYSCOLUMN.nulls = 'Y' THEN 1 ELSE 0 ENDIF AS nulls +FROM + SYS.SYSCOLUMN + INNER JOIN SYS.SYSTABLE ON SYS.SYSCOLUMN.table_id = SYS.SYSTABLE.table_id + INNER JOIN SYS.SYSDOMAIN ON SYS.SYSCOLUMN.domain_id = SYS.SYSDOMAIN.domain_id + INNER JOIN sys.sysUser ON sys.systable.creator = sys.sysuser.user_id AND sys.sysuser.user_name = 'dba' +WHERE + table_name = '#{table_name}' +SQL + structure = exec_query(sql, :skip_logging) + raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure == false + structure + end + + # Required to prevent DEFAULT NULL being added to primary keys + def options_include_default?(options) + options.include?(:default) && !(options[:null] == false && options[:default].nil?) + end + + private + + def set_connection_options + SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION non_keywords = 'LOGIN'") rescue nil + SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION timestamp_format = 'YYYY-MM-DD HH:NN:SS'") rescue nil + #SA.instance.api.sqlany_execute_immediate(@connection, "SET OPTION reserved_keywords = 'LIMIT'") rescue nil + # The liveness variable is used a low-cost "no-op" to test liveness + SA.instance.api.sqlany_execute_immediate(@connection, "CREATE VARIABLE liveness INT") rescue nil + end + end + end +end + diff --git a/lib/activerecord-sqlanywhere-adapter.rb b/lib/activerecord-sqlanywhere-adapter.rb deleted file mode 100755 index fa979e5..0000000 --- a/lib/activerecord-sqlanywhere-adapter.rb +++ /dev/null @@ -1,16 +0,0 @@ -# https://github.com/rsim/oracle-enhanced/blob/master/lib/activerecord-oracle_enhanced-adapter.rb - -if defined?(::Rails::Railtie) - - module ActiveRecord - module ConnectionAdapters - class SqlanywhereRailtie < ::Rails::Railtie - rake_tasks do - load 'active_record/connection_adapters/sqlanywhere.rake' - end - - end - end - end - -end \ No newline at end of file diff --git a/lib/arel/visitors/sqlanywhere.rb b/lib/arel/visitors/sqlanywhere.rb index 0496327..e1363a4 100755 --- a/lib/arel/visitors/sqlanywhere.rb +++ b/lib/arel/visitors/sqlanywhere.rb @@ -1,19 +1,24 @@ module Arel module Visitors class SQLAnywhere < Arel::Visitors::ToSql + def initialize connection + super + @quoted_table_aliases = {} + end + private def visit_Arel_Nodes_SelectStatement o o = order_hacks(o) is_distinct = using_distinct?(o) - + o.limit = 1000000 if (o.offset && !o.limit) o.limit = o.limit.expr if(o.limit.is_a?(Arel::Nodes::Limit)) o.limit = o.limit if(o.limit.is_a?(Fixnum)) [ "SELECT", - ("DISTINCT" if (o.limit || o.offset) && is_distinct), + ("DISTINCT" if is_distinct), ("TOP #{o.limit}" if o.limit), (visit_Arel_Nodes_Offset(o.offset) if o.offset), o.cores.map { |x| visit_Arel_Nodes_SelectCore x }.join, @@ -34,17 +39,70 @@ def visit_Arel_Nodes_SelectCore o ].compact.join ' ' end + def visit_Arel_Nodes_Group o + expr = o.expr.clone + if expr.class == Arel::Nodes::NamedFunction + expr.alias = nil + end + visit expr + end def visit_Arel_Nodes_Offset o "START AT #{visit(o.expr) + 1}" end + def visit_Arel_Nodes_True o + "1=1" + end + + def visit_Arel_Nodes_False o + "1=0" + end + + def visit_Arel_Nodes_Matches o + # The version in arel cannot like integer columns + left = visit o.left # This method sets last column + # If last column was left, visit o.right would return 0 + self.last_column = nil + "#{left} LIKE #{visit o.right}" + end + + def visit_Arel_Nodes_TableAlias o + "#{visit o.relation} #{quote_table_alias_name o.name}" + end + + def visit_Arel_Table o, a=nil + if o.table_alias + "#{quote_table_name o.name} #{quote_table_alias_name o.table_alias}" + else + quote_table_name o.name + end + end + + def visit_Arel_Attributes_Attribute o, a=nil + if o.relation.table_alias + join_name = o.relation.table_alias + "#{quote_table_alias_name join_name}.#{quote_column_name o.name}" + else + join_name = o.relation.name + "#{quote_table_name join_name}.#{quote_column_name o.name}" + end + end + + def quote_table_alias_name name + return name if Arel::Nodes::SqlLiteral === name + @quoted_table_aliases[name] ||= @connection.quote_table_alias_name(name) + end + + + + def using_distinct?(o) o.cores.any? do |core| core.set_quantifier.class == Arel::Nodes::Distinct end - end + end # The functions (order_hacks, split_order_string) are based on the Oracle Enhacned ActiveRecord driver maintained by Raimonds Simanovskis (2010) # (https://github.com/rsim/oracle-enhanced)