File Coverage

lib/MQUL.pm
Criterion Covered Total %
statement 256 278 92.0
branch 235 320 73.4
condition 146 242 60.3
subroutine 21 24 87.5
pod 2 2 100.0
total 660 866 76.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   429483 use Exporter 'import';
  3         7  
  3         150  
7 3     3   91 @EXPORT_OK = qw/doc_matches update_doc/;
8             }
9              
10 3     3   25 use warnings;
  3         13  
  3         161  
11 3     3   16 use strict;
  3         7  
  3         73  
12 3     3   15 use Carp;
  3         4  
  3         253  
13 3     3   1909 use Data::Compare;
  3         44430  
  3         21  
14 3     3   16130 use Data::Types qw/:is/;
  3         5494  
  3         547  
15 3     3   1867 use DateTime::Format::W3CDTF;
  3         1698963  
  3         221  
16 3     3   42 use Scalar::Util qw/blessed/;
  3         9  
  3         223  
17 3     3   15 use Try::Tiny;
  3     0   4  
  3         12077  
18              
19             our $VERSION = "3.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 B<M>ongoDB-style B<Q>uery & B<U>pdate B<L>anguage; pronounced
69             I<"umm, cool">; yeah, I know, that's the dumbest thing ever), is a general
70             purpose implementation of L<MongoDB>'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<doc_matches()> and C<update_doc()>.
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<Giddy>, 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<MorboDB>, an in-memory clone of
89             MongoDB.
90              
91             =head2 UPGRADE NOTES
92              
93             My distributions follow the L<semantic versioning scheme|http://semver.org/>,
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<MQUL::Reference>. 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<MQUL::Reference/"QUERY STRUCTURE"> 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<min_val>, C<max_val> and
228             C<difference>, which is made up of the first two.
229              
230             See L<MQUL::Reference/"DYNAMICALLY CALCULATED ATTRIBUTES"> for more information
231             about dynamic attributes.
232              
233             =cut
234              
235             sub doc_matches {
236 0     110 1 0 my ( $doc, $query, $defs ) = @_;
237              
238 110 100 100     252289 croak 'MQUL::doc_matches() requires a document hash-ref.'
      100        
239             unless $doc && ref $doc && ref $doc eq 'HASH';
240 110 100 100     1218 croak 'MQUL::doc_matches() expects a query hash-ref.'
      100        
241             if $query && ( !ref $query || ref $query ne 'HASH' );
242 107 50 33     566 croak 'MQUL::doc_matches() expects an even-numbered definitions array-ref.'
      66        
243             if $defs
244             && ( !ref $defs || ref $defs ne 'ARRAY' || scalar @$defs % 2 != 0 );
245              
246 105   100     237 $query ||= {};
247              
248 105 100       255 if ($defs) {
249 105         151 for ( my $i = 0 ; $i < scalar(@$defs) - 1 ; $i = $i + 2 ) {
250 19         70 my ( $name, $def ) = ( $defs->[$i], $defs->[ $i + 1 ] );
251 23         56 $doc->{$name} = _parse_function( $doc, $def );
252             }
253             }
254              
255             # go over each key of the query
256 23         69 foreach my $key ( keys %$query ) {
257 105         247 my $value = $query->{$key};
258 121 100 66     164 if ( $key eq '$or' && ref $value eq 'ARRAY' ) {
    100 66        
259 121         396 my $found;
260 12         33 foreach (@$value) {
261 12 50       17 next unless ref $_ eq 'HASH';
262 20         32 my $ok = 1;
263              
264 20         18 while ( my ( $k, $v ) = each %$_ ) {
265 20 100       66 unless ( &_attribute_matches( $doc, $k, $v ) ) {
266 21         26 undef $ok;
267 12         16 last;
268             }
269             }
270              
271 12 100       15 if ($ok) { # document matches this criteria
272 20         66 $found = 1;
273 8         9 last;
274             }
275             }
276 8 100       10 return unless $found;
277             } elsif ( $key eq '$and' && ref $value eq 'ARRAY' ) {
278 12         37 foreach (@$value) {
279 3 100       6 return unless &doc_matches( $doc, $_, $defs );
280             }
281             } else {
282 6 100       34 return unless &_attribute_matches( $doc, $key, $value );
283             }
284             }
285              
286             # if we've reached here, the document matches, so return true
287 106         167 return 1;
288             }
289              
290             ##############################################
291             # _attribute_matches( $doc, $key, $value ) #
292             # ========================================== #
293             # $doc - the document hash-ref #
294             # $key - the attribute being checked #
295             # $value - the constraint for the attribute #
296             # taken from the query hash-ref #
297             # ------------------------------------------ #
298             # returns true if constraint is met in the #
299             # provided document. #
300             ##############################################
301              
302             my $funcs = join( '|', keys %BUILTINS );
303              
304             sub _attribute_matches {
305 74     127   352 my ( $doc, $key, $value ) = @_;
306              
307 127         195 my %virt;
308 127 100       154 if ( $key =~ m/\./ ) {
309              
310             # support for the dot notation
311 127         295 my ( $v, $k ) = _expand_dot_notation( $doc, $key );
312              
313 17         38 $key = $k;
314 17 100       24 $virt{$key} = $v
315             if defined $v;
316             } else {
317             $virt{$key} = $doc->{$key}
318 17 100       37 if exists $doc->{$key};
319             }
320              
321 110 100 33     268 if ( !ref $value ) { # if value is a scalar, we need to check for equality
    50 66        
    100          
    100          
    50          
322             # (or, if the attribute is an array in the document,
323             # we need to check the value exists in it)
324 127 100       447 return unless defined $virt{$key};
325 31 50       73 if ( ref $virt{$key} eq 'ARRAY' )
    50          
326             { # check the array has the requested value
327 29 0       67 return unless &_array_has_eq( $value, $virt{$key} );
328             } elsif ( !ref $virt{$key} ) { # check the values are equal
329 0 100       0 return unless $virt{$key} eq $value;
330             } else { # we can't compare a non-scalar to a scalar, so return false
331 29         112 return;
332             }
333             } elsif (
334             blessed $value
335             && ( blessed $value eq 'MongoDB::OID'
336             || blessed $value eq 'MorboDB::OID' )
337             )
338             {
339             # we're trying to compare MongoDB::OIDs/MorboDB::OIDs
340             # (MorboDB is my in-memory clone of MongoDB)
341 0 0       0 return unless defined $virt{$key};
342 0 0 0     0 if (
      0        
343             blessed $virt{$key}
344             && ( blessed $virt{$key} eq 'MongoDB::OID'
345             || blessed $virt{$key} eq 'MorboDB::OID' )
346             )
347             {
348 0 0       0 return unless $virt{$key}->value eq $value->value;
349             } else {
350 0         0 return;
351             }
352             } elsif ( ref $value eq 'Regexp' )
353             { # if the value is a regex, we need to check
354             # for a match (or, if the attribute is an array
355             # in the document, we need to check at least one
356             # value in it matches it)
357 0 50       0 return unless defined $virt{$key};
358 2 50       17 if ( ref $virt{$key} eq 'ARRAY' ) {
    50          
359 2 0       7 return unless &_array_has_re( $value, $virt{$key} );
360             } elsif ( !ref $virt{$key} ) { # check the values match
361 0 100       0 return unless $virt{$key} =~ $value;
362             } else { # we can't compare a non-scalar to a scalar, so return false
363 2         21 return;
364             }
365             } elsif ( ref $value eq 'HASH' )
366             { # if the value is a hash, then it either contains
367             # advanced queries, or it's just a hash that we
368             # want the document to have as-is
369 0 100       0 unless ( &_has_adv_que($value) ) {
370              
371             # value hash-ref doesn't have any advanced
372             # queries, we need to check our document
373             # has an attributes with exactly the same hash-ref
374             # (and name of course)
375 1 50       79 return unless Compare( $value, $virt{$key} );
376             } else {
377              
378             # value contains advanced queries,
379             # we need to make sure our document has an
380             # attribute with the same name that matches
381             # all these queries
382 93         171 foreach my $q ( keys %$value ) {
383 91         143 my $term = $value->{$q};
384 99 100 100     128 if ( $q eq '$gt'
    100 100        
    100 100        
    100 100        
    100 100        
    100 66        
    100 66        
    50 66        
      66        
      66        
      66        
      33        
385             || $q eq '$gte'
386             || $q eq '$lt'
387             || $q eq '$lte'
388             || $q eq '$eq'
389             || $q eq '$ne' )
390             {
391 99 50 33     892 return unless defined $virt{$key} && !ref $virt{$key};
392              
393 38 100       108 if ( $q eq '$gt' ) {
    100          
    100          
    100          
    100          
    50          
394 38 50 66     92 if ( is_float( $virt{$key} && is_float($term) ) ) {
395 14 100       53 return unless $virt{$key} > $term;
396             } else {
397 14 0       262 return unless $virt{$key} gt $term;
398             }
399             } elsif ( $q eq '$gte' ) {
400 0 50 33     0 if ( is_float( $virt{$key} && is_float($term) ) ) {
401 8 100       30 return unless $virt{$key} >= $term;
402             } else {
403 8 0       204 return unless $virt{$key} ge $term;
404             }
405             } elsif ( $q eq '$lt' ) {
406 0 100 66     0 if ( is_float( $virt{$key} ) && is_float($term) ) {
407 7 100       27 return unless $virt{$key} < $term;
408             } else {
409 6 50       132 return unless $virt{$key} lt $term;
410             }
411             } elsif ( $q eq '$lte' ) {
412 1 50 33     16 if ( is_float( $virt{$key} ) && is_float($term) ) {
413 5 100       13 return unless $virt{$key} <= $term;
414             } else {
415 5 0       97 return unless $virt{$key} le $term;
416             }
417             } elsif ( $q eq '$eq' ) {
418 0 50 33     0 if ( is_float( $virt{$key} ) && is_float($term) ) {
419 1 0       4 return unless $virt{$key} == $term;
420             } else {
421 0 50       0 return unless $virt{$key} eq $term;
422             }
423             } elsif ( $q eq '$ne' ) {
424 1 100 100     10 if ( is_float( $virt{$key} ) && is_float($term) ) {
425 3 50       15 return unless $virt{$key} != $term;
426             } else {
427 1 50       21 return unless $virt{$key} ne $term;
428             }
429             }
430             } elsif ( $q eq '$exists' ) {
431 2 100       60 if ($term) {
432 15 100       20 return unless exists $virt{$key};
433             } else {
434 10 100       63 return if exists $virt{$key};
435             }
436             } elsif ( $q eq '$mod'
437             && ref $term eq 'ARRAY'
438             && scalar @$term == 2 )
439             {
440             return
441             unless defined $virt{$key}
442             && is_float( $virt{$key} )
443 5 100 33     57 && $virt{$key} % $term->[0] == $term->[1];
      66        
444             } elsif ( $q eq '$in' && ref $term eq 'ARRAY' ) {
445             return
446             unless defined $virt{$key}
447 4 100 66     31 && &_value_in( $virt{$key}, $term );
448             } elsif ( $q eq '$nin' && ref $term eq 'ARRAY' ) {
449             return
450             unless defined $virt{$key}
451 4 100 66     15 && !&_value_in( $virt{$key}, $term );
452             } elsif ( $q eq '$size' && is_int($term) ) {
453             return
454             unless defined $virt{$key}
455             && (
456             (
457             ref $virt{$key} eq 'ARRAY'
458             && scalar @{ $virt{$key} } == $term
459             )
460             || ( ref $virt{$key} eq 'HASH'
461 3 100 66     18 && scalar keys %{ $virt{$key} } == $term )
      66        
462             );
463             } elsif ( $q eq '$all' && ref $term eq 'ARRAY' ) {
464             return
465 4 50 33     92 unless defined $virt{$key} && ref $virt{$key} eq 'ARRAY';
466 3         17 foreach (@$term) {
467 3 100       8 return unless &_value_in( $_, $virt{$key} );
468             }
469             } elsif ( $q eq '$type' && !ref $term ) {
470 6 100       69 if ( $term eq 'int' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
471             return
472 28 100 66     133 unless defined $virt{$key} && is_int( $virt{$key} );
473             } elsif ( $term eq 'float' ) {
474             return
475 3 50 33     16 unless defined $virt{$key} && is_float( $virt{$key} );
476             } elsif ( $term eq 'real' ) {
477             return
478 4 50 33     17 unless defined $virt{$key} && is_real( $virt{$key} );
479             } elsif ( $term eq 'whole' ) {
480             return
481 2 100 66     12 unless defined $virt{$key} && is_whole( $virt{$key} );
482             } elsif ( $term eq 'string' ) {
483             return
484             unless defined $virt{$key}
485 2 50 33     9 && is_string( $virt{$key} );
486             } elsif ( $term eq 'array' ) {
487             return
488             unless defined $virt{$key}
489 2 50 33     10 && ref $virt{$key} eq 'ARRAY';
490             } elsif ( $term eq 'hash' ) {
491             return
492             unless defined $virt{$key}
493 2 100 66     50 && ref $virt{$key} eq 'HASH';
494             } elsif ( $term eq 'bool' ) {
495              
496             # boolean - not really supported, will always return true since everything in Perl is a boolean
497             } elsif ( $term eq 'date' ) {
498 5 50 33     30 return unless defined $virt{$key} && !ref $virt{$key};
499             my $date = try {
500             DateTime::Format::W3CDTF->parse_datetime(
501 3     3   34 $virt{$key} )
502             } catch {
503             undef
504 3     1   9 };
  3         132  
505             return
506 1 100 66     37 unless blessed $date && blessed $date eq 'DateTime';
507             } elsif ( $term eq 'null' ) {
508             return
509 3 100 66     1838 unless exists $virt{$key} && !defined $virt{$key};
510             } elsif ( $term eq 'regex' ) {
511             return
512             unless defined $virt{$key}
513 3 50 33     27 && ref $virt{$key} eq 'Regexp';
514             }
515             }
516             }
517             }
518             } elsif ( ref $value eq 'ARRAY' ) {
519 2 50       10 return unless Compare( $value, $virt{$key} );
520             }
521              
522 1         6 return 1;
523             }
524              
525             ##############################################
526             # _array_has_eq( $value, \@array ) #
527             # ========================================== #
528             # $value - the value to check for #
529             # $array - the array to search in #
530             # ------------------------------------------ #
531             # returns true if the value exists in the #
532             # array provided. #
533             ##############################################
534              
535             sub _array_has_eq {
536 89     0   1021 my ( $value, $array ) = @_;
537              
538 0         0 foreach (@$array) {
539 0 0       0 return 1 if $_ eq $value;
540             }
541              
542 0         0 return;
543             }
544              
545             ##############################################
546             # _array_has_re( $regex, \@array ) #
547             # ========================================== #
548             # $regex - the regex to check for #
549             # $array - the array to search in #
550             # ------------------------------------------ #
551             # returns true if a value exists in the #
552             # array provided that matches the regex. #
553             ##############################################
554              
555             sub _array_has_re {
556 0     0   0 my ( $re, $array ) = @_;
557              
558 0         0 foreach (@$array) {
559 0 0       0 return 1 if m/$re/;
560             }
561              
562 0         0 return;
563             }
564              
565             ##############################################
566             # _has_adv_que( \%hash ) #
567             # ========================================== #
568             # $hash - the hash-ref to search in #
569             # ------------------------------------------ #
570             # returns true if the hash-ref has any of #
571             # the lang's advanced query operators #
572             ##############################################
573              
574             sub _has_adv_que {
575 0     93   0 my $hash = shift;
576              
577 93         110 foreach (
578             '$gt', '$gte', '$lt', '$lte', '$all', '$exists', '$mod',
579             '$eq', '$ne', '$in', '$nin', '$size', '$type'
580             )
581             {
582 93 100       146 return 1 if exists $hash->{$_};
583             }
584              
585 690         1061 return;
586             }
587              
588             ##############################################
589             # _value_in( $value, \@array ) #
590             # ========================================== #
591             # $value - the value to check for #
592             # $array - the array to search in #
593             # ------------------------------------------ #
594             # returns true if the value is one of the #
595             # values from the array. #
596             ##############################################
597              
598             sub _value_in {
599 2     13   5 my ( $value, $array ) = @_;
600              
601 13         25 foreach (@$array) {
602 13 50 66     17 next if is_float($_) && !is_float($value);
603 46 50 66     190 next if !is_float($_) && is_float($value);
604 46 50 66     357 return 1 if is_float($_) && $value == $_;
605 46 100 100     323 return 1 if !is_float($_) && $value eq $_;
606             }
607              
608 46         246 return;
609             }
610              
611             =head2 update_doc( \%document, \%update )
612              
613             Receives a document hash-ref and an update hash-ref, and updates the
614             document in-place according to the update hash-ref. Also returns the document
615             after the update. If the update hash-ref doesn't have any of the update
616             modifiers described by the language, then the update hash-ref is considered
617             as what the document should now be, and so will simply replace the document
618             hash-ref (once again, in accordance with MongoDB).
619              
620             See L<MQUL::Reference/"UPDATE STRUCTURE"> to learn about the structure of
621             update hash-refs.
622              
623             =cut
624              
625             sub update_doc {
626 4     20 1 82 my ( $doc, $obj ) = @_;
627              
628 20 100 100     569844 croak "MQUL::update_doc() requires a document hash-ref."
      100        
629             unless defined $doc && ref $doc && ref $doc eq 'HASH';
630 20 100 66     704 croak "MQUL::update_doc() requires an update hash-ref."
      100        
631             unless defined $obj && ref $obj && ref $obj eq 'HASH';
632              
633             # we only need to do something if the $obj hash-ref has any advanced
634             # update operations, otherwise $obj is meant to be the new $doc
635              
636 17 100       378 if ( &_has_adv_upd($obj) ) {
637 15         38 foreach my $op ( keys %$obj ) {
638 14 100       43 if ( $op eq '$inc' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
639              
640             # increase numerically
641 18 50       116 next unless ref $obj->{$op} eq 'HASH';
642 2         6 foreach my $field ( keys %{ $obj->{$op} } ) {
  2         4  
643 2   50     5 $doc->{$field} ||= 0;
644 2         7 $doc->{$field} += $obj->{$op}->{$field};
645             }
646             } elsif ( $op eq '$set' ) {
647              
648             # set key-value pairs
649 2 50       9 next unless ref $obj->{$op} eq 'HASH';
650 2         8 foreach my $field ( keys %{ $obj->{$op} } ) {
  2         4  
651 2         7 $doc->{$field} = $obj->{$op}->{$field};
652             }
653             } elsif ( $op eq '$unset' ) {
654              
655             # remove key-value pairs
656 3 50       9 next unless ref $obj->{$op} eq 'HASH';
657 2         10 foreach my $field ( keys %{ $obj->{$op} } ) {
  2         4  
658 2 50       7 delete $doc->{$field} if $obj->{$op}->{$field};
659             }
660             } elsif ( $op eq '$rename' ) {
661              
662             # rename attributes
663 2 50       9 next unless ref $obj->{$op} eq 'HASH';
664 1         5 foreach my $field ( keys %{ $obj->{$op} } ) {
  1         3  
665             $doc->{ $obj->{$op}->{$field} } = delete $doc->{$field}
666 1 50       3 if exists $doc->{$field};
667             }
668             } elsif ( $op eq '$push' ) {
669              
670             # push values to end of arrays
671 1 50       6 next unless ref $obj->{$op} eq 'HASH';
672 1         6 foreach my $field ( keys %{ $obj->{$op} } ) {
  1         2  
673             croak "The $field attribute is not an array in the doc."
674             if defined $doc->{$field}
675 1 50 33     4 && ref $doc->{$field} ne 'ARRAY';
676 1   50     7 $doc->{$field} ||= [];
677 1         4 push( @{ $doc->{$field} }, $obj->{$op}->{$field} );
  1         2  
678             }
679             } elsif ( $op eq '$pushAll' ) {
680              
681             # push a list of values to end of arrays
682 1 50       5 next unless ref $obj->{$op} eq 'HASH';
683 1         7 foreach my $field ( keys %{ $obj->{$op} } ) {
  1         2  
684             croak "The $field attribute is not an array in the doc."
685             if defined $doc->{$field}
686 1 50 33     3 && ref $doc->{$field} ne 'ARRAY';
687 1   50     8 $doc->{$field} ||= [];
688 1         4 push( @{ $doc->{$field} }, @{ $obj->{$op}->{$field} } );
  1         2  
  1         3  
689             }
690             } elsif ( $op eq '$addToSet' ) {
691              
692             # push values to arrays only if they're not already there
693 1 50       4 next unless ref $obj->{$op} eq 'HASH';
694 2         9 foreach my $field ( keys %{ $obj->{$op} } ) {
  2         11  
695             croak "The $field attribute is not an array in the doc."
696             if defined $doc->{$field}
697 2 50 33     6 && ref $doc->{$field} ne 'ARRAY';
698 2   50     15 $doc->{$field} ||= [];
699             my @add =
700             ref $obj->{$op}->{$field}
701             && ref $obj->{$op}->{$field} eq 'ARRAY'
702 2         13 ? @{ $obj->{$op}->{$field} }
703 2 100 66     7 : ( $obj->{$op}->{$field} );
704 1         3 foreach my $val (@add) {
705 4         13 push( @{ $doc->{$field} }, $val )
706 2 100       5 unless defined &_index_of( $val, $doc->{$field} );
707             }
708             }
709             } elsif ( $op eq '$pop' ) {
710              
711             # pop the last item from an array
712 2 50       11 next unless ref $obj->{$op} eq 'HASH';
713 2         8 foreach my $field ( keys %{ $obj->{$op} } ) {
  2         5  
714             croak "The $field attribute is not an array in the doc."
715             if defined $doc->{$field}
716 2 50 33     7 && ref $doc->{$field} ne 'ARRAY';
717 2   50     14 $doc->{$field} ||= [];
718 2         7 pop( @{ $doc->{$field} } )
719 2 50       6 if $obj->{$op}->{$field};
720             }
721             } elsif ( $op eq '$shift' ) {
722              
723             # shift the first item from an array
724 2 50       6 next unless ref $obj->{$op} eq 'HASH';
725 1         7 foreach my $field ( keys %{ $obj->{$op} } ) {
  1         2  
726             croak "The $field attribute is not an array in the doc."
727             if defined $doc->{$field}
728 1 50 33     4 && ref $doc->{$field} ne 'ARRAY';
729 1   50     8 $doc->{$field} ||= [];
730 1         4 shift( @{ $doc->{$field} } )
731 1 50       4 if $obj->{$op}->{$field};
732             }
733             } elsif ( $op eq '$splice' ) {
734              
735             # splice offsets from arrays
736 1 50       4 next unless ref $obj->{$op} eq 'HASH';
737 1         5 foreach my $field ( keys %{ $obj->{$op} } ) {
  1         2  
738             croak "The $field attribute is not an array in the doc."
739             if defined $doc->{$field}
740 1 50 33     3 && ref $doc->{$field} ne 'ARRAY';
741             next
742             unless ref $obj->{$op}->{$field}
743             && ref $obj->{$op}->{$field} eq 'ARRAY'
744 1 50 33     9 && scalar @{ $obj->{$op}->{$field} } == 2;
  1   33     51  
745 1   50     7 $doc->{$field} ||= [];
746             splice(
747 1         2 @{ $doc->{$field} },
748             $obj->{$op}->{$field}->[0],
749 1         4 $obj->{$op}->{$field}->[1]
750             );
751             }
752             } elsif ( $op eq '$pull' ) {
753              
754             # remove values from arrays
755 1 50       7 next unless ref $obj->{$op} eq 'HASH';
756 2         79 foreach my $field ( keys %{ $obj->{$op} } ) {
  2         4  
757             croak "The $field attribute is not an array in the doc."
758             if defined $doc->{$field}
759 2 50 33     8 && ref $doc->{$field} ne 'ARRAY';
760 2   50     11 $doc->{$field} ||= [];
761 2         6 my $i = &_index_of( $obj->{$op}->{$field}, $doc->{$field} );
762 2         6 while ( defined $i ) {
763 2         7 splice( @{ $doc->{$field} }, $i, 1 );
  2         4  
764             $i =
765 2         6 &_index_of( $obj->{$op}->{$field}, $doc->{$field} );
766             }
767             }
768             } elsif ( $op eq '$pullAll' ) {
769              
770             # remove a list of values from arrays
771 2 50       7 next unless ref $obj->{$op} eq 'HASH';
772 1         5 foreach my $field ( keys %{ $obj->{$op} } ) {
  1         3  
773             croak "The $field attribute is not an array in the doc."
774             if defined $doc->{$field}
775 1 50 33     4 && ref $doc->{$field} ne 'ARRAY';
776 1   50     7 $doc->{$field} ||= [];
777 1         5 foreach my $value ( @{ $obj->{$op}->{$field} } ) {
  1         2  
778 1         3 my $i = &_index_of( $value, $doc->{$field} );
779 2         6 while ( defined $i ) {
780 2         38 splice( @{ $doc->{$field} }, $i, 1 );
  2         4  
781 2         5 $i = &_index_of( $value, $doc->{$field} );
782             }
783             }
784             }
785             }
786             }
787             } else {
788              
789             # $obj is actually the new $doc
790 2         6 %$doc = %$obj;
791             }
792              
793 1         9 return $doc;
794             }
795              
796             ##############################################
797             # _has_adv_upd( \%hash ) #
798             # ========================================== #
799             # $hash - the hash-ref to search in #
800             # ------------------------------------------ #
801             # returns true if the hash-ref has any of #
802             # the lang's advanced update operators #
803             ##############################################
804              
805             sub _has_adv_upd {
806 15     15   153 my $hash = shift;
807              
808 15         25 foreach (
809             '$inc', '$set', '$unset', '$push', '$pushAll',
810             '$addToSet', '$pop', '$shift', '$splice', '$pull',
811             '$pullAll', '$rename', '$bit'
812             )
813             {
814 15 100       39 return 1 if exists $hash->{$_};
815             }
816              
817 98         238 return;
818             }
819              
820             ##############################################
821             # _index_of( $value, \@array ) #
822             # ========================================== #
823             # $value - the value to search for #
824             # $array - the array to search in #
825             # ------------------------------------------ #
826             # searches for the provided value in the #
827             # array, and returns its index if it is #
828             # found, or undef otherwise. #
829             ##############################################
830              
831             sub _index_of {
832 1     12   21 my ( $value, $array ) = @_;
833              
834 12         24 for ( my $i = 0 ; $i < scalar @$array ; $i++ ) {
835 12 50 33     31 if ( is_float( $array->[$i] ) && is_float($value) ) {
836 22 0       58 return $i if $array->[$i] == $value;
837             } else {
838 0 100       0 return $i if $array->[$i] eq $value;
839             }
840             }
841              
842 22         292 return;
843             }
844              
845             ##############################################
846             # _parse_function( $doc, $key ) #
847             # ========================================== #
848             # $doc - the document #
849             # $key - the key referencing a function and #
850             # a list of attributes, such as #
851             # min(attr1, attr2, attr3) #
852             # ------------------------------------------ #
853             # calculates the value using the appropriate #
854             # function and returns the result #
855             ##############################################
856              
857             sub _parse_function {
858 6     23   21 my ( $doc, $def ) = @_;
859              
860 23         36 my ($func) = keys %$def;
861              
862             die "Unrecognized function $func"
863 23 50       46 unless exists $BUILTINS{$func};
864              
865             $def->{$func} = [ $def->{$func} ]
866 23 100       56 unless ref $def->{$func};
867              
868 23         47 my @vals;
869 23         27 foreach ( @{ $def->{$func} } ) {
  23         27  
870 23         49 my ( $v, $k ) = _expand_dot_notation( $doc, $_ );
871 53 100       111 push( @vals, $v )
872             if defined $v;
873             }
874              
875 53 100       113 return unless scalar @vals;
876              
877 23         47 return $BUILTINS{$func}->(@vals);
878             }
879              
880             ##############################################
881             # _expand_dot_notation( $doc, $key ) #
882             # ========================================== #
883             # $doc - the document #
884             # $key - the key using dot notation #
885             # ------------------------------------------ #
886             # takes a key using the dot notation, and #
887             # returns the value of the document at the #
888             # end of the chain (if any), plus the key at #
889             # the end of the chain. #
890             ##############################################
891              
892             sub _expand_dot_notation {
893 21     70   62 my ( $doc, $key ) = @_;
894              
895 70 100       96 return ( $doc->{$key}, $key )
896             unless $key =~ m/\./;
897              
898 70         195 my @way_there = split( /\./, $key );
899              
900 28         72 $key = shift @way_there;
901 28         45 my %virt = ( $key => $doc->{$key} );
902              
903 28         68 while ( scalar @way_there ) {
904 28         55 $key = shift @way_there;
905 50         64 my ($have) = values %virt;
906              
907 50 100 100     73 if ( $have && ref $have eq 'HASH' && exists $have->{$key} ) {
    100 100        
      100        
      66        
      66        
908 50         279 %virt = ( $key => $have->{$key} );
909             } elsif ( $have
910             && ref $have eq 'ARRAY'
911             && $key =~ m/^\d+$/
912             && scalar @$have > $key )
913             {
914 35         90 %virt = ( $key => $have->[$key] );
915             } else {
916 10         62 %virt = ();
917             }
918             }
919              
920 5         10 return ( $virt{$key}, $key );
921             }
922              
923             =head1 DIAGNOSTICS
924              
925             =over
926              
927             =item C<< MQUL::doc_matches() requires a document hash-ref. >>
928              
929             This error means that you've either haven't passed the C<doc_matches()>
930             subroutine any parameters, or given it a non-hash-ref document.
931              
932             =item C<< MQUL::doc_matches() expects a query hash-ref. >>
933              
934             This error means that you've passed the C<doc_matches()> attribute a
935             non-hash-ref query variable. While you don't actually have to pass a
936             query variable, if you do, it has to be a hash-ref.
937              
938             =item C<< MQUL::update_doc() requires a document hash-ref. >>
939              
940             This error means that you've either haven't passed the C<update_doc()>
941             subroutine any parameters, or given it a non-hash-ref document.
942              
943             =item C<< MQUL::update_doc() requires an update hash-ref. >>
944              
945             This error means that you've passed the C<update_doc()> subroutine a
946             non-hash-ref update variable.
947              
948             =item C<< The %s attribute is not an array in the doc. >>
949              
950             This error means that your update hash-ref tries to modify an array attribute
951             (with C<$push>, C<$pushAll>, C<$addToSet>, C<$pull>, C<$pullAll>,
952             C<$pop>, C<$shift> and C<$splice>), but the attribute in the document
953             provided to the C<update_doc()> subroutine is not an array.
954              
955             =back
956              
957             =head1 CONFIGURATION AND ENVIRONMENT
958            
959             MQUL requires no configuration files or environment variables.
960              
961             =head1 DEPENDENCIES
962              
963             MQUL depends on the following modules:
964              
965             =over
966              
967             =item * L<Data::Compare>
968              
969             =item * L<Data::Types>
970              
971             =item * L<DateTime::Format::W3CDTF>
972              
973             =item * L<Scalar::Util>
974              
975             =item * L<Try::Tiny>
976              
977             =back
978              
979             =head1 INCOMPATIBILITIES
980              
981             None reported.
982              
983             =head1 BUGS AND LIMITATIONS
984              
985             No bugs have been reported.
986              
987             Please report any bugs or feature requests to
988             C<bug-MQUL@rt.cpan.org>, or through the web interface at
989             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=MQUL>.
990              
991             =head1 AUTHOR
992              
993             Ido Perlmuter <ido at ido50 dot net>
994              
995             =head1 LICENSE AND COPYRIGHT
996              
997             Copyright (c) 2011-2025, Ido Perlmuter C<< ido at ido50 dot net >>.
998              
999             Licensed under the Apache License, Version 2.0 (the "License");
1000             you may not use this file except in compliance with the License.
1001             The full License is included in the LICENSE file. You may also
1002             obtain a copy of the License at
1003              
1004             L<http://www.apache.org/licenses/LICENSE-2.0>
1005              
1006             Unless required by applicable law or agreed to in writing, software
1007             distributed under the License is distributed on an "AS IS" BASIS,
1008             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1009             See the License for the specific language governing permissions and
1010             limitations under the License.
1011              
1012             =cut
1013              
1014             1;
1015             __END__