Dynamoid/dynamoid

View on GitHub
lib/dynamoid/criteria/key_fields_detector.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module Dynamoid
  module Criteria
    # @private
    class KeyFieldsDetector
      class Query
        def initialize(where_conditions)
          @where_conditions = where_conditions
          @fields_with_operator = where_conditions.keys.map(&:to_s)
          @fields = where_conditions.keys.map(&:to_s).map { |s| s.split('.').first }
        end

        def contain_only?(field_names)
          (@fields - field_names.map(&:to_s)).blank?
        end

        def contain?(field_name)
          @fields.include?(field_name.to_s)
        end

        def contain_with_eq_operator?(field_name)
          @fields_with_operator.include?(field_name.to_s)
        end
      end

      def initialize(where_conditions, source, forced_index_name: nil)
        @source = source
        @query = Query.new(where_conditions)
        @forced_index_name = forced_index_name
        @result = find_keys_in_query
      end

      def non_key_present?
        !@query.contain_only?([hash_key, range_key].compact)
      end

      def key_present?
        @result.present? && @query.contain_with_eq_operator?(hash_key)
      end

      def hash_key
        @result && @result[:hash_key]
      end

      def range_key
        @result && @result[:range_key]
      end

      def index_name
        @result && @result[:index_name]
      end

      private

      def find_keys_in_query
        return match_forced_index if @forced_index_name

        match_table_and_sort_key ||
          match_local_secondary_index ||
          match_global_secondary_index_and_sort_key ||
          match_table ||
          match_global_secondary_index
      end

      # Use table's default range key
      def match_table_and_sort_key
        return unless @query.contain_with_eq_operator?(@source.hash_key)
        return unless @source.range_key

        if @query.contain?(@source.range_key)
          {
            hash_key: @source.hash_key,
            range_key: @source.range_key
          }
        end
      end

      # See if can use any local secondary index range key
      # Chooses the first LSI found that can be utilized for the query
      def match_local_secondary_index
        return unless @query.contain_with_eq_operator?(@source.hash_key)

        lsi = @source.local_secondary_indexes.values.find do |i|
          @query.contain?(i.range_key)
        end

        if lsi.present?
          {
            hash_key: @source.hash_key,
            range_key: lsi.range_key,
            index_name: lsi.name,
          }
        end
      end

      # See if can use any global secondary index
      # Chooses the first GSI found that can be utilized for the query
      # GSI with range key involved into query conditions has higher priority
      # But only do so if projects ALL attributes otherwise we won't
      # get back full data
      def match_global_secondary_index_and_sort_key
        gsi = @source.global_secondary_indexes.values.find do |i|
          @query.contain_with_eq_operator?(i.hash_key) && i.projected_attributes == :all &&
            @query.contain?(i.range_key)
        end

        if gsi.present?
          {
            hash_key: gsi.hash_key,
            range_key: gsi.range_key,
            index_name: gsi.name,
          }
        end
      end

      def match_table
        return unless @query.contain_with_eq_operator?(@source.hash_key)

        {
          hash_key: @source.hash_key,
        }
      end

      def match_global_secondary_index
        gsi = @source.global_secondary_indexes.values.find do |i|
          @query.contain_with_eq_operator?(i.hash_key) && i.projected_attributes == :all
        end

        if gsi.present?
          {
            hash_key: gsi.hash_key,
            range_key: gsi.range_key,
            index_name: gsi.name,
          }
        end
      end

      def match_forced_index
        idx = @source.find_index_by_name(@forced_index_name)

        {
          hash_key: idx.hash_key,
          range_key: idx.range_key,
          index_name: idx.name,
        }
      end
    end
  end
end