123456789_123456789_123456789_123456789_123456789_

Class: ActiveRecord::ConnectionAdapters::QueryIntent

Do not use. This class is for internal use only.
Relationships & Source Files
Namespace Children
Classes:
Exceptions:
Inherits: Object
Defined in: activerecord/lib/active_record/connection_adapters/query_intent.rb

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(adapter:, arel: nil, raw_sql: nil, processed_sql: nil, name: "SQL", binds: [], prepare: false, allow_async: false, allow_retry: false, materialize_transactions: true, batch: false) ⇒ QueryIntent

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 72

def initialize(adapter:, arel: nil, raw_sql: nil, processed_sql: nil, name: "SQL", binds: [], prepare: false, allow_async: false,
               allow_retry: false, materialize_transactions: true, batch: false)
  if arel.nil? && raw_sql.nil? && processed_sql.nil?
    raise ArgumentError, "One of arel, raw_sql, or processed_sql must be provided"
  end

  @adapter = adapter
  @arel = arel
  @raw_sql = raw_sql
  @name = name
  @binds = binds
  @prepare = prepare
  @allow_async = allow_async
  @ran_async = nil
  @allow_retry = allow_retry
  @materialize_transactions = materialize_transactions
  @batch = batch
  @processed_sql = processed_sql
  @type_casted_binds = nil
  @notification_payload = nil
  @raw_result = nil
  @raw_result_available = false
  @warnings = nil
  @outcome_handled = false
  @warning_error = nil
  @executed = false
  @write_query = nil

  # Deferred execution state
  @pool = adapter.pool
  @session = nil
  @mutex = ActiveSupport::Concurrency::NullLock
  @error = nil
  @lock_wait = nil
  @event_buffer = nil
  @log_handle = nil
  @finalized = false

  @retry_budget = nil
end

Class Method Details

.shuffle_rows?(arel) ⇒ Boolean

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 119

def self.shuffle_rows?(arel)
  ActiveRecord.shuffle_unordered_selects && unordered_select?(arel)
end

.unordered_select?(arel) ⇒ Boolean

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 113

def self.unordered_select?(arel)
  ast = arel.respond_to?(:ast) ? arel.ast : arel

  Arel::Nodes::SelectStatement === ast && ast.orders.empty?
end

Instance Attribute Details

#adapter (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 69

attr_accessor :adapter, :binds, :ran_async, :notification_payload, :log_handle,
              :retry_budget

#allow_async (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#allow_retry (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#arel (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#batch (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#binds (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 69

attr_accessor :adapter, :binds, :ran_async, :notification_payload, :log_handle,
              :retry_budget

#can_run_async?Boolean (readonly, private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 470

def can_run_async?
  @allow_async && adapter.async_enabled?
end

#canceled?Boolean (readonly)

Was this intent canceled?

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 184

def canceled?
  @session && !@session.active?
end

#error (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#event_buffer (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#finalized? (readonly)

Alias for #finalized.

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 67

alias_method :finalized?, :finalized

#has_binds?Boolean (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 215

def has_binds?
  compile_arel!
  binds && !binds.empty?
end

#lock_wait (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#log_handle (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 69

attr_accessor :adapter, :binds, :ran_async, :notification_payload, :log_handle,
              :retry_budget

#materialize_transactions (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#name (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#notification_payload (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 69

attr_accessor :adapter, :binds, :ran_async, :notification_payload, :log_handle,
              :retry_budget

#pending?Boolean (readonly)

Is this intent still pending (result not yet available)?

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 179

def pending?
  !@raw_result_available && @session&.active?
end

#pool (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#prepare (readonly)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#ran_async (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 69

attr_accessor :adapter, :binds, :ran_async, :notification_payload, :log_handle,
              :retry_budget

#raw_result_available?Boolean (readonly)

Check if result has been populated yet (without blocking)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 287

def raw_result_available?
  @raw_result_available
end

#raw_sql (rw)

Returns raw SQL, compiling from arel if needed, memoized

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 194

def raw_sql
  @raw_sql ||
    begin
      compile_arel!
      @raw_sql
    end
end

#raw_sql=(value) (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 68

attr_writer :raw_sql, :session

#retry_budget (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 69

attr_accessor :adapter, :binds, :ran_async, :notification_payload, :log_handle,
              :retry_budget

#session (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#session=(value) (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 68

attr_writer :raw_sql, :session

