# oracle_enhanced_adapter.rb -- ActiveRecord adapter for Oracle 8i, 9i, 10g, 11g
#
# Authors or original oracle_adapter: Graham Jenkins, Michael Schoen
#
# Current maintainer: Raimonds Simanovskis (http://blog.rayapps.com)
#
#########################################################################
#
# See History.md for changes added to original oracle_adapter.rb
#
#########################################################################
#
# From original oracle_adapter.rb:
#
# Implementation notes:
# 1. Redefines (safely) a method in ActiveRecord to make it possible to
#    implement an autonumbering solution for Oracle.
# 2. The OCI8 driver is patched to properly handle values for LONG and
#    TIMESTAMP columns. The driver-author has indicated that a future
#    release of the driver will obviate this patch.
# 3. LOB support is implemented through an after_save callback.
# 4. Oracle does not offer native LIMIT and OFFSET options; this
#    functionality is mimiced through the use of nested selects.
#    See http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:127412348064
#
# Do what you want with this code, at your own peril, but if any
# significant portion of my code remains then please acknowledge my
# contribution.
# portions Copyright 2005 Graham Jenkins

require "active_record/connection_adapters/abstract_adapter"
require "active_record/connection_adapters/statement_pool"
require "active_record/connection_adapters/oracle_enhanced/connection"
require "active_record/connection_adapters/oracle_enhanced/database_statements"
require "active_record/connection_adapters/oracle_enhanced/schema_statements"
require "active_record/connection_adapters/oracle_enhanced/schema_statements_ext"
require "active_record/connection_adapters/oracle_enhanced/column_dumper"
require "active_record/connection_adapters/oracle_enhanced/context_index"
require "active_record/connection_adapters/oracle_enhanced/column"
require "active_record/connection_adapters/oracle_enhanced/quoting"

require "digest/sha1"

ActiveRecord::Base.class_eval do
  class_attribute :custom_create_method, :custom_update_method, :custom_delete_method
end

module ActiveRecord
  class Base
    def self.lob_columns
      columns.select do |column|
        column.respond_to?(:lob?) && column.lob?
      end
    end

    # After setting large objects to empty, select the OCI8::LOB
    # and write back the data.
    before_update :record_changed_lobs
    after_update :enhanced_write_lobs

    private

      def enhanced_write_lobs
        if self.class.connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter) &&
            !(
              (self.class.custom_create_method || self.class.custom_create_method) ||
              (self.class.custom_update_method || self.class.custom_update_method)
            )
          self.class.connection.write_lobs(self.class.table_name, self.class, attributes, @changed_lob_columns)
        end
      end

      def record_changed_lobs
        @changed_lob_columns = self.class.lob_columns.select do |col|
          self.attribute_changed?(col.name) && !self.class.readonly_attributes.to_a.include?(col.name)
        end
      end
  end
end

