#$Id: rdbc1.rb,v 1.26 2000/01/21 18:38:37 postgres Exp postgres $

=begin

module RDBC1
  #JDBC1 interfaces
  class Driver
  class Connection
  class DatabaseMetaData

  class RsultSet
  class ResultSetMetaData

  class Statement
  class PreparedStatement < Statement
  class CallableStatement < PreparedStatement

  #JDBC1 classes
  class DriverManager
  class DriverPropertyInfo
  class Type
  class Date
  class Time
  class Timestamp

  #JDBC1 exceptions
  class SQLException
  class SQLWarning
  class DataTruncation
=end




module RDBC1

  # How can I make "protected class" ?
  protected
  class Composite
  # A class for Composite-pattern.
    attr :parent
    def initialize(parent)
      #p type.to_s+" initializing.."
      @parent = parent
      @children=[]
      @closed=false
      if @parent
        @parent.add(self)
      end
    end
    def add(c)
      if c != nil
        @children.push c
      end
    end
    def delete(c)
      @children.delete(c)
    end
    def die
      if killchildren #if succeeded...
        if @parent
          @parent.delete(self)
          @parent = nil
          #p to_s + " is dead." #for debug
        end
      end
    end
    def killchildren
      if not canclose?
        raise "cannot close!"
        return false
      end
      #p @children.size
      @children.reverse.each { |c|
        #p c.to_s + " being killed..."
        #c.die
        c.close
      }
      true
    end
    def close
      die
      @closed=true
    end
    #def dead?
    # @parent == nil
    #end
    def closed?
      @closed
    end
    def canclose?
      @children.each { |c|
        if not c.canclose?
          return false
        end
      }
      true
    end
    def showtree(indent=0) #method for debug
      if indent==0; p "----begin----"; end
      p ((" " * indent*2) + inspect)
      @children.each { |c|
        if c; c.showtree(indent+1); end
      }
      if indent==0; p "---- end ----"; end
    end
  end
  public

  # Driver Creator will inherite me.
  class ConnectionBridge < Composite
    def commit; p "commit"; end
    def rollback; p "rollback"; end
    def autocommit; true; end
    def autocommit=(a); end
  end
  class StatementBridge < Composite
    attr :resultset
    attr :updatecount
    def initialize(st)
      if ! st.con.driver.preparable?
        #if st.sql
        # raise SQLException.new "emulated preparation not implemented NOW."
        #end
      end
      super
      @resultset=nil
      @updatecount=-1
      @sql=nil
      if st.con.driver.preparable?
        @sql=st.sql
      end
      p type.to_s+" prepares ["+@sql.to_s+"]"
    end
    def execute(sql)
      p type.to_s+" prepares ["+@sql.to_s+"]"
      p type.to_s+" gets ["+sql.to_s+"]"
      #@resultset=ResultSetBridge.new(self)
      @resultset=parent.con.driver.getResultSetBridgeClass.new(self)
      @updatecount=-1
    end
    def setParam(index, data)
      #nop if I can do nothing.
    end
    def getParam(index)
      #returns nil if I can do nothing.
      nil
    end
  end
  class ResultSetBridge < Composite
    def next
      false
    end
    def getData(col=nil)
      nil
    end
  end
  class ResultSetMetaDataBridge < Composite
    def columncount
      0
    end
    def columnName(index)
      ""
    end
    def columnIndex(name)
    # if your dbms can answer it, you will override this method.
      for i in 0..columncount-1
        if name == columnName(i)
          return i
        end
      end
      nil
    end
  end

  class DriverManager
  end
  protected
  #how can I make protected class method ?
  def DriverManager.makeSet
    if not defined?($DriverManagerSet)
      $DriverManagerSet=[]
      at_exit{
        $DriverManagerSet.dup.each{ |d|
          d.close
        }
      }
    end
  end
  public
  def DriverManager.registerDriver(driver)
    makeSet
    bingo=nil
    #if $DriverManagerSet.index(driver) == nil
    $DriverManagerSet.each{ |d|
      if driver.urlroot == d.urlroot
        bingo=d
        break
      end
    }
    if bingo == nil
      $DriverManagerSet.push driver
    else
      raise SQLException.new("driver already registered. urlroot=" \
        + driver.urlroot)
    end
  end
  def DriverManager.deregisterDriver(driver)
    makeSet
    #if $DriverManagerSet.index(driver) != nil
      $DriverManagerSet.delete driver
    #else
    # raise SQLException.new("not registered driver /url:" + driver.url)
    #end
  end
  def DriverManager.getConnection(url, prop)
    makeSet
    driver=getDriver(url)
    if driver == nil
      raise SQLException.new("no driver for the url [" + url + "]")
    end
    driver.createConnection(url, prop)
  end
  def DriverManager.getDriver(url)
    makeSet
    $DriverManagerSet.each{ |d|
      #if url == d.url
      if d.acceptURL?(url)
        return d
        #break
      end
    }
    nil
  end
  def DriverManager.getDrivers
    makeSet
    $DriverManagerSet.dup
  end
  def DriverManager.getLoginTimeout; 60; end
  def DriverManager.setLoginTimeout(sec); end
  def DriverManager.getLogStream; nil; end
  def DriverManager.setLogStream(stream); end

  class DriverPropertyInfo
  end

  # Driver Creator will inherite me.
  class Driver < Composite
    def urlroot
      "rdbc:"+type.name.split("::")[-1]+":"
      #example: "rdbc:Driver:"
      #Rdbc:Hogehoge://localhost:4333/customer.db
    end;
    def acceptURL?(url)
      if url == nil; return false; end
      false
      #if self.urlroot == url.split("//")[0]
      if self.urlroot == url.split(":")[0..1].join(":") + ":"
        true
      end
    end
    def initialize #(url=nil)
      super nil
      #@url=url
      DriverManager::registerDriver(self)
    end
    def createConnection(url, prop={})
      #prop is Hash
      if acceptURL?(url) != true
        raise SQLException.new("illigal url=["+url.to_s+"]")
      end
      prop2={}
      prop2.update(url2prop(url))
      if prop; prop2.update(prop); end
      a=Connection.new(self, prop2)
      #add(a)
      a
    end
    alias connect createConnection
    def url2prop(url)
      # Driver Creator will override me.
      u=url.split(":")[2..-1].join(":").split("/")
      p url
      if u.empty?; return {}; end
      if (u[0]!="") or (u[1]!="") #or (u[2]=="") or (u[3]=="")
        raise SQLException.new("illigal url=["+url.to_s+"]")
      end
      u2=u[2].split(":")
      {"HOSTNAME", u2[0], "IPPORT", u2[1], "DBNAME", u[3..-1].join("/")}
    end
    #def getMejorVersion; 0; end
    #def getMinorVersion; 0; end
    def getPropertyInfo
      [DriverPropertyInfo.new]
    end
    #def rdbcCompliant?;true;end
    def getConnectionBridgeClass
      # Driver Creator will override me.
      ConnectionBridge
    end
    def getStatementBridgeClass
      # Driver Creator will override me.
      StatementBridge
    end
    def getResultSetBridgeClass
      # Driver Creator will override me.
      ResultSetBridge
    end
    def getResultSetMetaDataBridgeClass
      # Driver Creator will override me.
      ResultSetMetaDataBridge
    end
    def preparable?
      false
    end
    def close
      DriverManager.deregisterDriver(self)
      super
    end
  end

  class Connection < Composite
    attr :driver
    attr :bridge
    attr :prop
    def initialize(driver, prop)
      prop2=prop.dup
      if driver.is_a?(String) #url
        url=driver
        driver=DriverManager.getDriver(url)
        prop2.update(driver.url2prop(url))
      end
      if driver == nil
        raise SQLException.new("no driver for the url [" + url + "]")
      end
      super driver
        @driver = driver
      @prop = prop2
      p @prop
      @bridge = @driver.getConnectionBridgeClass.new(self)
      #add(@bridge)
    end
    def createStatement(sql=nil)
      a=Statement.new(self, sql)
      #add(a)
      a
    end
    def close
      p "connection closing"
      @bridge.close
      @bridge = nil
        @driver = nil
      super
    end
    def commit; @bridge.commit; end
    def rollback; @bridge.rollback; end
    def autocommit; @bridge.autocommit; end
    def autocommit=(a); @bridge.autocommit=(a); end
  end
  def Connection.connect(url, prop={})
    DriverManager.getConnection(url, prop)
  end

  class DatabaseMetaData
  end

  class Statement < Composite
    attr :con
    attr :bridge
    attr :sql
    attr :prepared
    attr :resultset
    attr :last_sql
    def initialize(con, sql=nil)
      super con
        @con = con
      @sql = sql
      @resultset = nil
      @prepared = (sql != nil)
          @bridge = @con.driver.getStatementBridgeClass.new(self)
      #add(@bridge)
      @param = Hash[]
      @param.freeze
    end
    def execute(sql=nil)
      if ( sql == nil ) and ( @sql == nil)
        raise SQLException.new "no sql specified."
      end
      if ( sql != nil ) and ( @sql != nil)
        raise SQLException.new "duplicate sql specified."
      end
      if @resultset; @resultset.close; end
      @resultset=nil
      if con.driver.preparable?
        p "preparable"
        sqlnow=sql
      else
        p "not preparable"
        sqlnow=(if sql then sql else @sql end).dup
        # @param[index]=[data]
        if @sql
          @param.each_pair{|k,v|
            sqlnow<<" "
            sqlnow=sqlnow.split(':\<'+k.to_s+'\>').join("'"+v[0].to_s+"'")
            sqlnow.chop!
          }
        end
      end
      #p sqlnow
      @last_sql = sqlnow.dup.freeze
      @bridge.execute(sqlnow)
      if @bridge.resultset
        @resultset=ResultSet.new(self)
        #add(@resultset)
        #resultset
        true
      elsif @bridge.updatecount >= 0
        #updatecount
        false
      else
        nil
      end
    end
    def executeQuery(sql=nil)
      execute(sql)
      if resultset
        resultset
      else
          raise SQLException.new "no resultset"
      end
    end
    def executeUpdate(sql=nil)
      execute(sql)
      if updatecount>=0
        updatecount
      else
        raise SQLException.new "no updatecount"
      end
    end
    def close
      @bridge.close
      @bridge = nil
      @sql = nil
      @prepared = false
      @param = nil
      @con = nil
      @resultset = nil
      super
    end
    def setParam(index, data)
      if ! prepared
        raise SQLException.new "sql not prepared"
      end
      @bridge.setParam(index, data)
      #bridge may make exception. otherwise do below...
      @param = @param.dup # i.e. unfreeze
      #data may be nil. Hash in OLD ruby can not have nil. Array holds nil (or not nil).
      @param[index]=[data]
      @param.freeze
      if @bridge
        @bridge.setParam(index, data)
      end
    end
    def getParam(index)
      if ! prepared
       raise SQLException.new "sql not prepared"
      end
      #@param[index][0]
      if @bridge
        @bridge.getParam(index. data)
      else
        nil
      end
    end
    def updatecount
      if @bridge
        @bridge.updatecount
      else
        -1
      end
    end
  end

  #class PreparedStatement < Statement; end
  #class CallableStatement < PreparedStatement; end

  class ResultSet < Composite
    attr :statement
    attr :bridge
    attr :metadata
    def initialize(statement)
      super
        @statement = statement
          @bridge = @statement.con.driver.getResultSetBridgeClass.new(self)
      #add(@bridge)
      @metadata = ResultSetMetaData.new(self)
    end
    def next
      @bridge.next
    end
    def getData(col=nil)
      if (col.is_a?(String))
        #p "sorry not implemented NOW"
        idx=metadata.columnIndex(col)
        if idx == nil
          raise SQLException.new "no such columnname"
        end
        @bridge.getData(idx)
      elsif (col.is_a?(Integer))
        @bridge.getData(col)
      elsif (col.is_a?(Array))
        a=[]
        col.each{ |c|
          a.push getData(c)
        }
        a
      elsif (col==nil)
        @bridge.getData(col)
      else
        raise SQLException.new ""
      end
    end
    alias data getData
    #def createMetaData
    # if @metadata
    #   return @metadata
    # end
    # @metadata=ResultSetMetaData.new(self)
    #end
    def close
      if @bridge; @bridge.close; end
      @bridge = nil
      if @metadata; @metadata.close; end
      @metadata = nil
        @statement = nil
      super
    end
  end

  class ResultSetMetaData < Composite
    attr :resultset
    attr :bridge
    def initialize(rs)
      super
      @resultset=rs
          @bridge = rs.statement.con.driver.getResultSetMetaDataBridgeClass.new(self)
    end
    def close
      if @bridge; @bridge.close; end
      @bridge = nil
        @resultset = nil
      super
    end
    def catalogName; ""; end
    def columncount
      @bridge.columncount
    end
    def columnName(index)
      @bridge.columnName(index)
    end
    #def columnDisplaySize(index)
    #end
    #def columnLabel(index)
    #end
    #def columnType(index)
    #end
    #def columnTypeName(index)
    #end
    #def precision(index)
    #end
    #def scale(index)
    #end
    ##def schemaName(index)
    ##end
    #def tableName(index)
    #end
    #def autoIncrement?(index)
    #end
    #def caseSensitive?(index)
    #end
    #def currency?(index)
    #end
    ##def definitelyWritable?(index)
    ##end
    #def nullable?(index)
    #end
    #def searchable?(index)
    #end
    #def readOnly?
    # @bridge.readonly?
    #end
    def columnIndex(name) #JDBC does not have columnIndex.
      @bridge.columnIndex(name)
    end
  end

  #class Type
  #end

  class Date; end
  class Time; end
  class Timestamp; end

  class SQLException < StandardError ; end
  class SQLWarning < SQLException ; end
  class DataTruncation < SQLWarning ; end

  p "RDBC1 loaded"
  Driver.new
end


if $0 == __FILE__ #debug runner
  p "test start"
  include RDBC1

  #You can choice 1 of the 4 lines below.
  #co=dr.connect("rdbc:Driver:", {"USER","u", "PASSWD","p"})
  #co=DriverManager.getConnection("rdbc:Driver:", {"USER","u", "PASSWD","p"})
  co=Connection.connect("rdbc:Driver:////wakkanai", {"USER","u", "PASSWD","p"})
  #co=Connection.new("rdbc:Driver://wakkanai:8090/~/rumoidb/db1", {"USER","u", "PASSWD","p"})

  st=co.createStatement ("select* where :1 = :2 ")
  p st.setParam("1", "wakkanai") #Wakkanai is a city in Hokkaido, Japan.
  p st.setParam("2", "rumoi")    #Rumoi is a city in Hokkaido, Japan.
  st.execute
  rs=st.resultset
  p st.updatecount
  #p st.resultset

  p rs.metadata.columnName 0
  p rs.metadata.columnIndex "aa"
  while rs.next
    #??
  end

  co.showtree #for debug
  co.rollback
  p "test end"
  #co.close
end





