File Coverage

blib/lib/MQUL.pm
Criterion Covered Total %
statement 259 280 92.5
branch 235 324 72.5
condition 122 221 55.2
subroutine 20 22 90.9
pod 2 2 100.0
total 638 849 75.1


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   48951 use Exporter 'import';
  3         5  
  3         100  
7 3     3   40 @EXPORT_OK = qw/doc_matches update_doc/;
8             }
9              
10 3     3   13 use warnings;
  3         3  
  3         61  
11 3     3   13 use strict;
  3         3  
  3         72  
12 3     3   11 use Carp;
  3         5  
  3         182  
13 3     3   1403 use Data::Compare;
  3         26576  
  3         15  
14 3     3   9789 use Data::Types qw/:is/;
  3         3414  
  3         368  
15 3     3   1306 use DateTime::Format::W3CDTF;
  3         322876  
  3         101  
16 3     3   23 use Scalar::Util qw/blessed/;
  3         5  
  3         175  
17 3     3   14 use Try::Tiny;
  3         4  
  3         8801  
18              
19             our $VERSION = "2.000001";
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 100     100 1 2961 my ($doc, $query, $defs) = @_;
237              
238 100 100 100     968 croak 'MQUL::doc_matches() requires a document hash-ref.'
      100        
239             unless $doc && ref $doc && ref $doc eq 'HASH';
240 97 100 100     590 croak 'MQUL::doc_matches() expects a query hash-ref.'
      66        