module ActiveRecord
  module ConnectionHandling #:nodoc:
    # Establishes a connection to the database that's used by all Active Record objects.
    def oracle_enhanced_connection(config) #:nodoc:
      if config[:emulate_oracle_adapter] == true
        # allows the enhanced adapter to look like the OracleAdapter. Useful to pick up
        # conditionals in the rails activerecord test suite
        require "active_record/connection_adapters/emulation/oracle_adapter"
        ConnectionAdapters::OracleAdapter.new(
          ConnectionAdapters::OracleEnhancedConnection.create(config), logger, config)
      else
        ConnectionAdapters::OracleEnhancedAdapter.new(
          ConnectionAdapters::OracleEnhancedConnection.create(config), logger, config)
      end
    end
  end

  module ConnectionAdapters #:nodoc:
    # Oracle enhanced adapter will work with both
    # Ruby 1.8/1.9 ruby-oci8 gem (which provides interface to Oracle OCI client)
    # or with JRuby and Oracle JDBC driver.
    #
    # It should work with Oracle 9i, 10g and 11g databases.
    # Limited set of functionality should work on Oracle 8i as well but several features
    # rely on newer functionality in Oracle database.
    #
    # Usage notes:
    # * Key generation assumes a "${table_name}_seq" sequence is available
    #   for all tables; the sequence name can be changed using
    #   ActiveRecord::Base.set_sequence_name. When using Migrations, these
    #   sequences are created automatically.
    #   Use set_sequence_name :autogenerated with legacy tables that have
    #   triggers that populate primary keys automatically.
    # * Oracle uses DATE or TIMESTAMP datatypes for both dates and times.
    #   Consequently some hacks are employed to map data back to Date or Time
    #   in Ruby. Timezones and sub-second precision on timestamps are
    #   not supported.
    # * Default values that are functions (such as "SYSDATE") are not
    #   supported. This is a restriction of the way ActiveRecord supports
    #   default values.
    #
    # Required parameters:
    #
    # * <tt>:username</tt>
    # * <tt>:password</tt>
    # * <tt>:database</tt> - either TNS alias or connection string for OCI client or database name in JDBC connection string
    #
    # Optional parameters:
    #
    # * <tt>:host</tt> - host name for JDBC connection, defaults to "localhost"
    # * <tt>:port</tt> - port number for JDBC connection, defaults to 1521
    # * <tt>:privilege</tt> - set "SYSDBA" if you want to connect with this privilege
    # * <tt>:allow_concurrency</tt> - set to "true" if non-blocking mode should be enabled (just for OCI client)
    # * <tt>:prefetch_rows</tt> - how many rows should be fetched at one time to increase performance, defaults to 100
    # * <tt>:cursor_sharing</tt> - cursor sharing mode to minimize amount of unique statements, defaults to "force"
    # * <tt>:time_zone</tt> - database session time zone
    #   (it is recommended to set it using ENV['TZ'] which will be then also used for database session time zone)
    #
    # Optionals NLS parameters:
    #
    # * <tt>:nls_calendar</tt>
    # * <tt>:nls_comp</tt>
    # * <tt>:nls_currency</tt>
    # * <tt>:nls_date_format</tt> - format for :date columns, defaults to <tt>YYYY-MM-DD HH24:MI:SS</tt>
    # * <tt>:nls_date_language</tt>
    # * <tt>:nls_dual_currency</tt>
    # * <tt>:nls_iso_currency</tt>
    # * <tt>:nls_language</tt>
    # * <tt>:nls_length_semantics</tt> - semantics of size of VARCHAR2 and CHAR columns, defaults to <tt>CHAR</tt>
    #   (meaning that size specifies number of characters and not bytes)
    # * <tt>:nls_nchar_conv_excp</tt>
    # * <tt>:nls_numeric_characters</tt>
    # * <tt>:nls_sort</tt>
    # * <tt>:nls_territory</tt>
    # * <tt>:nls_timestamp_format</tt> - format for :timestamp columns, defaults to <tt>YYYY-MM-DD HH24:MI:SS:FF6</tt>
    # * <tt>:nls_timestamp_tz_format</tt>
    # * <tt>:nls_time_format</tt>
    # * <tt>:nls_time_tz_format</tt>
    #
    class OracleEnhancedAdapter < AbstractAdapter
      # TODO: Use relative
      include ActiveRecord::ConnectionAdapters::OracleEnhanced::DatabaseStatements
      include ActiveRecord::ConnectionAdapters::OracleEnhanced::SchemaStatements
      include ActiveRecord::ConnectionAdapters::OracleEnhanced::SchemaStatementsExt
      include ActiveRecord::ConnectionAdapters::OracleEnhanced::ColumnDumper
      include ActiveRecord::ConnectionAdapters::OracleEnhanced::ContextIndex
      include ActiveRecord::ConnectionAdapters::OracleEnhanced::Quoting

      def schema_creation
        OracleEnhanced::SchemaCreation.new self
      end

      ##
      # :singleton-method:
      # By default, the OracleEnhancedAdapter will consider all columns of type <tt>NUMBER(1)</tt>
      # as boolean. If you wish to disable this emulation you can add the following line
      # to your initializer file:
      #
      #   ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans = false
      cattr_accessor :emulate_booleans
      self.emulate_booleans = true

      ##
      # :singleton-method:
      # OracleEnhancedAdapter will use the default tablespace, but if you want specific types of
      # objects to go into specific tablespaces, specify them like this in an initializer:
      #
      #   ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces =
      #  {:clob => 'TS_LOB', :blob => 'TS_LOB', :index => 'TS_INDEX', :table => 'TS_DATA'}
      #
      # Using the :tablespace option where available (e.g create_table) will take precedence
      # over these settings.
      cattr_accessor :default_tablespaces
      self.default_tablespaces = {}

      ##
      # :singleton-method:
      # If you wish that CHAR(1), VARCHAR2(1) columns or VARCHAR2 columns with FLAG or YN at the end of their name
      # are typecasted to booleans then you can add the following line to your initializer file:
      #
      #   ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true
      cattr_accessor :emulate_booleans_from_strings
      self.emulate_booleans_from_strings = false

      ##
      # :singleton-method:
      # By default, OracleEnhanced adapter will use Oracle12 visitor
      # if database version is Oracle 12.1.
      # If you wish to use Oracle visitor which is intended to work with Oracle 11.2 or lower
      # for Oracle 12.1 database you can add the following line to your initializer file:
      #
      #   ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.use_old_oracle_visitor = true
      cattr_accessor :use_old_oracle_visitor
      self.use_old_oracle_visitor = false

      class StatementPool < ConnectionAdapters::StatementPool
        private

          def dealloc(stmt)
            stmt.close
          end
      end

      def initialize(connection, logger = nil, config = {}) # :nodoc:
        super(connection, logger, config)
        @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
        @enable_dbms_output = false
      end

      ADAPTER_NAME = "OracleEnhanced".freeze

      def adapter_name #:nodoc:
        ADAPTER_NAME
      end

      def arel_visitor # :nodoc:
        if supports_fetch_first_n_rows_and_offset?
          Arel::Visitors::Oracle12.new(self)
        else
          Arel::Visitors::Oracle.new(self)
        end
      end

      def supports_savepoints? #:nodoc:
        true
      end

      def supports_transaction_isolation? #:nodoc:
        true
      end

      def supports_foreign_keys?
        true
      end

      def supports_foreign_keys_in_create?
        supports_foreign_keys?
      end

      def supports_views?
        true
      end

      def supports_fetch_first_n_rows_and_offset?
        if !use_old_oracle_visitor && @connection.database_version.first >= 12
          true
        else
          false
        end
      end

      def supports_datetime_with_precision?
        true
      end

      def supports_comments?
        true
      end

      def supports_multi_insert?
        @connection.database_version.to_s >= [11, 2].to_s
      end

      def supports_virtual_columns?
        @connection.database_version.first >= 11
      end

      def supports_json?
        # No migration supported for :json type due to there is no `JSON` data type
        # in Oracle Database itself.
        #
        # 1.Define :string or :text in migration
        #
        # create_table :test_posts, force: true do |t|
        #   t.string  :title
        #   t.text    :article
        # end
        #
        # 2. Set :json attributes
        #
        # class TestPost < ActiveRecord::Base
        #  attribute :title, :json
        #  attribute :article, :json
        # end
        #
        # 3. Add `is json` database constraints by running sql statements
        #
        # alter table test_posts add constraint test_posts_title_is_json check (title is json)
        # alter table test_posts add constraint test_posts_article_is_json check (article is json)
        #
        @connection.database_version.first >= 12
      end

      #:stopdoc:
      DEFAULT_NLS_PARAMETERS = {
        nls_calendar: nil,
        nls_comp: nil,
        nls_currency: nil,
        nls_date_format: "YYYY-MM-DD HH24:MI:SS",
        nls_date_language: nil,
        nls_dual_currency: nil,
        nls_iso_currency: nil,
        nls_language: nil,
        nls_length_semantics: "CHAR",
        nls_nchar_conv_excp: nil,
        nls_numeric_characters: nil,
        nls_sort: nil,
        nls_territory: nil,
        nls_timestamp_format: "YYYY-MM-DD HH24:MI:SS:FF6",
        nls_timestamp_tz_format: nil,
        nls_time_format: nil,
        nls_time_tz_format: nil
      }

      #:stopdoc:
      NATIVE_DATABASE_TYPES = {
        primary_key: "NUMBER(38) NOT NULL PRIMARY KEY",
        string: { name: "VARCHAR2", limit: 255 },
        text: { name: "CLOB" },
        integer: { name: "NUMBER", limit: 38 },
        float: { name: "BINARY_FLOAT" },
        decimal: { name: "DECIMAL" },
        datetime: { name: "TIMESTAMP" },
        timestamp: { name: "TIMESTAMP" },
        time: { name: "TIMESTAMP" },
        date: { name: "DATE" },
        binary: { name: "BLOB" },
        boolean: { name: "NUMBER", limit: 1 },
        raw: { name: "RAW", limit: 2000 },
        bigint: { name: "NUMBER", limit: 19 }
      }
      # if emulate_booleans_from_strings then store booleans in VARCHAR2
      NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS = NATIVE_DATABASE_TYPES.dup.merge(
        boolean: { name: "VARCHAR2", limit: 1 }
      )
      #:startdoc:

      def native_database_types #:nodoc:
        emulate_booleans_from_strings ? NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS : NATIVE_DATABASE_TYPES
      end

      # maximum length of Oracle identifiers
      IDENTIFIER_MAX_LENGTH = 30

      def table_alias_length #:nodoc:
        IDENTIFIER_MAX_LENGTH
      end

      # the maximum length of a table name
      def table_name_length
        IDENTIFIER_MAX_LENGTH
      end

      # the maximum length of a column name
      def column_name_length
        IDENTIFIER_MAX_LENGTH
      end

      # Returns the maximum allowed length for an index name. This
      # limit is enforced by rails and Is less than or equal to
      # <tt>index_name_length</tt>. The gap between
      # <tt>index_name_length</tt> is to allow internal rails
      # opreations to use prefixes in temporary opreations.
      def allowed_index_name_length
        index_name_length
      end

      # the maximum length of an index name
      # supported by this database
      def index_name_length
        IDENTIFIER_MAX_LENGTH
      end

      # the maximum length of a sequence name
      def sequence_name_length
        IDENTIFIER_MAX_LENGTH
      end

      # To avoid ORA-01795: maximum number of expressions in a list is 1000
      # tell ActiveRecord to limit us to 1000 ids at a time
      def in_clause_length
        1000
      end

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

      # If SQL statement fails due to lost connection then reconnect
      # and retry SQL statement if autocommit mode is enabled.
      # By default this functionality is disabled.
      attr_reader :auto_retry #:nodoc:
      @auto_retry = false

      def auto_retry=(value) #:nodoc:
        @auto_retry = value
        @connection.auto_retry = value if @connection
      end

      # return raw OCI8 or JDBC connection
      def raw_connection
        @connection.raw_connection
      end

      # Returns true if the connection is active.
      def active? #:nodoc:
        # Pings the connection to check if it's still good. Note that an
        # #active? method is also available, but that simply returns the
        # last known state, which isn't good enough if the connection has
        # gone stale since the last use.
        @connection.ping
      rescue OracleEnhancedConnectionException
        false
      end

      # Reconnects to the database.
      def reconnect! #:nodoc:
        super
        @connection.reset!
      rescue OracleEnhancedConnectionException => e
        @logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}" if @logger
      end

      def reset!
        clear_cache!
        super
      end

      # Disconnects from the database.
      def disconnect! #:nodoc:
        super
        @connection.logoff rescue nil
      end

      # use in set_sequence_name to avoid fetching primary key value from sequence
      AUTOGENERATED_SEQUENCE_NAME = "autogenerated".freeze

      # 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)
        # if sequence_name is set to :autogenerated then it means that primary key will be populated by trigger
        return nil if sequence_name == AUTOGENERATED_SEQUENCE_NAME
        # call directly connection method to avoid prepared statement which causes fetching of next sequence value twice
        @connection.select_value("SELECT #{quote_table_name(sequence_name)}.NEXTVAL FROM dual")
      end

      @@do_not_prefetch_primary_key = {}

      # Returns true for Oracle adapter (since Oracle requires primary key
      # values to be pre-fetched before insert). See also #next_sequence_value.
      def prefetch_primary_key?(table_name = nil)
        return true if table_name.nil?
        table_name = table_name.to_s
        do_not_prefetch = @@do_not_prefetch_primary_key[table_name]
        if do_not_prefetch.nil?
          owner, desc_table_name, db_link = @connection.describe(table_name)
          @@do_not_prefetch_primary_key[table_name] = do_not_prefetch =
            !has_primary_key?(table_name, owner, desc_table_name, db_link) ||
            has_primary_key_trigger?(table_name, owner, desc_table_name, db_link)
        end
        !do_not_prefetch
      end

      # used just in tests to clear prefetch primary key flag for all tables
      def clear_prefetch_primary_key #:nodoc:
        @@do_not_prefetch_primary_key = {}
      end

      def reset_pk_sequence!(table_name, primary_key = nil, sequence_name = nil) #:nodoc:
        return nil unless data_source_exists?(table_name)
        unless primary_key && sequence_name
          # *Note*: Only primary key is implemented - sequence will be nil.
          primary_key, sequence_name = pk_and_sequence_for(table_name)
          # TODO This sequence_name implemantation is just enough
          # to satisty fixures. To get correct sequence_name always
          # pk_and_sequence_for method needs some work.
          begin
            sequence_name = table_name.classify.constantize.sequence_name
          rescue
            sequence_name = default_sequence_name(table_name)
          end
        end

        if @logger && primary_key && !sequence_name
          @logger.warn "#{table_name} has primary key #{primary_key} with no default sequence"
        end

        if primary_key && sequence_name
          new_start_value = select_value("
            select NVL(max(#{quote_column_name(primary_key)}),0) + 1 from #{quote_table_name(table_name)}
          ", new_start_value)

          execute "DROP SEQUENCE #{quote_table_name(sequence_name)}"
          execute "CREATE SEQUENCE #{quote_table_name(sequence_name)} START WITH #{new_start_value}"
        end
      end

      # Writes LOB values from attributes for specified columns
      def write_lobs(table_name, klass, attributes, columns) #:nodoc:
        id = quote(attributes[klass.primary_key])
        columns.each do |col|
          value = attributes[col.name]
          # changed sequence of next two lines - should check if value is nil before converting to yaml
          next if value.blank?
          if klass.attribute_types[col.name].is_a? ActiveRecord::Type::Serialized
            value = klass.attribute_types[col.name].serialize(value)
          end
          uncached do
            sql = "SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} WHERE #{quote_column_name(klass.primary_key)} = #{id} FOR UPDATE"
            unless lob_record = select_one(sql, "Writable Large Object")
              raise ActiveRecord::RecordNotFound, "statement #{sql} returned no rows"
            end
            lob = lob_record[col.name]
            @connection.write_lob(lob, value.to_s, col.type == :binary)
          end
        end
      end

      # Current database name
      def current_database
        select_value("SELECT SYS_CONTEXT('userenv', 'con_name') FROM dual")
      rescue ActiveRecord::StatementInvalid
        select_value("SELECT SYS_CONTEXT('userenv', 'db_name') FROM dual")
      end

      # Current database session user
      def current_user
        select_value("SELECT SYS_CONTEXT('userenv', 'session_user') FROM dual")
      end

      # Current database session schema
      def current_schema
        select_value("SELECT SYS_CONTEXT('userenv', 'current_schema') FROM dual")
      end

      # Default tablespace name of current user
      def default_tablespace
        select_value("SELECT LOWER(default_tablespace) FROM user_users WHERE username = SYS_CONTEXT('userenv', 'current_schema')")
      end

      def tables #:nodoc:
        select_values(<<-SQL, "SCHEMA")
          SELECT DECODE(table_name, UPPER(table_name), LOWER(table_name), table_name)
          FROM all_tables WHERE owner = SYS_CONTEXT('userenv', 'current_schema') AND secondary = 'N'
        SQL
      end

      def data_sources
        super
      end

      def table_exists?(table_name)
        table_name = table_name.to_s
        if table_name.include?("@")
          # db link is not table
          false
        else
          default_owner = current_schema
        end
        real_name = ActiveRecord::ConnectionAdapters::OracleEnhanced::Quoting.valid_table_name?(table_name) ?
          table_name.upcase : table_name
        if real_name.include?(".")
          table_owner, table_name = real_name.split(".")
        else
          table_owner, table_name = default_owner, real_name
        end
        select_values(<<-SQL, "SCHEMA").any?
          SELECT owner, table_name
          FROM all_tables
          WHERE owner = '#{table_owner}'
          AND table_name = q'[#{table_name}]'
        SQL
      end

      # Needs to consider how to support synonyms in Rails 5.1
      def data_source_exists?(table_name)
        (_owner, table_name, _db_link) = @connection.describe(table_name)
        true
      rescue
        false
      end

      def views # :nodoc:
        select_values("SELECT LOWER(view_name) FROM all_views WHERE owner = SYS_CONTEXT('userenv', 'current_schema')")
      end

      def materialized_views #:nodoc:
        select_values("SELECT LOWER(mview_name) FROM all_mviews WHERE owner = SYS_CONTEXT('userenv', 'current_schema')")
      end

      cattr_accessor :all_schema_indexes #:nodoc:

      # This method selects all indexes at once, and caches them in a class variable.
      # Subsequent index calls get them from the variable, without going to the DB.
      def indexes(table_name, name = nil) #:nodoc:
        (owner, table_name, db_link) = @connection.describe(table_name)
        unless all_schema_indexes
          default_tablespace_name = default_tablespace
          result = select_all(<<-SQL.strip.gsub(/\s+/, " "))
            SELECT LOWER(i.table_name) AS table_name, LOWER(i.index_name) AS index_name, i.uniqueness,
              i.index_type, i.ityp_owner, i.ityp_name, i.parameters,
              LOWER(i.tablespace_name) AS tablespace_name,
              LOWER(c.column_name) AS column_name, e.column_expression,
              atc.virtual_column
            FROM all_indexes#{db_link} i
              JOIN all_ind_columns#{db_link} c ON c.index_name = i.index_name AND c.index_owner = i.owner
              LEFT OUTER JOIN all_ind_expressions#{db_link} e ON e.index_name = i.index_name AND
                e.index_owner = i.owner AND e.column_position = c.column_position
              LEFT OUTER JOIN all_tab_cols#{db_link} atc ON i.table_name = atc.table_name AND
                c.column_name = atc.column_name AND i.owner = atc.owner AND atc.hidden_column = 'NO'
            WHERE i.owner = '#{owner}'
               AND i.table_owner = '#{owner}'
               AND NOT EXISTS (SELECT uc.index_name FROM all_constraints uc
                WHERE uc.index_name = i.index_name AND uc.owner = i.owner AND uc.constraint_type = 'P')
            ORDER BY i.index_name, c.column_position
          SQL

          current_index = nil
          self.all_schema_indexes = []

          result.each do |row|
            # have to keep track of indexes because above query returns dups
            # there is probably a better query we could figure out
            if current_index != row["index_name"]
              statement_parameters = nil
              if row["index_type"] == "DOMAIN" && row["ityp_owner"] == "CTXSYS" && row["ityp_name"] == "CONTEXT"
                procedure_name = default_datastore_procedure(row["index_name"])
                source = select_values(<<-SQL).join
                  SELECT text
                  FROM all_source#{db_link}
                  WHERE owner = '#{owner}'
                    AND name = '#{procedure_name.upcase}'
                  ORDER BY line
                SQL
                if source =~ /-- add_context_index_parameters (.+)\n/
                  statement_parameters = $1
                end
              end
              all_schema_indexes << OracleEnhanced::IndexDefinition.new(
                row["table_name"],
                row["index_name"],
                row["uniqueness"] == "UNIQUE",
                [],
                nil,
                nil,
                nil,
                row["index_type"] == "DOMAIN" ? "#{row['ityp_owner']}.#{row['ityp_name']}" : nil,
                nil,
                row["parameters"],
                statement_parameters,
                row["tablespace_name"] == default_tablespace_name ? nil : row["tablespace_name"])
              current_index = row["index_name"]
            end

            # Functional index columns and virtual columns both get stored as column expressions,
            # but re-creating a virtual column index as an expression (instead of using the virtual column's name)
            # results in a ORA-54018 error.  Thus, we only want the column expression value returned
            # when the column is not virtual.
            if row["column_expression"] && row["virtual_column"] != "YES"
              all_schema_indexes.last.columns << row["column_expression"]
            else
              all_schema_indexes.last.columns << row["column_name"].downcase
            end
          end
        end

        # Return the indexes just for the requested table, since AR is structured that way
        table_name = table_name.downcase
        all_schema_indexes.select { |i| i.table == table_name }
      end

      # check if table has primary key trigger with _pkt suffix
      def has_primary_key_trigger?(table_name, owner = nil, desc_table_name = nil, db_link = nil)
        (owner, desc_table_name, db_link) = @connection.describe(table_name) unless owner

        trigger_name = default_trigger_name(table_name).upcase
        pkt_sql = <<-SQL
          SELECT trigger_name
          FROM all_triggers#{db_link}
          WHERE owner = '#{owner}'
            AND trigger_name = q'[#{trigger_name}]'
            AND table_owner = '#{owner}'
            AND table_name = q'[#{desc_table_name}]'
            AND status = 'ENABLED'
        SQL
        select_value(pkt_sql, "Primary Key Trigger") ? true : false
      end

      ##
      # :singleton-method:
      # Cache column description between requests.
      # Could be used in development environment to avoid selecting table columns from data dictionary tables for each request.
      # This can speed up request processing in development mode if development database is not on local computer.
      #
      #   ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.cache_columns = true
      cattr_accessor :cache_columns
      self.cache_columns = false

      def columns(table_name, name = nil) #:nodoc:
        if @@cache_columns
          @@columns_cache ||= {}
          @@columns_cache[table_name] ||= columns_without_cache(table_name, name)
        else
          columns_without_cache(table_name, name)
        end
      end

      def columns_without_cache(table_name, name = nil) #:nodoc:
        table_name = table_name.to_s

        (owner, desc_table_name, db_link) = @connection.describe(table_name)

        # reset do_not_prefetch_primary_key cache for this table
        @@do_not_prefetch_primary_key[table_name] = nil

        table_cols = <<-SQL.strip.gsub(/\s+/, " ")
          SELECT cols.column_name AS name, cols.data_type AS sql_type,
                 cols.data_default, cols.nullable, cols.virtual_column, cols.hidden_column,
                 cols.data_type_owner AS sql_type_owner,
                 DECODE(cols.data_type, 'NUMBER', data_precision,
                                   'FLOAT', data_precision,
                                   'VARCHAR2', DECODE(char_used, 'C', char_length, data_length),
                                   'RAW', DECODE(char_used, 'C', char_length, data_length),
                                   'CHAR', DECODE(char_used, 'C', char_length, data_length),
                                    NULL) AS limit,
                 DECODE(data_type, 'NUMBER', data_scale, NULL) AS scale,
                 comments.comments as column_comment
            FROM all_tab_cols#{db_link} cols, all_col_comments#{db_link} comments
           WHERE cols.owner      = '#{owner}'
             AND cols.table_name = #{quote(desc_table_name)}
             AND cols.hidden_column = 'NO'
             AND cols.owner = comments.owner
             AND cols.table_name = comments.table_name
             AND cols.column_name = comments.column_name
           ORDER BY cols.column_id
        SQL

        # added deletion of ignored columns
        select_all(table_cols, name).to_a.map do |row|
          limit, scale = row["limit"], row["scale"]
          if limit || scale
            row["sql_type"] += "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")")
          end

          if row["sql_type_owner"]
            row["sql_type"] = row["sql_type_owner"] + "." + row["sql_type"]
          end

          is_virtual = row["virtual_column"] == "YES"

          # clean up odd default spacing from Oracle
          if row["data_default"] && !is_virtual
            row["data_default"].sub!(/^(.*?)\s*$/, '\1')

            # If a default contains a newline these cleanup regexes need to
            # match newlines.
            row["data_default"].sub!(/^'(.*)'$/m, '\1')
            row["data_default"] = nil if row["data_default"] =~ /^(null|empty_[bc]lob\(\))$/i
            # TODO: Needs better fix to fallback "N" to false
            row["data_default"] = false if (row["data_default"] == "N" && OracleEnhancedAdapter.emulate_booleans_from_strings)
          end

          type_metadata = fetch_type_metadata(row["sql_type"])
          new_column(oracle_downcase(row["name"]),
                           row["data_default"],
                           type_metadata,
                           row["nullable"] == "Y",
                           table_name,
                           is_virtual,
                           false,
                           row["column_comment"]
                    )
        end
      end

      def new_column(name, default, sql_type_metadata = nil, null = true, table_name = nil, virtual = false, returning_id = false, comment = nil) # :nodoc:
        OracleEnhancedColumn.new(name, default, sql_type_metadata, null, table_name, virtual, returning_id, comment)
      end

      # used just in tests to clear column cache
      def clear_columns_cache #:nodoc:
        @@columns_cache = nil
        @@pk_and_sequence_for_cache = nil
      end

      # used in migrations to clear column cache for specified table
      def clear_table_columns_cache(table_name)
        if @@cache_columns
          @@columns_cache ||= {}
          @@columns_cache[table_name.to_s] = nil
        end
      end

      ##
      # :singleton-method:
      # Specify default sequence start with value (by default 10000 if not explicitly set), e.g.:
      #
      #   ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_sequence_start_value = 1
      cattr_accessor :default_sequence_start_value
      self.default_sequence_start_value = 10000

      # Find a table's primary key and sequence.
      # *Note*: Only primary key is implemented - sequence will be nil.
      def pk_and_sequence_for(table_name, owner = nil, desc_table_name = nil, db_link = nil) #:nodoc:
        if @@cache_columns
          @@pk_and_sequence_for_cache ||= {}
          if @@pk_and_sequence_for_cache.key?(table_name)
            @@pk_and_sequence_for_cache[table_name]
          else
            @@pk_and_sequence_for_cache[table_name] = pk_and_sequence_for_without_cache(table_name, owner, desc_table_name, db_link)
          end
        else
          pk_and_sequence_for_without_cache(table_name, owner, desc_table_name, db_link)
        end
      end

      def pk_and_sequence_for_without_cache(table_name, owner = nil, desc_table_name = nil, db_link = nil) #:nodoc:
        (owner, desc_table_name, db_link) = @connection.describe(table_name) unless owner

        seqs = select_values(<<-SQL.strip.gsub(/\s+/, " "), "Sequence")
          select us.sequence_name
          from all_sequences#{db_link} us
          where us.sequence_owner = '#{owner}'
          and us.sequence_name = upper(#{quote(default_sequence_name(desc_table_name))})
        SQL

        # changed back from user_constraints to all_constraints for consistency
        pks = select_values(<<-SQL.strip.gsub(/\s+/, " "), "Primary Key")
          SELECT cc.column_name
            FROM all_constraints#{db_link} c, all_cons_columns#{db_link} cc
           WHERE c.owner = '#{owner}'
             AND c.table_name = #{quote(desc_table_name)}
             AND c.constraint_type = 'P'
             AND cc.owner = c.owner
             AND cc.constraint_name = c.constraint_name
        SQL

        warn <<-WARNING.strip_heredoc if pks.count > 1
          WARNING: Active Record does not support composite primary key.

          #{table_name} has composite primary key. Composite primary key is ignored.
        WARNING

        # only support single column keys
        pks.size == 1 ? [oracle_downcase(pks.first),
                         oracle_downcase(seqs.first)] : nil
      end

      # Returns just a table's primary key
      def primary_key(table_name)
        pk_and_sequence = pk_and_sequence_for(table_name)
        pk_and_sequence && pk_and_sequence.first
      end

      def has_primary_key?(table_name, owner = nil, desc_table_name = nil, db_link = nil) #:nodoc:
        !pk_and_sequence_for(table_name, owner, desc_table_name, db_link).nil?
      end

      def primary_keys(table_name) # :nodoc:
        (owner, desc_table_name, db_link) = @connection.describe(table_name) unless owner

        pks = select_values(<<-SQL.strip_heredoc, "Primary Keys")
          SELECT cc.column_name
            FROM all_constraints#{db_link} c, all_cons_columns#{db_link} cc
           WHERE c.owner = '#{owner}'
             AND c.table_name = '#{desc_table_name}'
             AND c.constraint_type = 'P'
             AND cc.owner = c.owner
             AND cc.constraint_name = c.constraint_name
             order by cc.position
        SQL
        pks.map { |pk| oracle_downcase(pk) }
      end

      def columns_for_distinct(columns, orders) #:nodoc:
        # construct a valid columns name for DISTINCT clause,
        # ie. one that includes the ORDER BY columns, using FIRST_VALUE such that
        # the inclusion of these columns doesn't invalidate the DISTINCT
        #
        # It does not construct DISTINCT clause. Just return column names for distinct.
        order_columns = orders.reject(&:blank?).map { |s|
            s = s.to_sql unless s.is_a?(String)
            # remove any ASC/DESC modifiers
            s.gsub(/\s+(ASC|DESC)\s*?/i, "")
          }.reject(&:blank?).map.with_index { |column, i|
            "FIRST_VALUE(#{column}) OVER (PARTITION BY #{columns} ORDER BY #{column}) AS alias_#{i}__"
          }
        [super, *order_columns].join(", ")
      end

      def temporary_table?(table_name) #:nodoc:
        select_value("SELECT temporary FROM all_tables WHERE table_name = '#{table_name.upcase}' and owner = SYS_CONTEXT('userenv', 'session_user')") == "Y"
      end

      def combine_bind_parameters(
        from_clause: [],
        join_clause: [],
        where_clause: [],
        having_clause: [],
        limit: nil,
        offset: nil
      ) # :nodoc:
        result = from_clause + join_clause + where_clause + having_clause
        if RUBY_ENGINE == "jruby" && !supports_fetch_first_n_rows_and_offset? && offset && limit
          result << offset
          result << limit
          result << offset
        else
          if offset
            result << offset
          end
          if limit
            result << limit
          end
        end
        result
      end

      protected

        def initialize_type_map(m)
          super
          # oracle
          register_class_with_precision m, %r(ZONE)i,  ActiveRecord::OracleEnhanced::Type::TimestampTz
          register_class_with_limit m, %r(raw)i,            ActiveRecord::OracleEnhanced::Type::Raw
          register_class_with_limit m, %r(char)i,           ActiveRecord::OracleEnhanced::Type::String
          register_class_with_limit m, %r(clob)i,           ActiveRecord::OracleEnhanced::Type::Text

          m.register_type "NCHAR", ActiveRecord::OracleEnhanced::Type::NationalCharacterString.new
          m.alias_type %r(NVARCHAR2)i,    "NCHAR"

          m.register_type(%r(NUMBER)i) do |sql_type|
            scale = extract_scale(sql_type)
            precision = extract_precision(sql_type)
            limit = extract_limit(sql_type)
            if scale == 0
              ActiveRecord::OracleEnhanced::Type::Integer.new(precision: precision, limit: limit)
            else
              Type::Decimal.new(precision: precision, scale: scale)
            end
          end

          if OracleEnhancedAdapter.emulate_booleans
            if OracleEnhancedAdapter.emulate_booleans_from_strings
              m.register_type %r(^VARCHAR2\(1\))i, ActiveRecord::OracleEnhanced::Type::Boolean.new
            else
              m.register_type %r(^NUMBER\(1\))i, Type::Boolean.new
            end
          end
        end

        def extract_limit(sql_type) #:nodoc:
          case sql_type
          when /^bigint/i
            19
          when /\((.*)\)/
            $1.to_i
          end
        end

        def translate_exception(exception, message) #:nodoc:
          case @connection.error_code(exception)
          when 1
            RecordNotUnique.new(message)
          when 942, 955, 1418
            ActiveRecord::StatementInvalid.new(message)
          when 1400
            ActiveRecord::NotNullViolation.new(message)
          when 2291
            InvalidForeignKey.new(message)
          when 12899
            ValueTooLong.new(message)
          else
            super
          end
        end

      private

        def oracle_downcase(column_name)
          @connection.oracle_downcase(column_name)
        end

        def compress_lines(string, join_with = "\n")
          string.split($/).map { |line| line.strip }.join(join_with)
        end

      public
      # DBMS_OUTPUT =============================================
      #
      # PL/SQL in Oracle uses dbms_output for logging print statements
      # These methods stick that output into the Rails log so Ruby and PL/SQL
      # code can can be debugged together in a single application

      # Maximum DBMS_OUTPUT buffer size
      DBMS_OUTPUT_BUFFER_SIZE = 10000  # can be 1-1000000

      # Turn DBMS_Output logging on
      def enable_dbms_output
        set_dbms_output_plsql_connection
        @enable_dbms_output = true
        plsql(:dbms_output).sys.dbms_output.enable(DBMS_OUTPUT_BUFFER_SIZE)
      end
      # Turn DBMS_Output logging off
      def disable_dbms_output
        set_dbms_output_plsql_connection
        @enable_dbms_output = false
        plsql(:dbms_output).sys.dbms_output.disable
      end
      # Is DBMS_Output logging enabled?
      def dbms_output_enabled?
        @enable_dbms_output
      end

      protected
        def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil)
          super
        ensure
          log_dbms_output if dbms_output_enabled?
        end

      private

        def set_dbms_output_plsql_connection
          raise OracleEnhancedConnectionException, "ruby-plsql gem is required for logging DBMS output" unless self.respond_to?(:plsql)
          # do not reset plsql connection if it is the same (as resetting will clear PL/SQL metadata cache)
          unless plsql(:dbms_output).connection && plsql(:dbms_output).connection.raw_connection == raw_connection
            plsql(:dbms_output).connection = raw_connection
          end
        end

        def log_dbms_output
          while true do
            result = plsql(:dbms_output).sys.dbms_output.get_line(line: "", status: 0)
            break unless result[:status] == 0
            @logger.debug "DBMS_OUTPUT: #{result[:line]}" if @logger
          end
        end
    end
  end
end

# Implementation of standard schema definition statements and extensions for schema definition
require "active_record/connection_adapters/oracle_enhanced/schema_statements"
require "active_record/connection_adapters/oracle_enhanced/schema_statements_ext"

# Extensions for schema definition
require "active_record/connection_adapters/oracle_enhanced/schema_definitions"

# Extensions for context index definition
require "active_record/connection_adapters/oracle_enhanced/context_index"

# Patches and enhancements for schema dumper
require "active_record/connection_adapters/oracle_enhanced/schema_dumper"

# Implementation of structure dump
require "active_record/connection_adapters/oracle_enhanced/structure_dump"

require "active_record/connection_adapters/oracle_enhanced/version"

module ActiveRecord
  autoload :OracleEnhancedProcedures, "active_record/connection_adapters/oracle_enhanced/procedures"
end

# Patches and enhancements for column dumper
require "active_record/connection_adapters/oracle_enhanced/column_dumper"

# Moved SchemaCreation class
require "active_record/connection_adapters/oracle_enhanced/schema_creation"

# Moved DatabaseStetements
require "active_record/connection_adapters/oracle_enhanced/database_statements"

# Add Type:Raw
require "active_record/oracle_enhanced/type/raw"

# Add OracleEnhanced::Type::Integer
require "active_record/oracle_enhanced/type/integer"

# Add OracleEnhanced::Type::String
require "active_record/oracle_enhanced/type/string"

# Add OracleEnhanced::Type::NationalCharacterString
require "active_record/oracle_enhanced/type/national_character_string"

# Add OracleEnhanced::Type::Text
require "active_record/oracle_enhanced/type/text"

# Add OracleEnhanced::Type::Boolean
require "active_record/oracle_enhanced/type/boolean"

# To use :boolean type for Attribute API, each type needs registered explicitly.
ActiveRecord::Type.register(:boolean, ActiveRecord::OracleEnhanced::Type::Boolean, adapter: :oracleenhanced)

# Add JSON attribute support
require "active_record/oracle_enhanced/type/json"
ActiveRecord::Type.register(:json, ActiveRecord::OracleEnhanced::Type::Json, adapter: :oracleenhanced)

# Add Type:TimestampTz
require "active_record/oracle_enhanced/type/timestamptz"