#write_query?Boolean (readonly, private)

Heuristically guesses whether this is a write query by examining the outermost SQL operation. Subqueries, function calls, etc are not considered.

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 411

def write_query?
  return @write_query unless @write_query.nil?

  @write_query =
    case arel
    when Arel::SelectManager
      false
    when Arel::InsertManager, Arel::UpdateManager, Arel::DeleteManager
      true
    else
      adapter.write_query?(raw_sql)
    end
end

Instance Method Details

#affected_rows

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 348

def affected_rows
  raise "Cannot call affected_rows before query has executed" unless @executed
  raise "Cannot call affected_rows after cast_result has been called" if defined?(@cast_result)

  ensure_result
  @affected_rows ||= adapter.send(:affected_rows, @raw_result)
end

#async_schedule!(session) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 390

def async_schedule!(session)
  if adapter.current_transaction.joinable?
    raise AsynchronousQueryInsideTransactionError, "Asynchronous queries are not allowed inside transactions"
  end

  # Upgrade to real mutex now that we'll have concurrent access
  @mutex = Mutex.new
  @session = session

  # Force preprocessing on original thread before queuing
  processed_sql

  # Detach from original adapter while in queue
  @adapter = nil

  # Schedule on the pool's async queue
  @pool.schedule_query(self)
end

#cancel

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 188

def cancel
  return unless pending?
  @error = FutureResult::Canceled.new
end

#cast_result

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 340

def cast_result
  raise "Cannot call cast_result before query has executed" unless @executed
  raise "Cannot call cast_result after affected_rows has been called" if defined?(@affected_rows)

  ensure_result
  @cast_result ||= adapter.send(:cast_result, @raw_result).shuffle_rows(@arel)
end

#compile_arel! (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 442

def compile_arel!
  return if @raw_sql || !@arel
  @raw_sql, @binds, @prepare, @allow_retry = adapter.to_sql_and_binds(@arel, @binds, @prepare, @allow_retry)
  nil
end

#deliver_failure(exception, warnings: nil)

Raises:

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 260

def deliver_failure(exception, warnings: nil)
  raise FinalizedError, "delivering failure to a finalized intent" if @finalized

  adapter.lock.synchronize do
    @error = exception
    @warnings = warnings

    adapter.downgrade_connection_after_error(exception)
    retryable = adapter.retryable_failure?(exception, @retry_budget)
    mark_transaction_dirty unless retryable

    @raw_result_available = !retryable
  end
end

#deliver_result(value, warnings: nil)

Raises:

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 244

def deliver_result(value, warnings: nil)
  raise FinalizedError, "delivering result to a finalized intent" if @finalized

  adapter.lock.synchronize do
    @raw_result = value
    @warnings = warnings
    @error = nil

    mark_transaction_dirty

    @raw_result_available = true
  end

  finish_log
end

#ensure_result

Ensure the result is available, blocking if necessary

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 298

def ensure_result
  if @outcome_handled
    raise @error if @error
    raise @warning_error if @warning_error
    return
  end

  if @session
    # Async was scheduled: wait for result (sets lock_wait)
    execute_or_wait
  end

  @event_buffer&.flush
  pump_retries
  @event_buffer&.flush

  if @error
    begin
      adapter.handle_warnings(self, @warnings)
    rescue
      # The query failed, so we need to swallow this exception
      # from handle_warnings to avoid masking the original.
    end
    finish_log(exception: @error)
    @outcome_handled = true
    @event_buffer&.flush
    raise @error
  end

  begin
    adapter.handle_warnings(self, @warnings)
  rescue => warning_error
    @warning_error = warning_error
    @outcome_handled = true
    @event_buffer&.flush
    raise
  end

  @outcome_handled = true
  @event_buffer&.flush
end

#execute!

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 220

def execute!
  if can_run_async?
    async_schedule!(ActiveRecord::Base.asynchronous_queries_session)
  else
    @ran_async = false
    run_query!
  end
ensure
  @executed = true
end

#execute_or_skip

Called by background thread to execute if not already done

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 147

def execute_or_skip
  return unless pending?

  @session.synchronize do
    return unless pending?

    @pool.with_connection do |connection|
      return unless @mutex.try_lock
      previous_instrumenter = ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]
      begin
        if pending?
          if connection.sql_notifications?
            @event_buffer = EventBuffer.new(self, ActiveSupport::Notifications.instrumenter)
            ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = @event_buffer
          end

          @adapter = connection
          @ran_async = true
          run_query!
          pump_retries
        end
      rescue => error
        @error = error
      ensure
        ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = previous_instrumenter
        @mutex.unlock
      end
    end
  end