241             if $query && (!ref $query || ref $query ne 'HASH');
242 95 50 33     249 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 95   100     145 $query ||= {};
246              
247 95 100       165 if ($defs) {
248 19         49 for (my $i = 0; $i < scalar(@$defs) - 1; $i = $i + 2) {
249 23         31 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 95         221 foreach my $key (keys %$query) {
256 113         135 my $value = $query->{$key};
257 113 100 66     247 if ($key eq '$or' && ref $value eq 'ARRAY') {
258 10         7 my $found;
259 10         17 foreach (@$value) {
260 17 50       27 next unless ref $_ eq 'HASH';
261 17         14 my $ok = 1;
262              
263 17         42 while (my ($k, $v) = each %$_) {
264 18 100       26 unless (&_attribute_matches($doc, $k, $v)) {
265 11         41 undef $ok;
266 11         12 last;
267             }
268             }
269              
270 17 100       33 if ($ok) { # document matches this criteria
271 6         4 $found = 1;
272 6         10 last;
273             }
274             }
275 10 100       27 return unless $found;
276             } else {
277 103 100       145 return unless &_attribute_matches($doc, $key, $value);
278             }
279             }
280              
281             # if we've reached here, the document matches, so return true
282 66         1008 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 121     121   121 my ($doc, $key, $value) = @_;
301              
302 121         103 my %virt;
303 121 100       192 if ($key =~ m/\./) {
304             # support for the dot notation
305 17         23 my ($v, $k) = _expand_dot_notation($doc, $key);
306              
307 17         18 $key = $k;
308 17 100       33 $virt{$key} = $v
309             if defined $v;
310             } else {
311 104 100       228 $virt{$key} = $doc->{$key}
312             if exists $doc->{$key};
313             }
314              
315 121 100 33     502 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       47 return unless defined $virt{$key};
319 24 50       57 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 24 100       80 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       25 return unless defined $virt{$key};
340 2 50       8 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 92 100       125 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       13 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 90         155 foreach my $q (keys %$value) {
362 98         89 my $term = $value->{$q};
363 98 100 66     696 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     48 return unless defined $virt{$key} && !ref $virt{$key};
365 14 50       34 if (is_float($virt{$key})) {
366 14 100       173 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     33 return unless defined $virt{$key} && !ref $virt{$key};
372 8 50       19 if (is_float($virt{$key})) {
373 8 100       111 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     26 return unless defined $virt{$key} && !ref $virt{$key};
379 6 100       25 if (is_float($virt{$key})) {
380 5 100       57 return unless $virt{$key} < $term;
381             } else {
382 1 50       14 return unless $virt{$key} lt $term;
383             }
384             } elsif ($q eq '$lte') {
385 6 50 33     37 return unless defined $virt{$key} && !ref $virt{$key};
386 6 50       11 if (is_float($virt{$key})) {
387 6 100       78 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     7 return unless defined $virt{$key} && !ref $virt{$key};
393 1 50       3 if (is_float($virt{$key})) {
394 0 0       0 return unless $virt{$key} == $term;
395             } else {
396 1 50       16 return unless $virt{$key} eq $term;
397             }
398             } elsif ($q eq '$ne') {
399 2 50 33     15 return unless defined $virt{$key} && !ref $virt{$key};
400 2 100       10 if (is_float($virt{$key})) {
401 1 50       15 return unless $virt{$key} != $term;
402             } else {
403 1 50       21 return unless $virt{$key} ne $term;
404             }
405             } elsif ($q eq '$exists') {
406 15 100       22 if ($term) {
407 10 100       38 return unless exists $virt{$key};
408             } else {
409 5 100       20 return if exists $virt{$key};
410             }
411             } elsif ($q eq '$mod' && ref $term eq 'ARRAY' && scalar @$term == 2) {
412 4 100 33     26 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     14 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     14 return unless defined $virt{$key} && ref $virt{$key} eq 'ARRAY';
421 3         5 foreach (@$term) {
422 6 100       26 return unless &_value_in($_, $virt{$key});
423             }
424             } elsif ($q eq '$type' && !ref $term) {
425 28 100       112 if ($term eq 'int') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
426 3 100 66     16 return unless defined $virt{$key} && is_int($virt{$key});
427             } elsif ($term eq 'float') {
428 4 50 33     19 return unless defined $virt{$key} && is_float($virt{$key});
429             } elsif ($term eq 'real') {
430 2 50 33     21 return unless defined $virt{$key} && is_real($virt{$key});
431             } elsif ($term eq 'whole') {
432 2 100 66     9 return unless defined $virt{$key} && is_whole($virt{$key});
433             } elsif ($term eq 'string') {
434 2 50 33     12 return unless defined $virt{$key} && is_string($virt{$key});
435             } elsif ($term eq 'array') {
436 2 50 33     13 return unless defined $virt{$key} && ref $virt{$key} eq 'ARRAY';
437             } elsif ($term eq 'hash') {
438 5 100 66     29 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     18 return unless defined $virt{$key} && !ref $virt{$key};
443 3     1   26 my $date = try { DateTime::Format::W3CDTF->parse_datetime($virt{$key}) } catch { undef };
  3         93  
  1         27  
444 3 100 66     975 return unless blessed $date && blessed $date eq 'DateTime';
445             } elsif ($term eq 'null') {
446 3 100 66     22 return unless exists $virt{$key} && !defined $virt{$key};
447             } elsif ($term eq 'regex') {
448 1 50 33     9 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 85         873 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 92     92   83 my $hash = shift;
511              
512 92         120 foreach ('$gt', '$gte', '$lt', '$lte', '$all', '$exists', '$mod', '$eq', '$ne', '$in', '$nin', '$size', '$type') {
513 682 100       1100 return 1 if exists $hash->{$_};
514             }
515              
516 2         5 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   12 my ($value, $array) = @_;
531              
532 13         15 foreach (@$array) {
533 46 50 66     224 next if is_float($_) && !is_float($value);
534 46 50 66     379 next if !is_float($_) && is_float($value);
535 46 50 66     348 return 1 if is_float($_) && $value == $_;
536 46 100 100     263 return 1 if !is_float($_) && $value eq $_;
537             }
538              
539 4         44 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 2262 my ($doc, $obj) = @_;
558              
559 20 100 100     474 croak "MQUL::update_doc() requires a document hash-ref."
      100        
560             unless defined $doc && ref $doc && ref $doc eq 'HASH';
561 17 100 66     274 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       22 if (&_has_adv_upd($obj)) {
568 14         32 foreach my $op (keys %$obj) {
569 18 100       101 if ($op eq '$inc') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
570             # increase numerically
571 2 50       6 next unless ref $obj->{$op} eq 'HASH';
572 2         3 foreach my $field (keys %{$obj->{$op}}) {
  2         6  
573 2   50     4 $doc->{$field} ||= 0;
574 2         8 $doc->{$field} += $obj->{$op}->{$field};
575             }
576             } elsif ($op eq '$set') {
577             # set key-value pairs
578 2 50       8 next unless ref $obj->{$op} eq 'HASH';
579 2         3 foreach my $field (keys %{$obj->{$op}}) {
  2         7  
580 3         6 $doc->{$field} = $obj->{$op}->{$field};
581             }
582             } elsif ($op eq '$unset') {
583             # remove key-value pairs
584 2 50       27 next unless ref $obj->{$op} eq 'HASH';
585 2         3 foreach my $field (keys %{$obj->{$op}}) {
  2         6  
586 2 50       10 delete $doc->{$field} if $obj->{$op}->{$field};
587             }
588             } elsif ($op eq '$rename') {
589             # rename attributes
590 1 50       3 next unless ref $obj->{$op} eq 'HASH';
591 1         1 foreach my $field (keys %{$obj->{$op}}) {
  1         4  
592 1 50       6 $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         1 foreach my $field (keys %{$obj->{$op}}) {
  1         4  
599 1 50 33     9 croak "The $field attribute is not an array in the doc."
600             if defined $doc->{$field} && ref $doc->{$field} ne 'ARRAY';
601 1   50     5 $doc->{$field} ||= [];
602 1         2 push(@{$doc->{$field}}, $obj->{$op}->{$field});
  1         7  
603             }
604             } elsif ($op eq '$pushAll') {
605             # push a list of values to end of arrays
606 1 50       16 next unless ref $obj->{$op} eq 'HASH';
607 1         2 foreach my $field (keys %{$obj->{$op}}) {
  1         3  
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     3 $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       5 next unless ref $obj->{$op} eq 'HASH';
616 2         2 foreach my $field (keys %{$obj->{$op}}) {
  2         6  
617 2 50 33     9 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     10 my @add = ref $obj->{$op}->{$field} && ref $obj->{$op}->{$field} eq 'ARRAY' ? @{$obj->{$op}->{$field}} : ($obj->{$op}->{$field});
  1         2  
621 2         3 foreach my $val (@add) {
622 4 100       8 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         1 foreach my $field (keys %{$obj->{$op}}) {
  2         5  
630 2 50 33     11 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       4 pop(@{$doc->{$field}})
  2         6  
634             if $obj->{$op}->{$field};
635             }
636             } elsif ($op eq '$shift') {
637             # shift the first item from an array
638 1 50       4 next unless ref $obj->{$op} eq 'HASH';
639 1         2 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     3 $doc->{$field} ||= [];
643 1 50       4 shift(@{$doc->{$field}})
  1         3  
644             if $obj->{$op}->{$field};
645             }
646             } elsif ($op eq '$splice') {
647             # splice offsets from arrays
648 1 50       4 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         5 next unless ref $obj->{$op}->{$field} &&
653             ref $obj->{$op}->{$field} eq 'ARRAY' &&
654 1 50 33     14 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         6  
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         3  
662 2 50 33     17 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         4 my $i = &_index_of($obj->{$op}->{$field}, $doc->{$field});
666 2         5 while (defined $i) {
667 2         2 splice(@{$doc->{$field}}, $i, 1);
  2         3  
668 2         6 $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       5 next unless ref $obj->{$op} eq 'HASH';
674 1         1 foreach my $field (keys %{$obj->{$op}}) {
  1         8  
675 1 50 33     7 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         3  
679 2         3 my $i = &_index_of($value, $doc->{$field});
680 2         5 while (defined $i) {
681 2         6 splice(@{$doc->{$field}}, $i, 1);
  2         4  
682 2         5 $i = &_index_of($value, $doc->{$field});
683             }
684             }
685             }
686             }
687             }
688             } else {
689             # $obj is actually the new $doc
690 1         4 %$doc = %$obj;
691             }
692              
693 15         76 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   13 my $hash = shift;
707              
708 15         24 foreach ('$inc', '$set', '$unset', '$push', '$pushAll', '$addToSet', '$pop', '$shift', '$splice', '$pull', '$pullAll', '$rename', '$bit') {
709 98 100       169 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   13 my ($value, $array) = @_;
728              
729 12         23 for (my $i = 0; $i < scalar @$array; $i++) {
730 22 50 33     47 if (is_float($array->[$i]) && is_float($value)) {
731 0 0       0 return $i if $array->[$i] == $value;
732             } else {
733 22 100       170 return $i if $array->[$i] eq $value;
734             }
735             }
736              
737 6         21 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 23     23   23 my ($doc, $def) = @_;
754              
755 23         44 my ($func) = keys %$def;
756              
757 23 50       43 die "Unrecognized function $func"
758             unless exists $BUILTINS{$func};
759              
760 23 100       52 $def->{$func} = [$def->{$func}]
761             unless ref $def->{$func};
762              
763 23         15 my @vals;
764 23         18 foreach (@{$def->{$func}}) {
  23         43  
765 53         58 my ($v, $k) = _expand_dot_notation($doc, $_);
766 53 100       113 push(@vals, $v)
767             if defined $v;
768             }
769              
770 23 100       49 return unless scalar @vals;
771              
772 21         43 return $BUILTINS{$func}->(@vals);
773             }
774              
775             ##############################################
776             # _expand_dot_notation( $doc, $key ) #
777             # ========================================== #
778             # $doc - the document #
779             # $key - the key using dot notation #
780             # ------------------------------------------ #
781             # takes a key using the dot notation, and #
782             # returns the value of the document at the #
783             # end of the chain (if any), plus the key at #
784             # the end of the chain. #
785             ##############################################
786              
787             sub _expand_dot_notation {
788 70     70   60 my ($doc, $key) = @_;
789              
790 70 100       159 return ($doc->{$key}, $key)
791             unless $key =~ m/\./;
792              
793 28         70 my @way_there = split(/\./, $key);
794              
795 28         32 $key = shift @way_there;
796 28         50 my %virt = ( $key => $doc->{$key} );
797              
798 28         53 while (scalar @way_there) {
799 50         47 $key = shift @way_there;
800 50         54 my ($have) = values %virt;
801              
802 50 100 100     305 if ($have && ref $have eq 'HASH' && exists $have->{$key}) {
    100 100        
      100        
      66        
      66        
803 35         99 %virt = ( $key => $have->{$key} );
804             } elsif ($have && ref $have eq 'ARRAY' && $key =~ m/^\d+$/ && scalar @$have > $key) {
805 10         32 %virt = ( $key => $have->[$key] )
806             } else {
807 5         10 %virt = ();
808             }
809             }
810              
811 28         57 return ($virt{$key}, $key);
812             }
813              
814             =head1 DIAGNOSTICS
815              
816             =over
817              
818             =item C<< MQUL::doc_matches() requires a document hash-ref. >>
819              
820             This error means that you've either haven't passed the C
821             subroutine any parameters, or given it a non-hash-ref document.
822              
823             =item C<< MQUL::doc_matches() expects a query hash-ref. >>
824              
825             This error means that you've passed the C attribute a
826             non-hash-ref query variable. While you don't actually have to pass a
827             query variable, if you do, it has to be a hash-ref.
828              
829             =item C<< MQUL::update_doc() requires a document hash-ref. >>
830              
831             This error means that you've either haven't passed the C
832             subroutine any parameters, or given it a non-hash-ref document.
833              
834             =item C<< MQUL::update_doc() requires an update hash-ref. >>
835              
836             This error means that you've passed the C subroutine a
837             non-hash-ref update variable.
838              
839             =item C<< The %s attribute is not an array in the doc. >>
840              
841             This error means that your update hash-ref tries to modify an array attribute
842             (with C<$push>, C<$pushAll>, C<$addToSet>, C<$pull>, C<$pullAll>,
843             C<$pop>, C<$shift> and C<$splice>), but the attribute in the document
844             provided to the C subroutine is not an array.
845              
846             =back
847              
848             =head1 CONFIGURATION AND ENVIRONMENT
849            
850             MQUL requires no configuration files or environment variables.
851              
852             =head1 DEPENDENCIES
853              
854             MQUL depends on the following modules:
855              
856             =over
857              
858             =item * L
859              
860             =item * L
861              
862             =item * L
863              
864             =item * L
865              
866             =item * L
867              
868             =back
869              
870             =head1 INCOMPATIBILITIES
871              
872             None reported.
873              
874             =head1 BUGS AND LIMITATIONS
875              
876             No bugs have been reported.
877              
878             Please report any bugs or feature requests to
879             C, or through the web interface at
880             L.
881              
882             =head1 AUTHOR
883              
884             Ido Perlmuter
885              
886             =head1 LICENSE AND COPYRIGHT
887              
888             Copyright (c) 2011-2015, Ido Perlmuter C<< ido at ido50 dot net >>.
889              
890             This module is free software; you can redistribute it and/or
891             modify it under the same terms as Perl itself, either version
892             5.8.1 or any later version. See L
893             and L.
894              
895             The full text of the license can be found in the
896             LICENSE file included with this module.
897              
898             =head1 DISCLAIMER OF WARRANTY
899              
900             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
901             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
902             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
903             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
904             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
905             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
906             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
907             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
908             NECESSARY SERVICING, REPAIR, OR CORRECTION.
909              
910             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
911             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
912             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
913             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
914             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
915             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
916             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
917             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
918             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
919             SUCH DAMAGES.
920              
921             =cut
922              
923             1;
924             __END__