module Motor::Queries::RunQuery

Constants

CTE_NAME
DEFAULT_LIMIT
PG_ERROR_REGEXP
QueryResult
RESERVED_VARIABLES
STATEMENT_VARIABLE_REGEXP
WITH_STATEMENT_START
WITH_STATEMENT_TEMPLATE

Public Instance Methods

build_columns_hash(result) click to toggle source

@param result [ActiveRecord::Result] @return [Hash]

# File lib/motor/queries/run_query.rb, line 85
def build_columns_hash(result)
  result.columns.map do |column_name|
    column_type = result.column_types[column_name]

    {
      name: column_name,
      display_name: column_name.humanize,
      column_type: ActiveRecordUtils::Types.find_name_for_type(column_type),
      is_array: column_type.class.to_s == 'ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array'
    }
  end
end
build_cte_select_sql(limit, filters) click to toggle source

@param limit [Number] @param filters [Hash] @return [String]

# File lib/motor/queries/run_query.rb, line 118
def build_cte_select_sql(limit, filters)
  table = Arel::Table.new(CTE_NAME)

  arel_filters = build_filters_arel(filters)

  expresion = table.project(table[Arel.star])
  expresion = expresion.where(arel_filters) if arel_filters.present?

  expresion.take(limit.to_i).to_sql
end
build_error_message(exception) click to toggle source

@param exception [ActiveRecord::StatementInvalid] @return [String]

# File lib/motor/queries/run_query.rb, line 53
def build_error_message(exception)
  exception.message.sub(WITH_STATEMENT_START, '').sub(PG_ERROR_REGEXP, '').strip.upcase_first
end
build_filters_arel(filters) click to toggle source

@param filters [Hash] @return [Arel::Nodes, nil]

# File lib/motor/queries/run_query.rb, line 131
def build_filters_arel(filters)
  return nil if filters.blank?

  table = Arel::Table.new(CTE_NAME)

  arel_filters = filters.map { |key, value| table[key].in(value) }

  arel_filters[1..].reduce(arel_filters.first) { |acc, arel| acc.and(arel) }
end
build_statement_attributes(variables) click to toggle source

@param variables [Array<(String, Object)>] @return [Array<ActiveRecord::Relation::QueryAttribute>]

# File lib/motor/queries/run_query.rb, line 143
def build_statement_attributes(variables)
  variables.map do |variable_name, value|
    [value].flatten.map do |val|
      ActiveRecord::Relation::QueryAttribute.new(
        variable_name,
        val,
        ActiveRecord::Type::Value.new
      )
    end
  end.flatten
end
call(query, variables_hash: nil, limit: nil, filters: nil) click to toggle source

@param query [Motor::Query] @param variables_hash [Hash] @param limit [Integer] @return [Motor::Queries::RunQuery::QueryResult]

# File lib/motor/queries/run_query.rb, line 45
def call(query, variables_hash: nil, limit: nil, filters: nil)
  call!(query, variables_hash: variables_hash, limit: limit, filters: filters)
rescue ActiveRecord::StatementInvalid => e
  QueryResult.new(error: build_error_message(e))
end
call!(query, variables_hash: nil, limit: nil, filters: nil) click to toggle source

@param query [Motor::Query] @param variables_hash [Hash] @param limit [Integer] @param filters [Hash] @return [Motor::Queries::RunQuery::QueryResult]

# File lib/motor/queries/run_query.rb, line 31
def call!(query, variables_hash: nil, limit: nil, filters: nil)
  variables_hash ||= {}
  limit ||= DEFAULT_LIMIT
  filters ||= {}

  result = execute_query(query, limit, variables_hash, filters)

  QueryResult.new(data: result.rows, columns: build_columns_hash(result))
end
connection_class() click to toggle source
# File lib/motor/queries/run_query.rb, line 176
def connection_class
  defined?(ResourceRecord) ? ResourceRecord : ActiveRecord::Base
end
execute_query(query, limit, variables_hash, filters) click to toggle source

@param query [Motor::Query] @param limit [Integer] @param variables_hash [Hash] @param filters [Hash] @return [ActiveRecord::Result]

# File lib/motor/queries/run_query.rb, line 62
def execute_query(query, limit, variables_hash, filters)
  result = nil
  statement = prepare_sql_statement(query, limit, variables_hash, filters)

  connection_class.transaction do
    result =
      case connection_class.connection.class.name
      when 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
        PostgresqlExecQuery.call(connection_class.connection, statement)
      else
        statement = normalize_statement_for_sql(statement)

        connection_class.connection.exec_query(*statement)
      end

    raise ActiveRecord::Rollback
  end

  result
end
merge_variable_default_values(variable_configs, variables_hash) click to toggle source

@param variable_configs [Array<Hash>] @param variables_hash [Hash] @return [Hash]

# File lib/motor/queries/run_query.rb, line 168
def merge_variable_default_values(variable_configs, variables_hash)
  variable_configs.each_with_object(variables_hash.slice(*RESERVED_VARIABLES)) do |variable, acc|
    next if RESERVED_VARIABLES.include?(variable[:name])

    acc[variable[:name]] ||= variables_hash[variable[:name]] || variable[:default_value]
  end
end
normalize_statement_for_sql(statement) click to toggle source

@param array [Array] @return [Array]

# File lib/motor/queries/run_query.rb, line 157
def normalize_statement_for_sql(statement)
  sql, _, attributes = statement

  sql = ActiveRecord::Base.sanitize_sql([sql.gsub(STATEMENT_VARIABLE_REGEXP, '?'), attributes.map(&:value)])

  [sql, 'SQL', attributes]
end
prepare_sql_statement(query, limit, variables_hash, filters) click to toggle source

@param query [Motor::Query] @param limit [Integer] @param variables_hash [Hash] @param filters [Hash] @return [Array]

# File lib/motor/queries/run_query.rb, line 103
def prepare_sql_statement(query, limit, variables_hash, filters)
  variables = merge_variable_default_values(query.preferences.fetch(:variables, []), variables_hash)

  sql, query_variables = RenderSqlTemplate.call(query.sql_body, variables)
  cte_sql = format(WITH_STATEMENT_TEMPLATE, sql_body: sql.strip.delete_suffix(';'))
  cte_select_sql = build_cte_select_sql(limit, filters)

  attributes = build_statement_attributes(query_variables)

  [[cte_sql, cte_select_sql].join, 'SQL', attributes]
end