]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Storage/QueryParser.pm
support * as a truncation operator in FTS. NOTE: this will require Postgres 8.4...
[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     $self->parse_tree(
336         $self->decompose(
337             $self->query( shift() )
338         )
339     );
340
341     return $self;
342 }
343
344 sub decompose {
345     my $self = shift;
346     my $pkg = ref($self) || $self;;
347
348     $_ = shift;
349     my $current_class = shift || $self->default_search_class;
350
351     my $recursing = shift || 0;
352
353     # Build the search class+field uber-regexp
354     my $search_class_re = '^\s*(';
355     my $first_class = 1;
356
357     for my $class ( keys %{$pkg->search_field_aliases} ) {
358
359         for my $field ( keys %{$pkg->search_field_aliases->{$class}} ) {
360
361             for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
362                 $alias = qr/$alias/;
363                 s/\b$alias[:=]/$class\|$field:/g;
364             }
365
366             $search_class_re .= '|' unless ($first_class);
367             $first_class = 0;
368
369             $search_class_re .= $class;
370         }
371     }
372
373     for my $class ( keys %{$pkg->search_class_aliases} ) {
374
375         for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
376             $alias = qr/$alias/;
377             s/(^|[^|])\b$alias\|/$1$class\|/g;
378             s/(^|[^|])\b$alias[:=]/$1$class:/g;
379         }
380
381         $search_class_re .= '|' unless ($first_class);
382         $first_class = 0;
383
384         $search_class_re .= $class . '(?:\|\w+)*';
385     }
386     $search_class_re .= '):';
387
388     my $required_re = $pkg->operator('required');
389     $required_re = qr/^\s*\Q$required_re\E/;
390     my $and_re = $pkg->operator('and');
391     $and_re = qr/^\s*\Q$and_re\E/;
392
393     my $or_re = $pkg->operator('or');
394     $or_re = qr/^\s*\Q$or_re\E/;
395
396     my $group_start_re = $pkg->operator('group_start');
397     $group_start_re = qr/^\s*\Q$group_start_re\E/;
398
399     my $group_end = $pkg->operator('group_end');
400     my $group_end_re = qr/^\s*\Q$group_end\E/;
401
402     my $modifier_tag_re = $pkg->operator('modifier');
403     $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
404
405
406     # Build the filter and modifier uber-regexps
407     my $filter_re = '^\s*(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
408     my $filter_as_class_re = '^\s*(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
409
410     my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
411     my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
412
413     my $struct = $self->new_plan( level => $recursing );
414     my $remainder = '';
415
416     my $last_type = '';
417     while (!$remainder) {
418         if (/$group_end_re/) { # end of an explicit group
419             warn "Encountered explicit group end\n" if $self->debug;
420
421             $_ = $';
422             $remainder = $';
423
424             $last_type = '';
425         } elsif ($self->filter_count && /$filter_re/) { # found a filter
426             warn "Encountered search filter: $1 set to $2\n" if $self->debug;
427
428             $_ = $';
429             $struct->new_filter( $1 => [ split '[, ]+', $2 ] );
430
431             $last_type = '';
432         } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
433             warn "Encountered search filter: $1 set to $2\n" if $self->debug;
434
435             $_ = $';
436             $struct->new_filter( $1 => [ split '[, ]+', $2 ] );
437
438             $last_type = '';
439         } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
440             warn "Encountered search modifier: $1\n" if $self->debug;
441
442             $_ = $';
443             if (!$struct->top_plan) {
444                 warn "  Search modifiers only allowed at the top level of the query\n" if $self->debug;
445             } else {
446                 $struct->new_modifier($1);
447             }
448
449             $last_type = '';
450         } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
451             warn "Encountered search modifier: $1\n" if $self->debug;
452
453             my $mod = $1;
454
455             $_ = $';
456             if (!$struct->top_plan) {
457                 warn "  Search modifiers only allowed at the top level of the query\n" if $self->debug;
458             } elsif ($2 =~ /^[ty1]/i) {
459                 $struct->new_modifier($mod);
460             }
461
462             $last_type = '';
463         } elsif (/$group_start_re/) { # start of an explicit group
464             warn "Encountered explicit group start\n" if $self->debug;
465
466             my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
467             $struct->add_node( $substruct );
468             $_ = $subremainder;
469
470             $last_type = '';
471         } elsif (/$and_re/) { # ANDed expression
472             $_ = $';
473             next if ($last_type eq 'AND');
474             next if ($last_type eq 'OR');
475             warn "Encountered AND\n" if $self->debug;
476
477             $struct->joiner( '&' );
478
479             $last_type = 'AND';
480         } elsif (/$or_re/) { # ORed expression
481             $_ = $';
482             next if ($last_type eq 'AND');
483             next if ($last_type eq 'OR');
484             warn "Encountered OR\n" if $self->debug;
485
486             $struct->joiner( '|' );
487
488             $last_type = 'OR';
489         } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
490             warn "Encountered class change: $1\n" if $self->debug;
491
492             $current_class = $1;
493             $struct->classed_node( $current_class );
494             $_ = $';
495
496             $last_type = '';
497         } elsif (/^\s*"([^"]+)"/) { # phrase, always anded
498             warn "Encountered phrase: $1\n" if $self->debug;
499
500             $struct->joiner( '&' );
501             my $phrase = $1;
502
503             my $class_node = $struct->classed_node($current_class);
504             $class_node->add_phrase( $phrase );
505             $_ = $phrase . $';
506
507             $last_type = '';
508         } elsif (/$required_re([^\s)]+)/) { # phrase, always anded
509             warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
510
511             my $phrase = $1;
512
513             my $class_node = $struct->classed_node($current_class);
514             $class_node->add_phrase( $phrase );
515             $_ = $phrase . $';
516             $struct->joiner( '&' );
517
518             $last_type = '';
519         } elsif (/^\s*([^$group_end\s]+)/o) { # atom
520             warn "Encountered atom: $1\n" if $self->debug;
521             warn "Remainder: $'\n" if $self->debug;
522
523             my $atom = $1;
524             my $after = $';
525
526             my $class_node = $struct->classed_node($current_class);
527             my $negator = ($atom =~ s/^-//o) ? '!' : '';
528             my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
529
530             $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $negator, node => $class_node );
531             $struct->joiner( '&' );
532
533             $_ = $after;
534             $last_type = '';
535         } 
536
537         last unless ($_);
538
539     }
540
541     return $struct if !wantarray;
542     return ($struct, $remainder);
543 }
544
545 sub find_class_index {
546     my $class = shift;
547     my $query = shift;
548
549     my ($class_part, @field_parts) = split '\|', $class;
550     $class_part ||= $class;
551
552     for my $idx ( 0 .. scalar(@$query) - 1 ) {
553         next unless ref($$query[$idx]);
554         return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
555     }
556
557     push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
558     return -1;
559 }
560
561 sub core_limit {
562     my $self = shift;
563     my $l = shift;
564     $self->{core_limit} = $l if ($l);
565     return $self->{core_limit};
566 }
567
568 sub superpage {
569     my $self = shift;
570     my $l = shift;
571     $self->{superpage} = $l if ($l);
572     return $self->{superpage};
573 }
574
575 sub superpage_size {
576     my $self = shift;
577     my $l = shift;
578     $self->{superpage_size} = $l if ($l);
579     return $self->{superpage_size};
580 }
581
582
583 #-------------------------------
584 package QueryParser::query_plan;
585
586 sub QueryParser {
587     my $self = shift;
588     return undef unless ref($self);
589     return $self->{QueryParser};
590 }
591
592 sub new {
593     my $pkg = shift;
594     $pkg = ref($pkg) || $pkg;
595     my %args = (joiner => '&', @_);
596
597     return bless \%args => $pkg;
598 }
599
600 sub new_node {
601     my $self = shift;
602     my $pkg = ref($self) || $self;
603     my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
604     $self->add_node( $node );
605     return $node;
606 }
607
608 sub new_filter {
609     my $self = shift;
610     my $pkg = ref($self) || $self;
611     my $name = shift;
612     my $args = shift;
613
614     my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args );
615     $self->add_filter( $node );
616
617     return $node;
618 }
619
620 sub find_filter {
621     my $self = shift;
622     my $needle = shift;;
623     return undef unless ($needle);
624     return grep { $_->name eq $needle } @{ $self->filters };
625 }
626
627 sub find_modifier {
628     my $self = shift;
629     my $needle = shift;;
630     return undef unless ($needle);
631     return grep { $_->name eq $needle } @{ $self->modifiers };
632 }
633
634 sub new_modifier {
635     my $self = shift;
636     my $pkg = ref($self) || $self;
637     my $name = shift;
638
639     my $node = do{$pkg.'::modifier'}->new( $name );
640     $self->add_modifier( $node );
641
642     return $node;
643 }
644
645 sub classed_node {
646     my $self = shift;
647     my $requested_class = shift;
648
649     my $node;
650     for my $n (@{$self->{query}}) {
651         next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
652         if ($n->requested_class eq $requested_class) {
653             $node = $n;
654             last;
655         }
656     }
657
658     if (!$node) {
659         $node = $self->new_node;
660         $node->requested_class( $requested_class );
661     }
662
663     return $node;
664 }
665
666 sub query_nodes {
667     my $self = shift;
668     return $self->{query};
669 }
670
671 sub add_node {
672     my $self = shift;
673     my $node = shift;
674
675     $self->{query} ||= [];
676     push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
677     push(@{$self->{query}}, $node);
678
679     return $self;
680 }
681
682 sub top_plan {
683     my $self = shift;
684
685     return $self->{level} ? 0 : 1;
686 }
687
688 sub plan_level {
689     my $self = shift;
690     return $self->{level};
691 }
692
693 sub joiner {
694     my $self = shift;
695     my $joiner = shift;
696
697     $self->{joiner} = $joiner if ($joiner);
698     return $self->{joiner};
699 }
700
701 sub modifiers {
702     my $self = shift;
703     $self->{modifiers} ||= [];
704     return $self->{modifiers};
705 }
706
707 sub add_modifier {
708     my $self = shift;
709     my $modifier = shift;
710
711     $self->{modifiers} ||= [];
712     return $self if (grep {$$_ eq $$modifier} @{$self->{modifiers}});
713
714     push(@{$self->{modifiers}}, $modifier);
715
716     return $self;
717 }
718
719 sub filters {
720     my $self = shift;
721     $self->{filters} ||= [];
722     return $self->{filters};
723 }
724
725 sub add_filter {
726     my $self = shift;
727     my $filter = shift;
728
729     $self->{filters} ||= [];
730     return $self if (grep {$_->name eq $filter->name} @{$self->{filters}});
731
732     push(@{$self->{filters}}, $filter);
733
734     return $self;
735 }
736
737
738 #-------------------------------
739 package QueryParser::query_plan::node;
740
741 sub new {
742     my $pkg = shift;
743     $pkg = ref($pkg) || $pkg;
744     my %args = @_;
745
746     return bless \%args => $pkg;
747 }
748
749 sub new_atom {
750     my $self = shift;
751     my $pkg = ref($self) || $self;
752     return do{$pkg.'::atom'}->new( @_ );
753 }
754
755 sub requested_class { # also split into classname and fields
756     my $self = shift;
757     my $class = shift;
758
759     if ($class) {
760         my ($class_part, @field_parts) = split '\|', $class;
761         $class_part ||= $class;
762
763         $self->{requested_class} = $class;
764         $self->{classname} = $class_part;
765         $self->{fields} = \@field_parts;
766     }
767
768     return $self->{requested_class};
769 }
770
771 sub plan {
772     my $self = shift;
773     my $plan = shift;
774
775     $self->{plan} = $plan if ($plan);
776     return $self->{plan};
777 }
778
779 sub classname {
780     my $self = shift;
781     my $class = shift;
782
783     $self->{classname} = $class if ($class);
784     return $self->{classname};
785 }
786
787 sub fields {
788     my $self = shift;
789     my @fields = @_;
790
791     $self->{fields} ||= [];
792     $self->{fields} = \@fields if (@fields);
793     return $self->{fields};
794 }
795
796 sub phrases {
797     my $self = shift;
798     my @phrases = @_;
799
800     $self->{phrases} ||= [];
801     $self->{phrases} = \@phrases if (@phrases);
802     return $self->{phrases};
803 }
804
805 sub add_phrase {
806     my $self = shift;
807     my $phrase = shift;
808
809     push(@{$self->phrases}, $phrase);
810
811     return $self;
812 }
813
814 sub query_atoms {
815     my $self = shift;
816     my @query_atoms = @_;
817
818     $self->{query_atoms} ||= [];
819     $self->{query_atoms} = \@query_atoms if (@query_atoms);
820     return $self->{query_atoms};
821 }
822
823 sub add_fts_atom {
824     my $self = shift;
825     my $atom = shift;
826
827     if (!ref($atom)) {
828         my $content = $atom;
829         my @parts = @_;
830
831         $atom = $self->new_atom( content => $content, @parts );
832     }
833
834     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
835     push(@{$self->query_atoms}, $atom);
836
837     return $self;
838 }
839
840 #-------------------------------
841 package QueryParser::query_plan::node::atom;
842
843 sub new {
844     my $pkg = shift;
845     $pkg = ref($pkg) || $pkg;
846     my %args = @_;
847
848     return bless \%args => $pkg;
849 }
850
851 sub node {
852     my $self = shift;
853     return undef unless (ref $self);
854     return $self->{node};
855 }
856
857 sub content {
858     my $self = shift;
859     return undef unless (ref $self);
860     return $self->{content};
861 }
862
863 sub prefix {
864     my $self = shift;
865     return undef unless (ref $self);
866     return $self->{prefix};
867 }
868
869 sub suffix {
870     my $self = shift;
871     return undef unless (ref $self);
872     return $self->{suffix};
873 }
874
875 #-------------------------------
876 package QueryParser::query_plan::filter;
877
878 sub new {
879     my $pkg = shift;
880     $pkg = ref($pkg) || $pkg;
881     my %args = @_;
882
883     return bless \%args => $pkg;
884 }
885
886 sub plan {
887     my $self = shift;
888     return $self->{plan};
889 }
890
891 sub name {
892     my $self = shift;
893     return $self->{name};
894 }
895
896 sub args {
897     my $self = shift;
898     return $self->{args};
899 }
900
901 #-------------------------------
902 package QueryParser::query_plan::modifier;
903
904 sub new {
905     my $pkg = shift;
906     $pkg = ref($pkg) || $pkg;
907     my $modifier = shift;
908
909     return bless \$modifier => $pkg;
910 }
911
912 sub name {
913     my $self = shift;
914     return $$self;
915 }
916
917 1;
918