module OccamsRecord::Results

Classes and methods for handing query results.

Constants

CASTER

ActiveRecord's internal type casting API changes from version to version.

Public Class Methods

klass(column_names, column_types, association_names = [], model: nil, modules: nil) click to toggle source

Dynamically build a class for a specific set of result rows. It inherits from OccamsRecord::Results::Row, and optionall prepends user-defined modules.

@param column_names [Array<String>] the column names in the result set. The order MUST match the order returned by the query. @param column_types [Hash] Column name => type from an ActiveRecord::Result @param association_names [Array<String>] names of associations that will be eager loaded into the results. @param model [ActiveRecord::Base] the AR model representing the table (it holds column & type info). @param modules [Array<Module>] (optional) @return [OccamsRecord::Results::Row] a class customized for this result set

# File lib/occams-record/results/results.rb, line 22
def self.klass(column_names, column_types, association_names = [], model: nil, modules: nil)
  Class.new(Results::Row) do
    Array(modules).each { |mod| prepend mod } if modules

    self.columns = column_names.map(&:to_s)
    self.associations = association_names.map(&:to_s)
    self._model = model
    self.model_name = model ? model.name : nil
    self.table_name = model ? model.table_name : nil
    self.primary_key = if model&.primary_key and (pkey = model.primary_key.to_s) and columns.include?(pkey)
                         pkey
                       end

    # Build getters & setters for associations. (We need setters b/c they're set AFTER the row is initialized
    attr_accessor(*association_names)

    # Build a getter for each attribute returned by the query. The values will be type converted on demand.
    model_column_types = model ? model.attributes_builder.types : nil
    self.columns.each_with_index do |col, idx|
      #
      # NOTE there's lots of variation between DB adapters and AR versions here. Some notes:
      # * Postgres AR < 6.1 `column_types` will contain entries for every column.
      # * Postgres AR >= 6.1 `column_types` only contains entries for "exotic" types. Columns with "common" types have already been converted by the PG adapter.
      # * SQLite `column_types` will always be empty. Some types will have already been convered by the SQLite adapter, but others will depend on
      #   `model_column_types` for converstion. See test/raw_query_test.rb#test_common_types for examples.
      # * MySQL ?
      #
      type = column_types[col] || model_column_types&.[](col)
      case type&.type
      when nil
        define_method(col) { @raw_values[idx] }
      when :datetime
        define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx])&.in_time_zone }
      when :boolean
        define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx]) }
        define_method("#{col}?") { !!send(col) }
      else
        define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx]) }
      end
    end
  end
end