Skip to content

Commit 0ea4faa

Browse files
[JAY-706] Add #clone to QueryBuilder
Adds a #clone method to the QueryBuilder class. The method properly clones the QueryBuilder instance and its nested objects. Without this method when #clone is called on a QueryBuilder object changes to the clone's query, aggregations, sort and, in certain cases, source clauses would propagate to the original object. With the addition of the #clone method these issues no longer happen.
1 parent 6104507 commit 0ea4faa

3 files changed

Lines changed: 168 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Please mark backwards incompatible changes with an exclamation mark at the start
99
## [Unreleased]
1010

1111
### Added
12+
- A `#clone` method to `Elasticsearch::QueryBuilder` that properly clones the
13+
`QueryBuilder` and its nested objects.
1214
- ActiveSupport's `#present?`, `#presence` and `#blank?` methods can now be used
1315
in ERB configuration files.
1416

lib/jay_api/elasticsearch/query_builder.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ def merge(other)
127127
)
128128
end
129129

130+
# @return [JayAPI::Elasticsearch::QueryBuilder] A copy of the receiver.
131+
def clone
132+
copy = super
133+
copy.source = @source.clone # source can be an Array or a Hash
134+
copy.sort = @sort.clone # sort is a Hash
135+
copy.query = query.clone
136+
copy.aggregations = aggregations.clone
137+
copy
138+
end
139+
130140
protected
131141

132142
attr_writer :from, :size, :source, :collapse, :sort, :query, :aggregations

spec/jay_api/elasticsearch/query_builder_spec.rb

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,4 +701,160 @@
701701
end
702702
end
703703
end
704+
705+
describe '#clone' do
706+
subject(:method_call) { query_builder.clone }
707+
708+
let(:query_clauses_clone) { query_clauses.clone }
709+
let(:aggregations_clone) { aggregations.clone }
710+
711+
before do
712+
allow(query_clauses).to receive(:clone).and_return(query_clauses_clone)
713+
allow(aggregations).to receive(:clone).and_return(aggregations_clone)
714+
end
715+
716+
shared_examples_for '#clone' do
717+
it "returns an instance of #{described_class}" do
718+
expect(method_call).to be_a(described_class)
719+
end
720+
721+
it 'does not return the same object' do
722+
expect(method_call).not_to be(query_builder)
723+
end
724+
end
725+
726+
context "when the receiver has no 'from' clause" do
727+
it_behaves_like '#clone'
728+
729+
it "does not have a 'from' clause either" do
730+
expect(method_call.to_query).not_to have_key(:from)
731+
end
732+
end
733+
734+
context "when the receiver has a 'from' clause" do
735+
before { query_builder.from(100) }
736+
737+
it_behaves_like '#clone'
738+
739+
it "has the expected 'from' clause" do
740+
expect(method_call.to_query).to include(from: 100)
741+
end
742+
end
743+
744+
context "when the receiver has no 'size' clause" do
745+
it_behaves_like '#clone'
746+
747+
it "does not have a 'size' clause either" do
748+
expect(method_call.to_query).not_to have_key(:size)
749+
end
750+
end
751+
752+
context "when the receiver has a 'size' clause" do
753+
before { query_builder.size(500) }
754+
755+
it_behaves_like '#clone'
756+
757+
it "has the expected 'size' clause" do
758+
expect(method_call.to_query).to include(size: 500)
759+
end
760+
end
761+
762+
context "when the receiver has no 'source' clause" do
763+
it_behaves_like '#clone'
764+
765+
it "does not have a 'source' clause either" do
766+
expect(method_call.to_query).not_to have_key(:_source)
767+
end
768+
end
769+
770+
context "when the receiver has a 'source' clause" do
771+
before { query_builder.source('profile.*') }
772+
773+
it_behaves_like '#clone'
774+
775+
it "has the expected 'source' clause" do
776+
expect(method_call.to_query).to include(_source: 'profile.*')
777+
end
778+
end
779+
780+
context "when the 'source' changes after cloning" do
781+
let(:source) { %w[profile.* permissions.*] }
782+
783+
before do
784+
query_builder.source(source)
785+
end
786+
787+
it_behaves_like '#clone'
788+
789+
it "does not change the 'source' of the clone" do
790+
clone = method_call
791+
expect { source << 'pictures.*' }.not_to change(clone, :to_query)
792+
end
793+
end
794+
795+
context "when the receiver has no 'collapse' clause" do
796+
it_behaves_like '#clone'
797+
798+
it "does not have a 'collapse' clause either" do
799+
expect(method_call.to_query).not_to have_key(:collapse)
800+
end
801+
end
802+
803+
context "when the receiver has a 'collapse' clause" do
804+
before { query_builder.collapse('profile.email') }
805+
806+
it_behaves_like '#clone'
807+
808+
it "has the expected 'collapse' clause" do
809+
expect(method_call.to_query).to include(collapse: { field: 'profile.email' })
810+
end
811+
end
812+
813+
context "when the receiver has no 'sort' clause" do
814+
it_behaves_like '#clone'
815+
816+
it "does not have a 'sort' clause either" do
817+
expect(method_call.to_query).not_to have_key(:sort)
818+
end
819+
end
820+
821+
context "when the receiver has a 'sort' clause" do
822+
before { query_builder.sort(age: :desc) }
823+
824+
it_behaves_like '#clone'
825+
826+
it "has the expected 'sort' clause" do
827+
expect(method_call.to_query).to include(sort: [{ age: { order: :desc } }])
828+
end
829+
end
830+
831+
context "when the 'sort' changes after cloning" do
832+
before { query_builder.sort(age: :desc) }
833+
834+
it_behaves_like '#clone'
835+
836+
it "does not change the 'sort' clause of the clone" do
837+
clone = method_call
838+
expect { query_builder.sort(name: :asc) }.not_to change(clone, :to_query)
839+
end
840+
end
841+
842+
it 'clones the nested QueryClauses object' do
843+
expect(query_clauses).to receive(:clone)
844+
method_call
845+
end
846+
847+
it 'has the cloned QueryClauses object' do
848+
expect(method_call.query).to be(query_clauses_clone)
849+
end
850+
851+
it 'clones the nested Aggregations object' do
852+
expect(aggregations).to receive(:clone)
853+
method_call
854+
end
855+
856+
it 'has the cloned Aggregations object' do
857+
expect(method_call.aggregations).to be(aggregations_clone)
858+
end
859+
end
704860
end

0 commit comments

Comments
 (0)