Advertise here!

Getting Capistrano to play with Oracle on Red Hat Enterprise Linux

I've been working on an internal project that connects to a legacy Oracle database. The process of getting the driver and client set up was a bit annoying, but I'd done it before and I managed to get through it again. (For those of you braving it, be sure you don't include a trailing slash on your ORACLE_HOME environment variable).

The Oracle driver and client are like most Oracle products - unnecessarily complex. One of its requirements is that you set an ORACLE_HOME environment variable. I'd managed to do this and add it to my .bashrc file so that when I manually logged into the app server via SSH I could get the rails app up and running. The problems was that remotely deploying via Capistrano was causing errors - anything that loaded the rails framework, and therefore the Oracle driver, would die with a stack trace about an undefined method. Luckily in my past troubles with the driver, I knew that meant that it didn't have the ORACLE_HOME environment variable set.

In case anyone else is running into this (or I do again), here's a patch:

First, add the following to your deploy.rb recipe:

set :use_sudo, false

Next, modify your capistrano/actor.rb file to force all run commands through bash with BASH_ENV set:

alias :run_without_env :run
def run(cmd, options={}, &block)
  cmd = <<-CMD
  BASH_ENV=.bashrc /bin/bash -c -- "#{cmd}"
  CMD
  run_without_env cmd, options, &block
end

These two changes will force the environment to be loaded and also make all command be run as the original user (which should have rights to do so).

You can read more about it on the Google group for Capistrano

Posted at 4pm on 10/27/06 | Posted in , | no responses | read on

Oracle patches for Rails

The code I described and provided earlier in my entry: Rails, db_structure_dump, and Oracle was submitted as a patch to Rails around the time of the 0.13.0 release. I just received an email today closing the ticket, showing that the patch was closed in favor of ticket #1798. Curious, I poked around looking at ticket 1798. It includes a number or oracle related patches for Rails, including my implementation of structure_dump.

The patches will give the Oracle adapter a number of improvements. Improvements related to the structure_dump implementation are the implementation of purging and cloning Oracle databases. Some bigger changes include the ability to use synonyms and a change which could break existing users: Using sequences on a per-table basis. Previously, Rails used a single sequence to generate the primary keys for all tables. The next release will change the behavior to create and use sequences in the form “#{table_name}_seq” which is more common practice.

The current stance is that existing Oracle users who want to retain the single, global sequence will have to modify their environment.rb (after the next Rails release) to include:

ActiveRecord::Base.set_sequence_name = "rails_sequence"

Update: As of Rails 1.0 RC3, the way to set the sequence name for all ActiveRecord's has changed from the initial proposal. Now, you'd do something like so:

# Post 0.13.1, Force the OCI Adapter to use the global sequence like it used to
class ActiveRecord::Base
  set_sequence_name "rails_sequence"
end

Posted at 7pm on 07/25/05 | Posted in , , | 1 responses | read on

Database conversion

Following on the heels of my earlier entry about db_structure_dump, I’ve created another small useful Ruby script.

As I said before, my production databse is Oracle 10g and so is my own development database - but the development databases of the others helping out are MySQL. Now that I can dump the CREATE TABLE statements for my Oracle databases I need some way to convert back and forth between MySQL and Oracle syntax – since both aren’t really standard. Or at least I need to be able to dump my development oracle DB to SQL and then convert it to MySQL for the others.

Below is the hack I created that will take an Oracle database’s structure (or  sqlite, odbc, mssql and mysql)and convert it to standard SQL, then to MySQL syntax(since MySQL can’t handle VARCHAR’s longer than 255, for intance).

The code is not generic enough to be useful for everyone, but should give a good start. Watch out if you:

  • Don’t use ID as your primary key. This is assumed right now, but a smarter script could check for keys.
  • Actually use NUMBER in Oracle as a DECIMAL (and want to retain that in MySQL, rather than have it converted to INTEGER).
  • Have join tables (since usually you won’t use ID as the primary key, you’ll have to manually fix the primary key).

 

class SQLStandardizer

def initialize(type, source)
    @data_type, @data = type, source
    init_substitutions
  end     
    
  # Convert data to compatible SQL
  def convert
    replacements = @subst[@data_type]
    new_data = @data
    replacements.each do |original, replacer|
      new_data.gsub!(original, replacer)
    end
    new_data
  end    
  
   # Initialize the substitution array
  private
  def init_substitutions
    @subst = {}
    @subst['mysql'] = {"int"=>"INTEGER","dec"=>"DECIMAL"}
    @subst['oracle'] = {"BFILE"=>"BLOB",
 				"CLOB"=>"TEXT",
 				"DATE"=>"DATETIME",
 				"LONG"=>"BIGINT",
 				"LONG RAW"=>"BIGINT",
 				"NCHAR"=>"CHAR",
 				"NCLOB"=>"TEXT",
 				"NUMBER"=>"DECIMAL",
 				"NVARCHAR2"=>"VARCHAR",
 				"RAW"=>"BLOB",
 				"VARCHAR2"=>"VARCHAR",
 				"DOUBLE PRECISION"=>"DECIMAL",
 				"\r\nLOGGING"=>"",
 				"\r\nNOCACHE"=>"",
 				"\r\nNOPARALLEL"=>"",
 				" BYTE"=>""}
    @subst['odbc'] = {"LONGCHAR"=>"BLOB"}
    @subst['sqlite'] = {"INTEGER(10)"=>"INTEGER"}
    @subst['mssql'] = {"nvarchar"=>"VARCHAR",
				"nchar"=>"CHAR",
				"int identity"=>"INTEGER",
				"ntext"=>"TEXT",
				"int"=>"INTEGER",
				"image"=>"LONGBLOB",
				"money"=>"FLOAT" }
  end
end

class DBSpecializer
  def initialize(type, source)
    @data_type, @data = type, source
  end
  
  # Right now we can only convert to MySQL
  def convert
    return @data if @data_type != 'mysql'
    new_data = @data.gsub(/VARCHAR\((\d+)\)/) do |match|
      length = $1.to_i
      if length > 255
        'TEXT'
      else
        "VARCHAR(#{length})"
      end
    end
    # We don't really use decimals
    new_data.gsub!('DECIMAL', 'INTEGER')
    # Make primary keys auto-incremented
    new_data.gsub!(' ID INTEGER NOT NULL', ' ID INTEGER NOT NULL AUTO_INCREMENT')
    # Set up primary key and initial auto-increment value
    # FIXME What if our primary key is not ID?
    new_data.gsub!(');', ",\n PRIMARY KEY (`id`)\n) AUTO_INCREMENT=1;")
    new_data
  end
end


if __FILE__ == $0
  from = 'oracle'
  to = 'mysql'
  source_file = 'production_structure.sql'
  output_file = "converted_#{source_file}"
  source = open(source_file) do |f|
    f.readlines.join
  end
  # Convert from oracle to mysql
  standardized = SQLStandardizer.new(from, source).convert
  converted = DBSpecializer.new(to, standardized).convert
  open(output_file, 'w') do |f|
    f << converted
  end
end
Posted at 3pm on 07/08/05 | Posted in , , | no responses | read on

Rails, db_structure_dump, and Oracle

I use Ruby on Rails at work with an Oracle 10g database. The problem is that I have a couple new people who are supposed to begin helping with the project and would like to set up development databases. When I was using MySQL this was easy, I used the db_structure_dump task in rake and gave them the output file.

I tried to do the same with Oracle, but apparently the task and adapters aren’t set up for this. So after some looking around the web I came up with my own solution – to implement structure_dump in the oci_adapter. So if anyone needs this, here’s the quick and very dirty code. It could use some loving and cleaning before it gets submitted as a patch.

Add the following method to the OciAdapter class in active_record/connection_adapters/oci_adapter.rb:

def structure_dump
  select_all("select table_name from user_tables") .inject("") do |structure, table|
    string = "CREATE TABLE #{table.to_a.first.last} (\n "
    array = select_all("select column_name, data_type, data_length, data_precision,
       data_scale, data_default, nullable from all_tab_columns where table_name =
       '#{table.to_a.first.last}' order by column_id")
    
    array.collect! do |hash|
      tmp = "#{hash['column_name']} #{hash['data_type']}"	      
      if hash['data_type'] =='NUMBER' and !hash['data_precision'].nil?
        tmp << "(#{hash['data_precision'].to_i}"
        tmp << ",#{hash['data_scale'].to_i})" if !hash['data_scale'].nil?
        tmp << ')'
      elsif hash['data_type'].include?('CHAR')
        tmp << "(#{hash['data_length'].to_i})"  
      end	      
      tmp << " DEFAULT #{hash['data_default']}" if !hash['data_default'].nil?
      tmp << ' NOT NULL' if hash['nullable'] ="=" 'N'
      tmp	      
    end
    array.uniq!
    array.delete_if {|item| item.nil?}
    string << array.join(",\n ")
    string << ");\n\n"
    structure << string
  end
end

…And add “oci” to the case statement in the Rakefile:

desc "Dump the database structure to a SQL file"
task :db_structure_dump => :environment do
  abcs = ActiveRecord::Base.configurations
  case abcs[RAILS_ENV]["adapter"] 
    when "mysql", "oci"
      ActiveRecord::Base.establish_connection(abcs[RAILS_ENV])
Posted at 7pm on 06/30/05 | Posted in , , | 3 responses | read on