#!/usr/local/bin/ruby
#!/usr/bin/env ruby
#
# $Id: sqlsh.rb,v 1.2 2001/11/17 15:53:30 michael Exp $
# by Michael Neumann
#

require "dbi"
begin
  require "readline"
  $use_readline = true
rescue LoadError
  $use_readline = false
end

require "irb"
require "irb/completion"

$irb_completion = Readline.completion_proc

require "getoptlong"

class ReadlineControl
  
  attr_accessor :keywords

  def initialize
    @keywords = []
    set_prompt
    initCompletion
  end

  def initCompletion
    if $use_readline
      Readline.completion_proc = proc {|str| complete(str) }
    end
  end

  def complete(str)
    @keywords.grep(/^#{Regexp.escape(str)}/i)
  end

  def set_prompt(prompt="> ")
    @prompt = prompt
  end

  def readline
    if $use_readline 
      Readline.readline(@prompt, true)
    else
      print @prompt
      $stdin.readline
    end
  end

end

class Command
  include DBI::SQL::BasicBind

  def readCommand
    line = ""

    $rd.set_prompt(PROMPT)
    begin
      if $input.nil? 
        # no source file to read from
        l = $rd.readline
      else                   
        # source file has still data
        l = $input.gets
        if l.nil?
          $input = nil
          next
        end
      end

      next if l.strip.empty?
      l = l.chomp + "\n"
      line << l 

      puts $file + INPUT + l unless $input.nil?
      $rd.set_prompt(PROMPT_CONT)
    end until complete?(line)

    return line.strip
  end

  private

  def complete?(line)
    line =~ /^\s*\\/ or (tokens(line).last || "") =~ /;\s*$/
  end
end

class Actions
  ACTIONS = [
    [ /^\\q(uit)?\s*$/i,     :quit ],
    [ /^\\h(elp)?\s*$/i,     :help ],
    [ /^\\t(ables)?/i,       :tables ],
    [ /^\\dt/i,              :describeTable ],
    [ /^\\s(elect)?/i,       :select ],

    [ /^\\rb/i,              :ruby ],
    [ /^\\irb/i,             :irb ],

    [ /^\\c(ommit)?\s*$/i,   :commit ],
    [ /^\\r(ollback)?\s*$/i, :rollback ],
    [ /^\\a(utocommit)?(\s+(on|off)?)?\s*$/i, :autocommit ],
    [ /^\\i(nput)?/i,        :input ],
    [ /^\\o(utput)?/i,       :output ],
    [ /^\\pl/i,              :pageLength ],

    [ //,                    :unknownCommand ]
  ] 

  def dispatchCommand(line)
    ACTIONS.each do |regexp, action|
      if line =~ regexp then
        send(action, $~)
        return
      end
    end
  end

  def quit(match)
    puts
    puts "BYE"
    puts

    begin
      Conn.disconnect
    rescue DBI::Error => err
      puts
      puts err.message
      p err.backtrace if $DEBUG
      puts
    end

    exit
  end

  def help(match)
    head = %w(Function Description)
    rows = [
      ["\\h[elp]",     "Display this help screen"],
      ["", ""],

      ["\\t[ables]",   "Display all available tables"],
      ["\\dt table",   "Describe columns of 'table'"],
      ["\\s[elect] table", "short for SELECT * FROM 'table'"],

      ["", ""],
      ["\\c[ommit]",   "Commits the current transaction"],
      ["\\r[ollback]", "Rolls back the current transaction"],
      ["\\a[utocommit]", "Show current autocommit mode"],
      ["\\a[utocommit] on|off", "Switch autocommit mode on/off"],
      ["", ""],

      ["\\i[nput] filename", "Read and execute lines from 'filename'"],
      ["\\o[utput]", "Disable output"],
      ["\\o[utput] filename", "Store SQL statments the user inputs into 'filename'"],
      ["", ""],

      ["\\pl n",   "Set page length to 'n'"],
      ["", ""],
      ["\\rb ...", "Execute the rest of the line as Ruby sourcecode"],
      ["\\irb",    "Execute irb within this context"],

      ["", ""],

      ["\\q[uit]",     "Quit this program"]
    ]

    puts
    puts "Help: "
    output_table(head, rows)
    puts
  end

  def tables(match)
    head = ["Table name"]
    rows = Conn.tables.collect {|name| [name]}

    puts
    puts "Tables: "
    output_table(head, rows)
    puts
  end

  def describeTable(match)
    table = match.post_match.strip

    head = %w(name type_name precision scale default nullable indexed primary unique)

    rows = Conn.columns(table).collect {|col| head.collect{|a| col[a]} }

    puts
    puts "Table '#{table}': "
    output_table(head, rows)
    puts
  end

  def select(match)
    executeSQL("SELECT * FROM #{match.post_match};")
  end

  def commit(match)
    Conn.commit
    puts
    puts "COMMIT"
    puts
  end

  def rollback(match)
    Conn.rollback
    puts
    puts "ROLLBACK"
    puts
  end

  def autocommit(match)
    mode = match[3]
    if mode =~ /on/i
      Conn['AutoCommit'] = true
      puts
      puts "AUTOCOMMIT IS NOW ON"
      puts
    elsif mode =~ /off/i
      Conn['AutoCommit'] = false
      puts
      puts "AUTOCOMMIT IS NOW OFF"
      puts
    else
      puts
      if Conn['AutoCommit'] == true
        puts "AUTOCOMMIT is currently switched ON"
      elsif Conn['AutoCommit'] == false
        puts "AUTOCOMMIT is currently switched OFF"
      else
        puts "AUTOCOMMIT is in unknown state"
      end
      puts 
    end 
  end

  def input(match)
    puts
    $file = match.post_match.strip

    begin
      $input = File.open($file)
      puts "EXECUTE file #{$file}" 
      puts
    rescue
      puts "Couldn't read from file #{$file}"
      puts
    end
  end

  def output(match)
    puts
    file = match.post_match.strip

    if file.empty?
      $output.close if $output
      $output = nil
      puts "Disabled OUTPUT"
      puts
    else
      begin
        $output = File.new(file, "w+")
        puts "Set OUTPUT to file #{file}" 
        puts
      rescue
        puts "Couldn't set OUTPUT to file #{file}"
        puts
      end
    end
  end

  def pageLength(match)
    puts
    $page_len = match.post_match.strip.to_i
    $page_len = DEFAULT_PAGE_LENGTH if $page_len <= 0

    puts "New page length is #{$page_len}."
    puts
  end

  def irb(match)
    Readline.completion_proc = $irb_completion
    puts
    puts "================================== IRB ==============================="
    begin 
      IRB.start 
    rescue SystemExit 
    end
    puts "======================================================================"
    $rd.initCompletion
  end

  def ruby(match)
    puts
    eval match.post_match
    puts
  end

  def unknownCommand(match)
    puts
    puts "Unknown command!"
    puts
  end

end

def output_table(header, rows)
  DBI::Utils::TableFormatter.ascii(header, rows, nil, nil, nil, nil, $page_len) do   
    break if $stdin.readline.chomp == "a"
  end
end

def executeSQL(sql)
  sql = $` if sql =~ /;\s*$/

  start = ::Time.now
  stmt = Conn.execute(sql)

  head = stmt.column_names

  # DDL, DCL
  if head.empty? 
    puts
    nr = stmt.rows
    if nr == 0
      puts "  No rows affected"
    elsif nr == 1
      puts "  1 row affected"
    else 
      puts "  #{nr} rows affected"
    end
    puts
  else
    rows = stmt.fetch_all
    tm = ::Time.now - start

    puts
    output_table(head, rows || [])
    print "  "
    if rows.nil?
      print "No rows in set"
    elsif rows.size == 1
      print "1 row in set"
    else
      print "#{rows.size} rows in set"
    end

    puts " (#{(tm.to_f*1000).to_i / 1000.0} sec)"
          puts
        end

        $rd.keywords = SQL_KEYWORDS + Conn.tables
      end

DEFAULT_PAGE_LENGTH = 37 

$output     = nil
$input      = nil
$page_len   = DEFAULT_PAGE_LENGTH 
PROMPT      = "dbi => "
PROMPT_CONT = "dbi -> "
INPUT       = " >> "

SQL_KEYWORDS = %w(
  INSERT DELETE UPDATE SELECT FROM WHERE IN LIKE SET VALUES INTO
  CREATE TABLE DROP 
  COMMIT ROLLBACK
  CHAR VARCHAR VARCHAR2 INT INTEGER NUMBER FLOAT REAL LONG CLOB BLOB DECIMAL 
  DBCLOB DBBLOB
)

# ---------------------------------------------------------------------------

opts = GetoptLong.new(
  ["--file", "-f", GetoptLong::REQUIRED_ARGUMENT ]
)
opts.each do |opt, arg|
  case opt
  when "--file"
    $input_file_name = arg
  end
end

if ARGV.size < 1 or ARGV.size > 3
  puts
  puts "USAGE: #{$0} [--file file] driver_url [user [password] ]"
  puts

  puts "Available driver and datasources:"
  puts
  for driver in DBI.available_drivers do
    puts driver 
    begin
      ds = DBI.data_sources(driver)
      for datasource in ds
        puts "  " + datasource
      end
    rescue => err
    end
    puts
  end
  puts 

  exit 1
else
  DRIVER_URL = ARGV.shift
  USER       = ARGV.shift
  PASS       = ARGV.shift
end

puts
begin
  Conn = DBI.connect(DRIVER_URL, USER, PASS)
  print "CONNECT TO #{DRIVER_URL} "
  print "USER #{USER} " unless USER.nil?
  print "PASS #{PASS} " unless PASS.nil?
  print "\n"

rescue DBI::Error, DBI::Warning => err
    p err
    exit
end

puts

$rd = ReadlineControl.new
$rd.keywords = SQL_KEYWORDS + Conn.tables

cmd = Command.new
act = Actions.new

# --file option
if $input_file_name
  def $input_file_name.post_match
    $input_file_name
  end
  act.input($input_file_name) 
end

# Main-Loop -----------------------------------

loop do 
  line = cmd.readCommand

  $output.puts line unless $output.nil?

  begin
    if line =~ /^\\/ then
      # Internal Command
      act.dispatchCommand(line)
    else
      # SQL Command
      executeSQL(line)
    end
  rescue DBI::Error => err
    puts
    puts err.message
    p err.backtrace if $DEBUG
    puts
  end
end

