aboutsummaryrefslogblamecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb
blob: 43c7dc1ed818a789ea7935cdd1a9792f9599e31e (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12











                                                                                       









                                                                                 











                                                                                                                                    

         






                                                          
                                                           



                                                            



                                                                                










                                                                                        
                                                                                   

                                       
     
                                         
                                           

                                                            


                      
 

                               
                                                                         



                                                              
                                                 







                                                   
 
                              


            


                                                 
 


                                                                  
 








                                                 
 
                                                                  
 


                                                                     





                                                                                             



               
 


                     
 


                      
 



                                                                  
                                                  








                                                                  
              
                                
             



                                                                 

           





                                                                                         
 



                                                   



                                                   




                                            
 
















                                          
 
 
                                                                  



                                                                






                                                                               
                                                    
                                                         


                                                                                                         



               







                                                                               







                                                                                                     
                                                                                          



               


















































































                                                                                                                                                             


                                   
















                                                                        
             
 

                                      



                                                                             
                 
                         



               



                                         


                                          



                                                           

                                                      

                                              
                                    
 

                                                      



                                                                                          
                      
           










                                                                                               





                                        












                                                                                                       
               
             



                                   








                                                                             

             






                                                                                                                                                             














                                                                                                     
                                                                              
           


         
require 'active_record/connection_adapters/abstract_adapter'

module ActiveRecord
  class Base
    # Establishes a connection to the database that's used by all Active Record objects
    def self.openbase_connection(config) # :nodoc:
      require_library_or_gem 'openbase' unless self.class.const_defined?(:OpenBase)

      config = config.symbolize_keys
      host     = config[:host]
      username = config[:username].to_s
      password = config[:password].to_s

      if config.has_key?(:database)
        database = config[:database]
      else
        raise ArgumentError, "No database specified. Missing argument: database."
      end

      oba = ConnectionAdapters::OpenBaseAdapter.new(
        OpenBase.new(database, host, username, password), logger
      )

      if oba.raw_connection.connected?
        unless oba.tables.include?(ConnectionAdapters::OpenBaseAdapter::COLUMN_SUPPORT_TABLE)
          oba.execute(<<-SQL,"Creating OpenBase Column Support Table")
          CREATE TABLE #{ConnectionAdapters::OpenBaseAdapter::COLUMN_SUPPORT_TABLE} (name char, type char, precision int, scale int)
          SQL
        end
        oba.select_all("SELECT * FROM #{ConnectionAdapters::OpenBaseAdapter::COLUMN_SUPPORT_TABLE}").each do |col|
          ConnectionAdapters::OpenBaseAdapter::DECIMAL_COLUMNS.store(col["name"],[col["precision"],col["scale"]])
        end
      end

      oba
    end
  end

  module ConnectionAdapters
    class OpenBaseColumn < Column #:nodoc:
      private
        def simplified_type(field_type)
          return :integer if field_type.downcase =~ /long/
          return :decimal if field_type.downcase == "money"
          return :binary  if field_type.downcase == "object"
          super
        end
    end

    # The OpenBase adapter works with the Ruby/Openbase driver by Derrick Spell,
    # provided with the distribution of OpenBase 10.0.6 and later
    # http://www.openbase.com
    #
    # Options:
    #
    # * <tt>:host</tt> -- Defaults to localhost
    # * <tt>:username</tt> -- Defaults to nothing
    # * <tt>:password</tt> -- Defaults to nothing
    # * <tt>:database</tt> -- The name of the database. No default, must be provided.
    #
    # The OpenBase adapter will make use of OpenBase's ability to generate unique ids
    # for any column with an unique index applied.  Thus, if the value of a primary
    # key is not specified at the time an INSERT is performed, the adapter will prefetch
    # a unique id for the primary key.  This prefetching is also necessary in order
    # to return the id after an insert.
    #
    #
    # Maintainer: derrick.spell@gmail.com
    class OpenBaseAdapter < AbstractAdapter
      DECIMAL_COLUMNS = {}
      COLUMN_SUPPORT_TABLE = "rails_openbase_column_support"
      def adapter_name
        'OpenBase'
      end

      def native_database_types
        {
          :primary_key => "integer NOT NULL UNIQUE INDEX DEFAULT _rowid",
          :string      => { :name => "char", :limit => 4096 },
          :text        => { :name => "text" },
          :integer     => { :name => "integer" },
          :float       => { :name => "float" },
          :decimal     => { :name => "decimal" },
          :datetime    => { :name => "datetime" },
          :timestamp   => { :name => "timestamp" },
          :time        => { :name => "time" },
          :date        => { :name => "date" },
          :binary      => { :name => "object" },
          :boolean     => { :name => "boolean" }
        }
      end

      def supports_migrations?
        true
      end

      def prefetch_primary_key?(table_name = nil)
        true
      end

      def default_sequence_name(table_name, primary_key) # :nodoc:
        "#{table_name} #{primary_key}"
      end

      def next_sequence_value(sequence_name)
        ary = sequence_name.split(' ')
        if (!ary[1]) then
          ary[0] =~ /(\w+)_nonstd_seq/
          ary[0] = $1
        end
        @connection.unique_row_id(ary[0], ary[1])
      end


      # QUOTING ==================================================

      def quote(value, column = nil)
        if value.kind_of?(String) && column && column.type == :binary
          "'#{@connection.insert_binary(value)}'"
        elsif value.kind_of?(BigDecimal)
          return "'#{value.to_s}'"
        elsif column && column.type == :integer && column.sql_type =~ /decimal/
          return "'#{value.to_s}'"
        elsif [Float,Fixnum,Bignum].include?(value.class) && column && column.type == :string
          return "'#{value.to_s}'"
        else
          super
        end
      end

      def quoted_true
        "1"
      end

      def quoted_false
        "0"
      end



      # DATABASE STATEMENTS ======================================

      def add_limit_offset!(sql, options) #:nodoc:
        return if options[:limit].nil?
        limit = options[:limit]
        offset = options[:offset]
        if limit == 0
          # Mess with the where clause to ensure we get no results
          if sql =~ /WHERE/i
            sql.sub!(/WHERE/i, 'WHERE 1 = 2 AND ')
          elsif sql =~ /ORDER\s+BY/i
            sql.sub!(/ORDER\s+BY/i, 'WHERE 1 = 2 ORDER BY')
          else
            sql << 'WHERE 1 = 2'
          end
        elsif offset.nil?
          sql << " RETURN RESULTS #{limit}"
        else
          sql << " RETURN RESULTS #{offset} TO #{limit + offset}"
        end
      end

      def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
        execute(sql, name)
        update_nulls_after_insert(sql, name, pk, id_value, sequence_name)
        id_value
      end

      def execute(sql, name = nil) #:nodoc:
        log(sql, name) { @connection.execute(sql) }
      end

      def direct_execute(sql, name = nil) #:nodoc:
        log(sql, name) { @connection.execute(sql) }
      end

      def update(sql, name = nil) #:nodoc:
        execute(sql, name).rows_affected
      end

      alias_method :delete, :update #:nodoc:

      def begin_db_transaction #:nodoc:
        execute "START TRANSACTION"
      rescue Exception
        # Transactions aren't supported
      end

      def commit_db_transaction #:nodoc:
        execute "COMMIT"
      rescue Exception
        # Transactions aren't supported
      end

      def rollback_db_transaction #:nodoc:
        execute "ROLLBACK"
      rescue Exception
        # Transactions aren't supported
      end


      # SCHEMA STATEMENTS ========================================
      # Return the list of all tables in the schema search path.
      def tables(name = nil) #:nodoc:
        tables = @connection.tables
        tables.reject! { |t| /\A_SYS_/ === t }
      end

      def columns(table_name, name = nil) #:nodoc:
        sql = "SELECT * FROM _sys_tables "
        sql << "WHERE tablename='#{table_name}' AND INDEXOF(fieldname,'_')<>0 "
        sql << "ORDER BY columnNumber"
        columns = []
        direct_execute(sql, name).each_hash do |row|
          columns << OpenBaseColumn.new(row["fieldname"],
                                default_value(row["defaultvalue"],row["typename"]),
                                sql_type_name(table_name,row["fieldname"],row["typename"],row["length"]),
                                row["notnull"] == 1 ? false : true)
        end
        columns
      end

      def column_names(table_name) #:nodoc:
        sql = "SELECT fieldname FROM _sys_tables "
        sql << "WHERE tablename='#{table_name}' AND INDEXOF(fieldname,'_')<>0 "
        sql << "ORDER BY columnNumber"
        names = direct_execute(sql).fetch_all
        names.flatten! || names
      end

      def indexes(table_name, name = nil)#:nodoc:
        sql = "SELECT fieldname, notnull, searchindex, uniqueindex, clusteredindex FROM _sys_tables "
        sql << "WHERE tablename='#{table_name}' AND INDEXOF(fieldname,'_')<>0 "
        sql << "AND primarykey=0 "
        sql << "AND (searchindex=1 OR uniqueindex=1 OR clusteredindex=1) "
        sql << "ORDER BY columnNumber"
        indexes = []
        execute(sql, name).each do |row|
          indexes << IndexDefinition.new(table_name,ob_index_name(row),row[3]==1,[row[0]])
        end
        indexes
      end

      def create_table(name, options = {}) #:nodoc:
        return_value = super

        # Get my own copy of TableDefinition so that i can detect decimal columns
        table_definition = TableDefinition.new(self)
        yield table_definition

        table_definition.columns.each do |col|
          if col.type == :decimal
            record_decimal(name, col.name, col.precision, col.scale)
          end
        end

        unless options[:id] == false
          primary_key = (options[:primary_key] || "id")
          direct_execute("CREATE PRIMARY KEY #{name} (#{primary_key})")
        end
        return_value
      end

      def rename_table(name, new_name)
        execute "RENAME #{name} #{new_name}"
      end

      def add_column(table_name, column_name, type, options = {})
        return_value = super(table_name, "COLUMN " + column_name.to_s, type, options)
        if type == :decimal
          record_decimal(table_name, column_name, options[:precision], options[:scale])
        end
      end

      def remove_column(table_name, column_name)
        execute "ALTER TABLE #{table_name} REMOVE COLUMN #{quote_column_name(column_name)}"
      end

      def rename_column(table_name, column_name, new_column_name)
        execute "ALTER TABLE #{table_name} RENAME #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
      end

      def add_column_options!(sql, options) #:nodoc:
        sql << " NOT NULL" if options[:null] == false
        sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
      end

      def change_column(table_name, column_name, type, options = {}) #:nodoc:
        unless options_include_default?(options)
          options[:default] = select_one("SELECT * FROM _sys_tables WHERE tablename='#{table_name}' AND fieldname='#{column_name}'")["defaultvalue"]
        end

        change_column_sql = "ALTER TABLE #{table_name} ADD COLUMN #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
        add_column_options!(change_column_sql, options)
        execute(change_column_sql)
      end

      def change_column_default(table_name, column_name, default)
        execute "ALTER TABLE #{table_name} COLUMN #{column_name} SET DEFAULT #{quote(default)}"
      end

      def add_index(table_name, column_name, options = {})
        if Hash === options # legacy support, since this param was a string
          index_type = options[:unique] ? "UNIQUE" : ""
        else
          index_type = options
        end
        execute "CREATE #{index_type} INDEX #{table_name} #{column_name}"
      end

      def remove_index(table_name, options = {})
        execute "DROP INDEX #{table_name} #{options === Hash ? options[:column] : options}"
      end

      def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
        return super unless type.to_s == 'decimal'

        if (scale.to_i == 2)
          return 'money'
        elsif (scale.to_i == 0)
          return 'longlong'
        else
          return "char(#{precision.to_i + 1})"
        end
      end


      private
        def select(sql, name = nil)
          decimals = detect_decimals(sql) || []
          sql = add_order_by_rowid(sql)

          # OpenBase ignores the return results when there is a group by
          # so limit the result set that we return to rails if need be
          if (sql =~ /GROUP BY/i)
            sql.sub!(/RETURN RESULTS (\d+)( TO (\d+))?/i,"")

            results = execute(sql, name)
            if ($2)
              results.fetch_offset = $1.to_i
              results.fetch_limit = $3.to_i - $1.to_i
            elsif ($1)
              results.fetch_limit = $1.to_i
            end
          else
            results = execute(sql, name)
          end

          rows = []
          if ( results.rows_affected )
            results.each_hash do |row|  # loop through result rows
              row.delete("_rowid") if row.key?("_rowid")
              decimals.each do |name, precision, scale|
                row[name] = BigDecimal.new(row[name]) if row[name] === String
              end
              rows << row
            end
          end
          rows
        end

        def default_value(value,type=nil)
          return value if value.nil?

          # Boolean type values
          return true if value =~ /true/
          return false if value =~ /false/
          # Alternative boolean default declarations
          return true if (value == 1 && type == "boolean")
          return false if (value == 0 && type == "boolean")

          # Date / Time magic values
          return Time.now.to_s if value =~ /^now\(\)/i

          # Empty strings should be set to nil
          return nil if value.empty?

          # Otherwise return what we got from OpenBase
          # and hope for the best...
          # Take off the leading space and unquote
          value.lstrip!
          value = value[1,value.length-2] if value.first.eql?("'") && value.last.eql?("'")
          return nil if value.eql?("NULL")
          return value
        end

        def sql_type_name(table_name, col_name, type, length)
          full_name = table_name.to_s + "." + col_name.to_s
          if DECIMAL_COLUMNS.include?(full_name) && type != "longlong"
            return "decimal(#{DECIMAL_COLUMNS[full_name][0]},#{DECIMAL_COLUMNS[full_name][1]})"
          end
          return "#{type}(#{length})" if ( type =~ /char/ )
          type
        end

        def ob_index_name(row = [])
          name = ""
          name << "UNIQUE " if row[3]
          name << "CLUSTERED " if row[4]
          name << "INDEX"
          name
        end

        def detect_decimals(sql)
          # Detect any decimal columns that will need to be cast when fetched
          decimals = []
          sql =~ /SELECT\s+(.*)\s+FROM\s+(\w+)/i
          select_clause = $1
          main_table = $2
          if select_clause == "*"
            column_names(main_table).each do |col|
              full_name = main_table + "." + col
              if DECIMAL_COLUMNS.include?(full_name)
                decimals << [col,DECIMAL_COLUMNS[full_name][0].to_i,DECIMAL_COLUMNS[full_name][1].to_i]
              end
            end
          end
          return decimals
        end

        def add_order_by_rowid(sql)
          # ORDER BY _rowid if no explicit ORDER BY
          # This will ensure that find(:first) returns the first inserted row
          if (sql !~ /(ORDER BY)|(GROUP BY)/)
            if (sql =~ /RETURN RESULTS/)
              sql.sub!(/RETURN RESULTS/,"ORDER BY _rowid RETURN RESULTS")
            else
              sql << " ORDER BY _rowid"
            end
          end
          sql
        end

        def record_decimal(table_name, column_name, precision, scale)
          full_name = table_name.to_s + "." + column_name.to_s
          DECIMAL_COLUMNS.store(full_name, [precision.to_i,scale.to_i])
          direct_execute("INSERT INTO #{COLUMN_SUPPORT_TABLE} (name,type,precision,scale) VALUES ('#{full_name}','decimal',#{precision.to_i},#{scale.to_i})")
        end

        def update_nulls_after_insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
          sql =~ /INSERT INTO (\w+) \((.*)\) VALUES\s*\((.*)\)/m
          table = $1
          cols = $2
          values = $3
          cols = cols.split(',')
          values.gsub!(/'[^']*'/,"''")
          values.gsub!(/"[^"]*"/,"\"\"")
          values = values.split(',')
          update_cols = []
          values.each_index { |index| update_cols << cols[index] if values[index] =~ /\s*NULL\s*/ }
          update_sql = "UPDATE #{table} SET"
          update_cols.each { |col| update_sql << " #{col}=NULL," unless col.empty? }
          update_sql.chop!()
          update_sql << " WHERE #{pk}=#{quote(id_value)}"
          direct_execute(update_sql,"Null Correction") if update_cols.size > 0
        end
      end
  end
end