Skip to content

Commit 44ba9ed

Browse files
authored
Merge pull request #476 from bmalinconico/allow_index_selection
Adding #with_index to chain to force allow the user to force an index
2 parents 4fc207a + 53d2ea0 commit 44ba9ed

File tree

4 files changed

+131
-2
lines changed

4 files changed

+131
-2
lines changed

lib/dynamoid/criteria/chain.rb

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def where(args)
110110
query.update(args.symbolize_keys)
111111

112112
# we should re-initialize keys detector every time we change query
113-
@key_fields_detector = KeyFieldsDetector.new(@query, @source)
113+
@key_fields_detector = KeyFieldsDetector.new(@query, @source, forced_index_name: @forced_index_name)
114114

115115
self
116116
end
@@ -358,6 +358,36 @@ def scan_index_forward(scan_index_forward)
358358
self
359359
end
360360

361+
# Force the index name to use for queries.
362+
#
363+
# By default allows the library to select the most appropriate index.
364+
# Sometimes you have more than one index which will fulfill your query's
365+
# needs. When this case occurs you may want to force an order. This occurs
366+
# when you are searching by hash key, but not specifying a range key.
367+
#
368+
# class Comment
369+
# include Dynamoid::Document
370+
#
371+
# table key: :post_id
372+
# range_key :author_id
373+
#
374+
# field :post_date, :datetime
375+
#
376+
# global_secondary_index name: :time_sorted_comments, hash_key: :post_id, range_key: post_date, projected_attributes: :all
377+
# end
378+
#
379+
#
380+
# Comment.where(post_id: id).with_index(:time_sorted_comments).scan_index_forward(false)
381+
#
382+
# @return [Dynamoid::Criteria::Chain]
383+
def with_index(index_name)
384+
raise Dynamoid::Errors::InvalidIndex, "Unknown index #{index_name}" unless @source.find_index_by_name(index_name)
385+
386+
@forced_index_name = index_name
387+
@key_fields_detector = KeyFieldsDetector.new(@query, @source, forced_index_name: index_name)
388+
self
389+
end
390+
361391
# Allows to use the results of a search as an enumerable over the results
362392
# found.
363393
#

lib/dynamoid/criteria/key_fields_detector.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ def contain_with_eq_operator?(field_name)
2424
end
2525
end
2626

27-
def initialize(query, source)
27+
def initialize(query, source, forced_index_name: nil)
2828
@query = query
2929
@source = source
3030
@query = Query.new(query)
31+
@forced_index_name = forced_index_name
3132
@result = find_keys_in_query
3233
end
3334

@@ -54,6 +55,8 @@ def index_name
5455
private
5556

5657
def find_keys_in_query
58+
return match_forced_index if @forced_index_name
59+
5760
match_table_and_sort_key ||
5861
match_local_secondary_index ||
5962
match_global_secondary_index_and_sort_key ||
@@ -133,6 +136,16 @@ def match_global_secondary_index
133136
}
134137
end
135138
end
139+
140+
def match_forced_index
141+
idx = @source.find_index_by_name(@forced_index_name)
142+
143+
{
144+
hash_key: idx.hash_key,
145+
range_key: idx.range_key,
146+
index_name: idx.name,
147+
}
148+
end
136149
end
137150
end
138151
end

lib/dynamoid/indexes.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ def find_index(hash, range = nil)
154154
index
155155
end
156156

157+
# Returns an index by its name
158+
#
159+
# @param name [string, symbol] the name of the index to lookup
160+
# @return [Dynamoid::Indexes::Index, nil] index object or nil if it isn't found
161+
def find_index_by_name(name)
162+
string_name = name.to_s
163+
indexes.each_value.detect{ |i| i.name.to_s == string_name }
164+
end
165+
166+
157167
# Returns true iff the provided hash[,range] key combo is a local
158168
# secondary index.
159169
#

spec/dynamoid/criteria/chain_spec.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,6 +1803,82 @@ def request_params
18031803
end
18041804
end
18051805

1806+
describe '#with_index' do
1807+
context 'when Local Secondary Index (LSI) used' do
1808+
let(:klass_with_local_secondary_index) do
1809+
new_class do
1810+
range :owner_id
1811+
1812+
field :age, :integer
1813+
1814+
local_secondary_index range_key: :age,
1815+
name: :age_index, projected_attributes: :all
1816+
end
1817+
end
1818+
1819+
before do
1820+
klass_with_local_secondary_index.create(id: 'the same id', owner_id: 'a', age: 3)
1821+
klass_with_local_secondary_index.create(id: 'the same id', owner_id: 'c', age: 2)
1822+
klass_with_local_secondary_index.create(id: 'the same id', owner_id: 'b', age: 1)
1823+
end
1824+
1825+
it 'sorts the results in ascending order' do
1826+
chain = Dynamoid::Criteria::Chain.new(klass_with_local_secondary_index)
1827+
models = chain.where(id: 'the same id').with_index(:age_index).scan_index_forward(true)
1828+
expect(models.map(&:owner_id)).to eq %w[b c a]
1829+
end
1830+
1831+
it 'sorts the results in desc order' do
1832+
chain = Dynamoid::Criteria::Chain.new(klass_with_local_secondary_index)
1833+
models = chain.where(id: 'the same id').with_index(:age_index).scan_index_forward(false)
1834+
expect(models.map(&:owner_id)).to eq %w[a c b]
1835+
end
1836+
end
1837+
1838+
context 'when Global Secondary Index (GSI) used' do
1839+
let(:klass_with_global_secondary_index) do
1840+
new_class do
1841+
range :owner_id
1842+
1843+
field :age, :integer
1844+
1845+
global_secondary_index hash_key: :owner_id, range_key: :age,
1846+
name: :age_index, projected_attributes: :all
1847+
end
1848+
end
1849+
1850+
before do
1851+
klass_with_global_secondary_index.create(id: 'the same id', owner_id: 'a', age: 3)
1852+
klass_with_global_secondary_index.create(id: 'the same id', owner_id: 'c', age: 2)
1853+
klass_with_global_secondary_index.create(id: 'other id', owner_id: 'a', age: 1)
1854+
end
1855+
1856+
let(:chain) { Dynamoid::Criteria::Chain.new(klass_with_global_secondary_index) }
1857+
1858+
it 'sorts the results in ascending order' do
1859+
models = chain.where(owner_id: 'a').with_index(:age_index).scan_index_forward(true)
1860+
expect(models.map(&:age)).to eq [1, 3]
1861+
end
1862+
1863+
it 'sorts the results in desc order' do
1864+
models = chain.where(owner_id: 'a').with_index(:age_index).scan_index_forward(false)
1865+
expect(models.map(&:age)).to eq [3, 1]
1866+
end
1867+
1868+
it 'works with string names' do
1869+
models = chain.where(owner_id: 'a').with_index('age_index').scan_index_forward(false)
1870+
expect(models.map(&:age)).to eq [3, 1]
1871+
end
1872+
1873+
it 'raises an error when an unknown index is passed' do
1874+
expect do
1875+
chain.where(owner_id: 'a').with_index(:missing_index)
1876+
end.to raise_error Dynamoid::Errors::InvalidIndex, /Unknown index/
1877+
end
1878+
end
1879+
1880+
end
1881+
18061882
describe '#scan_index_forward' do
18071883
let(:klass_with_range_key) do
18081884
new_class do

0 commit comments

Comments
 (0)