File Coverage

blib/lib/MorboDB/Cursor.pm
Criterion Covered Total %
statement 91 116 78.4
branch 44 74 59.4
condition 17 45 37.7
subroutine 14 19 73.6
pod 12 12 100.0
total 178 266 66.9


line stmt bran cond sub pod time code
1             package MorboDB::Cursor;
2              
3             # ABSTRACT: A cursor/iterator for MorboDB query results
4              
5 4     4   26 use Moo;
  4         9  
  4         37  
6 4     4   1489 use Carp;
  4         19  
  4         293  
7 4     4   22 use Clone qw/clone/;
  4         7  
  4         187  
8 4     4   3955 use MQUL 0.003 qw/doc_matches/;
  4         972163  
  4         283  
9 4     4   3908 use Tie::IxHash;
  4         9454  
  4         6229  
10              
11             our $VERSION = "1.000000";
12             $VERSION = eval $VERSION;
13              
14             =head1 NAME
15              
16             MorboDB::Cursor - A cursor/iterator for MorboDB query results
17              
18             =head1 VERSION
19              
20             version 1.000000
21              
22             =head1 SYNOPSIS
23              
24             my $cursor = $coll->find({ year => { '$gte' => 2000 } })->sort({ year => -1 });
25             while (my $object = $cursor->next) {
26             ...
27             }
28              
29             my @objects = $cursor->all;
30              
31             =head1 DESCRIPTION
32              
33             This module provides an iterator/cursor for query operations performed
34             on a L using the C/C methods.
35              
36             =head1 ATTRIBUTES
37              
38             =head2 started_iterating
39              
40             A boolean value indicating whether the cursor has started looking for
41             documents in the database. Initially false. When true, setting modifiers
42             such as C, C, C and C is not possible without
43             first calling C.
44              
45             =head2 immortal
46              
47             Boolean value, means nothing in MorboDB.
48              
49             =head2 tailable
50              
51             Boolean value, not implemented in MorboDB.
52              
53             =head2 partial
54              
55             Boolean value, not implemented in MorboDB.
56              
57             =head2 slave_okay
58              
59             Boolean value, not implemented in MorboDB.
60              
61             =cut
62              
63             has 'started_iterating' => (is => 'ro', default => 0, writer => '_set_started_iterating');
64              
65             has 'immortal' => (is => 'rw', default => 0); # unimplemented
66              
67             has 'tailable' => (is => 'rw', default => 0); # unimplemented
68              
69             has 'partial' => (is => 'rw', default => 0); # unimplemented
70              
71             has 'slave_okay' => (is => 'rw', default => 0); # unimplemented
72              
73             has '_coll' => (is => 'ro', required => 1);
74              
75             has '_query' => (is => 'ro', required => 1);
76              
77             has '_fields' => (is => 'ro', writer => '_set_fields', clearer => '_clear_fields');
78              
79             has '_limit' => (is => 'ro', default => 0, writer => '_set_limit', clearer => '_clear_limit');
80              
81             has '_skip' => (is => 'ro', default => 0, writer => '_set_skip', clearer => '_clear_skip');
82              
83             has '_sort' => (is => 'ro', predicate => '_has_sort', writer => '_set_sort', clearer => '_clear_sort');
84              
85             has '_docs' => (is => 'ro', writer => '_set_docs', clearer => '_clear_docs');
86              
87             has '_index' => (is => 'ro', default => 0, writer => '_set_index');
88              
89             =head1 OBJECT METHODS
90              
91             =head2 fields( \%fields )
92              
93             Selects which fields are returned. The default is all fields. C<_id> is always returned.
94             Returns this cursor for chaining operations.
95              
96             =cut
97              
98             sub fields {
99 0     0 1 0 my ($self, $f) = @_;
100              
101 0 0       0 confess 'cannot set fields after querying'
102             if $self->started_iterating;
103              
104 0 0 0     0 confess 'not a hash reference'
105             unless ref $f && ref $f eq 'HASH';
106              
107 0         0 $self->_set_fields($f);
108              
109 0         0 return $self;
110             }
111              
112             =head2 limit( $num )
113              
114             Returns a maximum of C<$num> results. Returns this cursor for chaining operations.
115              
116             =cut
117              
118             sub limit {
119 9     9 1 787 my ($self, $num) = @_;
120              
121 9 50       37 confess 'cannot set limit after querying'
122             if $self->started_iterating;
123              
124 9         28 $self->_set_limit($num);
125              
126 9         26 return $self;
127             }
128              
129             =head2 skip( $num )
130              
131             Skips the first C<$num> results. Returns this cursor for chaining operations.
132              
133             =cut
134              
135             sub skip {
136 0     0 1 0 my ($self, $num) = @_;
137              
138 0 0       0 confess 'cannot set skip after querying'
139             if $self->started_iterating;
140              
141 0         0 $self->_set_skip($num);
142              
143 0         0 return $self;
144             }
145              
146             =head2 sort( $order )
147              
148             Adds a sort to the cursor. Argument is either a hash reference or a
149             L object. Returns this cursor for chaining operations.
150              
151             =cut
152              
153             sub sort {
154 6     6 1 3016 my ($self, $order) = @_;
155              
156 6 100       488 confess 'cannot set sort after querying'
157             if $self->started_iterating;
158              
159 4 100 66     38 if ($order && ref $order eq 'Tie::IxHash') {
    50 33        
160 3         14 $self->_set_sort($order);
161             } elsif ($order && ref $order eq 'HASH') {
162 1         9 my $obj = Tie::IxHash->new;
163 1         17 foreach (keys %$order) {
164 1         6 $obj->Push($_ => $order->{$_});
165             }
166 1         28 $self->_set_sort($obj);
167             } else {
168 0         0 confess 'sort() needs a Tie::IxHash object or a hash reference.';
169             }
170              
171 4         13 return $self;
172             }
173              
174             =head2 snapshot()
175              
176             Not implemented. Simply returns true here.
177              
178             =cut
179              
180             sub snapshot {
181             # NOT IMPLEMENTED YET (IF EVEN SHOULD BE)
182 0     0 1 0 1;
183             }
184              
185             =head2 explain()
186              
187             Not implemented. Simply returns true here.
188              
189             =cut
190              
191             sub explain {
192             # NOT IMPLEMENTED YET
193 0     0 1 0 1;
194             }
195              
196             =head2 reset()
197              
198             Resets the cursor. After being reset, pre-query methods can be called
199             on the cursor (C, C, etc.) and subsequent calls to C,
200             C, or C will re-query the database.
201              
202             =cut
203              
204             sub reset {
205 2     2 1 2028 my $self = shift;
206              
207 2         12 $self->_set_started_iterating(0);
208 2         15 $self->_clear_fields;
209 2         1160 $self->_clear_limit;
210 2         765 $self->_clear_skip;
211 2         6898 $self->_clear_sort;
212 2         809 $self->_clear_docs;
213 2         750 $self->_set_index(0);
214              
215 2         6 return 1;
216             }
217              
218             =head2 info()
219              
220             Not implemented. Returns an empty hash-ref here.
221              
222             =cut
223              
224             sub info {
225             # NOT IMPLEMENTED YET
226 0     0 1 0 {};
227             }
228              
229             =head2 count()
230              
231             Returns the number of documents the query matched.
232              
233             =cut
234              
235             sub count {
236 85     85 1 5192 my $self = shift;
237              
238 85 100       209 unless ($self->started_iterating) {
239             # haven't started iterating yet, let's query the database
240 18         44 $self->_query_db;
241             }
242              
243 85         98 return scalar @{$self->_docs};
  85         390  
244             }
245              
246             =head2 has_next()
247              
248             Checks if there is another result to fetch.
249              
250             =cut
251              
252             sub has_next {
253 58     58 1 957 my $self = shift;
254              
255 58 100       143 unless ($self->started_iterating) {
256             # haven't started iterating yet, let's query the database
257 8         22 $self->_query_db;
258             }
259              
260 58         128 return $self->_index < $self->count;
261             }
262              
263             =head2 next()
264              
265             Returns the next object in the cursor. Returns C if no more data is available.
266              
267             =cut
268              
269             sub next {
270 35     35 1 57 my $self = shift;
271              
272             # return nothing if we've started iterating but have no more results
273 35 50 66     130 return if $self->started_iterating && !$self->has_next;
274              
275 35 100       101 unless ($self->started_iterating) {
276             # haven't started iterating yet, let's query the database
277 9         62 $self->_query_db;
278 9 100       71 return unless $self->count;
279             }
280              
281 33         861 my $doc = clone($self->_coll->_data->{$self->_docs->[$self->_index]});
282 33         139 $self->_inc_index;
283              
284 33 50       76 if ($self->_fields) {
285 0         0 my $ret = {};
286 0         0 foreach (keys %{$self->_fields}) {
  0         0  
287 0 0 0     0 $ret->{$_} = $doc->{$_}
288             if exists $self->_fields->{$_} || $_ eq '_id';
289             }
290 0         0 return $ret;
291             } else {
292 33         96 return $doc;
293             }
294             }
295              
296             =head2 all()
297              
298             Returns an array of all objects in the result.
299              
300             =cut
301              
302             sub all {
303 6     6 1 164 my $self = shift;
304              
305 6         12 my @docs;
306 6         18 while ($self->has_next) {
307 16         40 push(@docs, $self->next);
308             }
309              
310 6         27 return @docs;
311             }
312              
313             sub _query_db {
314 35     35   47 my $self = shift;
315              
316 35         47 my @docs;
317 35         46 my $skipped = 0;
318 35 100       43 foreach (keys %{$self->_coll->_data || {}}) {
  35         198  
319 100 100       423 if (doc_matches($self->_coll->_data->{$_}, $self->_query)) {
320             # are we skipping this? we should only skip
321             # here if we're not sorting, otherwise we
322             # need to do that later, after we've sorted
323 57 50 66     1454 if (!$self->_has_sort && $self->_skip && $skipped < $self->_skip) {
      33        
324 0         0 $skipped++;
325 0         0 next;
326             } else {
327 57         105 push(@docs, $_);
328             }
329             }
330              
331             # have we reached our limit yet? if so, bail, but
332             # only if we're not sorting, otherwise we need to
333             # sort _all_ results first
334 100 100 100     1713 last if $self->_limit && scalar @docs == $self->_limit;
335             }
336              
337             # okay, are we sorting?
338 35 100       110 if ($self->_has_sort) {
339 15         50 @docs = sort {
340             # load the documents
341 4         19 my $doc_a = $self->_coll->_data->{$a};
342 15         47 my $doc_b = $self->_coll->_data->{$b};
343            
344             # start comparing according to $order
345             # this is stolen from my own Giddy::Collection::sort() code
346 15         61 foreach my $attr ($self->_sort->Keys) {
347 17         166 my $dir = $self->_sort->FETCH($attr);
348 17 50 33     261 if (defined $doc_a->{$attr} && !ref $doc_a->{$attr} && defined $doc_b->{$attr} && !ref $doc_b->{$attr}) {
      33        
      33        
349             # are we comparing numerically or alphabetically?
350 17 100 66     147 if ($doc_a->{$attr} =~ m/^\d+(\.\d+)?$/ && $doc_b->{$attr} =~ m/^\d+(\.\d+)?$/) {
351             # numerically
352 15 100       56 if ($dir > 0) {
    50          
353             # when $dir is positive, we want $a to be larger than $b
354 2 100       9 return 1 if $doc_a->{$attr} > $doc_b->{$attr};
355 1 50       5 return -1 if $doc_a->{$attr} < $doc_b->{$attr};
356             } elsif ($dir < 0) {
357             # when $dir is negative, we want $a to be smaller than $b
358 13 100       43 return -1 if $doc_a->{$attr} > $doc_b->{$attr};
359 7 100       32 return 1 if $doc_a->{$attr} < $doc_b->{$attr};
360             }
361             } else {
362             # alphabetically
363 2 50       8 if ($dir > 0) {
    0          
364             # when $dir is positive, we want $a to be larger than $b
365 2 100       12 return 1 if $doc_a->{$attr} gt $doc_b->{$attr};
366 1 50       7 return -1 if $doc_a->{$attr} lt $doc_b->{$attr};
367             } elsif ($dir < 0) {
368             # when $dir is negative, we want $a to be smaller than $b
369 0 0       0 return -1 if $doc_a->{$attr} gt $doc_b->{$attr};
370 0 0       0 return 1 if $doc_a->{$attr} lt $doc_b->{$attr};
371             }
372             }
373             } else {
374             # documents cannot be compared for this attribute
375             # we want documents that have the attribute to appear
376             # earlier in the results, so let's find out if
377             # one of the documents has the attribute
378 0 0 0     0 return -1 if defined $doc_a->{$attr} && !defined $doc_b->{$attr};
379 0 0 0     0 return 1 if defined $doc_b->{$attr} && !defined $doc_a->{$attr};
380            
381             # if we're here, either both documents have the
382             # attribute but it's non comparable (since it's a
383             # reference) or both documents don't have that
384             # attribute at all. in both cases, we consider them
385             # to be equal when comparing these attributes,
386             # so we don't return anything and just continue to
387             # the next attribute to sort according to (if any)
388             }
389             }
390              
391             # if we've reached this point, the documents compare entirely
392             # so we need to return zero
393 0         0 return 0;
394             } @docs;
395              
396             # let's limit (and possibly skip) the results if we need to
397 4 50       20 splice(@docs, 0, $self->_skip)
398             if $self->_skip;
399 4 50 33     26 splice(@docs, $self->_limit, scalar(@docs) - $self->_limit)
400             if $self->_limit && scalar @docs > $self->_limit;
401             }
402              
403 35         120 $self->_set_started_iterating(1);
404 35         99 $self->_set_docs(\@docs);
405             }
406              
407             sub _inc_index {
408 33     33   44 my $self = shift;
409              
410 33         94 $self->_set_index($self->_index + 1);
411             }
412              
413             =head1 DIAGNOSTICS
414              
415             This module throws the following exceptions:
416              
417             =over
418              
419             =item C<< cannot set fields/skip/limit/sort after querying >>
420              
421             This error will be thrown when you're trying to modify the cursor after
422             it has already started querying the database. You can tell if the cursor
423             already started querying the database by taking a look at the C
424             attribute. If you want to modify the cursor after iteration has started,
425             you can used the C method, but the query will have to run again.
426              
427             =item C<< not a hash reference >>
428              
429             This error is thrown by the C method when you're not providing it
430             with a hash-reference of fields like so:
431              
432             $cursor->fields({ name => 1, datetime => 1 });
433              
434             =item C<< sort() needs a Tie::IxHash object or a hash reference. >>
435              
436             This error is thrown by the C method when you're not giving it
437             a hash reference or L object to sort according to, like so:
438              
439             $cursor->sort(Tie::IxHash->new(name => 1, datetime => -1));
440              
441             =back
442              
443             =head1 BUGS AND LIMITATIONS
444              
445             No bugs have been reported.
446              
447             Please report any bugs or feature requests to
448             C, or through the web interface at
449             L.
450              
451             =head1 SEE ALSO
452              
453             L.
454              
455             =head1 AUTHOR
456              
457             Ido Perlmuter
458              
459             =head1 LICENSE AND COPYRIGHT
460              
461             Copyright (c) 2011-2013, Ido Perlmuter C<< ido@ido50.net >>.
462              
463             This module is free software; you can redistribute it and/or
464             modify it under the same terms as Perl itself, either version
465             5.8.1 or any later version. See L
466             and L.
467              
468             The full text of the license can be found in the
469             LICENSE file included with this module.
470              
471             =head1 DISCLAIMER OF WARRANTY
472              
473             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
474             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
475             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
476             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
477             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
478             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
479             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
480             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
481             NECESSARY SERVICING, REPAIR, OR CORRECTION.
482              
483             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
484             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
485             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
486             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
487             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
488             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
489             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
490             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
491             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
492             SUCH DAMAGES.
493              
494             =cut
495              
496             __PACKAGE__->meta->make_immutable;
497             __END__