File Coverage

blib/lib/MQUL.pm
Criterion Covered Total %
statement 261 282 92.5
branch 239 328 72.8
condition 124 224 55.3
subroutine 20 22 90.9
pod 2 2 100.0
total 646 858 75.2


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   56978 use Exporter 'import';
  3         5  
  3         111  
7 3     3   43 @EXPORT_OK = qw/doc_matches update_doc/;
8             }
9              
10 3     3   12 use warnings;
  3         3  
  3         74  
11 3     3   15 use strict;
  3         2  
  3         88  
12 3     3   13 use Carp;
  3         7  
  3         215  
13 3     3   1378 use Data::Compare;
  3         29854  
  3         25  
14 3     3   11952 use Data::Types qw/:is/;
  3         3515  
  3         465  
15 3     3   1609 use DateTime::Format::W3CDTF;
  3         385189  
  3         137  
16 3     3   39 use Scalar::Util qw/blessed/;
  3         6  
  3         241  
17 3     3   14 use Try::Tiny;
  3         5  
  3         9951  
18              
19             our $VERSION = "2.001000";
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 109     109 1 2313 my ($doc, $query, $defs) = @_;
237              
238 109 100 100     1076 croak 'MQUL::doc_matches() requires a document hash-ref.'
      100        
239             unless $doc && ref $doc && ref $doc eq 'HASH';
240 106 100 100     642 croak 'MQUL::doc_matches() expects a query hash-ref.'
      66        
