File Coverage

blib/lib/MongoDB/QueryBuilder.pm
Criterion Covered Total %
statement 173 193 89.6
branch 12 28 42.8
condition 4 23 17.3
subroutine 27 30 90.0
pod 23 23 100.0
total 239 297 80.4


line stmt bran cond sub pod time code
1             # ABSTRACT: Query Builder for MongoDB
2              
3             package MongoDB::QueryBuilder;
4              
5 1     1   53858 use strict;
  1         1  
  1         42  
6 1     1   6 use warnings;
  1         2  
  1         29  
7              
8 1     1   5 use boolean;
  1         12  
  1         6  
9 1     1   873 use Hash::Flatten 'flatten', 'unflatten';
  1         2229  
  1         59  
10 1     1   1202 use Hash::Merge 'merge';
  1         2454  
  1         1922  
11              
12             Hash::Merge::set_behavior('RIGHT_PRECEDENT');
13              
14             our $VERSION = '0.0005'; # VERSION
15              
16              
17              
18             sub new {
19              
20 22     22 1 27080 my $class = shift;
21 22   33     123 $class = ref $class || $class;
22              
23 22         40 my @actions = ();
24              
25 22 100       88 if (scalar @_ == 1) {
    100          
26 1 50 33     10 if (defined $_[0] && ref $_[0] eq 'HASH') {
27 1         2 push @actions, %{(shift(@_))};
  1         5  
28             }
29             }
30              
31             elsif (@_ % 2) {
32 1         15 die sprintf
33             'The new() method for %s expects a hash reference or a '
34             . 'key/value list. You passed an odd number of arguments',
35             $class
36             ;
37             }
38              
39             else {
40 20         41 push @actions, @_;
41             }
42              
43 21         158 my $self = bless {
44              
45             criteria => {
46             select => {},
47             where => {},
48             order => {},
49             options => {}
50             },
51             collection => undef,
52              
53             }, $class;
54              
55             # execute startup actions
56              
57 21         75 for (my $i=0; $i<@actions; $i++) {
58              
59 24         43 my $action = $actions[$i];
60 24         40 my $values = $actions[++$i];
61              
62 24 50       127 $self->$action($values) unless $action eq 'cursor';
63              
64             }
65              
66             # end scene
67              
68 21         74 return $self;
69              
70             }
71              
72             sub _get_args_array {
73              
74 30     30   45 my $self = shift;
75              
76 30 100       111 return "ARRAY" eq ref $_[0] ? $_[0] : [@_];
77              
78             }
79              
80             sub _set_where_clause {
81              
82 15     15   25 my $self = shift;
83 15         24 my $criteria = shift;
84              
85             # collapse and explode query operators
86              
87 15         57 $criteria = flatten $criteria;
88              
89 15         7299 for my $key (keys %{$criteria}) {
  15         58  
90 23 100       91 if ($key =~ /\$/) {
91 21         36 my $nkey = $key; $nkey =~ s/(\w)\$/$1.\$/g;
  21         100  
92 21         94 $criteria->{$nkey} = delete $criteria->{$key};
93             }
94             }
95              
96             # expand and merge conditions
97              
98 15         54 $self->criteria->{where} =
99             merge $self->criteria->{where}, unflatten $criteria
100             ;
101              
102 15         82 return $self;
103              
104             }
105              
106              
107             sub all_in {
108              
109 1     1 1 2 my $self = shift;
110 1         6 my $args = $self->_get_args_array(@_);
111              
112 1         7 my $criteria = {$args->[0] => { '$all' => $args->[1] }};
113              
114 1         5 $self->_set_where_clause($criteria);
115              
116 1         5 return $self;
117              
118             }
119              
120              
121             sub and_where {
122              
123 1     1 1 3 my $self = shift;
124 1         5 my $args = $self->_get_args_array(@_);
125              
126 1         3 my $criteria = {};
127 1         3 my $and = [];
128 1         3 my $i = 0;
129              
130 1         2 while (my($key, $val) = splice @{$args}, 0, 2) {
  3         17  
131 2         7 $and->[$i]->{$key} = $val;
132 2         5 $i++;
133             }
134              
135 1         4 $criteria->{'$and'} = $and;
136              
137 1         4 $self->_set_where_clause($criteria);
138              
139 1         6 return $self;
140              
141             }
142              
143              
144             sub any_in {
145              
146 3     3 1 8 my $self = shift;
147 3         10 my $args = $self->_get_args_array(@_);
148              
149 3         14 my $criteria = {$args->[0] => {'$in' => $args->[1]}};
150              
151 3         12 $self->_set_where_clause($criteria);
152              
153 3         16 return $self;
154              
155             }
156              
157              
158             sub asc_sort {
159              
160 1     1 1 3 my $self = shift;
161 1         3 my $args = $self->_get_args_array(@_);
162              
163 1         2 foreach my $key (@{$args}) {
  1         3  
164 2         7 $self->criteria->{order}->{$key} = 1;
165             }
166              
167 1         5 return $self;
168              
169             }
170              
171              
172              
173             sub collection {
174              
175 0     0 1 0 my $self = shift;
176 0         0 my $args = $self->_get_args_array(@_);
177              
178 0         0 my $collection = $args->[0];
179              
180 0         0 my $die_msg = sprintf
181             'The collection() method for %s requires a ' .
182             'MongoDB compatible collection object',
183             ref $self
184             ;
185              
186 0 0 0     0 die $die_msg if
      0        
      0        
      0        
      0        
187             (@_ && !$collection) or
188             (@_ && $collection) and
189             !$collection->isa('MongoDB::Collection') &&
190             !$collection->isa('Mango::Collection')
191             ;
192              
193 0 0       0 $self->{collection} = $collection if $collection;
194              
195 0         0 return $self->{collection};
196              
197             }
198              
199              
200             sub criteria {
201              
202 66     66 1 23919 my ($self) = @_;
203              
204 66         326 return $self->{criteria};
205              
206             }
207              
208              
209             sub criteria_where {
210              
211 0     0 1 0 my ($self) = @_;
212              
213 0         0 return $self->{criteria}->{where};
214              
215             }
216              
217              
218             sub cursor {
219              
220 0     0 1 0 my $self = shift;
221              
222 0         0 my $cri = $self->criteria;
223 0         0 my $col = $self->collection;
224 0         0 my $cur = $col->find($cri->{where});
225              
226 0 0       0 $cur->fields($cri->{select}) if values %{$cri->{select}};
  0         0  
227 0 0       0 $cur->sort($cri->{order}) if values %{$cri->{order}};
  0         0  
228 0 0       0 $cur->limit($cri->{options}->{limit}) if $cri->{options}->{limit};
229 0 0       0 $cur->skip($cri->{options}->{offset}) if $cri->{options}->{offset};
230              
231 0         0 return $cur;
232             }
233              
234              
235             sub desc_sort {
236              
237 1     1 1 3 my $self = shift;
238 1         4 my $args = $self->_get_args_array(@_);
239              
240 1         2 foreach my $key (@{$args}) {
  1         3  
241 2         6 $self->criteria->{order}->{$key} = -1;
242             }
243              
244 1         5 return $self;
245              
246             }
247              
248              
249             sub limit {
250              
251 4     4 1 6 my $self = shift;
252 4         12 my $args = $self->_get_args_array(@_);
253              
254 4 50       21 $self->criteria->{options}->{limit} = $args->[0] if $args->[0];
255              
256 4         12 return $self;
257              
258             }
259              
260              
261             sub near {
262              
263 1     1 1 3 my $self = shift;
264 1         4 my $args = $self->_get_args_array(@_);
265              
266 1         7 my $criteria_nil = {$args->[0] => {'$near' => []}};
267 1         4 my $criteria_set = {$args->[0] => {'$near' => $args->[1]}};
268              
269 1         6 $self->_set_where_clause($criteria_nil);
270 1         3 $self->_set_where_clause($criteria_set);
271              
272 1         6 return $self;
273              
274             }
275              
276              
277             sub never {
278              
279 1     1 1 3 my $self = shift;
280 1         4 my $args = $self->_get_args_array(@_);
281              
282 1         3 foreach my $key (@{$args}) {
  1         3  
283 2         7 $self->criteria->{select}->{$key} = 0;
284             }
285              
286 1         6 return $self;
287              
288             }
289              
290              
291             sub nor_where {
292              
293 1     1 1 3 my $self = shift;
294 1         5 my $args = $self->_get_args_array(@_);
295              
296 1         3 my $criteria = {};
297 1         2 my $nor = [];
298 1         4 my $i = 0;
299              
300 1         2 while (my($key, $val) = splice @{$args}, 0, 2) {
  3         14  
301 2         8 $nor->[$i]->{$key} = $val;
302 2         5 $i++;
303             }
304              
305 1         3 $criteria->{'$nor'} = $nor;
306              
307 1         5 $self->_set_where_clause($criteria);
308              
309 1         7 return $self;
310              
311             }
312              
313              
314             sub not_in {
315              
316 1     1 1 3 my $self = shift;
317 1         5 my $args = $self->_get_args_array(@_);
318              
319 1         5 my $criteria = {$args->[0] => {'$nin' => $args->[1]}};
320              
321 1         4 $self->_set_where_clause($criteria);
322              
323 1         5 return $self;
324              
325             }
326              
327              
328             sub offset {
329              
330 4     4 1 10 my $self = shift;
331 4         12 my $args = $self->_get_args_array(@_);
332              
333 4 50       21 $self->criteria->{options}->{offset} = $args->[0] if defined $args->[0];
334              
335 4         28 return $self;
336              
337             }
338              
339              
340             sub only {
341              
342 1     1 1 4 my $self = shift;
343 1         4 my $args = $self->_get_args_array(@_);
344              
345 1         2 foreach my $key (@{$args}) {
  1         4  
346 2         7 $self->criteria->{select}->{$key} = 1;
347             }
348              
349 1         4 return $self;
350              
351             }
352              
353              
354             sub or_where {
355              
356 1     1 1 3 my $self = shift;
357 1         20 my $args = $self->_get_args_array(@_);
358              
359 1         2 my $criteria = {};
360 1         4 my $or = [];
361 1         2 my $i = 0;
362              
363 1         4 while (my($key, $val) = splice @{$args}, 0, 2) {
  3         17  
364 2         14 $or->[$i]->{$key} = $val;
365 2         5 $i++;
366             }
367              
368 1         3 $criteria->{'$or'} = $or;
369              
370 1         4 $self->_set_where_clause($criteria);
371              
372 1         7 return $self;
373              
374             }
375              
376              
377             sub page {
378              
379 3     3 1 8 my $self = shift;
380 3         10 my $args = $self->_get_args_array(@_);
381 3         9 my $limit = $args->[0];
382 3   100     16 my $page = $args->[1] || 0;
383 3         8 my $offset = $limit * $page;
384              
385 3         11 $self->limit($limit);
386              
387 3         10 $self->offset($offset);
388              
389 3         13 return $self;
390              
391             }
392              
393              
394             sub sort {
395              
396 1     1 1 2 my $self = shift;
397 1         4 my $args = $self->_get_args_array(@_);
398              
399 1         4 while (my($key, $val) = splice @{$args}, 0, 2) {
  3         14  
400 2         6 $self->criteria->{order}->{$key} = $val;
401             }
402              
403 1         4 return $self;
404              
405             }
406              
407              
408             sub where {
409              
410 3     3 1 5 my $self = shift;
411 3         9 my $args = $self->_get_args_array(@_);
412              
413 3         6 my $criteria = {};
414              
415 3         7 while (my($key, $val) = splice @{$args}, 0, 2) {
  6         27  
416 3         12 $criteria->{$key} = $val;
417             }
418              
419 3         12 $self->_set_where_clause($criteria);
420              
421 3         21 return $self;
422              
423             }
424              
425              
426             sub where_exists {
427              
428 1     1 1 2 my $self = shift;
429 1         5 my $args = $self->_get_args_array(@_);
430              
431 1         4 my $criteria = {};
432              
433 1         3 foreach my $key (@{$args}) {
  1         3  
434 1         9 $criteria->{$key}->{'$exists'} = boolean::true;
435             }
436              
437 1         18 $self->_set_where_clause($criteria);
438              
439 1         5 return $self;
440              
441             }
442              
443              
444              
445             sub where_not_exists {
446              
447 1     1 1 2 my $self = shift;
448 1         4 my $args = $self->_get_args_array(@_);
449              
450 1         2 my $criteria = {};
451              
452 1         3 foreach my $key (@{$args}) {
  1         4  
453 1         5 $criteria->{$key}->{'$exists'} = boolean::false;
454             }
455              
456 1         9 $self->_set_where_clause($criteria);
457              
458 1         6 return $self;
459              
460             }
461              
462             1;
463              
464             __END__
465              
466             =pod
467              
468             =head1 NAME
469              
470             MongoDB::QueryBuilder - Query Builder for MongoDB
471              
472             =head1 VERSION
473              
474             version 0.0005
475              
476             =head1 SYNOPSIS
477              
478             use MongoDB;
479             use MongoDB::QueryBuilder;
480              
481             # build query conditions
482              
483             my $query = MongoDB::QueryBuilder->new(
484             and_where => ['cd.title' => $some_title],
485             and_where => ['cd.released$gt$date' => $some_isodate],
486             any_in => ['cd.artist' => $some_artist],
487             page => [25, 0],
488             );
489              
490             # .. in sql
491             # .. select * from cds cd where cd.title = ? and
492             # .. cd.released > ? and cd.artist IN (?) limit 25 offset 0
493              
494             # connect and query
495              
496             my $client = MongoDB::MongoClient->new(host => 'localhost:27017');
497             my $database = $client->get_database('musicbox');
498             my $collection = $query->collection($database->get_collection('cds'));
499             my $cursor = $query->cursor;
500              
501             while (my $album = $cursor->next) {
502             say $album->{name};
503             }
504              
505             =head1 DESCRIPTION
506              
507             MongoDB::QueryBuilder provides an interface to query L<MongoDB> using chainable
508             objects for building complex and dynamic queries. This module will only hit the
509             database when you ask it to return a L<MongoDB::Cursor> object.
510              
511             =head1 METHODS
512              
513             =head2 new
514              
515             The new method is the object constructor, it accepts a single hashref or list
516             containing action/value pairs where an action is a MongoDB::QueryBuilder class
517             method and a value is a scalar or arrayref. All methods that do not interact
518             with the database can be passed as an argument.
519              
520             my $query = MongoDB::QueryBuilder->new(
521             where => ['record_label' => 'Time-Warner'],
522             limit => 25
523             );
524              
525             =head2 all_in
526              
527             The all_in method adds a criterion which returns documents where the field
528             specified holds an array which contains all of the values specified. The
529             corresponding MongoDB operation is $all.
530              
531             $query->all_in(tags => ['hip-hop', 'rap']);
532              
533             # e.g. { "tags" : { "$all" : ['hip-hop', 'rap'] } }
534              
535             Please see L<http://docs.mongodb.org/manual/reference/operator/all/> for more
536             information.
537              
538             =head2 and_where
539              
540             The and_where method adds a criterion which returns documents where the clauses
541             specified must all match in order to return results. The corresponding MongoDB
542             operation is $and.
543              
544             $query->and_where('quantity$lt' => 50, status => 'available');
545              
546             # e.g. { "$and" : [ { "quantity" : { "$lt" : 50 } }, { "status" : "available" } ] }
547              
548             Please see L<http://docs.mongodb.org/manual/reference/operator/and/> for more
549             information.
550              
551             =head2 any_in
552              
553             The any_in method adds a criterion which returns documents where the field
554             specified must holds one of the values specified in order to return results.
555             The corresponding MongoDB operation is $in.
556              
557             $query->any_in(tags => ['hip-hop', 'rap']);
558              
559             # e.g. { "tags" : { "$in" : ['hip-hop', 'rap'] } }
560              
561             Please see L<http://docs.mongodb.org/manual/reference/operator/in/> for more
562             information.
563              
564             =head2 asc_sort
565              
566             The asc_sort method adds a criterion that instructs the L<MongoDB::Cursor>
567             object to sort the results on specified key(s) in ascending order.
568              
569             $query->asc_sort('artist.first_name', 'artist.last_name');
570              
571             =head2 collection
572              
573             The collection method provides an accessor for the L<MongoDB::Collection>
574             object used to query the database.
575              
576             my $collection = $query->collection($new_collection);
577              
578             =head2 criteria
579              
580             The criteria method provides an accessor for the query-specification-object used
581             to query the database.
582              
583             my $criteria = $query->criteria;
584              
585             =head2 criteria_where
586              
587             The criteria_where method is a convenience method which provides access to the
588             query-specification where-clause.
589              
590             my $criteria = $query->criteria_where
591              
592             =head2 cursor
593              
594             The cursor method analyzes the current query criteria, generates a
595             L<MongoDB::Cursor> object and queries the databases.
596              
597             my $cursor = $query->cursor;
598              
599             =head2 desc_sort
600              
601             The desc_sort method adds a criterion that instructs the L<MongoDB::Cursor>
602             object to sort the results on specified key(s) in ascending order.
603              
604             $query->desc_sort('artist.first_name', 'artist.last_name');
605              
606             =head2 limit
607              
608             The limit method adds a criterion that instructs the L<MongoDB::Cursor> object
609             to limit the results by the number specified.
610              
611             $query->limit(25);
612              
613             =head2 near
614              
615             The near method adds a criterion to find locations that are near the supplied
616             coordinates. This performs a MongoDB $near selection and requires a 2d index on
617             the field specified. The corresponding MongoDB operation is $near.
618              
619             $query->near('store.location.latlng' => [52.30, 13.25]);
620              
621             # e.g. { "store.location.latlng" : { "$near" : [52.30, 13.25] } }
622              
623             Please see L<http://docs.mongodb.org/manual/reference/operator/near/> for more
624             information.
625              
626             =head2 never
627              
628             The never method adds a criterion that instructs the L<MongoDB::Cursor> object
629             to select all columns except the ones specified. The opposite of this is the
630             only() method, these two methods can't be used together.
631              
632             $query->never('password', 'apikey');
633              
634             =head2 nor_where
635              
636             The nor_where method adds a criterion which returns documents where none of the
637             clauses specified should match in order to return results. The corresponding
638             MongoDB operation is $nor.
639              
640             $query->nor_where('quantity$lte' => 30, 'quantity$gte' => 10);
641              
642             # e.g. { "$nor" : [ { "quantity" : { "$lte" : 30 } }, { "quantity" : { "$gte" : 10 } } ] }
643              
644             Please see L<http://docs.mongodb.org/manual/reference/operator/nor/> for more
645             information.
646              
647             =head2 not_in
648              
649             The not_in method adds a criterion which returns documents where the field
650             specified must not hold any of the values specified in order to return results.
651             The corresponding MongoDB operation is $nin.
652              
653             $query->not_in('artist.last_name' => ['Jackson', 'Nelson']);
654              
655             # e.g. { "artist.last_name" : { "$nin" : ['Jackson', 'Nelson'] } }
656              
657             Please see L<http://docs.mongodb.org/manual/reference/operator/nin/> for more
658             information.
659              
660             =head2 offset
661              
662             The offset method adds a criterion that instructs the L<MongoDB::Cursor> object
663             to offset the results by the number specified.
664              
665             $query->limit(25);
666              
667             =head2 only
668              
669             The only method adds a criterion that instructs the L<MongoDB::Cursor> object
670             to select only the columns specified. The opposite of this is the never() method,
671             these two methods can't be used together.
672              
673             $query->only('artist.first_name', 'artist.last_name');
674              
675             =head2 or_where
676              
677             The or_where method adds a criterion which returns documents where at-least one
678             of the clauses specified must match in order to return results. The
679             corresponding MongoDB operation is $or.
680              
681             $query->or_where('quantity$lte' => 30, 'quantity$gte' => 10);
682              
683             # e.g. { "$or" : [ { "quantity" : { "$lte" : 30 } }, { "quantity" : { "$gte" : 10 } } ] }
684              
685             Please see L<http://docs.mongodb.org/manual/reference/operator/or/> for more
686             information.
687              
688             =head2 page
689              
690             The page method is a purely a convenience method which adds a limit and offset
691             criterion to the query. The page parameter is optional and defaults to 0, it
692             should be a number that when multiplied by the limit will represents how many
693             documents should be offset.
694              
695             $query->page($limit, $page); # page is optional and defaults to 0
696              
697             =head2 sort
698              
699             The sort method adds a criterion that instructs the L<MongoDB::Cursor>
700             object to sort the results on specified key(s) in the specified order.
701              
702             $query->sort('artist.first_name' => -1, 'artist.last_name' => 1);
703              
704             =head2 where
705              
706             The where method adds a criterion which returns documents where all or the
707             clauses specified must be truthful in order to return results.
708              
709             $query->where('agent.office.location$within$center' => [[0,0],10]);
710              
711             # e.g. { "agent.office.location" : { "$within" : { "$center" : [[0,0],10] } } }
712              
713             =head2 where_exists
714              
715             The where_exists method adds a criterion which returns documents where the
716             fields specified must all exist in order to return results. The corresponding
717             MongoDB operation is $exists.
718              
719             $query->where_exists('artist.email_address', 'artist.phone_number');
720              
721             # e.g. { "artist.email_address" : { "$exists" : true }, "artist.phone_number" : { "$exists" : true } }
722              
723             Please see L<http://docs.mongodb.org/manual/reference/operator/exists/> for more
724             information.
725              
726             =head2 where_not_exists
727              
728             The where_not_exists method adds a criterion which returns documents where the
729             fields specified must NOT exist in order to return results. The corresponding
730             MongoDB operation is $exists.
731              
732             $query->where_not_exists('artist.terminated');
733              
734             # e.g. { "artist.terminated" : { "$exists" : false } }
735              
736             Please see L<http://docs.mongodb.org/manual/reference/operator/exists/> for more
737             information.
738              
739             =head1 AUTHOR
740              
741             Al Newkirk <anewkirk@ana.io>
742              
743             =head1 COPYRIGHT AND LICENSE
744              
745             This software is copyright (c) 2011 by Al Newkirk.
746              
747             This is free software; you can redistribute it and/or modify it under
748             the same terms as the Perl 5 programming language system itself.
749              
750             =cut