aboutsummaryrefslogblamecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/firebird_adapter.rb
blob: 04f9f754a489e14cb8f16f466866b1303dd69fd7 (plain) (tree)
1
2
3
4
5
6
7




                                                            
                                         
                











                                                                                                        







                                             




                                                                                            
                            
                                                     
                                                                

                                                                                    







                                           

                                                                                                       
 
                                                             
 
                                                                  
                                     
                                                                                       









                                                 

         
                 
                                             

         

                                                                                          


             




                                                     




























                                                                                                                  






                                                                                           












                                                                                          
                                                                   





















                                                                                
                                                                                    

































































                                                                                                         
















                                                                              



































                                                                                


                                                                                               

                                    
                                                                 



                                              



                                









                                                                

                                                 









                                                         

                                                                               
                                                 


            
                                                                        


                           
 














                                                                       
                                                









                                     


                                                                  
                           


                               
                               


                                    
                              
                   



                                                                       






                                                                  
                               


















































                                                                                          
 

                                                                  




























                                                                                               
                                                   
                        
                                                                                              




                                                                                         
                                                                 
                                       
               











                                                        































                                                                              

                                                                                                              




























                                                                                             
                                                                                  



























                                                                                              
                                                                                


                                                    

                                                                 


           
             











                                                        


                                             


                                                            




                      

















































































































                                                                                                                  













                                                                             
# Author: Ken Kunz <kennethkunz@gmail.com>

require 'active_record/connection_adapters/abstract_adapter'

module FireRuby # :nodoc: all
  NON_EXISTENT_DOMAIN_ERROR = "335544569"
  class Database
    def self.db_string_for(config)
      unless config.has_key?(:database)
        raise ArgumentError, "No database specified. Missing argument: database."
      end
      host_string = config.values_at(:host, :service, :port).compact.first(2).join("/") if config[:host]
      [host_string, config[:database]].join(":")
    end

    def self.new_from_config(config)
      db = new db_string_for(config)
      db.character_set = config[:charset]
      return db
    end
  end
end

