lib/dynamoid/criteria/key_fields_detector.rb
# 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