]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Storage/QueryParser.pm
switching to the query parser ... truncation support!
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Storage / QueryParser.pm
1 package QueryParser;
2 our %parser_config = (
3     QueryParser => {
4         filters => [],
5         modifiers => [],
6         operators => { 
7             'and' => '&&',
8             'or' => '||',
9             group_start => '(',
10             group_end => ')',
11             required => '+',
12             modifier => '#'
13         }
14     }
15 );
16
17 sub search_class_count {
18     my $self = shift;
19     return @{$self->search_classes};
20 }
21
22 sub filter_count {
23     my $self = shift;
24     return @{$self->filters};
25 }
26
27 sub modifier_count {
28     my $self = shift;
29     return @{$self->modifiers};
30 }
31
32 sub custom_data {
33     my $class = shift;
34     $class = ref($class) || $class;
35
36     $parser_config{$class}{custom_data} ||= {};
37     return $parser_config{$class}{custom_data};
38 }
39
40 sub operators {
41     my $class = shift;
42     $class = ref($class) || $class;
43
44     $parser_config{$class}{operators} ||= {};
45     return $parser_config{$class}{operators};
46 }
47
48 sub filters {
49     my $class = shift;
50     $class = ref($class) || $class;
51
52     $parser_config{$class}{filters} ||= [];
53     return $parser_config{$class}{filters};
54 }
55
56 sub modifiers {
57     my $class = shift;
58     $class = ref($class) || $class;
59
60     $parser_config{$class}{modifiers} ||= [];
61     return $parser_config{$class}{modifiers};
62 }
63
64 sub new {
65     my $class = shift;
66     $class = ref($class) || $class;
67
68     my %opts = @_;
69
70     my $self = bless {} => $class;
71
72     for my $o (keys %{QueryParser->operators}) {
73         $class->operator($o => QueryParser->operator($o)) unless ($class->operator($o));
74     }
75
76     for my $opt ( keys %opts) {
77         $self->$opt( $opts{$opt} ) if ($self->can($opt));
78     }
79
80     return $self;
81 }
82
83 sub new_plan {
84     my $self = shift;
85     my $pkg = ref($self) || $self;
86     return do{$pkg.'::query_plan'}->new( QueryParser => $self, @_ );
87 }
88
89 sub add_search_filter {
90     my $pkg = shift;
91     $pkg = ref($pkg) || $pkg;
92     my $filter = shift;
93
94     return $filter if (grep { $_ eq $filter } @{$pkg->filters});
95     push @{$pkg->filters}, $filter;
96     return $filter;
97 }
98
99 sub add_search_modifier {
100     my $pkg = shift;
101     $pkg = ref($pkg) || $pkg;
102     my $modifier = shift;
103
104     return $modifier if (grep { $_ eq $modifier } @{$pkg->modifiers});
105     push @{$pkg->modifiers}, $modifier;
106     return $modifier;
107 }
108
109 sub add_search_class {
110     my $pkg = shift;
111     $pkg = ref($pkg) || $pkg;
112     my $class = shift;
113
114     return $class if (grep { $_ eq $class } @{$pkg->search_classes});
115
116     push @{$pkg->search_classes}, $class;
117     $pkg->search_fields->{$class} = [];
118     $pkg->default_search_class( $pkg->search_classes->[0] ) if (@{$pkg->search_classes} == 1);
119
120     return $class;
121 }
122
123 sub operator {
124     my $class = shift;
125     $class = ref($class) || $class;
126     my $opname = shift;
127     my $op = shift;
128
129     return undef unless ($opname);
130
131     $parser_config{$class}{operators} ||= {};
132     $parser_config{$class}{operators}{$opname} = $op if ($op);
133
134     return $parser_config{$class}{operators}{$opname};
135 }
136
137 sub search_classes {
138     my $class = shift;
139     $class = ref($class) || $class;
140     my $classes = shift;
141
142     $parser_config{$class}{classes} ||= [];
143     $parser_config{$class}{classes} = $classes if (ref($classes) && @$classes);
144     return $parser_config{$class}{classes};
145 }
146
147 sub add_query_normalizer {
148     my $pkg = shift;
149     $pkg = ref($pkg) || $pkg;
150     my $class = shift;
151     my $field = shift;
152     my $func = shift;
153     my $params = shift || [];
154
155     return $func if (grep { $_ eq $func } @{$pkg->query_normalizers->{$class}->{$field}});
156
157     push(@{$pkg->query_normalizers->{$class}->{$field}}, { function => $func, params => $params });
158
159     return $func;
160 }
161
162 sub query_normalizers {
163     my $pkg = shift;
164     $pkg = ref($pkg) || $pkg;
165
166     my $class = shift;
167     my $field = shift;
168
169     $parser_config{$pkg}{normalizers} ||= {};
170     if ($class) {
171         if ($field) {
172             $parser_config{$pkg}{normalizers}{$class}{$field} ||= [];
173             return $parser_config{$pkg}{normalizers}{$class}{$field};
174         } else {
175             return $parser_config{$pkg}{normalizers}{$class};
176         }
177     }
178
179     return $parser_config{$pkg}{normalizers};
180 }
181
182 sub default_search_class {
183     my $pkg = shift;
184     $pkg = ref($pkg) || $pkg;
185     my $class = shift;
186     $QueryParser::parser_config{$pkg}{default_class} = $pkg->add_search_class( $class ) if $class;
187
188     return $QueryParser::parser_config{$pkg}{default_class};
189 }
190
191 sub remove_search_class {
192     my $pkg = shift;
193     $pkg = ref($pkg) || $pkg;
194     my $class = shift;
195
196     return $class if (!grep { $_ eq $class } @{$pkg->search_classes});
197
198     $pkg->search_classes( [ grep { $_ ne $class } @{$pkg->search_classes} ] );
199     delete $QueryParser::parser_config{$pkg}{fields}{$class};
200
201     return $class;
202 }
203
204 sub add_search_field {
205     my $pkg = shift;
206     $pkg = ref($pkg) || $pkg;
207     my $class = shift;
208     my $field = shift;
209
210     $pkg->add_search_class( $class );
211
212     return { $class => $field }  if (grep { $_ eq $field } @{$pkg->search_fields->{$class}});
213
214     push @{$pkg->search_fields->{$class}}, $field;
215
216     return { $class => $field };
217 }
218
219 sub search_fields {
220     my $class = shift;
221     $class = ref($class) || $class;
222
223     $parser_config{$class}{fields} ||= {};
224     return $parser_config{$class}{fields};
225 }
226
227 sub add_search_class_alias {
228     my $pkg = shift;
229     $pkg = ref($pkg) || $pkg;
230     my $class = shift;
231     my $alias = shift;
232
233     $pkg->add_search_class( $class );
234
235     return { $class => $alias }  if (grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
236
237     push @{$pkg->search_class_aliases->{$class}}, $alias;
238
239     return { $class => $alias };
240 }
241
242 sub search_class_aliases {
243     my $class = shift;
244     $class = ref($class) || $class;
245
246     $parser_config{$class}{class_map} ||= {};
247     return $parser_config{$class}{class_map};
248 }
249
250 sub add_search_field_alias {
251     my $pkg = shift;
252     $pkg = ref($pkg) || $pkg;
253     my $class = shift;
254     my $field = shift;
255     my $alias = shift;
256
257     return { $class => { $field => $alias } }  if (grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
258
259     push @{$pkg->search_field_aliases->{$class}{$field}}, $alias;
260
261     return { $class => { $field => $alias } };
262 }
263
264 sub search_field_aliases {
265     my $class = shift;
266     $class = ref($class) || $class;
267
268     $parser_config{$class}{field_alias_map} ||= {};
269     return $parser_config{$class}{field_alias_map};
270 }
271
272 sub remove_search_field {
273     my $pkg = shift;
274     $pkg = ref($pkg) || $pkg;
275     my $class = shift;
276     my $field = shift;
277
278     return { $class => $field }  if (!$pkg->search_fields->{$class} || !grep { $_ eq $field } @{$pkg->search_fields->{$class}});
279
280     $pkg->search_fields->{$class} = [ grep { $_ ne $field } @{$pkg->search_fields->{$class}} ];
281
282     return { $class => $field };
283 }
284
285 sub remove_search_field_alias {
286     my $pkg = shift;
287     $pkg = ref($pkg) || $pkg;
288     my $class = shift;
289     my $field = shift;
290     my $alias = shift;
291
292     return { $class => { $field => $alias } }  if (!$pkg->search_field_aliases->{$class}{$field} || !grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
293
294     $pkg->search_field_aliases->{$class}{$field} = [ grep { $_ ne $alias } @{$pkg->search_field_aliases->{$class}{$field}} ];
295
296     return { $class => { $field => $alias } };
297 }
298
299 sub remove_search_class_alias {
300     my $pkg = shift;
301     $pkg = ref($pkg) || $pkg;
302     my $class = shift;
303     my $alias = shift;
304
305     return { $class => $alias }  if (!$pkg->search_class_aliases->{$class} || !grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
306
307     $pkg->search_class_aliases->{$class} = [ grep { $_ ne $alias } @{$pkg->search_class_aliases->{$class}} ];
308
309     return { $class => $alias };
310 }
311
312 sub debug {
313     my $self = shift;
314     my $q = shift;
315     $self->{_debug} = $q if (defined $q);
316     return $self->{_debug};
317 }
318
319 sub query {
320     my $self = shift;
321     my $q = shift;
322     $self->{_query} = $q if (defined $q);
323     return $self->{_query};
324 }
325
326 sub parse_tree {
327     my $self = shift;
328     my $q = shift;
329     $self->{_parse_tree} = $q if (defined $q);
330     return $self->{_parse_tree};
331 }
332
333 sub parse {
334     my $self = shift;
335     my $pkg = ref($self) || $self;
336     warn " ** parse package is $pkg\n" if $self->debug;
337     $self->parse_tree(
338         $self->decompose(
339             $self->query( shift() )
340         )
341     );
342
343     return $self;
344 }
345
346 sub decompose {
347     my $self = shift;
348     my $pkg = ref($self) || $self;
349
350     warn " ** decompose package is $pkg\n" if $self->debug;
351
352     $_ = shift;
353     my $current_class = shift || $self->default_search_class;
354
355     my $recursing = shift || 0;
356
357     # Build the search class+field uber-regexp
358     my $search_class_re = '^\s*(';
359     my $first_class = 1;
360
361     my %seen_classes;
362     for my $class ( keys %{$pkg->search_fields} ) {
363
364         for my $field ( @{$pkg->search_fields->{$class}} ) {
365
366             for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
367                 $alias = qr/$alias/;
368                 s/\b$alias[:=]/$class\|$field:/g;
369             }
370         }
371
372         $search_class_re .= '|' unless ($first_class);
373         $first_class = 0;
374         $search_class_re .= $class . '(?:\|\w+)*';
375         $seeen_class{$class} = 1;
376     }
377
378     for my $class ( keys %{$pkg->search_class_aliases} ) {
379
380         for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
381             $alias = qr/$alias/;
382             s/(^|[^|])\b$alias\|/$1$class\|/g;
383             s/(^|[^|])\b$alias[:=]/$1$class:/g;
384         }
385
386         $search_class_re .= '|' unless ($first_class);
387         $first_class = 0;
388
389         $search_class_re .= $class . '(?:\|\w+)*' if (!$seeen_class{$class});
390         $seeen_class{$class} = 1;
391     }
392     $search_class_re .= '):';
393
394     warn " ** Search class RE: $search_class_re\n" if $self->debug;
395
396     my $required_re = $pkg->operator('required');
397     $required_re = qr/^\s*\Q$required_re\E/;
398     my $and_re = $pkg->operator('and');
399     $and_re = qr/^\s*\Q$and_re\E/;
400
401     my $or_re = $pkg->operator('or');
402     $or_re = qr/^\s*\Q$or_re\E/;
403
404     my $group_start_re = $pkg->operator('group_start');
405     $group_start_re = qr/^\s*\Q$group_start_re\E/;
406
407     my $group_end = $pkg->operator('group_end');
408     my $group_end_re = qr/^\s*\Q$group_end\E/;
409
410     my $modifier_tag_re = $pkg->operator('modifier');
411     $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
412
413
414     # Build the filter and modifier uber-regexps
415     my $filter_re = '^\s*(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
416     my $filter_as_class_re = '^\s*(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
417
418     my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
419     my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
420
421     my $struct = $self->new_plan( level => $recursing );
422     my $remainder = '';
423
424     my $last_type = '';
425     while (!$remainder) {
426         if (/$group_end_re/) { # end of an explicit group
427             warn "Encountered explicit group end\n" if $self->debug;
428
429             $_ = $';
430             $remainder = $';
431
432             $last_type = '';
433         } elsif ($self->filter_count && /$filter_re/) { # found a filter
434             warn "Encountered search filter: $1 set to $2\n" if $self->debug;
435
436             $_ = $';
437             $struct->new_filter( $1 => [ split '[, ]+', $2 ] );
438
439             $last_type = '';
440         } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
441             warn "Encountered search filter: $1 set to $2\n" if $self->debug;
442
443             $_ = $';
444             $struct->new_filter( $1 => [ split '[, ]+', $2 ] );
445
446             $last_type = '';
447         } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
448             warn "Encountered search modifier: $1\n" if $self->debug;
449
450             $_ = $';
451             if (!$struct->top_plan) {
452                 warn "  Search modifiers only allowed at the top level of the query\n" if $self->debug;
453             } else {
454                 $struct->new_modifier($1);
455             }
456
457             $last_type = '';
458         } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
459             warn "Encountered search modifier: $1\n" if $self->debug;
460
461             my $mod = $1;
462
463             $_ = $';
464             if (!$struct->top_plan) {
465                 warn "  Search modifiers only allowed at the top level of the query\n" if $self->debug;
466             } elsif ($2 =~ /^[ty1]/i) {
467                 $struct->new_modifier($mod);
468             }
469
470             $last_type = '';
471         } elsif (/$group_start_re/) { # start of an explicit group
472             warn "Encountered explicit group start\n" if $self->debug;
473
474             my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
475             $struct->add_node( $substruct );
476             $_ = $subremainder;
477
478             $last_type = '';
479         } elsif (/$and_re/) { # ANDed expression
480             $_ = $';
481             next if ($last_type eq 'AND');
482             next if ($last_type eq 'OR');
483             warn "Encountered AND\n" if $self->debug;
484
485             $struct->joiner( '&' );
486
487             $last_type = 'AND';
488         } elsif (/$or_re/) { # ORed expression
489             $_ = $';
490             next if ($last_type eq 'AND');
491             next if ($last_type eq 'OR');
492             warn "Encountered OR\n" if $self->debug;
493
494             $struct->joiner( '|' );
495
496             $last_type = 'OR';
497         } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
498             warn "Encountered class change: $1\n" if $self->debug;
499
500             $current_class = $1;
501             $struct->classed_node( $current_class );
502             $_ = $';
503
504             $last_type = '';
505         } elsif (/^\s*"([^"]+)"/) { # phrase, always anded
506             warn "Encountered phrase: $1\n" if $self->debug;
507
508             $struct->joiner( '&' );
509             my $phrase = $1;
510
511             my $class_node = $struct->classed_node($current_class);
512             $class_node->add_phrase( $phrase );
513             $_ = $phrase . $';
514
515             $last_type = '';
516         } elsif (/$required_re([^\s)]+)/) { # phrase, always anded
517             warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
518
519             my $phrase = $1;
520
521             my $class_node = $struct->classed_node($current_class);
522             $class_node->add_phrase( $phrase );
523             $_ = $phrase . $';
524             $struct->joiner( '&' );
525
526             $last_type = '';
527         } elsif (/^\s*([^$group_end\s]+)/o) { # atom
528             warn "Encountered atom: $1\n" if $self->debug;
529             warn "Remainder: $'\n" if $self->debug;
530
531             my $atom = $1;
532             my $after = $';
533
534             my $class_node = $struct->classed_node($current_class);
535             my $negator = ($atom =~ s/^-//o) ? '!' : '';
536             my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
537
538             $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $negator, node => $class_node );
539             $struct->joiner( '&' );
540
541             $_ = $after;
542             $last_type = '';
543         } 
544
545         last unless ($_);
546
547     }
548
549     return $struct if !wantarray;
550     return ($struct, $remainder);
551 }
552
553 sub find_class_index {
554     my $class = shift;
555     my $query = shift;
556
557     my ($class_part, @field_parts) = split '\|', $class;
558     $class_part ||= $class;
559
560     for my $idx ( 0 .. scalar(@$query) - 1 ) {
561         next unless ref($$query[$idx]);
562         return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
563     }
564
565     push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
566     return -1;
567 }
568
569 sub core_limit {
570     my $self = shift;
571     my $l = shift;
572     $self->{core_limit} = $l if ($l);
573     return $self->{core_limit};
574 }
575
576 sub superpage {
577     my $self = shift;
578     my $l = shift;
579     $self->{superpage} = $l if ($l);
580     return $self->{superpage};
581 }
582
583 sub superpage_size {
584     my $self = shift;
585     my $l = shift;
586     $self->{superpage_size} = $l if ($l);
587     return $self->{superpage_size};
588 }
589
590
591 #-------------------------------
592 package QueryParser::query_plan;
593
594 sub QueryParser {
595     my $self = shift;
596     return undef unless ref($self);
597     return $self->{QueryParser};
598 }
599
600 sub new {
601     my $pkg = shift;
602     $pkg = ref($pkg) || $pkg;
603     my %args = (joiner => '&', @_);
604
605     return bless \%args => $pkg;
606 }
607
608 sub new_node {
609     my $self = shift;
610     my $pkg = ref($self) || $self;
611     my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
612     $self->add_node( $node );
613     return $node;
614 }
615
616 sub new_filter {
617     my $self = shift;
618     my $pkg = ref($self) || $self;
619     my $name = shift;
620     my $args = shift;
621
622     my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args );
623     $self->add_filter( $node );
624
625     return $node;
626 }
627
628 sub find_filter {
629     my $self = shift;
630     my $needle = shift;;
631     return undef unless ($needle);
632     return grep { $_->name eq $needle } @{ $self->filters };
633 }
634
635 sub find_modifier {
636     my $self = shift;
637     my $needle = shift;;
638     return undef unless ($needle);
639     return grep { $_->name eq $needle } @{ $self->modifiers };
640 }
641
642 sub new_modifier {
643     my $self = shift;
644     my $pkg = ref($self) || $self;
645     my $name = shift;
646
647     my $node = do{$pkg.'::modifier'}->new( $name );
648     $self->add_modifier( $node );
649
650     return $node;
651 }
652
653 sub classed_node {
654     my $self = shift;
655     my $requested_class = shift;
656
657     my $node;
658     for my $n (@{$self->{query}}) {
659         next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
660         if ($n->requested_class eq $requested_class) {
661             $node = $n;
662             last;
663         }
664     }
665
666     if (!$node) {
667         $node = $self->new_node;
668         $node->requested_class( $requested_class );
669     }
670
671     return $node;
672 }
673
674 sub query_nodes {
675     my $self = shift;
676     return $self->{query};
677 }
678
679 sub add_node {
680     my $self = shift;
681     my $node = shift;
682
683     $self->{query} ||= [];
684     push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
685     push(@{$self->{query}}, $node);
686
687     return $self;
688 }
689
690 sub top_plan {
691     my $self = shift;
692
693     return $self->{level} ? 0 : 1;
694 }
695
696 sub plan_level {
697     my $self = shift;
698     return $self->{level};
699 }
700
701 sub joiner {
702     my $self = shift;
703     my $joiner = shift;
704
705     $self->{joiner} = $joiner if ($joiner);
706     return $self->{joiner};
707 }
708
709 sub modifiers {
710     my $self = shift;
711     $self->{modifiers} ||= [];
712     return $self->{modifiers};
713 }
714
715 sub add_modifier {
716     my $self = shift;
717     my $modifier = shift;
718
719     $self->{modifiers} ||= [];
720     return $self if (grep {$$_ eq $$modifier} @{$self->{modifiers}});
721
722     push(@{$self->{modifiers}}, $modifier);
723
724     return $self;
725 }
726
727 sub filters {
728     my $self = shift;
729     $self->{filters} ||= [];
730     return $self->{filters};
731 }
732
733 sub add_filter {
734     my $self = shift;
735     my $filter = shift;
736
737     $self->{filters} ||= [];
738     return $self if (grep {$_->name eq $filter->name} @{$self->{filters}});
739
740     push(@{$self->{filters}}, $filter);
741
742     return $self;
743 }
744
745
746 #-------------------------------
747 package QueryParser::query_plan::node;
748
749 sub new {
750     my $pkg = shift;
751     $pkg = ref($pkg) || $pkg;
752     my %args = @_;
753
754     return bless \%args => $pkg;
755 }
756
757 sub new_atom {
758     my $self = shift;
759     my $pkg = ref($self) || $self;
760     return do{$pkg.'::atom'}->new( @_ );
761 }
762
763 sub requested_class { # also split into classname and fields
764     my $self = shift;
765     my $class = shift;
766
767     if ($class) {
768         my ($class_part, @field_parts) = split '\|', $class;
769         $class_part ||= $class;
770
771         $self->{requested_class} = $class;
772         $self->{classname} = $class_part;
773         $self->{fields} = \@field_parts;
774     }
775
776     return $self->{requested_class};
777 }
778
779 sub plan {
780     my $self = shift;
781     my $plan = shift;
782
783     $self->{plan} = $plan if ($plan);
784     return $self->{plan};
785 }
786
787 sub classname {
788     my $self = shift;
789     my $class = shift;
790
791     $self->{classname} = $class if ($class);
792     return $self->{classname};
793 }
794
795 sub fields {
796     my $self = shift;
797     my @fields = @_;
798
799     $self->{fields} ||= [];
800     $self->{fields} = \@fields if (@fields);
801     return $self->{fields};
802 }
803
804 sub phrases {
805     my $self = shift;
806     my @phrases = @_;
807
808     $self->{phrases} ||= [];
809     $self->{phrases} = \@phrases if (@phrases);
810     return $self->{phrases};
811 }
812
813 sub add_phrase {
814     my $self = shift;
815     my $phrase = shift;
816
817     push(@{$self->phrases}, $phrase);
818
819     return $self;
820 }
821
822 sub query_atoms {
823     my $self = shift;
824     my @query_atoms = @_;
825
826     $self->{query_atoms} ||= [];
827     $self->{query_atoms} = \@query_atoms if (@query_atoms);
828     return $self->{query_atoms};
829 }
830
831 sub add_fts_atom {
832     my $self = shift;
833     my $atom = shift;
834
835     if (!ref($atom)) {
836         my $content = $atom;
837         my @parts = @_;
838
839         $atom = $self->new_atom( content => $content, @parts );
840     }
841
842     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
843     push(@{$self->query_atoms}, $atom);
844
845     return $self;
846 }
847
848 #-------------------------------
849 package QueryParser::query_plan::node::atom;
850
851 sub new {
852     my $pkg = shift;
853     $pkg = ref($pkg) || $pkg;
854     my %args = @_;
855
856     return bless \%args => $pkg;
857 }
858
859 sub node {
860     my $self = shift;
861     return undef unless (ref $self);
862     return $self->{node};
863 }
864
865 sub content {
866     my $self = shift;
867     return undef unless (ref $self);
868     return $self->{content};
869 }
870
871 sub prefix {
872     my $self = shift;
873     return undef unless (ref $self);
874     return $self->{prefix};
875 }
876
877 sub suffix {
878     my $self = shift;
879     return undef unless (ref $self);
880     return $self->{suffix};
881 }
882
883 #-------------------------------
884 package QueryParser::query_plan::filter;
885
886 sub new {
887     my $pkg = shift;
888     $pkg = ref($pkg) || $pkg;
889     my %args = @_;
890
891     return bless \%args => $pkg;
892 }
893
894 sub plan {
895     my $self = shift;
896     return $self->{plan};
897 }
898
899 sub name {
900     my $self = shift;
901     return $self->{name};
902 }
903
904 sub args {
905     my $self = shift;
906     return $self->{args};
907 }
908
909 #-------------------------------
910 package QueryParser::query_plan::modifier;
911
912 sub new {
913     my $pkg = shift;
914     $pkg = ref($pkg) || $pkg;
915     my $modifier = shift;
916
917     return bless \$modifier => $pkg;
918 }
919
920 sub name {
921     my $self = shift;
922     return $$self;
923 }
924
925 1;
926