module ActiveRecord
  class << Base
    def firebird_connection(config) # :nodoc:
      require_library_or_gem 'fireruby'
      unless defined? FireRuby::SQLType
        raise AdapterNotFound,
          'The Firebird adapter requires FireRuby version 0.4.0 or greater; you appear ' <<
          'to be running an older version -- please update FireRuby (gem install fireruby).'
      end
      config.symbolize_keys!
      db = FireRuby::Database.new_from_config(config)
      connection_params = config.values_at(:username, :password)
      connection = db.connect(*connection_params)
      ConnectionAdapters::FirebirdAdapter.new(connection, logger, connection_params)
    end
  end

  module ConnectionAdapters
    class FirebirdColumn < Column # :nodoc:
      VARCHAR_MAX_LENGTH = 32_765
      BLOB_MAX_LENGTH    = 32_767

      def initialize(name, domain, type, sub_type, length, precision, scale, default_source, null_flag)
        @firebird_type = FireRuby::SQLType.to_base_type(type, sub_type).to_s

        super(name.downcase, nil, @firebird_type, !null_flag)

        @default = parse_default(default_source) if default_source
        @limit = decide_limit(length)
        @domain, @sub_type, @precision, @scale = domain, sub_type, precision, scale.abs
      end

      def type
        if @domain =~ /BOOLEAN/
          :boolean
        elsif @type == :binary and @sub_type == 1
          :text
        else
          @type
        end
      end

      def default
        type_cast(decide_default) if @default
      end

      def self.value_to_boolean(value)
        %W(#{FirebirdAdapter.boolean_domain[:true]} true t 1).include? value.to_s.downcase
      end

      private
        def parse_default(default_source)
          default_source =~ /^\s*DEFAULT\s+(.*)\s*$/i
          return $1 unless $1.upcase == "NULL"
        end

        def decide_default
          if @default =~ /^'?(\d*\.?\d+)'?$/ or
             @default =~ /^'(.*)'$/ && [:text, :string, :binary, :boolean].include?(type)
            $1
          else
            firebird_cast_default
          end
        end

        # Submits a _CAST_ query to the database, casting the default value to the specified SQL type.
        # This enables Firebird to provide an actual value when context variables are used as column
        # defaults (such as CURRENT_TIMESTAMP).
        def firebird_cast_default
          sql = "SELECT CAST(#{@default} AS #{column_def}) FROM RDB$DATABASE"
          if connection = Base.active_connections.values.detect { |conn| conn && conn.adapter_name == 'Firebird' }
            connection.execute(sql).to_a.first['CAST']
          else
            raise ConnectionNotEstablished, "No Firebird connections established."
          end
        end

        def decide_limit(length)
          if text? or number?
            length
          elsif @firebird_type == 'BLOB'
            BLOB_MAX_LENGTH
          end
        end

        def column_def
          case @firebird_type
            when 'BLOB'               then "VARCHAR(#{VARCHAR_MAX_LENGTH})"
            when 'CHAR', 'VARCHAR'    then "#{@firebird_type}(#{@limit})"
            when 'NUMERIC', 'DECIMAL' then "#{@firebird_type}(#{@precision},#{@scale.abs})"
            when 'DOUBLE'             then "DOUBLE PRECISION"
            else @firebird_type
          end
        end

        def simplified_type(field_type)
          if field_type == 'TIMESTAMP'
            :datetime
          else
            super
          end
        end
    end

    # The Firebird adapter relies on the FireRuby[http://rubyforge.org/projects/fireruby/]
    # extension, version 0.4.0 or later (available as a gem or from
    # RubyForge[http://rubyforge.org/projects/fireruby/]). FireRuby works with
    # Firebird 1.5.x on Linux, OS X and Win32 platforms.
    #
    # == Usage Notes
    #
    # === Sequence (Generator) Names
    # The Firebird adapter supports the same approach adopted for the Oracle
    # adapter. See ActiveRecord::Base#set_sequence_name for more details.
    #
    # Note that in general there is no need to create a <tt>BEFORE INSERT</tt>
    # trigger corresponding to a Firebird sequence generator when using
    # ActiveRecord. In other words, you don't have to try to make Firebird
    # simulate an <tt>AUTO_INCREMENT</tt> or +IDENTITY+ column. When saving a
    # new record, ActiveRecord pre-fetches the next sequence value for the table
    # and explicitly includes it in the +INSERT+ statement. (Pre-fetching the
    # next primary key value is the only reliable method for the Firebird
    # adapter to report back the +id+ after a successful insert.)
    #
    # === BOOLEAN Domain
    # Firebird 1.5 does not provide a native +BOOLEAN+ type. But you can easily
    # define a +BOOLEAN+ _domain_ for this purpose, e.g.:
    #
    #  CREATE DOMAIN D_BOOLEAN AS SMALLINT CHECK (VALUE IN (0, 1) OR VALUE IS NULL);
    #
    # When the Firebird adapter encounters a column that is based on a domain
    # that includes "BOOLEAN" in the domain name, it will attempt to treat
    # the column as a +BOOLEAN+.
    #
    # By default, the Firebird adapter will assume that the BOOLEAN domain is
    # defined as above.  This can be modified if needed.  For example, if you
    # have a legacy schema with the following +BOOLEAN+ domain defined:
    #
    #  CREATE DOMAIN BOOLEAN AS CHAR(1) CHECK (VALUE IN ('T', 'F'));
    #
    # ...you can add the following line to your <tt>environment.rb</tt> file:
    #
    #  ActiveRecord::ConnectionAdapters::FirebirdAdapter.boolean_domain = { :true => 'T', :false => 'F' }
    #
    # === BLOB Elements
    # The Firebird adapter currently provides only limited support for +BLOB+
    # columns. You cannot currently retrieve or insert a +BLOB+ as an IO stream.
    # When selecting a +BLOB+, the entire element is converted into a String.
    # When inserting or updating a +BLOB+, the entire value is included in-line
    # in the SQL statement, limiting you to values <= 32KB in size.
    #
    # === Column Name Case Semantics
    # Firebird and ActiveRecord have somewhat conflicting case semantics for
    # column names.
    #
    # [*Firebird*]
    #   The standard practice is to use unquoted column names, which can be
    #   thought of as case-insensitive. (In fact, Firebird converts them to
    #   uppercase.) Quoted column names (not typically used) are case-sensitive.
    # [*ActiveRecord*]
    #   Attribute accessors corresponding to column names are case-sensitive.
    #   The defaults for primary key and inheritance columns are lowercase, and
    #   in general, people use lowercase attribute names.
    #
    # In order to map between the differing semantics in a way that conforms
    # to common usage for both Firebird and ActiveRecord, uppercase column names
    # in Firebird are converted to lowercase attribute names in ActiveRecord,
    # and vice-versa. Mixed-case column names retain their case in both
    # directions. Lowercase (quoted) Firebird column names are not supported.
    # This is similar to the solutions adopted by other adapters.
    #
    # In general, the best approach is to use unqouted (case-insensitive) column
    # names in your Firebird DDL (or if you must quote, use uppercase column
    # names). These will correspond to lowercase attributes in ActiveRecord.
    #
    # For example, a Firebird table based on the following DDL:
    #
    #  CREATE TABLE products (
    #    id BIGINT NOT NULL PRIMARY KEY,
    #    "TYPE" VARCHAR(50),
    #    name VARCHAR(255) );
    #
    # ...will correspond to an ActiveRecord model class called +Product+ with
    # the following attributes: +id+, +type+, +name+.
    #
    # ==== Quoting <tt>"TYPE"</tt> and other Firebird reserved words:
    # In ActiveRecord, the default inheritance column name is +type+. The word
    # _type_ is a Firebird reserved word, so it must be quoted in any Firebird
    # SQL statements. Because of the case mapping described above, you should
    # always reference this column using quoted-uppercase syntax
    # (<tt>"TYPE"</tt>) within Firebird DDL or other SQL statements (as in the
    # example above). This holds true for any other Firebird reserved words used
    # as column names as well.
    #
    # === Migrations
    # The Firebird Adapter now supports Migrations.
    #
    # ==== Create/Drop Table and Sequence Generators
    # Creating or dropping a table will automatically create/drop a
    # correpsonding sequence generator, using the default naming convension.
    # You can specify a different name using the <tt>:sequence</tt> option; no
    # generator is created if <tt>:sequence</tt> is set to +false+.
    #
    # ==== Rename Table
    # The Firebird #rename_table Migration should be used with caution.
    # Firebird 1.5 lacks built-in support for this feature, so it is
    # implemented by making a copy of the original table (including column
    # definitions, indexes and data records), and then dropping the original
    # table. Constraints and Triggers are _not_ properly copied, so avoid
    # this method if your original table includes constraints (other than
    # the primary key) or triggers. (Consider manually copying your table
    # or using a view instead.)
    #
    # == Connection Options
    # The following options are supported by the Firebird adapter. None of the
    # options have default values.
    #
    # <tt>:database</tt>::
    #   <i>Required option.</i> Specifies one of: (i) a Firebird database alias;
    #   (ii) the full path of a database file; _or_ (iii) a full Firebird
    #   connection string. <i>Do not specify <tt>:host</tt>, <tt>:service</tt>
    #   or <tt>:port</tt> as separate options when using a full connection
    #   string.</i>
    # <tt>:host</tt>::
    #   Set to <tt>"remote.host.name"</tt> for remote database connections.
    #   May be omitted for local connections if a full database path is
    #   specified for <tt>:database</tt>. Some platforms require a value of
    #   <tt>"localhost"</tt> for local connections when using a Firebird
    #   database _alias_.
    # <tt>:service</tt>::
    #   Specifies a service name for the connection. Only used if <tt>:host</tt>
    #   is provided. Required when connecting to a non-standard service.
    # <tt>:port</tt>::
    #   Specifies the connection port. Only used if <tt>:host</tt> is provided
    #   and <tt>:service</tt> is not. Required when connecting to a non-standard
    #   port and <tt>:service</tt> is not defined.
    # <tt>:username</tt>::
    #   Specifies the database user. May be omitted or set to +nil+ (together
    #   with <tt>:password</tt>) to use the underlying operating system user
    #   credentials on supported platforms.
    # <tt>:password</tt>::
    #   Specifies the database password. Must be provided if <tt>:username</tt>
    #   is explicitly specified; should be omitted if OS user credentials are
    #   are being used.
    # <tt>:charset</tt>::
    #   Specifies the character set to be used by the connection. Refer to
    #   Firebird documentation for valid options.
    class FirebirdAdapter < AbstractAdapter
      TEMP_COLUMN_NAME = 'AR$TEMP_COLUMN'

      @@boolean_domain = { :name => "d_boolean", :type => "smallint", :true => 1, :false => 0 }
      cattr_accessor :boolean_domain

      def initialize(connection, logger, connection_params = nil)
        super(connection, logger)
        @connection_params = connection_params
      end

      def adapter_name # :nodoc:
        'Firebird'
      end

      def supports_migrations? # :nodoc:
        true
      end

      def native_database_types # :nodoc:
        {
          :primary_key => "BIGINT NOT NULL PRIMARY KEY",
          :string      => { :name => "varchar", :limit => 255 },
          :text        => { :name => "blob sub_type text" },
          :integer     => { :name => "bigint" },
          :decimal     => { :name => "decimal" },
          :numeric     => { :name => "numeric" },
          :float       => { :name => "float" },
          :datetime    => { :name => "timestamp" },
          :timestamp   => { :name => "timestamp" },
          :time        => { :name => "time" },
          :date        => { :name => "date" },
          :binary      => { :name => "blob sub_type 0" },
          :boolean     => boolean_domain
        }
      end

      # Returns true for Firebird adapter (since Firebird requires primary key
      # values to be pre-fetched before insert). See also #next_sequence_value.
      def prefetch_primary_key?(table_name = nil)
        true
      end

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


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

      def quote(value, column = nil) # :nodoc:
        if [Time, DateTime].include?(value.class)
          "CAST('#{value.strftime("%Y-%m-%d %H:%M:%S")}' AS TIMESTAMP)"
        else
          super
        end
      end

      def quote_string(string) # :nodoc:
        string.gsub(/'/, "''")
      end

      def quote_column_name(column_name) # :nodoc:
        %Q("#{ar_to_fb_case(column_name.to_s)}")
      end

      def quoted_true # :nodoc:
        quote(boolean_domain[:true])
      end

      def quoted_false # :nodoc:
        quote(boolean_domain[:false])
      end


      # CONNECTION MANAGEMENT ====================================

      def active? # :nodoc:
        not @connection.closed?
      end

      def disconnect! # :nodoc:
        @connection.close rescue nil
      end

      def reconnect! # :nodoc:
        disconnect!
        @connection = @connection.database.connect(*@connection_params)
      end


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

      def select_all(sql, name = nil) # :nodoc:
        select(sql, name)
      end

      def select_one(sql, name = nil) # :nodoc:
        select(sql, name).first
      end

      def execute(sql, name = nil, &block) # :nodoc:
        log(sql, name) do
          if @transaction
            @connection.execute(sql, @transaction, &block)
          else
            @connection.execute_immediate(sql, &block)
          end
        end
      end

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

      alias_method :update, :execute
      alias_method :delete, :execute

      def begin_db_transaction() # :nodoc:
        @transaction = @connection.start_transaction
      end

      def commit_db_transaction() # :nodoc:
        @transaction.commit
      ensure
        @transaction = nil
      end

      def rollback_db_transaction() # :nodoc:
        @transaction.rollback
      ensure
        @transaction = nil
      end

      def add_limit_offset!(sql, options) # :nodoc:
        if options[:limit]
          limit_string = "FIRST #{options[:limit]}"
          limit_string << " SKIP #{options[:offset]}" if options[:offset]
          sql.sub!(/\A(\s*SELECT\s)/i, '\&' + limit_string + ' ')
        end
      end

      # Returns the next sequence value from a sequence generator. Not generally
      # called directly; used by ActiveRecord to get the next primary key value
      # when inserting a new database record (see #prefetch_primary_key?).
      def next_sequence_value(sequence_name)
        FireRuby::Generator.new(sequence_name, @connection).next(1)
      end


      # SCHEMA STATEMENTS ========================================

      def current_database # :nodoc:
        file = @connection.database.file.split(':').last
        File.basename(file, '.*')
      end

      def recreate_database! # :nodoc:
        sql = "SELECT rdb$character_set_name FROM rdb$database"
        charset = execute(sql).to_a.first[0].rstrip
        disconnect!
        @connection.database.drop(*@connection_params)
        FireRuby::Database.create(@connection.database.file,
          @connection_params[0], @connection_params[1], 4096, charset)
      end

      def tables(name = nil) # :nodoc:
        sql = "SELECT rdb$relation_name FROM rdb$relations WHERE rdb$system_flag = 0"
        execute(sql, name).collect { |row| row[0].rstrip.downcase }
      end

      def indexes(table_name, name = nil) # :nodoc:
        index_metadata(table_name, false, name).inject([]) do |indexes, row|
          if indexes.empty? or indexes.last.name != row[0]
            indexes << IndexDefinition.new(table_name, row[0].rstrip.downcase, row[1] == 1, [])
          end
          indexes.last.columns << row[2].rstrip.downcase
          indexes
        end
      end

      def columns(table_name, name = nil) # :nodoc:
        sql = <<-end_sql
          SELECT r.rdb$field_name, r.rdb$field_source, f.rdb$field_type, f.rdb$field_sub_type,
                 f.rdb$field_length, f.rdb$field_precision, f.rdb$field_scale,
                 COALESCE(r.rdb$default_source, f.rdb$default_source) rdb$default_source,
                 COALESCE(r.rdb$null_flag, f.rdb$null_flag) rdb$null_flag
          FROM rdb$relation_fields r
          JOIN rdb$fields f ON r.rdb$field_source = f.rdb$field_name
          WHERE r.rdb$relation_name = '#{table_name.to_s.upcase}'
          ORDER BY r.rdb$field_position
        end_sql
        execute(sql, name).collect do |field|
          field_values = field.values.collect do |value|
            case value
              when String         then value.rstrip
              when FireRuby::Blob then value.to_s
              else value
            end
          end
          FirebirdColumn.new(*field_values)
        end
      end

      def create_table(name, options = {}) # :nodoc:
        begin
          super
        rescue StatementInvalid
          raise unless non_existent_domain_error?
          create_boolean_domain
          super
        end
        unless options[:id] == false or options[:sequence] == false
          sequence_name = options[:sequence] || default_sequence_name(name)
          create_sequence(sequence_name)
        end
      end

      def drop_table(name, options = {}) # :nodoc:
        super(name)
        unless options[:sequence] == false
          sequence_name = options[:sequence] || default_sequence_name(name)
          drop_sequence(sequence_name) if sequence_exists?(sequence_name)
        end
      end

      def add_column(table_name, column_name, type, options = {}) # :nodoc:
        super
      rescue StatementInvalid
        raise unless non_existent_domain_error?
        create_boolean_domain
        super
      end

      def change_column(table_name, column_name, type, options = {}) # :nodoc:
        change_column_type(table_name, column_name, type, options)
        change_column_position(table_name, column_name, options[:position]) if options.include?(:position)
        change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
      end

      def change_column_default(table_name, column_name, default) # :nodoc:
        table_name = table_name.to_s.upcase
        sql = <<-end_sql
          UPDATE rdb$relation_fields f1
          SET f1.rdb$default_source =
                (SELECT f2.rdb$default_source FROM rdb$relation_fields f2
                 WHERE f2.rdb$relation_name = '#{table_name}'
                 AND f2.rdb$field_name = '#{TEMP_COLUMN_NAME}'),
              f1.rdb$default_value =
                (SELECT f2.rdb$default_value FROM rdb$relation_fields f2
                 WHERE f2.rdb$relation_name = '#{table_name}'
                 AND f2.rdb$field_name = '#{TEMP_COLUMN_NAME}')
          WHERE f1.rdb$relation_name = '#{table_name}'
          AND f1.rdb$field_name = '#{ar_to_fb_case(column_name.to_s)}'
        end_sql
        transaction do
          add_column(table_name, TEMP_COLUMN_NAME, :string, :default => default)
          execute sql
          remove_column(table_name, TEMP_COLUMN_NAME)
        end
      end

      def rename_column(table_name, column_name, new_column_name) # :nodoc:
        execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TO #{new_column_name}"
      end

      def remove_index(table_name, options) #:nodoc:
        execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
      end

      def rename_table(name, new_name) # :nodoc:
        if table_has_constraints_or_dependencies?(name)
          raise ActiveRecordError,
            "Table #{name} includes constraints or dependencies that are not supported by " <<
            "the Firebird rename_table migration. Try explicitly removing the constraints/" <<
            "dependencies first, or manually renaming the table."
        end

        transaction do
          copy_table(name, new_name)
          copy_table_indexes(name, new_name)
        end
        begin
          copy_table_data(name, new_name)
          copy_sequence_value(name, new_name)
        rescue
          drop_table(new_name)
          raise
        end
        drop_table(name)
      end

      def dump_schema_information # :nodoc:
        super << ";\n"
      end

      def type_to_sql(type, limit = nil, precision = nil, scale = nil) # :nodoc:
        case type
          when :integer then integer_sql_type(limit)
          when :float   then float_sql_type(limit)
          when :string  then super(type, limit, precision, scale)
          else super(type, limit, precision, scale)
        end
      end

      private
        def integer_sql_type(limit)
          case limit
            when (1..2) then 'smallint'
            when (3..4) then 'integer'
            else 'bigint'
          end
        end

        def float_sql_type(limit)
          limit.to_i <= 4 ? 'float' : 'double precision'
        end

        def select(sql, name = nil)
          execute(sql, name).collect do |row|
            hashed_row = {}
            row.each do |column, value|
              value = value.to_s if FireRuby::Blob === value
              hashed_row[fb_to_ar_case(column)] = value
            end
            hashed_row
          end
        end

        def primary_key(table_name)
          if pk_row = index_metadata(table_name, true).to_a.first
            pk_row[2].rstrip.downcase
          end
        end

        def index_metadata(table_name, pk, name = nil)
          sql = <<-end_sql
            SELECT i.rdb$index_name, i.rdb$unique_flag, s.rdb$field_name
            FROM rdb$indices i
            JOIN rdb$index_segments s ON i.rdb$index_name = s.rdb$index_name
            LEFT JOIN rdb$relation_constraints c ON i.rdb$index_name = c.rdb$index_name
            WHERE i.rdb$relation_name = '#{table_name.to_s.upcase}'
          end_sql
          if pk
            sql << "AND c.rdb$constraint_type = 'PRIMARY KEY'\n"
          else
            sql << "AND (c.rdb$constraint_type IS NULL OR c.rdb$constraint_type != 'PRIMARY KEY')\n"
          end
          sql << "ORDER BY i.rdb$index_name, s.rdb$field_position\n"
          execute sql, name
        end

        def change_column_type(table_name, column_name, type, options = {})
          sql = "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TYPE #{type_to_sql(type, options[:limit])}"
          execute sql
        rescue StatementInvalid
          raise unless non_existent_domain_error?
          create_boolean_domain
          execute sql
        end

        def change_column_position(table_name, column_name, position)
          execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} POSITION #{position}"
        end

        def copy_table(from, to)
          table_opts = {}
          if pk = primary_key(from)
            table_opts[:primary_key] = pk
          else
            table_opts[:id] = false
          end
          create_table(to, table_opts) do |table|
            from_columns = columns(from).reject { |col| col.name == table_opts[:primary_key] }
            from_columns.each do |column|
              col_opts = [:limit, :default, :null].inject({}) { |opts, opt| opts.merge(opt => column.send(opt)) }
              table.column column.name, column.type, col_opts
            end
          end
        end

        def copy_table_indexes(from, to)
          indexes(from).each do |index|
            unless index.name[from.to_s]
              raise ActiveRecordError,
                "Cannot rename index #{index.name}, because the index name does not include " <<
                "the original table name (#{from}). Try explicitly removing the index on the " <<
                "original table and re-adding it on the new (renamed) table."
            end
            options = {}
            options[:name] = index.name.gsub(from.to_s, to.to_s)
            options[:unique] = index.unique
            add_index(to, index.columns, options)
          end
        end

        def copy_table_data(from, to)
          execute "INSERT INTO #{to} SELECT * FROM #{from}", "Copy #{from} data to #{to}"
        end

        def copy_sequence_value(from, to)
          sequence_value = FireRuby::Generator.new(default_sequence_name(from), @connection).last
          execute "SET GENERATOR #{default_sequence_name(to)} TO #{sequence_value}"
        end

        def sequence_exists?(sequence_name)
          FireRuby::Generator.exists?(sequence_name, @connection)
        end

        def create_sequence(sequence_name)
          FireRuby::Generator.create(sequence_name.to_s, @connection)
        end

        def drop_sequence(sequence_name)
          FireRuby::Generator.new(sequence_name.to_s, @connection).drop
        end

        def create_boolean_domain
          sql = <<-end_sql
            CREATE DOMAIN #{boolean_domain[:name]} AS #{boolean_domain[:type]}
            CHECK (VALUE IN (#{quoted_true}, #{quoted_false}) OR VALUE IS NULL)
          end_sql
          execute sql rescue nil
        end

        def table_has_constraints_or_dependencies?(table_name)
          table_name = table_name.to_s.upcase
          sql = <<-end_sql
            SELECT 1 FROM rdb$relation_constraints
            WHERE rdb$relation_name = '#{table_name}'
            AND rdb$constraint_type IN ('UNIQUE', 'FOREIGN KEY', 'CHECK')
            UNION
            SELECT 1 FROM rdb$dependencies
            WHERE rdb$depended_on_name = '#{table_name}'
            AND rdb$depended_on_type = 0
          end_sql
          !select(sql).empty?
        end

        def non_existent_domain_error?
          $!.message.include? FireRuby::NON_EXISTENT_DOMAIN_ERROR
        end

        # Maps uppercase Firebird column names to lowercase for ActiveRecord;
        # mixed-case columns retain their original case.
        def fb_to_ar_case(column_name)
          column_name =~ /[[:lower:]]/ ? column_name : column_name.downcase
        end

        # Maps lowercase ActiveRecord column names to uppercase for Fierbird;
        # mixed-case columns retain their original case.
        def ar_to_fb_case(column_name)
          column_name =~ /[[:upper:]]/ ? column_name : column_name.upcase
        end
    end
  end
end