File Coverage

blib/lib/Catmandu/Store/MongoDB/Bag.pm
Criterion Covered Total %
statement 24 93 25.8
branch 0 26 0.0
condition 0 19 0.0
subroutine 8 24 33.3
pod 0 10 0.0
total 32 172 18.6


line stmt bran cond sub pod time code
1             package Catmandu::Store::MongoDB::Bag;
2              
3 3     3   18 use Catmandu::Sane;
  3         6  
  3         18  
4              
5             our $VERSION = '0.0803';
6              
7 3     3   473 use Catmandu::Util qw(:is);
  3         6  
  3         791  
8 3     3   1045 use Catmandu::Store::MongoDB::Searcher;
  3         10  
  3         114  
9 3     3   1130 use Catmandu::Hits;
  3         25010  
  3         125  
10 3     3   27 use Cpanel::JSON::XS qw(decode_json);
  3         8  
  3         231  
11 3     3   18 use Moo;
  3         6  
  3         34  
12 3     3   2589 use Catmandu::Store::MongoDB::CQL;
  3         11  
  3         89  
13 3     3   22 use namespace::clean;
  3         5  
  3         24  
14              
15             with 'Catmandu::Bag';
16             with 'Catmandu::Droppable';
17             with 'Catmandu::CQLSearchable';
18              
19             has collection => (
20             is => 'ro',
21             init_arg => undef,
22             lazy => 1,
23             builder => '_build_collection',
24             );
25              
26             has cql_mapping => (is => 'ro');
27              
28             sub _build_collection {
29 0     0     my ($self) = @_;
30 0           $self->store->database->get_collection($self->name);
31             }
32              
33             sub _options {
34 0     0     my ($self, $opts) = @_;
35 0   0       $opts //= {};
36 0 0         $opts->{session} = $self->store->session if $self->store->has_session;
37 0           $opts;
38             }
39              
40             sub _cursor {
41 0     0     my ($self, $filter, $opts) = @_;
42 0   0       $self->collection->find($filter // {}, $self->_options($opts));
43             }
44              
45             sub generator {
46 0     0 0   my ($self) = @_;
47             sub {
48 0     0     state $cursor = do {
49 0           my $c = $self->_cursor;
50 0           $c->immortal(1);
51 0           $c;
52             };
53 0           $cursor->next;
54 0           };
55             }
56              
57             sub to_array {
58 0     0 0   my ($self) = @_;
59 0           my @all = $self->_cursor->all;
60 0           \@all;
61             }
62              
63             sub each {
64 0     0 0   my ($self, $sub) = @_;
65 0           my $cursor = $self->_cursor;
66 0           my $n = 0;
67 0           while (my $data = $cursor->next) {
68 0           $sub->($data);
69 0           $n++;
70             }
71 0           $n;
72             }
73              
74             sub count {
75 0     0 0   my ($self) = @_;
76 0           $self->collection->count_documents({}, $self->_options);
77             }
78              
79             # efficiently handle:
80             # $bag->detect('foo' => 'bar')
81             # $bag->detect('foo' => /bar/)
82             # $bag->detect('foo' => ['bar', 'baz'])
83             around detect => sub {
84             my ($orig, $self, $arg1, $arg2) = @_;
85             if (is_string($arg1)) {
86             if (is_value($arg2) || is_regex_ref($arg2)) {
87             return $self->collection->find_one({$arg1 => $arg2},
88             {}, $self->_options);
89             }
90             if (is_array_ref($arg2)) {
91             return $self->collection->find_one({$arg1 => {'$in' => $arg2}},
92             {}, $self->_options);
93             }
94             }
95             $self->$orig($arg1, $arg2);
96             };
97              
98             # efficiently handle:
99             # $bag->select('foo' => 'bar')
100             # $bag->select('foo' => /bar/)
101             # $bag->select('foo' => ['bar', 'baz'])
102             around select => sub {
103             my ($orig, $self, $arg1, $arg2) = @_;
104             if (is_string($arg1)) {
105             if (is_value($arg2) || is_regex_ref($arg2)) {
106             return Catmandu::Iterator->new(
107             sub {
108             sub {
109             state $cursor = $self->_cursor({$arg1 => $arg2});
110             $cursor->next;
111             }
112             }
113             );
114             }
115             if (is_array_ref($arg2)) {
116             return Catmandu::Iterator->new(
117             sub {
118             sub {
119             state $cursor
120             = $self->_cursor({$arg1 => {'$in' => $arg2}});
121             $cursor->next;
122             }
123             }
124             );
125             }
126             }
127             $self->$orig($arg1, $arg2);
128             };
129              
130             # efficiently handle:
131             # $bag->reject('foo' => 'bar')
132             # $bag->reject('foo' => ['bar', 'baz'])
133             around reject => sub {
134             my ($orig, $self, $arg1, $arg2) = @_;
135             if (is_string($arg1)) {
136             if (is_value($arg2)) {
137             return Catmandu::Iterator->new(
138             sub {
139             sub {
140             state $cursor
141             = $self->_cursor({$arg1 => {'$ne' => $arg2}});
142             $cursor->next;
143             }
144             }
145             );
146             }
147             if (is_array_ref($arg2)) {
148             return Catmandu::Iterator->new(
149             sub {
150             sub {
151             state $cursor
152             = $self->_cursor({$arg1 => {'$nin' => $arg2}});
153             $cursor->next;
154             }
155             }
156             );
157             }
158             }
159             $self->$orig($arg1, $arg2);
160             };
161              
162             sub pluck {
163 0     0 0   my ($self, $key) = @_;
164             Catmandu::Iterator->new(
165             sub {
166             sub {
167 0           state $cursor
168             = $self->_cursor({}, {projection => {$key => 1}});
169 0   0       ($cursor->next || return)->{$key};
170             }
171 0     0     }
172 0           );
173             }
174              
175             sub get {
176             my ($self, $id) = @_;
177             $self->collection->find_one({_id => $id}, {}, $self->_options);
178             }
179              
180             sub add {
181             my ($self, $data) = @_;
182             $self->collection->replace_one({_id => $data->{_id}},
183             $data, $self->_options({upsert => 1}));
184             }
185              
186             sub delete {
187             my ($self, $id) = @_;
188             $self->collection->delete_one({_id => $id}, $self->_options);
189             }
190              
191             sub delete_all {
192             my ($self) = @_;
193             $self->collection->delete_many({}, $self->_options);
194             }
195              
196             sub delete_by_query {
197             my ($self, %args) = @_;
198             $self->collection->delete_many($args{query}, $self->_options);
199             }
200              
201             sub search {
202             my ($self, %args) = @_;
203              
204             my $query = $args{query};
205             my $start = $args{start};
206             my $limit = $args{limit};
207             my $bag = $args{reify};
208             my $fields = $args{fields};
209              
210             my $cursor = $self->_cursor($query)->skip($start)->limit($limit);
211             if ($bag) { # only retrieve _id
212             $cursor->fields({});
213             }
214             elsif ($fields) { # only retrieve specified fields
215             $cursor->fields($fields);
216             }
217              
218             if (my $sort = $args{sort}) {
219             $cursor->sort($sort);
220             }
221              
222             my @hits = $cursor->all;
223             if ($bag) {
224             @hits = map {$bag->get($_->{_id})} @hits;
225             }
226              
227             Catmandu::Hits->new(
228             {
229             start => $start,
230             limit => $limit,
231             total =>
232             $self->collection->count_documents($query, $self->_options),
233             hits => \@hits,
234             }
235             );
236             }
237              
238             sub searcher {
239             my ($self, %args) = @_;
240             Catmandu::Store::MongoDB::Searcher->new(%args, bag => $self);
241             }
242              
243             sub translate_sru_sortkeys {
244 0     0 0   my ($self, $sortkeys) = @_;
245             my $keys = [
246 0           grep {defined $_} map {$self->_translate_sru_sortkey($_)} split /\s+/,
  0            
  0            
247             $sortkeys
248             ];
249 0           my $mongo_sort = [];
250              
251             # flatten sortkeys
252 0           for (@$keys) {
253 0           push @$mongo_sort, @$_;
254             }
255 0           $self->log->debugf("translating sortkeys '$sortkeys' to mongo sort: %s",
256             $mongo_sort);
257 0           $mongo_sort;
258             }
259              
260             sub _translate_sru_sortkey {
261 0     0     my ($self, $sortkey) = @_;
262 0           my ($field, $schema, $asc) = split /,/, $sortkey;
263 0 0         $field || return;
264 0 0 0       ($asc && ($asc == 1 || $asc == -1)) || return;
      0        
265 0 0         if (my $map = $self->cql_mapping) {
266 0           $field = lc $field;
267             $field =~ s/(?<=[^_])_(?=[^_])//g
268 0 0         if $map->{strip_separating_underscores};
269 0   0       $map = $map->{indexes} || return;
270 0   0       $map = $map->{$field} || return;
271 0 0         $map->{sort} || return;
272 0 0 0       if (ref $map->{sort} && $map->{sort}{field}) {
    0          
    0          
273 0           $field = $map->{sort}{field};
274             }
275             elsif (ref $map->{field}) {
276 0           $field = $map->{field}->[0];
277             }
278             elsif ($map->{field}) {
279 0           $field = $map->{field};
280             }
281             }
282              
283             # Use a bad trick to force $asc interpreted as an integer
284 0           [$field => $asc + 0];
285             }
286              
287             sub translate_cql_query {
288 0     0 0   my ($self, $query) = @_;
289 0           my $mongo_query
290             = Catmandu::Store::MongoDB::CQL->new(mapping => $self->cql_mapping)
291             ->parse($query);
292 0           $self->log->debugf("translating cql '$query' to mongo query: %s",
293             $mongo_query);
294 0           $mongo_query;
295             }
296              
297             # assume a string query is a JSON encoded MongoDB query
298             sub normalize_query {
299 0     0 0   my ($self, $query) = @_;
300 0 0         return $query if ref $query;
301 0 0         return {} if !$query;
302 0           decode_json($query);
303             }
304              
305             # assume a sort option is a JSON encoded MongoDB sort specification
306             sub normalize_sort {
307 0     0 0   my ($self, $sort) = @_;
308 0 0         return $sort if ref $sort;
309 0 0         return {} if !$sort;
310 0           decode_json($sort);
311             }
312              
313             sub drop {
314 0     0 0   $_[0]->collection->drop;
315             }
316              
317             1;
318              
319             __END__
320              
321             =pod
322              
323             =head1 NAME
324              
325             Catmandu::Store::MongoDB::Bag - Catmandu::Bag implementation for MongoDB
326              
327             =head1 DESCRIPTION
328              
329             This class isn't normally used directly. Instances are constructed using the store's C<bag> method.
330              
331             =head1 SEE ALSO
332              
333             L<Catmandu::Bag>, L<Catmandu::CQLSearchable>, L<Catmandu::Droppable>
334              
335             =cut