end

#execute_or_wait (private)

Block until result is available, or execute as foreground fallback

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 449

def execute_or_wait
  return (@lock_wait = 0.0) if @raw_result_available

  start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
  @mutex.synchronize do
    if pending?
      @pool.with_connection do |connection|
        @adapter = connection
        @ran_async = false  # Foreground fallback, not actually async
        run_query!
        pump_retries
      end
    else
      # Result was computed by background thread while we waited for mutex
      @lock_wait = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start
    end
  rescue => error
    @error = error
  end
end

#finalized (readonly) Also known as: #finalized?

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 64

attr_reader :arel, :name, :prepare, :allow_retry, :allow_async,
            :materialize_transactions, :batch, :pool, :session, :lock_wait,
            :event_buffer, :error, :finalized

#finish

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 239

def finish
  affected_rows # just to consume/close the result
  nil
end

#finish_log(exception: nil)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 356

def finish_log(exception: nil) # :nodoc:
  return if @finalized

  settle_failure if exception
  @finalized = true
  adapter.finish_intent_log(self, exception: exception)
end

#future_result

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 231

def future_result
  if pending? || can_run_async?
    FutureResult.new(self)
  else
    FutureResult.wrap(cast_result)
  end
end

#inspect

Returns a string representation showing key attributes

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 142

def inspect
  "#<#{self.class.name} name=#{name.inspect} allow_retry=#{allow_retry} materialize_transactions=#{materialize_transactions}>"
end

#mark_transaction_dirty (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 386

def mark_transaction_dirty
  adapter.send(:dirty_current_transaction) if @materialize_transactions
end

#preprocess_query (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 425

def preprocess_query
  if adapter.preventing_writes? && write_query?
    raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{raw_sql}"
  end

  sql = raw_sql

  # We call transformers after the write checks so we don't need to parse the
  # transformed result (which probably just adds comments we'd need to ignore).
  # This means we assume no transformer will change a read into a write.
  ActiveRecord.query_transformers&.each do |transformer|
    sql = transformer.call(sql, adapter)
  end

  sql
end

#processed_sql

Returns preprocessed SQL, memoized

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 203

def processed_sql
  @processed_sql ||= preprocess_query
end

#pump_retries (private)

Retry a delivered failure while the adapter remains available. Scoped connection owners call this before releasing their connection; otherwise result observation takes responsibility for pumping retries.

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 368

def pump_retries
  while !@finalized && @error && adapter.attempt_retry(@error, @retry_budget)
    reset_for_retry
    run_query!
  end

  settle_failure
end

#raw_result

Access the raw result, ensuring it's available first

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 292

def raw_result
  ensure_result
  @raw_result
end

#reset_for_retry

Raises:

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 275

def reset_for_retry
  raise FinalizedError, "resetting a finalized intent" if @finalized

  @raw_result = nil
  @raw_result_available = false
  @warnings = nil
  @outcome_handled = false
  @warning_error = nil
  @error = nil
end

#run_query! (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 474

def run_query!
  adapter.execute_intent(self)
rescue ::RangeError
  raise if write_query?
  @cast_result = ActiveRecord::Result.empty
  @raw_result_available = true
end

#settle_failure (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 377

def settle_failure
  return unless @error && !@raw_result_available

  adapter.lock.synchronize do
    mark_transaction_dirty
    @raw_result_available = true
  end
end

#to_h

Returns a hash representation of the QueryIntent for debugging/introspection

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 124

def to_h
  {
    arel: arel,
    raw_sql: raw_sql,
    processed_sql: processed_sql,
    name: name,
    binds: binds,
    prepare: prepare,
    allow_async: allow_async,
    allow_retry: allow_retry,
    materialize_transactions: materialize_transactions,
    batch: batch,
    type_casted_binds: type_casted_binds,
    notification_payload: notification_payload
  }
end

#type_casted_binds

[ GitHub ]

  
# File 'activerecord/lib/active_record/connection_adapters/query_intent.rb', line 207

def type_casted_binds
  @type_casted_binds ||=
    begin
      compile_arel!
      adapter.type_casted_binds(binds)
    end
end