File Coverage

blib/lib/MQUL.pm
Criterion Covered Total %
statement 258 279 92.4
branch 232 322 72.0
condition 122 221 55.2
subroutine 20 22 90.9
pod 2 2 100.0
total 634 846 74.9


line stmt bran cond sub pod time code
1             package MQUL;
2              
3             # ABSTRACT: General purpose, MongoDB-style query and update language
4              
5             BEGIN {
6 3     3   47176 use Exporter 'import';
  3         8  
  3         122  
7 3     3   50 @EXPORT_OK = qw/doc_matches update_doc/;
8             }
9              
10 3     3   10 use warnings;
  3         4  
  3         65  
11 3     3   12 use strict;
  3         3  
  3         79  
12 3     3   10 use Carp;
  3         6  
  3         198  
13 3     3   1472 use Data::Compare;
  3         26745  
  3         16  
14 3     3   10071 use Data::Types qw/:is/;
  3         3265  
  3         373  
15 3     3   1323 use DateTime::Format::W3CDTF;
  3         327369  
  3         102  
16 3     3   25 use Scalar::Util qw/blessed/;
  3         4  
  3         184  
17 3     3   12 use Try::Tiny;
  3         5  
  3         8934  
18              
19             our $VERSION = "2.000000";
20             $VERSION = eval $VERSION;
21              
22             =head1 NAME
23              
24             MQUL - General purpose, MongoDB-style query and update language
25              
26             =head1 SYNOPSIS
27              
28             use MQUL qw/doc_matches update_doc/;
29              
30             my $doc = {
31             title => 'Freaks and Geeks',
32             genres => [qw/comedy drama/],
33             imdb_score => 9.4,
34             seasons => 1,
35             starring => ['Linda Cardellini', 'James Franco', 'Jason Segel'],
36             likes => { up => 45, down => 11 }
37             };
38              
39             if (doc_matches($doc, {
40             title => qr/geeks/i,
41             genres => 'comedy',
42             imdb_score => { '$gte' => 5, '$lte' => 9.5 },
43             starring => { '$type' => 'array', '$size' => 3 },
44             'likes.up' => { '$gt' => 40 }
45             })) {
46             # will be true in this example
47             }
48              
49             update_doc($doc, {
50             '$set' => { title => 'Greeks and Feaks' },
51             '$pop' => { genres => 1 },
52             '$inc' => { imdb_score => 0.6 },
53             '$unset' => { seasons => 1 },
54             '$push' => { starring => 'John Francis Daley' },
55             });
56              
57             # $doc will now be:
58             {
59             title => 'Greeks and Feaks',
60             genres => ['comedy'],
61             imdb_score => 10,
62             starring => ['Linda Cardellini', 'James Franco', 'Jason Segel', 'John Francis Daley'],
63             likes => { up => 45, down => 11 }
64             }
65              
66             =head1 DESCRIPTION
67              
68             MQUL (for BongoDB-style Buery & Bpdate Banguage; pronounced
69             I<"umm, cool">; yeah, I know, that's the dumbest thing ever), is a general
70             purpose implementation of L's query and update language. The
71             implementation is not 100% compatible, but it only slightly deviates from
72             MongoDB's behavior, actually extending it a bit.
73              
74             The module exports two subroutines: C and C.
75             The first subroutine takes a document, which is really just a hash-ref (of
76             whatever complexity), and a query hash-ref built in the MQUL query language.
77             It returns a true value if the document matches the query, and a
78             false value otherwise. The second subroutine takes a document and an update
79             hash-ref built in the MQUL update language. The subroutine modifies the document
80             (in-place) according to the update hash-ref.
81              
82             You can use this module for whatever purpose you see fit. It was actually
83             written for L, my Git-database, and was extracted from its
84             original code. Outside of the database world, I plan to use it in an application
85             that performs tests (such as process monitoring for example), and uses the
86             query language to determine whether the results are valid or not (in our
87             monitoring example, that could be CPU usage above a certain threshold and
88             stuff like that). It is also used by L, an in-memory clone of
89             MongoDB.
90              
91             =head2 UPGRADE NOTES
92              
93             My distributions follow the L,
94             so whenever the major version changes, that means that API changes incompatible
95             with previous versions have been made. Always read the Changes file before upgrading.
96              
97             =head2 THE LANGUAGE
98              
99             The language itself is described in L. This document
100             only describes the interface of this module.
101              
102             The reference document also details MQUL's current differences from the
103             original MongoDB language.
104              
105             =cut
106              
107             our %BUILTINS = (
108             '$abs' => sub {
109             ##############################################
110             # abs( $value ) #
111             # ========================================== #
112             # $value - a numerical value #
113             # ------------------------------------------ #
114             # returns the absolute value of $value #
115             ##############################################
116             abs shift;
117             },
118             '$min' => sub {
119             ##############################################
120             # min( @values ) #
121             # ========================================== #
122             # @values - a list of numerical values #
123             # ------------------------------------------ #
124             # returns the smallest number in @values #
125             ##############################################
126             my $min = shift;
127             foreach (@_) {
128             $min = $_ if $_ < $min;
129             }
130             return $min;
131             },
132             '$max' => sub {
133             ##############################################
134             # max( @values ) #
135             # ========================================== #
136             # @values - a list of numerical values #
137             # ------------------------------------------ #
138             # returns the largest number in @values #
139             ##############################################
140             my $max = shift;
141             foreach (@_) {
142             $max = $_ if $_ > $max;
143             }
144             return $max;
145             },
146             '$diff' => sub {
147             ##############################################
148             # diff( @values ) #
149             # ========================================== #
150             # @values - a list of numerical values #
151             # ------------------------------------------ #
152             # returns the difference between the values #
153             ##############################################
154             my $diff = shift;
155             foreach (@_) {
156             $diff -= $_;
157             }
158             return $diff;
159             },
160             '$sum' => sub {
161             ##############################################
162             # sum( @values ) #
163             # ========================================== #
164             # @values - a list of numerical values #
165             # ------------------------------------------ #
166             # returns the summation of the values #
167             ##############################################
168             my $sum = shift;
169             foreach (@_) {
170             $sum += $_;
171             }
172             return $sum;
173             },
174             '$product' => sub {
175             ##############################################
176             # product( @values ) #
177             # ========================================== #
178             # @values - a list of numerical values #
179             # ------------------------------------------ #
180             # returns the product of the values #
181             ##############################################
182             my $prod = shift;
183             foreach (@_) {
184             $prod *= $_;
185             }
186             return $prod;
187             },
188             '$div' => sub {
189             ##############################################
190             # div( @values ) #
191             # ========================================== #
192             # @values - a list of numerical values #
193             # ------------------------------------------ #
194             # returns the division of the values. #
195             # if the function encounters zero anywhere #
196             # after the first value, it will immediately #
197             # return zero instead of raise an error. #
198             ##############################################
199             my $div = shift;
200             foreach (@_) {
201             return 0 if $_ == 0;
202             $div /= $_;
203             }
204             return $div;
205             }
206             );
207              
208             =head1 INTERFACE
209              
210             =head2 doc_matches( \%document, [ \%query, \@defs ] )
211              
212             Receives a document hash-ref and possibly a query hash-ref, and returns
213             true if the document matches the query, false otherwise. If no query
214             is given (or an empty hash-ref is given), true will be returned (every
215             document will match an empty query - in accordance with MongoDB).
216              
217             See L to learn about the structure of
218             query hash-refs.
219              
220             Optionally, an even-numbered array reference of dynamically calculated
221             attribute definitions can be provided. For example:
222              
223             [ min_val => { '$min' => ['attr1', 'attr2', 'attr3' ] },
224             max_val => { '$max' => ['attr1', 'attr2', 'attr3' ] },
225             difference => { '$diff' => ['max_val', 'min_val'] } ]
226              
227             This defines three dynamic attributes: C, C and
228             C, which is made up of the first two.
229              
230             See L for more information
231             about dynamic attributes.
232              
233             =cut
234              
235             sub doc_matches {
236 98     98 1 2989 my ($doc, $query, $defs) = @_;
237              
238 98 100 100     976 croak 'MQUL::doc_matches() requires a document hash-ref.'
      100        
239             unless $doc && ref $doc && ref $doc eq 'HASH';
240 95 100 100     570 croak 'MQUL::doc_matches() expects a query hash-ref.'
      66        
241             if $query && (!ref $query || ref $query ne 'HASH');
242 93 50 33     240 croak 'MQUL::doc_matches() expects an even-numbered definitions array-ref.'
      66        
243             if $defs && (!ref $defs || ref $defs ne 'ARRAY' || scalar @$defs % 2 != 0);
244              
245 93   100     148 $query ||= {};
246              
247 93 100       138 if ($defs) {
248 17         40 for (my $i = 0; $i < scalar(@$defs) - 1; $i = $i + 2) {
249 21         34 my ($name, $def) = ($defs->[$i], $defs->[$i+1]);
250 21         29 $doc->{$name} = _parse_function($doc, $def);
251             }
252             }
253              
254             # go over each key of the query
255 93         365 foreach my $key (keys %$query) {
256 111         127 my $value = $query->{$key};
257 111 100 66     247 if ($key eq '$or' && ref $value eq 'ARRAY') {
258 11         9 my $found;
259 11         17 foreach (@$value) {
260 18 50       32 next unless ref $_ eq 'HASH';
261 18         14 my $ok = 1;
262              
263 18         48 while (my ($k, $v) = each %$_) {
264 19 100       24 unless (&_attribute_matches($doc, $k, $v)) {
265 11         46 undef $ok;
266 11         11 last;
267             }
268             }
269              
270 18 100       35 if ($ok) { # document matches this criteria
271 7         6 $found = 1;
272 7         8 last;
273             }
274             }
275 11 100       34 return unless $found;
276             } else {
277 100 100       212 return unless &_attribute_matches($doc, $key, $value);
278             }
279             }
280              
281             # if we've reached here, the document matches, so return true
282 65         201 return 1;
283             }
284              
285             ##############################################
286             # _attribute_matches( $doc, $key, $value ) #
287             # ========================================== #
288             # $doc - the document hash-ref #
289             # $key - the attribute being checked #
290             # $value - the constraint for the attribute #
291             # taken from the query hash-ref #
292             # ------------------------------------------ #
293             # returns true if constraint is met in the #
294             # provided document. #
295             ##############################################
296              
297             my $funcs = join('|', keys %BUILTINS);
298              
299             sub _attribute_matches {
300 119     119   147 my ($doc, $key, $value) = @_;
301              
302 119         88 my %virt;
303 119 100       207 if ($key =~ m/\./) {
304             # support for the dot notation
305 17         21 my ($v, $k) = _expand_dot_notation($doc, $key);
306              
307 17         16 $key = $k;
308 17 100       34 $virt{$key} = $v
309             if defined $v;
310             } else {
311 102 100       233 $virt{$key} = $doc->{$key}
312             if exists $doc->{$key};
313             }
314              
315 119 100 33     469 if (!ref $value) { # if value is a scalar, we need to check for equality
    50 66        
    100          
    100          
    50          
316             # (or, if the attribute is an array in the document,
317             # we need to check the value exists in it)
318 26 100       51 return unless defined $virt{$key};
319 25 50       58 if (ref $virt{$key} eq 'ARRAY') { # check the array has the requested value
    50          
320 0 0       0 return unless &_array_has_eq($value, $virt{$key});
321             } elsif (!ref $virt{$key}) { # check the values are equal
322 25 100       86 return unless $virt{$key} eq $value;
323             } else { # we can't compare a non-scalar to a scalar, so return false
324 0         0 return;
325             }
326             } elsif (blessed $value && (blessed $value eq 'MongoDB::OID' || blessed $value eq 'MorboDB::OID')) {
327             # we're trying to compare MongoDB::OIDs/MorboDB::OIDs
328             # (MorboDB is my in-memory clone of MongoDB)
329 0 0       0 return unless defined $virt{$key};
330 0 0 0     0 if (blessed $virt{$key} && (blessed $virt{$key} eq 'MongoDB::OID' || blessed $virt{$key} eq 'MorboDB::OID')) {
      0        
331 0 0       0 return unless $virt{$key}->value eq $value->value;
332             } else {
333 0         0 return;
334             }
335             } elsif (ref $value eq 'Regexp') { # if the value is a regex, we need to check
336             # for a match (or, if the attribute is an array
337             # in the document, we need to check at least one
338             # value in it matches it)
339 2 50       24 return unless defined $virt{$key};
340 2 50       6 if (ref $virt{$key} eq 'ARRAY') {
    50          
341 0 0       0 return unless &_array_has_re($value, $virt{$key});
342             } elsif (!ref $virt{$key}) { # check the values match
343 2 100       15 return unless $virt{$key} =~ $value;
344             } else { # we can't compare a non-scalar to a scalar, so return false
345 0         0 return;
346             }
347             } elsif (ref $value eq 'HASH') { # if the value is a hash, than it either contains
348             # advanced queries, or it's just a hash that we
349             # want the document to have as-is
350 90 100       120 unless (&_has_adv_que($value)) {
351             # value hash-ref doesn't have any advanced
352             # queries, we need to check our document
353             # has an attributes with exactly the same hash-ref
354             # (and name of course)
355 2 50       12 return unless Compare($value, $virt{$key});
356             } else {
357             # value contains advanced queries,
358             # we need to make sure our document has an
359             # attribute with the same name that matches
360             # all these queries
361 88         180 foreach my $q (keys %$value) {
362 96         144 my $term = $value->{$q};
363 96 100 66     832 if ($q eq '$gt') {
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 33        
    100          
    100          
    100          
    100          
    100          
    50          
364 14 50 33     57 return unless defined $virt{$key} && !ref $virt{$key};
365 14 50       35 if (is_float($virt{$key})) {
366 14 100       185 return unless $virt{$key} > $term;
367             } else {
368 0 0       0 return unless $virt{$key} gt $term;
369             }
370             } elsif ($q eq '$gte') {
371 8 50 33     35 return unless defined $virt{$key} && !ref $virt{$key};
372 8 50       17 if (is_float($virt{$key})) {
373 8 100       130 return unless $virt{$key} >= $term;
374             } else {
375 0 0       0 return unless $virt{$key} ge $term;
376             }
377             } elsif ($q eq '$lt') {
378 6 50 33     29 return unless defined $virt{$key} && !ref $virt{$key};
379 6 100       16 if (is_float($virt{$key})) {
380 5 100       54 return unless $virt{$key} < $term;
381             } else {
382 1 50       11 return unless $virt{$key} lt $term;
383             }
384             } elsif ($q eq '$lte') {
385 5 50 33     31 return unless defined $virt{$key} && !ref $virt{$key};
386 5 50       13 if (is_float($virt{$key})) {
387 5 100       76 return unless $virt{$key} <= $term;
388             } else {
389 0 0       0 return unless $virt{$key} le $term;
390             }
391             } elsif ($q eq '$eq') {
392 1 50 33     8 return unless defined $virt{$key} && !ref $virt{$key};
393 1 50       6 if (is_float($virt{$key})) {
394 0 0       0 return unless $virt{$key} == $term;
395             } else {
396 1 50       15 return unless $virt{$key} eq $term;
397             }
398             } elsif ($q eq '$ne') {
399 2 50 33     12 return unless defined $virt{$key} && !ref $virt{$key};
400 2 100       6 if (is_float($virt{$key})) {
401 1 50       21 return unless $virt{$key} != $term;
402             } else {
403 1 50       9 return unless $virt{$key} ne $term;
404             }
405             } elsif ($q eq '$exists') {
406 15 100       20 if ($term) {
407 10 100       69 return unless exists $virt{$key};
408             } else {
409 5 100       18 return if exists $virt{$key};
410             }
411             } elsif ($q eq '$mod' && ref $term eq 'ARRAY' && scalar @$term == 2) {
412 4 100 33     14 return unless defined $virt{$key} && is_float($virt{$key}) && $virt{$key} % $term->[0] == $term->[1];
      66        
413             } elsif ($q eq '$in' && ref $term eq 'ARRAY') {
414 4 100 66     13 return unless defined $virt{$key} && &_value_in($virt{$key}, $term);
415             } elsif ($q eq '$nin' && ref $term eq 'ARRAY') {
416 3 100 66     13 return unless defined $virt{$key} && !&_value_in($virt{$key}, $term);
417             } elsif ($q eq '$size' && is_int($term)) {
418 4 100 66     58 return unless defined $virt{$key} && ((ref $virt{$key} eq 'ARRAY' && scalar @{$virt{$key}} == $term) || (ref $virt{$key} eq 'HASH' && scalar keys %{$virt{$key}} == $term));
      33        
419             } elsif ($q eq '$all' && ref $term eq 'ARRAY') {
420 3 50 33     22 return unless defined $virt{$key} && ref $virt{$key} eq 'ARRAY';
421 3         6 foreach (@$term) {
422 6 100       30 return unless &_value_in($_, $virt{$key});
423             }
424             } elsif ($q eq '$type' && !ref $term) {
425 27 100       110 if ($term eq 'int') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
426 3 100 66     14 return unless defined $virt{$key} && is_int($virt{$key});
427             } elsif ($term eq 'float') {
428 4 50 33     18 return unless defined $virt{$key} && is_float($virt{$key});
429             } elsif ($term eq 'real') {
430 2 50 33     24 return unless defined $virt{$key} && is_real($virt{$key});
431             } elsif ($term eq 'whole') {
432 2 100 66     15 return unless defined $virt{$key} && is_whole($virt{$key});
433             } elsif ($term eq 'string') {
434 2 50 33     18 return unless defined $virt{$key} && is_string($virt{$key});
435             } elsif ($term eq 'array') {
436 2 50 33     16 return unless defined $virt{$key} && ref $virt{$key} eq 'ARRAY';
437             } elsif ($term eq 'hash') {
438 5 100 66     39 return unless defined $virt{$key} && ref $virt{$key} eq 'HASH';
439             } elsif ($term eq 'bool') {
440             # boolean - not really supported, will always return true since everything in Perl is a boolean
441             } elsif ($term eq 'date') {
442 3 50 33     13 return unless defined $virt{$key} && !ref $virt{$key};
443 3     3   27 my $date = try { DateTime::Format::W3CDTF->parse_datetime($virt{$key}) } catch { undef };
  3         96  
  1         25  
444 3 100 66     961 return unless blessed $date && blessed $date eq 'DateTime';
445             } elsif ($term eq 'null') {
446 2 100 66     16 return unless exists $virt{$key} && !defined $virt{$key};
447             } elsif ($term eq 'regex') {
448 1 50 33     8 return unless defined $virt{$key} && ref $virt{$key} eq 'Regexp';
449             }
450             }
451             }
452             }
453             } elsif (ref $value eq 'ARRAY') {
454 1 50       4 return unless Compare($value, $virt{$key});
455             }
456              
457 84         845 return 1;
458             }
459              
460             ##############################################
461             # _array_has_eq( $value, \@array ) #
462             # ========================================== #
463             # $value - the value to check for #
464             # $array - the array to search in #
465             # ------------------------------------------ #
466             # returns true if the value exists in the #
467             # array provided. #
468             ##############################################
469              
470             sub _array_has_eq {
471 0     0   0 my ($value, $array) = @_;
472              
473 0         0 foreach (@$array) {
474 0 0       0 return 1 if $_ eq $value;
475             }
476              
477 0         0 return;
478             }
479              
480             ##############################################
481             # _array_has_re( $regex, \@array ) #
482             # ========================================== #
483             # $regex - the regex to check for #
484             # $array - the array to search in #
485             # ------------------------------------------ #
486             # returns true if a value exists in the #
487             # array provided that matches the regex. #
488             ##############################################
489              
490             sub _array_has_re {
491 0     0   0 my ($re, $array) = @_;
492              
493 0         0 foreach (@$array) {
494 0 0       0 return 1 if m/$re/;
495             }
496              
497 0         0 return;
498             }
499              
500             ##############################################
501             # _has_adv_que( \%hash ) #
502             # ========================================== #
503             # $hash - the hash-ref to search in #
504             # ------------------------------------------ #
505             # returns true if the hash-ref has any of #
506             # the lang's advanced query operators #
507             ##############################################
508              
509             sub _has_adv_que {
510 90     90   77 my $hash = shift;
511              
512 90         115 foreach ('$gt', '$gte', '$lt', '$lte', '$all', '$exists', '$mod', '$eq', '$ne', '$in', '$nin', '$size', '$type') {
513 665 100       1104 return 1 if exists $hash->{$_};
514             }
515              
516 2         4 return;
517             }
518              
519             ##############################################
520             # _value_in( $value, \@array ) #
521             # ========================================== #
522             # $value - the value to check for #
523             # $array - the array to search in #
524             # ------------------------------------------ #
525             # returns true if the value is one of the #
526             # values from the array. #
527             ##############################################
528              
529             sub _value_in {
530 13     13   17 my ($value, $array) = @_;
531              
532 13         16 foreach (@$array) {
533 46 50 66     224 next if is_float($_) && !is_float($value);
534 46 50 66     552 next if !is_float($_) && is_float($value);
535 46 50 66     343 return 1 if is_float($_) && $value == $_;
536 46 100 100     260 return 1 if !is_float($_) && $value eq $_;
537             }
538              
539 4         52 return;
540             }
541              
542             =head2 update_doc( \%document, \%update )
543              
544             Receives a document hash-ref and an update hash-ref, and updates the
545             document in-place according to the update hash-ref. Also returns the document
546             after the update. If the update hash-ref doesn't have any of the update
547             modifiers described by the language, then the update hash-ref is considered
548             as what the document should now be, and so will simply replace the document
549             hash-ref (once again, in accordance with MongoDB).
550              
551             See L to learn about the structure of
552             update hash-refs.
553              
554             =cut
555              
556             sub update_doc {
557 20     20 1 2900 my ($doc, $obj) = @_;
558              
559 20 100 100     453 croak "MQUL::update_doc() requires a document hash-ref."
      100        
560             unless defined $doc && ref $doc && ref $doc eq 'HASH';
561 17 100 66     287 croak "MQUL::update_doc() requires an update hash-ref."
      100        
562             unless defined $obj && ref $obj && ref $obj eq 'HASH';
563              
564             # we only need to do something if the $obj hash-ref has any advanced
565             # update operations, otherwise $obj is meant to be the new $doc
566              
567 15 100       19 if (&_has_adv_upd($obj)) {
568 14         34 foreach my $op (keys %$obj) {
569 18 100       102 if ($op eq '$inc') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
570             # increase numerically
571 2 50       7 next unless ref $obj->{$op} eq 'HASH';
572 2         2 foreach my $field (keys %{$obj->{$op}}) {
  2         6  
573 2   50     5 $doc->{$field} ||= 0;
574 2         7 $doc->{$field} += $obj->{$op}->{$field};
575             }
576             } elsif ($op eq '$set') {
577             # set key-value pairs
578 2 50       6 next unless ref $obj->{$op} eq 'HASH';
579 2         3 foreach my $field (keys %{$obj->{$op}}) {
  2         4  
580 3         7 $doc->{$field} = $obj->{$op}->{$field};
581             }
582             } elsif ($op eq '$unset') {
583             # remove key-value pairs
584 2 50       32 next unless ref $obj->{$op} eq 'HASH';
585 2         5 foreach my $field (keys %{$obj->{$op}}) {
  2         7  
586 2 50       13 delete $doc->{$field} if $obj->{$op}->{$field};
587             }
588             } elsif ($op eq '$rename') {
589             # rename attributes
590 1 50       5 next unless ref $obj->{$op} eq 'HASH';
591 1         1 foreach my $field (keys %{$obj->{$op}}) {
  1         4  
592 1 50       8 $doc->{$obj->{$op}->{$field}} = delete $doc->{$field}
593             if exists $doc->{$field};
594             }
595             } elsif ($op eq '$push') {
596             # push values to end of arrays
597 1 50       5 next unless ref $obj->{$op} eq 'HASH';
598 1         3 foreach my $field (keys %{$obj->{$op}}) {
  1         3  
599 1 50 33     7 croak "The $field attribute is not an array in the doc."
600             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
601 1   50     3 $doc->{$field} ||= [];
602 1         2 push(@{$doc->{$field}}, $obj->{$op}->{$field});
  1         5  
603             }
604             } elsif ($op eq '$pushAll') {
605             # push a list of values to end of arrays
606 1 50       12 next unless ref $obj->{$op} eq 'HASH';
607 1         1 foreach my $field (keys %{$obj->{$op}}) {
  1         4  
608 1 50 33     8 croak "The $field attribute is not an array in the doc."
609             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
610 1   50     5 $doc->{$field} ||= [];
611 1         2 push(@{$doc->{$field}}, @{$obj->{$op}->{$field}});
  1         2  
  1         4  
612             }
613             } elsif ($op eq '$addToSet') {
614             # push values to arrays only if they're not already there
615 2 50       8 next unless ref $obj->{$op} eq 'HASH';
616 2         5 foreach my $field (keys %{$obj->{$op}}) {
  2         6  
617 2 50 33     10 croak "The $field attribute is not an array in the doc."
618             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
619 2   50     5 $doc->{$field} ||= [];
620 2 100 66     11 my @add = ref $obj->{$op}->{$field} && ref $obj->{$op}->{$field} eq 'ARRAY' ? @{$obj->{$op}->{$field}} : ($obj->{$op}->{$field});
  1         2  
621 2         4 foreach my $val (@add) {
622 4 100       7 push(@{$doc->{$field}}, $val)
  2         5  
623             unless defined &_index_of($val, $doc->{$field});
624             }
625             }
626             } elsif ($op eq '$pop') {
627             # pop the last item from an array
628 2 50       6 next unless ref $obj->{$op} eq 'HASH';
629 2         2 foreach my $field (keys %{$obj->{$op}}) {
  2         5  
630 2 50 33     19 croak "The $field attribute is not an array in the doc."
631             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
632 2   50     6 $doc->{$field} ||= [];
633 2 50       6 pop(@{$doc->{$field}})
  2         4  
634             if $obj->{$op}->{$field};
635             }
636             } elsif ($op eq '$shift') {
637             # shift the first item from an array
638 1 50       8 next unless ref $obj->{$op} eq 'HASH';
639 1         1 foreach my $field (keys %{$obj->{$op}}) {
  1         3  
640 1 50 33     7 croak "The $field attribute is not an array in the doc."
641             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
642 1   50     4 $doc->{$field} ||= [];
643 1 50       3 shift(@{$doc->{$field}})
  1         3  
644             if $obj->{$op}->{$field};
645             }
646             } elsif ($op eq '$splice') {
647             # splice offsets from arrays
648 1 50       3 next unless ref $obj->{$op} eq 'HASH';
649 1         2 foreach my $field (keys %{$obj->{$op}}) {
  1         3  
650 1 50 33     7 croak "The $field attribute is not an array in the doc."
651             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
652 1         7 next unless ref $obj->{$op}->{$field} &&
653             ref $obj->{$op}->{$field} eq 'ARRAY' &&
654 1 50 33     13 scalar @{$obj->{$op}->{$field}} == 2;
      33        
655 1   50     4 $doc->{$field} ||= [];
656 1         1 splice(@{$doc->{$field}}, $obj->{$op}->{$field}->[0], $obj->{$op}->{$field}->[1]);
  1         7  
657             }
658             } elsif ($op eq '$pull') {
659             # remove values from arrays
660 2 50       6 next unless ref $obj->{$op} eq 'HASH';
661 2         3 foreach my $field (keys %{$obj->{$op}}) {
  2         4  
662 2 50 33     15 croak "The $field attribute is not an array in the doc."
663             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
664 2   50     5 $doc->{$field} ||= [];
665 2         5 my $i = &_index_of($obj->{$op}->{$field}, $doc->{$field});
666 2         4 while (defined $i) {
667 2         2 splice(@{$doc->{$field}}, $i, 1);
  2         5  
668 2         4 $i = &_index_of($obj->{$op}->{$field}, $doc->{$field});
669             }
670             }
671             } elsif ($op eq '$pullAll') {
672             # remove a list of values from arrays
673 1 50       4 next unless ref $obj->{$op} eq 'HASH';
674 1         2 foreach my $field (keys %{$obj->{$op}}) {
  1         3  
675 1 50 33     13 croak "The $field attribute is not an array in the doc."
676             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
677 1   50     4 $doc->{$field} ||= [];
678 1         1 foreach my $value (@{$obj->{$op}->{$field}}) {
  1         2  
679 2         5 my $i = &_index_of($value, $doc->{$field});
680 2         6 while (defined $i) {
681 2         3 splice(@{$doc->{$field}}, $i, 1);
  2         3  
682 2         4 $i = &_index_of($value, $doc->{$field});
683             }
684             }
685             }
686             }
687             }
688             } else {
689             # $obj is actually the new $doc
690 1         5 %$doc = %$obj;
691             }
692              
693 15         85 return $doc;
694             }
695              
696             ##############################################
697             # _has_adv_upd( \%hash ) #
698             # ========================================== #
699             # $hash - the hash-ref to search in #
700             # ------------------------------------------ #
701             # returns true if the hash-ref has any of #
702             # the lang's advanced update operators #
703             ##############################################
704              
705             sub _has_adv_upd {
706 15     15   18 my $hash = shift;
707              
708 15         25 foreach ('$inc', '$set', '$unset', '$push', '$pushAll', '$addToSet', '$pop', '$shift', '$splice', '$pull', '$pullAll', '$rename', '$bit') {
709 98 100       172 return 1 if exists $hash->{$_};
710             }
711              
712 1         4 return;
713             }
714              
715             ##############################################
716             # _index_of( $value, \@array ) #
717             # ========================================== #
718             # $value - the value to search for #
719             # $array - the array to search in #
720             # ------------------------------------------ #
721             # searches for the provided value in the #
722             # array, and returns its index if it is #
723             # found, or undef otherwise. #
724             ##############################################
725              
726             sub _index_of {
727 12     12   11 my ($value, $array) = @_;
728              
729 12         24 for (my $i = 0; $i < scalar @$array; $i++) {
730 23 50 33     43 if (is_float($array->[$i]) && is_float($value)) {
731 0 0       0 return $i if $array->[$i] == $value;
732             } else {
733 23 100       170 return $i if $array->[$i] eq $value;
734             }
735             }
736              
737 6         22 return;
738             }
739              
740             ##############################################
741             # _parse_function( $doc, $key ) #
742             # ========================================== #
743             # $doc - the document #
744             # $key - the key referencing a function and #
745             # a list of attributes, such as #
746             # min(attr1, attr2, attr3) #
747             # ------------------------------------------ #
748             # calculates the value using the appropriate #
749             # function and returns the result #
750             ##############################################
751              
752             sub _parse_function {
753 21     21   21 my ($doc, $def) = @_;
754              
755 21         42 my ($func) = keys %$def;
756              
757 21 50       42 die "Unrecognized function $func"
758             unless exists $BUILTINS{$func};
759              
760 21 100       41 $def->{$func} = [$def->{$func}]
761             unless ref $def->{$func};
762              
763 21         18 my @vals;
764 21         17 foreach (@{$def->{$func}}) {
  21         37  
765 49         59 my ($v, $k) = _expand_dot_notation($doc, $_);
766 49 50       101 push(@vals, $v)
767             if defined $v;
768             }
769              
770 21         49 return $BUILTINS{$func}->(@vals);
771             }
772              
773             ##############################################
774             # _expand_dot_notation( $doc, $key ) #
775             # ========================================== #
776             # $doc - the document #
777             # $key - the key using dot notation #
778             # ------------------------------------------ #
779             # takes a key using the dot notation, and #
780             # returns the value of the document at the #
781             # end of the chain (if any), plus the key at #
782             # the end of the chain. #
783             ##############################################
784              
785             sub _expand_dot_notation {
786 66     66   55 my ($doc, $key) = @_;
787              
788 66 100       155 return ($doc->{$key}, $key)
789             unless $key =~ m/\./;
790              
791 28         72 my @way_there = split(/\./, $key);
792              
793 28         33 $key = shift @way_there;
794 28         46 my %virt = ( $key => $doc->{$key} );
795              
796 28         47 while (scalar @way_there) {
797 50         44 $key = shift @way_there;
798 50         52 my ($have) = values %virt;
799              
800 50 100 100     319 if ($have && ref $have eq 'HASH' && exists $have->{$key}) {
    100 100        
      100        
      66        
      66        
801 35         107 %virt = ( $key => $have->{$key} );
802             } elsif ($have && ref $have eq 'ARRAY' && $key =~ m/^\d+$/ && scalar @$have > $key) {
803 10         29 %virt = ( $key => $have->[$key] )
804             } else {
805 5         11 %virt = ();
806             }
807             }
808              
809 28         55 return ($virt{$key}, $key);
810             }
811              
812             =head1 DIAGNOSTICS
813              
814             =over
815              
816             =item C<< MQUL::doc_matches() requires a document hash-ref. >>
817              
818             This error means that you've either haven't passed the C
819             subroutine any parameters, or given it a non-hash-ref document.
820              
821             =item C<< MQUL::doc_matches() expects a query hash-ref. >>
822              
823             This error means that you've passed the C attribute a
824             non-hash-ref query variable. While you don't actually have to pass a
825             query variable, if you do, it has to be a hash-ref.
826              
827             =item C<< MQUL::update_doc() requires a document hash-ref. >>
828              
829             This error means that you've either haven't passed the C
830             subroutine any parameters, or given it a non-hash-ref document.
831              
832             =item C<< MQUL::update_doc() requires an update hash-ref. >>
833              
834             This error means that you've passed the C subroutine a
835             non-hash-ref update variable.
836              
837             =item C<< The %s attribute is not an array in the doc. >>
838              
839             This error means that your update hash-ref tries to modify an array attribute
840             (with C<$push>, C<$pushAll>, C<$addToSet>, C<$pull>, C<$pullAll>,
841             C<$pop>, C<$shift> and C<$splice>), but the attribute in the document
842             provided to the C subroutine is not an array.
843              
844             =back
845              
846             =head1 CONFIGURATION AND ENVIRONMENT
847            
848             MQUL requires no configuration files or environment variables.
849              
850             =head1 DEPENDENCIES
851              
852             MQUL depends on the following modules:
853              
854             =over
855              
856             =item * L
857              
858             =item * L
859              
860             =item * L
861              
862             =item * L
863              
864             =item * L
865              
866             =back
867              
868             =head1 INCOMPATIBILITIES
869              
870             None reported.
871              
872             =head1 BUGS AND LIMITATIONS
873              
874             No bugs have been reported.
875              
876             Please report any bugs or feature requests to
877             C, or through the web interface at
878             L.
879              
880             =head1 AUTHOR
881              
882             Ido Perlmuter
883              
884             =head1 LICENSE AND COPYRIGHT
885              
886             Copyright (c) 2011-2015, Ido Perlmuter C<< ido at ido50 dot net >>.
887              
888             This module is free software; you can redistribute it and/or
889             modify it under the same terms as Perl itself, either version
890             5.8.1 or any later version. See L
891             and L.
892              
893             The full text of the license can be found in the
894             LICENSE file included with this module.
895              
896             =head1 DISCLAIMER OF WARRANTY
897              
898             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
899             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
900             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
901             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
902             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
903             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
904             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
905             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
906             NECESSARY SERVICING, REPAIR, OR CORRECTION.
907              
908             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
909             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
910             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
911             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
912             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
913             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
914             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
915             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
916             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
917             SUCH DAMAGES.
918              
919             =cut
920              
921             1;
922             __END__