241             if $query && (!ref $query || ref $query ne 'HASH');
242 104 50 33     270 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 104   100     152 $query ||= {};
246              
247 104 100       163 if ($defs) {
248 19         43 for (my $i = 0; $i < scalar(@$defs) - 1; $i = $i + 2) {
249 23         42 my ($name, $def) = ($defs->[$i], $defs->[$i+1]);
250 23         34 $doc->{$name} = _parse_function($doc, $def);
251             }
252             }
253              
254             # go over each key of the query
255 104         238 foreach my $key (keys %$query) {
256 120         129 my $value = $query->{$key};
257 120 100 66     387 if ($key eq '$or' && ref $value eq 'ARRAY') {
    100 66        
258 13         14 my $found;
259 13         19 foreach (@$value) {
260 21 50       37 next unless ref $_ eq 'HASH';
261 21         30 my $ok = 1;
262              
263 21         59 while (my ($k, $v) = each %$_) {
264 22 100       34 unless (&_attribute_matches($doc, $k, $v)) {
265 12         49 undef $ok;
266 12         13 last;
267             }
268             }
269              
270 21 100       40 if ($ok) { # document matches this criteria
271 9         7 $found = 1;
272 9         11 last;
273             }
274             }
275 13 100       38 return unless $found;
276             } elsif ($key eq '$and' && ref $value eq 'ARRAY') {
277 3         6 foreach (@$value) {
278 6 100       13 return unless &doc_matches($doc, $_, $defs);
279             }
280             } else {
281 104 100       148 return unless &_attribute_matches($doc, $key, $value);
282             }
283             }
284              
285             # if we've reached here, the document matches, so return true
286 73         256 return 1;
287             }
288              
289             ##############################################
290             # _attribute_matches( $doc, $key, $value ) #
291             # ========================================== #
292             # $doc - the document hash-ref #
293             # $key - the attribute being checked #
294             # $value - the constraint for the attribute #
295             # taken from the query hash-ref #
296             # ------------------------------------------ #
297             # returns true if constraint is met in the #
298             # provided document. #
299             ##############################################
300              
301             my $funcs = join('|', keys %BUILTINS);
302              
303             sub _attribute_matches {
304 126     126   162 my ($doc, $key, $value) = @_;
305              
306 126         99 my %virt;
307 126 100       202 if ($key =~ m/\./) {
308             # support for the dot notation
309 17         23 my ($v, $k) = _expand_dot_notation($doc, $key);
310              
311 17         20 $key = $k;
312 17 100       36 $virt{$key} = $v
313             if defined $v;
314             } else {
315 109 100       241 $virt{$key} = $doc->{$key}
316             if exists $doc->{$key};
317             }
318              
319 126 100 33     516 if (!ref $value) { # if value is a scalar, we need to check for equality
    50 66        
    100          
    100          
    50          
320             # (or, if the attribute is an array in the document,
321             # we need to check the value exists in it)
322 32 100       62 return unless defined $virt{$key};
323 30 50       62 if (ref $virt{$key} eq 'ARRAY') { # check the array has the requested value
    50          
324 0 0       0 return unless &_array_has_eq($value, $virt{$key});
325             } elsif (!ref $virt{$key}) { # check the values are equal
326 30 100       97 return unless $virt{$key} eq $value;
327             } else { # we can't compare a non-scalar to a scalar, so return false
328 0         0 return;
329             }
330             } elsif (blessed $value && (blessed $value eq 'MongoDB::OID' || blessed $value eq 'MorboDB::OID')) {
331             # we're trying to compare MongoDB::OIDs/MorboDB::OIDs
332             # (MorboDB is my in-memory clone of MongoDB)
333 0 0       0 return unless defined $virt{$key};
334 0 0 0     0 if (blessed $virt{$key} && (blessed $virt{$key} eq 'MongoDB::OID' || blessed $virt{$key} eq 'MorboDB::OID')) {
      0        
335 0 0       0 return unless $virt{$key}->value eq $value->value;
336             } else {
337 0         0 return;
338             }
339             } elsif (ref $value eq 'Regexp') { # if the value is a regex, we need to check
340             # for a match (or, if the attribute is an array
341             # in the document, we need to check at least one
342             # value in it matches it)
343 2 50       25 return unless defined $virt{$key};
344 2 50       8 if (ref $virt{$key} eq 'ARRAY') {
    50          
345 0 0       0 return unless &_array_has_re($value, $virt{$key});
346             } elsif (!ref $virt{$key}) { # check the values match
347 2 100       17 return unless $virt{$key} =~ $value;
348             } else { # we can't compare a non-scalar to a scalar, so return false
349 0         0 return;
350             }
351             } elsif (ref $value eq 'HASH') { # if the value is a hash, than it either contains
352             # advanced queries, or it's just a hash that we
353             # want the document to have as-is
354 91 100       122 unless (&_has_adv_que($value)) {
355             # value hash-ref doesn't have any advanced
356             # queries, we need to check our document
357             # has an attributes with exactly the same hash-ref
358             # (and name of course)
359 2 50       9 return unless Compare($value, $virt{$key});
360             } else {
361             # value contains advanced queries,
362             # we need to make sure our document has an
363             # attribute with the same name that matches
364             # all these queries
365 89         148 foreach my $q (keys %$value) {
366 97         229 my $term = $value->{$q};
367 97 100 66     704 if ($q eq '$gt') {
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 33        
    100          
    100          
    100          
    100          
    100          
    50          
368 14 50 33     55 return unless defined $virt{$key} && !ref $virt{$key};
369 14 50       36 if (is_float($virt{$key})) {
370 14 100       180 return unless $virt{$key} > $term;
371             } else {
372 0 0       0 return unless $virt{$key} gt $term;
373             }
374             } elsif ($q eq '$gte') {
375 7 50 33     36 return unless defined $virt{$key} && !ref $virt{$key};
376 7 50       22 if (is_float($virt{$key})) {
377 7 100       117 return unless $virt{$key} >= $term;
378             } else {
379 0 0       0 return unless $virt{$key} ge $term;
380             }
381             } elsif ($q eq '$lt') {
382 7 50 33     44 return unless defined $virt{$key} && !ref $virt{$key};
383 7 100       40 if (is_float($virt{$key})) {
384 6 100       70 return unless $virt{$key} < $term;
385             } else {
386 1 50       17 return unless $virt{$key} lt $term;
387             }
388             } elsif ($q eq '$lte') {
389 5 50 33     26 return unless defined $virt{$key} && !ref $virt{$key};
390 5 50       12 if (is_float($virt{$key})) {
391 5 100       71 return unless $virt{$key} <= $term;
392             } else {
393 0 0       0 return unless $virt{$key} le $term;
394             }
395             } elsif ($q eq '$eq') {
396 1 50 33     7 return unless defined $virt{$key} && !ref $virt{$key};
397 1 50       3 if (is_float($virt{$key})) {
398 0 0       0 return unless $virt{$key} == $term;
399             } else {
400 1 50       10 return unless $virt{$key} eq $term;
401             }
402             } elsif ($q eq '$ne') {
403 2 50 33     19 return unless defined $virt{$key} && !ref $virt{$key};
404 2 100       10 if (is_float($virt{$key})) {
405 1 50       28 return unless $virt{$key} != $term;
406             } else {
407 1 50       15 return unless $virt{$key} ne $term;
408             }
409             } elsif ($q eq '$exists') {
410 15 100       22 if ($term) {
411 10 100       41 return unless exists $virt{$key};
412             } else {
413 5 100       24 return if exists $virt{$key};
414             }
415             } elsif ($q eq '$mod' && ref $term eq 'ARRAY' && scalar @$term == 2) {
416 4 100 33     19 return unless defined $virt{$key} && is_float($virt{$key}) && $virt{$key} % $term->[0] == $term->[1];
      66        
417             } elsif ($q eq '$in' && ref $term eq 'ARRAY') {
418 4 100 66     20 return unless defined $virt{$key} && &_value_in($virt{$key}, $term);
419             } elsif ($q eq '$nin' && ref $term eq 'ARRAY') {
420 3 100 66     17 return unless defined $virt{$key} && !&_value_in($virt{$key}, $term);
421             } elsif ($q eq '$size' && is_int($term)) {
422 4 100 66     74 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        
423             } elsif ($q eq '$all' && ref $term eq 'ARRAY') {
424 3 50 33     16 return unless defined $virt{$key} && ref $virt{$key} eq 'ARRAY';
425 3         6 foreach (@$term) {
426 6 100       28 return unless &_value_in($_, $virt{$key});
427             }
428             } elsif ($q eq '$type' && !ref $term) {
429 28 100       134 if ($term eq 'int') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
430 3 100 66     17 return unless defined $virt{$key} && is_int($virt{$key});
431             } elsif ($term eq 'float') {
432 4 50 33     22 return unless defined $virt{$key} && is_float($virt{$key});
433             } elsif ($term eq 'real') {
434 2 50 33     32 return unless defined $virt{$key} && is_real($virt{$key});
435             } elsif ($term eq 'whole') {
436 2 100 66     11 return unless defined $virt{$key} && is_whole($virt{$key});
437             } elsif ($term eq 'string') {
438 2 50 33     13 return unless defined $virt{$key} && is_string($virt{$key});
439             } elsif ($term eq 'array') {
440 2 50 33     16 return unless defined $virt{$key} && ref $virt{$key} eq 'ARRAY';
441             } elsif ($term eq 'hash') {
442 5 100 66     32 return unless defined $virt{$key} && ref $virt{$key} eq 'HASH';
443             } elsif ($term eq 'bool') {
444             # boolean - not really supported, will always return true since everything in Perl is a boolean
445             } elsif ($term eq 'date') {
446 3 50 33     14 return unless defined $virt{$key} && !ref $virt{$key};
447 3     1   26 my $date = try { DateTime::Format::W3CDTF->parse_datetime($virt{$key}) } catch { undef };
  3         103  
  1         47  
448 3 100 66     1110 return unless blessed $date && blessed $date eq 'DateTime';
449             } elsif ($term eq 'null') {
450 3 100 66     22 return unless exists $virt{$key} && !defined $virt{$key};
451             } elsif ($term eq 'regex') {
452 1 50 33     8 return unless defined $virt{$key} && ref $virt{$key} eq 'Regexp';
453             }
454             }
455             }
456             }
457             } elsif (ref $value eq 'ARRAY') {
458 1 50       4 return unless Compare($value, $virt{$key});
459             }
460              
461 88         896 return 1;
462             }
463              
464             ##############################################
465             # _array_has_eq( $value, \@array ) #
466             # ========================================== #
467             # $value - the value to check for #
468             # $array - the array to search in #
469             # ------------------------------------------ #
470             # returns true if the value exists in the #
471             # array provided. #
472             ##############################################
473              
474             sub _array_has_eq {
475 0     0   0 my ($value, $array) = @_;
476              
477 0         0 foreach (@$array) {
478 0 0       0 return 1 if $_ eq $value;
479             }
480              
481 0         0 return;
482             }
483              
484             ##############################################
485             # _array_has_re( $regex, \@array ) #
486             # ========================================== #
487             # $regex - the regex to check for #
488             # $array - the array to search in #
489             # ------------------------------------------ #
490             # returns true if a value exists in the #
491             # array provided that matches the regex. #
492             ##############################################
493              
494             sub _array_has_re {
495 0     0   0 my ($re, $array) = @_;
496              
497 0         0 foreach (@$array) {
498 0 0       0 return 1 if m/$re/;
499             }
500              
501 0         0 return;
502             }
503              
504             ##############################################
505             # _has_adv_que( \%hash ) #
506             # ========================================== #
507             # $hash - the hash-ref to search in #
508             # ------------------------------------------ #
509             # returns true if the hash-ref has any of #
510             # the lang's advanced query operators #
511             ##############################################
512              
513             sub _has_adv_que {
514 91     91   83 my $hash = shift;
515              
516 91         124 foreach ('$gt', '$gte', '$lt', '$lte', '$all', '$exists', '$mod', '$eq', '$ne', '$in', '$nin', '$size', '$type') {
517 679 100       1128 return 1 if exists $hash->{$_};
518             }
519              
520 2         5 return;
521             }
522              
523             ##############################################
524             # _value_in( $value, \@array ) #
525             # ========================================== #
526             # $value - the value to check for #
527             # $array - the array to search in #
528             # ------------------------------------------ #
529             # returns true if the value is one of the #
530             # values from the array. #
531             ##############################################
532              
533             sub _value_in {
534 13     13   17 my ($value, $array) = @_;
535              
536 13         15 foreach (@$array) {
537 46 50 66     225 next if is_float($_) && !is_float($value);
538 46 50 66     657 next if !is_float($_) && is_float($value);
539 46 50 66     368 return 1 if is_float($_) && $value == $_;
540 46 100 100     253 return 1 if !is_float($_) && $value eq $_;
541             }
542              
543 4         45 return;
544             }
545              
546             =head2 update_doc( \%document, \%update )
547              
548             Receives a document hash-ref and an update hash-ref, and updates the
549             document in-place according to the update hash-ref. Also returns the document
550             after the update. If the update hash-ref doesn't have any of the update
551             modifiers described by the language, then the update hash-ref is considered
552             as what the document should now be, and so will simply replace the document
553             hash-ref (once again, in accordance with MongoDB).
554              
555             See L to learn about the structure of
556             update hash-refs.
557              
558             =cut
559              
560             sub update_doc {
561 20     20 1 2845 my ($doc, $obj) = @_;
562              
563 20 100 100     483 croak "MQUL::update_doc() requires a document hash-ref."
      100        
564             unless defined $doc && ref $doc && ref $doc eq 'HASH';
565 17 100 66     213 croak "MQUL::update_doc() requires an update hash-ref."
      100        
566             unless defined $obj && ref $obj && ref $obj eq 'HASH';
567              
568             # we only need to do something if the $obj hash-ref has any advanced
569             # update operations, otherwise $obj is meant to be the new $doc
570              
571 15 100       28 if (&_has_adv_upd($obj)) {
572 14         33 foreach my $op (keys %$obj) {
573 18 100       91 if ($op eq '$inc') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
574             # increase numerically
575 2 50       10 next unless ref $obj->{$op} eq 'HASH';
576 2         3 foreach my $field (keys %{$obj->{$op}}) {
  2         5  
577 2   50     3 $doc->{$field} ||= 0;
578 2         8 $doc->{$field} += $obj->{$op}->{$field};
579             }
580             } elsif ($op eq '$set') {
581             # set key-value pairs
582 2 50       6 next unless ref $obj->{$op} eq 'HASH';
583 2         3 foreach my $field (keys %{$obj->{$op}}) {
  2         5  
584 3         7 $doc->{$field} = $obj->{$op}->{$field};
585             }
586             } elsif ($op eq '$unset') {
587             # remove key-value pairs
588 2 50       21 next unless ref $obj->{$op} eq 'HASH';
589 2         2 foreach my $field (keys %{$obj->{$op}}) {
  2         5  
590 2 50       8 delete $doc->{$field} if $obj->{$op}->{$field};
591             }
592             } elsif ($op eq '$rename') {
593             # rename attributes
594 1 50       3 next unless ref $obj->{$op} eq 'HASH';
595 1         2 foreach my $field (keys %{$obj->{$op}}) {
  1         9  
596 1 50       8 $doc->{$obj->{$op}->{$field}} = delete $doc->{$field}
597             if exists $doc->{$field};
598             }
599             } elsif ($op eq '$push') {
600             # push values to end of arrays
601 1 50       9 next unless ref $obj->{$op} eq 'HASH';
602 1         2 foreach my $field (keys %{$obj->{$op}}) {
  1         4  
603 1 50 33     7 croak "The $field attribute is not an array in the doc."
604             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
605 1   50     2 $doc->{$field} ||= [];
606 1         2 push(@{$doc->{$field}}, $obj->{$op}->{$field});
  1         4  
607             }
608             } elsif ($op eq '$pushAll') {
609             # push a list of values to end of arrays
610 1 50       14 next unless ref $obj->{$op} eq 'HASH';
611 1         2 foreach my $field (keys %{$obj->{$op}}) {
  1         3  
612 1 50 33     7 croak "The $field attribute is not an array in the doc."
613             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
614 1   50     6 $doc->{$field} ||= [];
615 1         2 push(@{$doc->{$field}}, @{$obj->{$op}->{$field}});
  1         2  
  1         5  
616             }
617             } elsif ($op eq '$addToSet') {
618             # push values to arrays only if they're not already there
619 2 50       6 next unless ref $obj->{$op} eq 'HASH';
620 2         6 foreach my $field (keys %{$obj->{$op}}) {
  2         5  
621 2 50 33     9 croak "The $field attribute is not an array in the doc."
622             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
623 2   50     4 $doc->{$field} ||= [];
624 2 100 66     9 my @add = ref $obj->{$op}->{$field} && ref $obj->{$op}->{$field} eq 'ARRAY' ? @{$obj->{$op}->{$field}} : ($obj->{$op}->{$field});
  1         3  
625 2         3 foreach my $val (@add) {
626 4 100       7 push(@{$doc->{$field}}, $val)
  2         11  
627             unless defined &_index_of($val, $doc->{$field});
628             }
629             }
630             } elsif ($op eq '$pop') {
631             # pop the last item from an array
632 2 50       10 next unless ref $obj->{$op} eq 'HASH';
633 2         1 foreach my $field (keys %{$obj->{$op}}) {
  2         5  
634 2 50 33     9 croak "The $field attribute is not an array in the doc."
635             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
636 2   50     4 $doc->{$field} ||= [];
637 2 50       5 pop(@{$doc->{$field}})
  2         4  
638             if $obj->{$op}->{$field};
639             }
640             } elsif ($op eq '$shift') {
641             # shift the first item from an array
642 1 50       4 next unless ref $obj->{$op} eq 'HASH';
643 1         2 foreach my $field (keys %{$obj->{$op}}) {
  1         3  
644 1 50 33     6 croak "The $field attribute is not an array in the doc."
645             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
646 1   50     2 $doc->{$field} ||= [];
647 1 50       8 shift(@{$doc->{$field}})
  1         4  
648             if $obj->{$op}->{$field};
649             }
650             } elsif ($op eq '$splice') {
651             # splice offsets from arrays
652 1 50       5 next unless ref $obj->{$op} eq 'HASH';
653 1         1 foreach my $field (keys %{$obj->{$op}}) {
  1         4  
654 1 50 33     12 croak "The $field attribute is not an array in the doc."
655             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
656 1         3 next unless ref $obj->{$op}->{$field} &&
657             ref $obj->{$op}->{$field} eq 'ARRAY' &&
658 1 50 33     9 scalar @{$obj->{$op}->{$field}} == 2;
      33        
659 1   50     4 $doc->{$field} ||= [];
660 1         2 splice(@{$doc->{$field}}, $obj->{$op}->{$field}->[0], $obj->{$op}->{$field}->[1]);
  1         6  
661             }
662             } elsif ($op eq '$pull') {
663             # remove values from arrays
664 2 50       4 next unless ref $obj->{$op} eq 'HASH';
665 2         3 foreach my $field (keys %{$obj->{$op}}) {
  2         5  
666 2 50 33     14 croak "The $field attribute is not an array in the doc."
667             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
668 2   50     4 $doc->{$field} ||= [];
669 2         9 my $i = &_index_of($obj->{$op}->{$field}, $doc->{$field});
670 2         4 while (defined $i) {
671 2         2 splice(@{$doc->{$field}}, $i, 1);
  2         4  
672 2         5 $i = &_index_of($obj->{$op}->{$field}, $doc->{$field});
673             }
674             }
675             } elsif ($op eq '$pullAll') {
676             # remove a list of values from arrays
677 1 50       5 next unless ref $obj->{$op} eq 'HASH';
678 1         1 foreach my $field (keys %{$obj->{$op}}) {
  1         4  
679 1 50 33     7 croak "The $field attribute is not an array in the doc."
680             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
681 1   50     6 $doc->{$field} ||= [];
682 1         2 foreach my $value (@{$obj->{$op}->{$field}}) {
  1         3  
683 2         3 my $i = &_index_of($value, $doc->{$field});
684 2         6 while (defined $i) {
685 2         2 splice(@{$doc->{$field}}, $i, 1);
  2         4  
686 2         4 $i = &_index_of($value, $doc->{$field});
687             }
688             }
689             }
690             }
691             }
692             } else {
693             # $obj is actually the new $doc
694 1         5 %$doc = %$obj;
695             }
696              
697 15         77 return $doc;
698             }
699              
700             ##############################################
701             # _has_adv_upd( \%hash ) #
702             # ========================================== #
703             # $hash - the hash-ref to search in #
704             # ------------------------------------------ #
705             # returns true if the hash-ref has any of #
706             # the lang's advanced update operators #
707             ##############################################
708              
709             sub _has_adv_upd {
710 15     15   18 my $hash = shift;
711              
712 15         26 foreach ('$inc', '$set', '$unset', '$push', '$pushAll', '$addToSet', '$pop', '$shift', '$splice', '$pull', '$pullAll', '$rename', '$bit') {
713 98 100       160 return 1 if exists $hash->{$_};
714             }
715              
716 1         3 return;
717             }
718              
719             ##############################################
720             # _index_of( $value, \@array ) #
721             # ========================================== #
722             # $value - the value to search for #
723             # $array - the array to search in #
724             # ------------------------------------------ #
725             # searches for the provided value in the #
726             # array, and returns its index if it is #
727             # found, or undef otherwise. #
728             ##############################################
729              
730             sub _index_of {
731 12     12   11 my ($value, $array) = @_;
732              
733 12         28 for (my $i = 0; $i < scalar @$array; $i++) {
734 23 50 33     68 if (is_float($array->[$i]) && is_float($value)) {
735 0 0       0 return $i if $array->[$i] == $value;
736             } else {
737 23 100       161 return $i if $array->[$i] eq $value;
738             }
739             }
740              
741 6         18 return;
742             }
743              
744             ##############################################
745             # _parse_function( $doc, $key ) #
746             # ========================================== #
747             # $doc - the document #
748             # $key - the key referencing a function and #
749             # a list of attributes, such as #
750             # min(attr1, attr2, attr3) #
751             # ------------------------------------------ #
752             # calculates the value using the appropriate #
753             # function and returns the result #
754             ##############################################
755              
756             sub _parse_function {
757 23     23   23 my ($doc, $def) = @_;
758              
759 23         47 my ($func) = keys %$def;
760              
761 23 50       49 die "Unrecognized function $func"
762             unless exists $BUILTINS{$func};
763              
764 23 100       42 $def->{$func} = [$def->{$func}]
765             unless ref $def->{$func};
766              
767 23         21 my @vals;
768 23         17 foreach (@{$def->{$func}}) {
  23         42  
769 53         995 my ($v, $k) = _expand_dot_notation($doc, $_);
770 53 100       111 push(@vals, $v)
771             if defined $v;
772             }
773              
774 23 100       44 return unless scalar @vals;
775              
776 21         48 return $BUILTINS{$func}->(@vals);
777             }
778              
779             ##############################################
780             # _expand_dot_notation( $doc, $key ) #
781             # ========================================== #
782             # $doc - the document #
783             # $key - the key using dot notation #
784             # ------------------------------------------ #
785             # takes a key using the dot notation, and #
786             # returns the value of the document at the #
787             # end of the chain (if any), plus the key at #
788             # the end of the chain. #
789             ##############################################
790              
791             sub _expand_dot_notation {
792 70     70   62 my ($doc, $key) = @_;
793              
794 70 100       170 return ($doc->{$key}, $key)
795             unless $key =~ m/\./;
796              
797 28         70 my @way_there = split(/\./, $key);
798              
799 28         33 $key = shift @way_there;
800 28         52 my %virt = ( $key => $doc->{$key} );
801              
802 28         46 while (scalar @way_there) {
803 50         50 $key = shift @way_there;
804 50         56 my ($have) = values %virt;
805              
806 50 100 100     312 if ($have && ref $have eq 'HASH' && exists $have->{$key}) {
    100 100        
      100        
      66        
      66        
807 35         100 %virt = ( $key => $have->{$key} );
808             } elsif ($have && ref $have eq 'ARRAY' && $key =~ m/^\d+$/ && scalar @$have > $key) {
809 10         36 %virt = ( $key => $have->[$key] )
810             } else {
811 5         12 %virt = ();
812             }
813             }
814              
815 28         56 return ($virt{$key}, $key);
816             }
817              
818             =head1 DIAGNOSTICS
819              
820             =over
821              
822             =item C<< MQUL::doc_matches() requires a document hash-ref. >>
823              
824             This error means that you've either haven't passed the C
825             subroutine any parameters, or given it a non-hash-ref document.
826              
827             =item C<< MQUL::doc_matches() expects a query hash-ref. >>
828              
829             This error means that you've passed the C attribute a
830             non-hash-ref query variable. While you don't actually have to pass a
831             query variable, if you do, it has to be a hash-ref.
832              
833             =item C<< MQUL::update_doc() requires a document hash-ref. >>
834              
835             This error means that you've either haven't passed the C
836             subroutine any parameters, or given it a non-hash-ref document.
837              
838             =item C<< MQUL::update_doc() requires an update hash-ref. >>
839              
840             This error means that you've passed the C subroutine a
841             non-hash-ref update variable.
842              
843             =item C<< The %s attribute is not an array in the doc. >>
844              
845             This error means that your update hash-ref tries to modify an array attribute
846             (with C<$push>, C<$pushAll>, C<$addToSet>, C<$pull>, C<$pullAll>,
847             C<$pop>, C<$shift> and C<$splice>), but the attribute in the document
848             provided to the C subroutine is not an array.
849              
850             =back
851              
852             =head1 CONFIGURATION AND ENVIRONMENT
853            
854             MQUL requires no configuration files or environment variables.
855              
856             =head1 DEPENDENCIES
857              
858             MQUL depends on the following modules:
859              
860             =over
861              
862             =item * L
863              
864             =item * L
865              
866             =item * L
867              
868             =item * L
869              
870             =item * L
871              
872             =back
873              
874             =head1 INCOMPATIBILITIES
875              
876             None reported.
877              
878             =head1 BUGS AND LIMITATIONS
879              
880             No bugs have been reported.
881              
882             Please report any bugs or feature requests to
883             C, or through the web interface at
884             L.
885              
886             =head1 AUTHOR
887              
888             Ido Perlmuter
889              
890             =head1 LICENSE AND COPYRIGHT
891              
892             Copyright (c) 2011-2015, Ido Perlmuter C<< ido at ido50 dot net >>.
893              
894             This module is free software; you can redistribute it and/or
895             modify it under the same terms as Perl itself, either version
896             5.8.1 or any later version. See L
897             and L.
898              
899             The full text of the license can be found in the
900             LICENSE file included with this module.
901              
902             =head1 DISCLAIMER OF WARRANTY
903              
904             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
905             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
906             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
907             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
908             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
909             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
910             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
911             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
912             NECESSARY SERVICING, REPAIR, OR CORRECTION.
913              
914             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
915             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
916             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
917             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
918             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
919             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
920             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
921             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
922             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
923             SUCH DAMAGES.
924              
925             =cut
926              
927             1;
928             __END__