LP#1775958: Rework pullup mechanism to flatten more nested queries
[evergreen-equinox.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Storage / QueryParser.pm
1 use strict;
2 use warnings;
3
4 package QueryParser;
5 use OpenSRF::Utils::JSON;
6 use Data::Dumper;
7
8 =head1 NAME
9
10 QueryParser - basic QueryParser class
11
12 =head1 SYNOPSIS
13
14 use QueryParser;
15 my $QParser = QueryParser->new(%args);
16
17 =head1 DESCRIPTION
18
19 Main entrypoint into the QueryParser functionality.
20
21 =head1 FUNCTIONS
22
23 =cut
24
25 # Note that the first key must match the name of the package.
26 our %parser_config = (
27     QueryParser => {
28         filters => [],
29         modifiers => [],
30         operators => { 
31             'and' => '&&',
32             'or' => '||',
33             float_start => '{{',
34             float_end => '}}',
35             group_start => '(',
36             group_end => ')',
37             required => '+',
38             disallowed => '-',
39             modifier => '#',
40             negated => '!'
41         }
42     }
43 );
44
45 sub canonicalize {
46     my $self = shift;
47     return QueryParser::Canonicalize::abstract_query2str_impl(
48         $self->parse_tree->to_abstract_query(@_)
49     );
50 }
51
52
53 =head2 facet_class_count
54
55     $count = $QParser->facet_class_count();
56 =cut
57
58 sub facet_class_count {
59     my $self = shift;
60     return @{$self->facet_classes};
61 }
62
63 =head2 search_class_count
64
65     $count = $QParser->search_class_count();
66 =cut
67
68 sub search_class_count {
69     my $self = shift;
70     return @{$self->search_classes};
71 }
72
73 =head2 filter_count
74
75     $count = $QParser->filter_count();
76 =cut
77
78 sub filter_count {
79     my $self = shift;
80     return @{$self->filters};
81 }
82
83 =head2 modifier_count
84
85     $count = $QParser->modifier_count();
86 =cut
87
88 sub modifier_count {
89     my $self = shift;
90     return @{$self->modifiers};
91 }
92
93 =head2 custom_data
94
95     $data = $QParser->custom_data($class);
96 =cut
97
98 sub custom_data {
99     my $class = shift;
100     $class = ref($class) || $class;
101
102     $parser_config{$class}{custom_data} ||= {};
103     return $parser_config{$class}{custom_data};
104 }
105
106 =head2 operators
107
108     $operators = $QParser->operators();
109
110 Returns hashref of the configured operators.
111 =cut
112
113 sub operators {
114     my $class = shift;
115     $class = ref($class) || $class;
116
117     $parser_config{$class}{operators} ||= {};
118     return $parser_config{$class}{operators};
119 }
120
121 sub allow_nested_modifiers {
122     my $class = shift;
123     my $v = shift;
124     $class = ref($class) || $class;
125
126     $parser_config{$class}{allow_nested_modifiers} = $v if (defined $v);
127     return $parser_config{$class}{allow_nested_modifiers};
128 }
129
130 =head2 filters
131
132     $filters = $QParser->filters();
133
134 Returns arrayref of the configured filters.
135 =cut
136
137 sub filters {
138     my $class = shift;
139     $class = ref($class) || $class;
140
141     $parser_config{$class}{filters} ||= [];
142     return $parser_config{$class}{filters};
143 }
144
145 =head2 filter_callbacks
146
147     $filter_callbacks = $QParser->filter_callbacks();
148
149 Returns hashref of the configured filter callbacks.
150 =cut
151
152 sub filter_callbacks {
153     my $class = shift;
154     $class = ref($class) || $class;
155
156     $parser_config{$class}{filter_callbacks} ||= {};
157     return $parser_config{$class}{filter_callbacks};
158 }
159
160 =head2 modifiers
161
162     $modifiers = $QParser->modifiers();
163
164 Returns arrayref of the configured modifiers.
165 =cut
166
167 sub modifiers {
168     my $class = shift;
169     $class = ref($class) || $class;
170
171     $parser_config{$class}{modifiers} ||= [];
172     return $parser_config{$class}{modifiers};
173 }
174
175 =head2 new
176
177     $QParser = QueryParser->new(%args);
178
179 Creates a new QueryParser object.
180 =cut
181
182 sub new {
183     my $class = shift;
184     $class = ref($class) || $class;
185
186     my %opts = @_;
187
188     my $self = bless {} => $class;
189
190     for my $o (keys %{QueryParser->operators}) {
191         $class->operator($o => QueryParser->operator($o)) unless ($class->operator($o));
192     }
193
194     for my $opt ( keys %opts) {
195         $self->$opt( $opts{$opt} ) if ($self->can($opt));
196     }
197
198     return $self;
199 }
200
201 =head2 new_plan
202
203     $query_plan = $QParser->new_plan();
204
205 Create a new query plan.
206 =cut
207
208 sub new_plan {
209     my $self = shift;
210     my $pkg = ref($self) || $self;
211     return do{$pkg.'::query_plan'}->new( QueryParser => $self, @_ );
212 }
213
214 =head2 add_search_filter
215
216     $QParser->add_search_filter($filter, [$callback]);
217
218 Adds a filter with the specified name and an optional callback to the
219 QueryParser configuration.
220 =cut
221
222 sub add_search_filter {
223     my $pkg = shift;
224     $pkg = ref($pkg) || $pkg;
225     my $filter = shift;
226     my $callback = shift;
227
228     return $filter if (grep { $_ eq $filter } @{$pkg->filters});
229     push @{$pkg->filters}, $filter;
230     $pkg->filter_callbacks->{$filter} = $callback if ($callback);
231     return $filter;
232 }
233
234 =head2 add_search_modifier
235
236     $QParser->add_search_modifier($modifier);
237
238 Adds a modifier with the specified name to the QueryParser configuration.
239 =cut
240
241 sub add_search_modifier {
242     my $pkg = shift;
243     $pkg = ref($pkg) || $pkg;
244     my $modifier = shift;
245
246     return $modifier if (grep { $_ eq $modifier } @{$pkg->modifiers});
247     push @{$pkg->modifiers}, $modifier;
248     return $modifier;
249 }
250
251 =head2 add_facet_class
252
253     $QParser->add_facet_class($facet_class);
254
255 Adds a facet class with the specified name to the QueryParser configuration.
256 =cut
257
258 sub add_facet_class {
259     my $pkg = shift;
260     $pkg = ref($pkg) || $pkg;
261     my $class = shift;
262
263     return $class if (grep { $_ eq $class } @{$pkg->facet_classes});
264
265     push @{$pkg->facet_classes}, $class;
266     $pkg->facet_fields->{$class} = [];
267
268     return $class;
269 }
270
271 =head2 add_search_class
272
273     $QParser->add_search_class($class);
274
275 Adds a search class with the specified name to the QueryParser configuration.
276 =cut
277
278 sub add_search_class {
279     my $pkg = shift;
280     $pkg = ref($pkg) || $pkg;
281     my $class = shift;
282
283     return $class if (grep { $_ eq $class } @{$pkg->search_classes});
284
285     push @{$pkg->search_classes}, $class;
286     $pkg->search_fields->{$class} = [];
287     $pkg->default_search_class( $pkg->search_classes->[0] ) if (@{$pkg->search_classes} == 1);
288
289     return $class;
290 }
291
292 =head2 add_search_modifier
293
294     $op = $QParser->operator($operator, [$newvalue]);
295
296 Retrieves or sets value for the specified operator. Valid operators and
297 their defaults are as follows:
298
299 =over 4
300
301 =item * and => &&
302
303 =item * or => ||
304
305 =item * group_start => (
306
307 =item * group_end => )
308
309 =item * required => +
310
311 =item * disallowed => -
312
313 =item * modifier => #
314
315 =back
316
317 =cut
318
319 sub operator {
320     my $class = shift;
321     $class = ref($class) || $class;
322     my $opname = shift;
323     my $op = shift;
324
325     return undef unless ($opname);
326
327     $parser_config{$class}{operators} ||= {};
328     $parser_config{$class}{operators}{$opname} = $op if ($op);
329
330     return $parser_config{$class}{operators}{$opname};
331 }
332
333 =head2 facet_classes
334
335     $classes = $QParser->facet_classes([\@newclasses]);
336
337 Returns arrayref of all configured facet classes after optionally
338 replacing configuration.
339 =cut
340
341 sub facet_classes {
342     my $class = shift;
343     $class = ref($class) || $class;
344     my $classes = shift;
345
346     $parser_config{$class}{facet_classes} ||= [];
347     $parser_config{$class}{facet_classes} = $classes if (ref($classes) && @$classes);
348     return $parser_config{$class}{facet_classes};
349 }
350
351 =head2 search_classes
352
353     $classes = $QParser->search_classes([\@newclasses]);
354
355 Returns arrayref of all configured search classes after optionally
356 replacing the previous configuration.
357 =cut
358
359 sub search_classes {
360     my $class = shift;
361     $class = ref($class) || $class;
362     my $classes = shift;
363
364     $parser_config{$class}{classes} ||= [];
365     $parser_config{$class}{classes} = $classes if (ref($classes) && @$classes);
366     return $parser_config{$class}{classes};
367 }
368
369 =head2 add_query_normalizer
370
371     $function = $QParser->add_query_normalizer($class, $field, $func, [\@params]);
372
373 =cut
374
375 sub add_query_normalizer {
376     my $pkg = shift;
377     $pkg = ref($pkg) || $pkg;
378     my $class = shift;
379     my $field = shift;
380     my $func = shift;
381     my $params = shift || [];
382
383     # do not add if function AND params are identical to existing member
384     return $func if (grep {
385         $_->{function} eq $func and 
386         OpenSRF::Utils::JSON->perl2JSON($_->{params}) eq OpenSRF::Utils::JSON->perl2JSON($params)
387     } @{$pkg->query_normalizers->{$class}->{$field}});
388
389     push(@{$pkg->query_normalizers->{$class}->{$field}}, { function => $func, params => $params });
390
391     return $func;
392 }
393
394 =head2 query_normalizers
395
396     $normalizers = $QParser->query_normalizers($class, $field);
397
398 Returns a list of normalizers associated with the specified search class
399 and field
400 =cut
401
402 sub query_normalizers {
403     my $pkg = shift;
404     $pkg = ref($pkg) || $pkg;
405
406     my $class = shift;
407     my $field = shift;
408
409     $parser_config{$pkg}{normalizers} ||= {};
410     if ($class) {
411         if ($field) {
412             $parser_config{$pkg}{normalizers}{$class}{$field} ||= [];
413             return $parser_config{$pkg}{normalizers}{$class}{$field};
414         } else {
415             return $parser_config{$pkg}{normalizers}{$class};
416         }
417     }
418
419     return $parser_config{$pkg}{normalizers};
420 }
421
422 =head2 add_filter_normalizer
423
424     $normalizer = $QParser->add_filter_normalizer($filter, $func, [\@params]);
425
426 Adds a normalizer function to the specified filter.
427 =cut
428
429 sub add_filter_normalizer {
430     my $pkg = shift;
431     $pkg = ref($pkg) || $pkg;
432     my $filter = shift;
433     my $func = shift;
434     my $params = shift || [];
435
436     return $func if (grep { $_ eq $func } @{$pkg->filter_normalizers->{$filter}});
437
438     push(@{$pkg->filter_normalizers->{$filter}}, { function => $func, params => $params });
439
440     return $func;
441 }
442
443 =head2 filter_normalizers
444
445     $normalizers = $QParser->filter_normalizers($filter);
446
447 Return arrayref of normalizer functions associated with the specified filter.
448 =cut
449
450 sub filter_normalizers {
451     my $pkg = shift;
452     $pkg = ref($pkg) || $pkg;
453
454     my $filter = shift;
455
456     $parser_config{$pkg}{filter_normalizers} ||= {};
457     if ($filter) {
458         $parser_config{$pkg}{filter_normalizers}{$filter} ||= [];
459         return $parser_config{$pkg}{filter_normalizers}{$filter};
460     }
461
462     return $parser_config{$pkg}{filter_normalizers};
463 }
464
465 =head2 default_search_class
466
467     $default_class = $QParser->default_search_class([$class]);
468
469 Set or return the default search class.
470 =cut
471
472 sub default_search_class {
473     my $pkg = shift;
474     $pkg = ref($pkg) || $pkg;
475     my $class = shift;
476     $QueryParser::parser_config{$pkg}{default_class} = $pkg->add_search_class( $class ) if $class;
477
478     return $QueryParser::parser_config{$pkg}{default_class};
479 }
480
481 =head2 remove_facet_class
482
483     $QParser->remove_facet_class($class);
484
485 Remove the specified facet class from the configuration.
486 =cut
487
488 sub remove_facet_class {
489     my $pkg = shift;
490     $pkg = ref($pkg) || $pkg;
491     my $class = shift;
492
493     return $class if (!grep { $_ eq $class } @{$pkg->facet_classes});
494
495     $pkg->facet_classes( [ grep { $_ ne $class } @{$pkg->facet_classes} ] );
496     delete $QueryParser::parser_config{$pkg}{facet_fields}{$class};
497
498     return $class;
499 }
500
501 =head2 remove_search_class
502
503     $QParser->remove_search_class($class);
504
505 Remove the specified search class from the configuration.
506 =cut
507
508 sub remove_search_class {
509     my $pkg = shift;
510     $pkg = ref($pkg) || $pkg;
511     my $class = shift;
512
513     return $class if (!grep { $_ eq $class } @{$pkg->search_classes});
514
515     $pkg->search_classes( [ grep { $_ ne $class } @{$pkg->search_classes} ] );
516     delete $QueryParser::parser_config{$pkg}{fields}{$class};
517
518     return $class;
519 }
520
521 =head2 add_facet_field
522
523     $QParser->add_facet_field($class, $field);
524
525 Adds the specified field (and facet class if it doesn't already exist)
526 to the configuration.
527 =cut
528
529 sub add_facet_field {
530     my $pkg = shift;
531     $pkg = ref($pkg) || $pkg;
532     my $class = shift;
533     my $field = shift;
534
535     $pkg->add_facet_class( $class );
536
537     return { $class => $field }  if (grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
538
539     push @{$pkg->facet_fields->{$class}}, $field;
540
541     return { $class => $field };
542 }
543
544 =head2 facet_fields
545
546     $fields = $QParser->facet_fields($class);
547
548 Returns arrayref with list of fields for specified facet class.
549 =cut
550
551 sub facet_fields {
552     my $class = shift;
553     $class = ref($class) || $class;
554
555     $parser_config{$class}{facet_fields} ||= {};
556     return $parser_config{$class}{facet_fields};
557 }
558
559 =head2 add_search_field
560
561     $QParser->add_search_field($class, $field);
562
563 Adds the specified field (and facet class if it doesn't already exist)
564 to the configuration.
565 =cut
566
567 sub add_search_field {
568     my $pkg = shift;
569     $pkg = ref($pkg) || $pkg;
570     my $class = shift;
571     my $field = shift;
572
573     $pkg->add_search_class( $class );
574
575     return { $class => $field }  if (grep { $_ eq $field } @{$pkg->search_fields->{$class}});
576
577     push @{$pkg->search_fields->{$class}}, $field;
578
579     return { $class => $field };
580 }
581
582 =head2 search_fields
583
584     $fields = $QParser->search_fields();
585
586 Returns arrayref with list of configured search fields.
587 =cut
588
589 sub search_fields {
590     my $class = shift;
591     $class = ref($class) || $class;
592
593     $parser_config{$class}{fields} ||= {};
594     return $parser_config{$class}{fields};
595 }
596
597 =head2 add_search_class_alias
598
599     $QParser->add_search_class_alias($class, $alias);
600 =cut
601
602 sub add_search_class_alias {
603     my $pkg = shift;
604     $pkg = ref($pkg) || $pkg;
605     my $class = shift;
606     my $alias = shift;
607
608     $pkg->add_search_class( $class );
609
610     return { $class => $alias }  if (grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
611
612     push @{$pkg->search_class_aliases->{$class}}, $alias;
613
614     return { $class => $alias };
615 }
616
617 =head2 search_class_aliases
618
619     $aliases = $QParser->search_class_aliases($class);
620 =cut
621
622 sub search_class_aliases {
623     my $class = shift;
624     $class = ref($class) || $class;
625
626     $parser_config{$class}{class_map} ||= {};
627     return $parser_config{$class}{class_map};
628 }
629
630 =head2 add_search_field_alias
631
632     $QParser->add_search_field_alias($class, $field, $alias);
633 =cut
634
635 sub add_search_field_alias {
636     my $pkg = shift;
637     $pkg = ref($pkg) || $pkg;
638     my $class = shift;
639     my $field = shift;
640     my $alias = shift;
641
642     return { $class => { $field => $alias } }  if (grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
643
644     push @{$pkg->search_field_aliases->{$class}{$field}}, $alias;
645
646     return { $class => { $field => $alias } };
647 }
648
649 =head2 search_field_aliases
650
651     $aliases = $QParser->search_field_aliases();
652 =cut
653
654 sub search_field_aliases {
655     my $class = shift;
656     $class = ref($class) || $class;
657
658     $parser_config{$class}{field_alias_map} ||= {};
659     return $parser_config{$class}{field_alias_map};
660 }
661
662 =head2 remove_facet_field
663
664     $QParser->remove_facet_field($class, $field);
665 =cut
666
667 sub remove_facet_field {
668     my $pkg = shift;
669     $pkg = ref($pkg) || $pkg;
670     my $class = shift;
671     my $field = shift;
672
673     return { $class => $field }  if (!$pkg->facet_fields->{$class} || !grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
674
675     $pkg->facet_fields->{$class} = [ grep { $_ ne $field } @{$pkg->facet_fields->{$class}} ];
676
677     return { $class => $field };
678 }
679
680 =head2 remove_search_field
681
682     $QParser->remove_search_field($class, $field);
683 =cut
684
685 sub remove_search_field {
686     my $pkg = shift;
687     $pkg = ref($pkg) || $pkg;
688     my $class = shift;
689     my $field = shift;
690
691     return { $class => $field }  if (!$pkg->search_fields->{$class} || !grep { $_ eq $field } @{$pkg->search_fields->{$class}});
692
693     $pkg->search_fields->{$class} = [ grep { $_ ne $field } @{$pkg->search_fields->{$class}} ];
694
695     return { $class => $field };
696 }
697
698 =head2 remove_search_field_alias
699
700     $QParser->remove_search_field_alias($class, $field, $alias);
701 =cut
702
703 sub remove_search_field_alias {
704     my $pkg = shift;
705     $pkg = ref($pkg) || $pkg;
706     my $class = shift;
707     my $field = shift;
708     my $alias = shift;
709
710     return { $class => { $field => $alias } }  if (!$pkg->search_field_aliases->{$class}{$field} || !grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
711
712     $pkg->search_field_aliases->{$class}{$field} = [ grep { $_ ne $alias } @{$pkg->search_field_aliases->{$class}{$field}} ];
713
714     return { $class => { $field => $alias } };
715 }
716
717 =head2 remove_search_class_alias
718
719     $QParser->remove_search_class_alias($class, $alias);
720 =cut
721
722 sub remove_search_class_alias {
723     my $pkg = shift;
724     $pkg = ref($pkg) || $pkg;
725     my $class = shift;
726     my $alias = shift;
727
728     return { $class => $alias }  if (!$pkg->search_class_aliases->{$class} || !grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
729
730     $pkg->search_class_aliases->{$class} = [ grep { $_ ne $alias } @{$pkg->search_class_aliases->{$class}} ];
731
732     return { $class => $alias };
733 }
734
735 =head2 debug
736
737     $debug = $QParser->debug([$debug]);
738
739 Return or set whether debugging output is enabled.
740 =cut
741
742 sub debug {
743     my $self = shift;
744     my $q = shift;
745     $self->{_debug} = $q if (defined $q);
746     return $self->{_debug};
747 }
748
749 =head2 query
750
751     $query = $QParser->query([$query]);
752
753 Return or set the query.
754 =cut
755
756 sub query {
757     my $self = shift;
758     my $q = shift;
759     $self->{_query} = " $q " if (defined $q);
760     return $self->{_query};
761 }
762
763 =head2 parse_tree
764
765     $parse_tree = $QParser->parse_tree([$parse_tree]);
766
767 Return or set the parse tree associated with the QueryParser.
768 =cut
769
770 sub parse_tree {
771     my $self = shift;
772     my $q = shift;
773     $self->{_parse_tree} = $q if (defined $q);
774     return $self->{_parse_tree};
775 }
776
777 sub floating_plan {
778     my $self = shift;
779     my $q = shift;
780     $self->{_top} = $q if (defined $q);
781     return $self->{_top};
782 }
783
784 =head2 parse
785
786     $QParser->parse([$query]);
787
788 Parse the specified query, or the query already associated with the QueryParser
789 object.
790 =cut
791
792 our $last_type = '';
793 our $last_class = '';
794 our $floating = 0;
795 our $fstart;
796 sub parse {
797     my $self = shift;
798     my $pkg = ref($self) || $self;
799     warn " ** parse package is $pkg\n" if $self->debug;
800
801     # Reset at each top-level parsing request
802     $last_class = '';
803     $last_type = '';
804     $floating = 0;
805     $fstart = undef;
806
807     $self->decompose( $self->query( shift() ) );
808
809     if ($self->floating_plan) {
810         $self->floating_plan->add_node( $self->parse_tree );
811         $self->parse_tree( $self->floating_plan );
812     }
813
814     warn "Query tree before pullup:\n" . Dumper($self->parse_tree) if $self->debug;
815     $self->parse_tree( $self->parse_tree->pullup );
816     warn "Query tree after pullup:\n" . Dumper($self->parse_tree) if $self->debug;
817     $self->parse_tree->plan_level(0);
818
819     return $self;
820 }
821
822 =head2 decompose
823
824     ($struct, $remainder) = $QParser->decompose($querystring, [$current_class], [$recursing], [$phrase_helper]);
825
826 This routine does the heavy work of parsing the query string recursively.
827 Returns the top level query plan, or the query plan from a lower level plus
828 the portion of the query string that needs to be processed at a higher level.
829 =cut
830
831 our $_compiled_decomposer = {};
832 sub decompose {
833     my $self = shift;
834     my $pkg = ref($self) || $self;
835
836     my $r = $$_compiled_decomposer{$pkg};
837     my $compiled = defined($r);
838
839     $_ = shift;
840     my $current_class = shift || $self->default_search_class;
841
842     my $recursing = shift || 0;
843     my $phrase_helper = shift || 0;
844
845     warn '  'x$recursing." ** QP: decompose package is $pkg" if $self->debug;
846
847     if (!$compiled) {
848         $r = $$_compiled_decomposer{$pkg} = {};
849         warn '  'x$recursing." ** Compiling decomposer\n" if $self->debug;
850
851         # Build the search class+field uber-regexp
852         $$r{search_class_re} = '^\s*(';
853     } else {
854         warn '  'x$recursing." ** Decomposer already compiled\n" if $self->debug;
855     }
856
857     my $first_class = 1;
858
859     my %seen_classes;
860     for my $class ( keys %{$pkg->search_field_aliases} ) {
861         warn '  'x$recursing." *** ... Looking for search fields in $class\n" if $self->debug;
862
863         for my $field ( keys %{$pkg->search_field_aliases->{$class}} ) {
864             warn '  'x$recursing." *** ... Looking for aliases of $field\n" if $self->debug;
865
866             for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
867                 my $aliasr = qr/$alias/;
868                 s/(^|\s+)$aliasr\|/$1$class\|$field#$alias\|/g;
869                 s/(^|\s+)$aliasr[:=]/$1$class\|$field#$alias:/g;
870                 warn '  'x$recursing." *** Rewriting: $alias ($aliasr) as $class\|$field\n" if $self->debug;
871             }
872         }
873
874         if (!$compiled) {
875             $$r{search_class_re} .= '|' unless ($first_class);
876             $first_class = 0;
877             $$r{search_class_re} .= $class . '(?:[|#][^:|]+)*';
878             $seen_classes{$class} = 1;
879         }
880     }
881
882     for my $class ( keys %{$pkg->search_class_aliases} ) {
883
884         for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
885             my $aliasr = qr/$alias/;
886             s/(^|[^|])\b$aliasr\|/$1$class#$alias\|/g;
887             s/(^|[^|])\b$aliasr[:=]/$1$class#$alias:/g;
888             warn '  'x$recursing." *** Rewriting: $alias ($aliasr) as $class\n" if $self->debug;
889         }
890
891         if (!$compiled and !$seen_classes{$class}) {
892             $$r{search_class_re} .= '|' unless ($first_class);
893             $first_class = 0;
894
895             $$r{search_class_re} .= $class . '(?:[|#][^:|]+)*';
896             $seen_classes{$class} = 1;
897         }
898     }
899     $$r{search_class_re} .= '):' if (!$compiled);
900
901     warn '  'x$recursing." ** Rewritten query: $_\n" if $self->debug;
902
903     my $group_start = $pkg->operator('group_start');
904     my $group_end = $pkg->operator('group_end');
905     if (!$compiled) {
906         warn '  'x$recursing." ** Search class RE: $$r{search_class_re}\n" if $self->debug;
907
908         my $required_op = $pkg->operator('required');
909         $$r{required_re} = qr/\Q$required_op\E/;
910
911         my $disallowed_op = $pkg->operator('disallowed');
912         $$r{disallowed_re} = qr/\Q$disallowed_op\E/;
913
914         my $negated_op = $pkg->operator('negated');
915         $$r{negated_re} = qr/\Q$negated_op\E/;
916
917         my $and_op = $pkg->operator('and');
918         $$r{and_re} = qr/^\s*\Q$and_op\E/;
919
920         my $or_op = $pkg->operator('or');
921         $$r{or_re} = qr/^\s*\Q$or_op\E/;
922
923         $$r{group_start_re} = qr/^\s*($$r{negated_re}|$$r{disallowed_re})?\Q$group_start\E/;
924
925         $$r{group_end_re} = qr/^\s*\Q$group_end\E/;
926
927         my $float_start = $pkg->operator('float_start');
928         $$r{float_start_re} = qr/^\s*\Q$float_start\E/;
929
930         my $float_end = $pkg->operator('float_end');
931         $$r{float_end_re} = qr/^\s*\Q$float_end\E/;
932
933         $$r{atom_re} = qr/.+?(?=\Q$float_start\E|\Q$group_start\E|\Q$float_end\E|\Q$group_end\E|\s|"|$)/;
934
935         my $modifier_tag = $pkg->operator('modifier');
936         $$r{modifier_tag_re} = qr/^\s*\Q$modifier_tag\E/;
937
938         # Group start/end normally are ( and ), but can be overridden.
939         # We thus include ( and ) specifically due to filters, as well as : for classes.
940         $$r{phrase_cleanup_re} = qr/\s*(\Q$required_op\E|\Q$disallowed_op\E|\Q$and_op\E|\Q$or_op\E|\Q$group_start\E|\Q$group_end\E|\Q$float_start\E|\Q$float_end\E|\Q$modifier_tag\E|\Q$negated_op\E|:|\(|\))/;
941
942         # Build the filter and modifier uber-regexps
943         $$r{facet_re} = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\](?!\[)';
944
945         $$r{filter_re} = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
946         $$r{filter_as_class_re} = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
947
948         $$r{modifier_re} = '^\s*'.$$r{modifier_tag_re}.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
949         $$r{modifier_as_class_re} = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
950
951     }
952
953     my $struct = shift || $self->new_plan( level => $recursing );
954     $self->parse_tree( $struct ) if (!$self->parse_tree);
955
956     my $remainder = '';
957
958     my $loops = 0;
959     while (!$remainder) {
960         $loops++;
961         warn '  'x$recursing."Start of the loop. loop: $loops last_type: $last_type, joiner: ".$struct->joiner.", struct: $struct\n" if $self->debug;
962         if ($loops > 1000) { # the most magical of numbers...
963             warn '  'x$recursing." got to $loops loops; aborting\n" if $self->debug;
964             last;
965         }
966         if ($last_type eq 'FEND' and $fstart and $fstart !=  $struct) { # fall back further
967             $remainder = $_;
968             last;
969         } elsif ($last_type eq 'FEND') {
970             $fstart = undef;
971             $last_type = '';
972         }
973
974         if (/^\s*$/) { # end of an explicit group
975             $last_type = '';
976             last;
977         } elsif (/$$r{float_end_re}/) { # end of an explicit group
978             warn '  'x$recursing."Encountered explicit float end, remainder: $'\n" if $self->debug;
979
980             $remainder = $';
981             $_ = '';
982
983             $floating = 0;
984             $last_type = 'FEND';
985             last;
986         } elsif (/$$r{group_end_re}/) { # end of an explicit group
987             warn '  'x$recursing."Encountered explicit group end, remainder: $'\n" if $self->debug;
988
989             $remainder = $';
990             $_ = '';
991
992             $last_type = '';
993         } elsif ($self->filter_count && /$$r{filter_re}/) { # found a filter
994             warn '  'x$recursing."Encountered search filter: $1$2 set to $3\n" if $self->debug;
995
996             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
997             $_ = $';
998
999             my $filter = $2;
1000             my $params = [ split '[,]+', $3 ];
1001
1002             if ($pkg->filter_callbacks->{$filter}) {
1003                 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
1004                 $_ = "$replacement $_" if ($replacement);
1005             } else {
1006                 $struct->new_filter( $filter => $params, $negate );
1007             }
1008
1009
1010             $last_type = '';
1011         } elsif ($self->filter_count && /$$r{filter_as_class_re}/) { # found a filter
1012             warn '  'x$recursing."Encountered search filter: $1$2 set to $3\n" if $self->debug;
1013
1014             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
1015             $_ = $';
1016
1017             my $filter = $2;
1018             my $params = [ split '[,]+', $3 ];
1019
1020             if ($pkg->filter_callbacks->{$filter}) {
1021                 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
1022                 $_ = "$replacement $_" if ($replacement);
1023             } else {
1024                 $struct->new_filter( $filter => $params, $negate );
1025             }
1026
1027             $last_type = '';
1028         } elsif ($self->modifier_count && /$$r{modifier_re}/) { # found a modifier
1029             warn '  'x$recursing."Encountered search modifier: $1\n" if $self->debug;
1030
1031             $_ = $';
1032             if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
1033                 warn '  'x$recursing."  Search modifiers only allowed at the top level of the query\n" if $self->debug;
1034             } else {
1035                 $struct->new_modifier($1);
1036             }
1037
1038             $last_type = '';
1039         } elsif ($self->modifier_count && /$$r{modifier_as_class_re}/) { # found a modifier
1040             warn '  'x$recursing."Encountered search modifier: $1\n" if $self->debug;
1041
1042             my $mod = $1;
1043
1044             $_ = $';
1045             if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
1046                 warn '  'x$recursing."  Search modifiers only allowed at the top level of the query\n" if $self->debug;
1047             } elsif ($2 =~ /^[ty1]/i) {
1048                 $struct->new_modifier($mod);
1049             }
1050
1051             $last_type = '';
1052         } elsif (/$$r{float_start_re}/) { # start of an explicit float
1053             warn '  'x$recursing."Encountered explicit float start\n" if $self->debug;
1054             $floating = 1;
1055             $fstart = $struct;
1056
1057             $last_class = $current_class;
1058             $current_class = undef;
1059
1060             $self->floating_plan( $self->new_plan( floating => 1 ) ) if (!$self->floating_plan);
1061
1062             # pass the floating_plan struct to be modified by the float'ed chunk
1063             my ($floating_plan, $subremainder) = $self->new( debug => $self->debug )->decompose( $', undef, undef, undef,  $self->floating_plan);
1064             $_ = $subremainder;
1065             warn '  'x$recursing."Remainder after explicit float: $_\n" if $self->debug;
1066
1067             $current_class = $last_class;
1068
1069             $last_type = '';
1070         } elsif (/$$r{group_start_re}/) { # start of an explicit group
1071             warn '  'x$recursing."Encountered explicit group start\n" if $self->debug;
1072
1073             if ($last_type eq 'CLASS') {
1074                 warn '  'x$recursing."Previous class change generated an empty node. Removing...\n" if $self->debug;
1075                 $struct->remove_last_node;
1076             }
1077
1078             my $negate = $1;
1079             my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
1080             if ($substruct) {
1081                 $substruct->negate(1) if ($negate);
1082                 $substruct->explicit(1);
1083                 $struct->add_node( $substruct );
1084             }
1085             $_ = $subremainder;
1086             warn '  'x$recursing."Query remainder after bool group: $_\n" if $self->debug;
1087
1088             $last_type = '';
1089
1090         } elsif (/$$r{and_re}/) { # ANDed expression
1091             $_ = $';
1092             warn '  'x$recursing."Encountered AND\n" if $self->debug;
1093             do {warn '  'x$recursing."!!! Already doing the bool dance for AND\n" if $self->debug; next} if ($last_type eq 'AND');
1094             do {warn '  'x$recursing."!!! Already doing the bool dance for OR\n" if $self->debug; next} if ($last_type eq 'OR');
1095             $last_type = 'AND';
1096
1097             warn '  'x$recursing."Saving LHS, building RHS\n" if $self->debug;
1098             my $LHS = $struct;
1099             #my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
1100             my ($RHS, $subremainder) = $self->decompose( $_, $current_class, $recursing + 1 );
1101             $_ = $subremainder;
1102
1103             warn '  'x$recursing."RHS built\n" if $self->debug;
1104             warn '  'x$recursing."Post-AND remainder: $subremainder\n" if $self->debug;
1105
1106             my $wrapper = $self->new_plan( level => $recursing + 1, joiner => '&'  );
1107
1108             if ($LHS->floating) {
1109                 $wrapper->{query} = $LHS->{query};
1110                 my $outer_wrapper = $self->new_plan( level => $recursing + 1, joiner => '&'  );
1111                 $outer_wrapper->add_node($_) for ($wrapper,$RHS);
1112                 $LHS->{query} = [$outer_wrapper];
1113                 $struct = $LHS;
1114             } else {
1115                 $wrapper->add_node($_) for ($LHS, $RHS);
1116                 $wrapper->plan_level($wrapper->plan_level); # reset levels all the way down
1117                 $struct = $self->new_plan( level => $recursing );
1118                 $struct->add_node($wrapper);
1119             }
1120
1121             $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
1122
1123             $last_type = '';
1124         } elsif (/$$r{or_re}/) { # ORed expression
1125             $_ = $';
1126             warn '  'x$recursing."Encountered OR\n" if $self->debug;
1127             do {warn '  'x$recursing."!!! Already doing the bool dance for AND\n" if $self->debug; next} if ($last_type eq 'AND');
1128             do {warn '  'x$recursing."!!! Already doing the bool dance for OR\n" if $self->debug; next} if ($last_type eq 'OR');
1129             $last_type = 'OR';
1130
1131             warn '  'x$recursing."Saving LHS, building RHS\n" if $self->debug;
1132             my $LHS = $struct;
1133             #my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
1134             my ($RHS, $subremainder) = $self->decompose( $_, $current_class, $recursing + 2 );
1135             $remainder = $subremainder;
1136
1137             warn '  'x$recursing."RHS built\n" if $self->debug;
1138             warn '  'x$recursing."Post-OR remainder: $subremainder\n" if $self->debug;
1139
1140             my $wrapper = $self->new_plan( level => $recursing + 1, joiner => '|' );
1141
1142             if ($LHS->floating) {
1143                 $wrapper->{query} = $LHS->{query};
1144                 my $outer_wrapper = $self->new_plan( level => $recursing + 1, joiner => '|' );
1145                 $outer_wrapper->add_node($_) for ($wrapper,$RHS);
1146                 $LHS->{query} = [$outer_wrapper];
1147                 $struct = $LHS;
1148             } else {
1149                 $wrapper->add_node($_) for ($LHS, $RHS);
1150                 $wrapper->plan_level($wrapper->plan_level); # reset levels all the way down
1151                 $struct = $self->new_plan( level => $recursing );
1152                 $struct->add_node($wrapper);
1153             }
1154
1155             $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
1156
1157             $last_type = '';
1158         } elsif ($self->facet_class_count && /$$r{facet_re}/) { # changing current class
1159             warn '  'x$recursing."Encountered facet: $1$2 => $3\n" if $self->debug;
1160
1161             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
1162             my $facet = $2;
1163             my $facet_value = [ split '\s*\]\[\s*', $3 ];
1164             $struct->new_facet( $facet => $facet_value, $negate );
1165             $_ = $';
1166
1167             $last_type = '';
1168         } elsif ($self->search_class_count && /$$r{search_class_re}/) { # changing current class
1169
1170             if ($last_type eq 'CLASS') {
1171                 $struct->remove_last_node( $current_class );
1172                 warn '  'x$recursing."Encountered class change with no searches!\n" if $self->debug;
1173             }
1174
1175             warn '  'x$recursing."Encountered class change: $1\n" if $self->debug;
1176
1177             $current_class = $struct->classed_node( $1 )->requested_class();
1178             $_ = $';
1179
1180             $last_type = 'CLASS';
1181         } elsif (/^\s*($$r{required_re}|$$r{disallowed_re}|$$r{negated_re})?"([^"]+)(?:"|$)/) { # phrase, always anded
1182             warn '  'x$recursing.'Encountered' . ($1 ? " ['$1' modified]" : '') . " phrase: $2\n" if $self->debug;
1183
1184             my $req_ness = $1 || '';
1185             $req_ness = $pkg->operator('disallowed') if ($req_ness eq $pkg->operator('negated'));
1186             my $phrase = $2;
1187
1188             if (!$phrase_helper) {
1189                 warn '  'x$recursing."Recursing into decompose with the phrase as a subquery\n" if $self->debug;
1190                 my $after = $';
1191                 my ($substruct, $subremainder) = $self->decompose( qq/$req_ness"$phrase"/, $current_class, $recursing + 1, 1 );
1192                 $struct->add_node( $substruct ) if ($substruct);
1193                 $_ = $after;
1194             } else {
1195                 warn '  'x$recursing."Directly parsing the phrase [ $phrase ] subquery\n" if $self->debug;
1196                 $struct->joiner( '&' );
1197
1198                 my $class_node = $struct->classed_node($current_class);
1199
1200                 if (grep { $req_ness eq $_ } ($pkg->operator('disallowed'), $pkg->operator('negated'))) {
1201                     $class_node->negate(1);
1202                     $req_ness = $pkg->operator('negated');
1203                 } else {
1204                     $req_ness = '';
1205                 }
1206                 $phrase =~ s/$$r{phrase_cleanup_re}/ /g;
1207                 $class_node->add_phrase( $phrase );
1208                 $class_node->add_dummy_atom;
1209
1210                 last;
1211             }
1212
1213             $last_type = '';
1214
1215         } elsif (/^\s*((?:$$r{required_re}|$$r{disallowed_re}|$$r{negated_re})?)($$r{atom_re})/) { # atoms
1216             warn '  'x$recursing."Encountered atom: $1\n" if $self->debug;
1217             warn '  'x$recursing."Remainder: $'\n" if $self->debug;
1218
1219             my $req_ness = $1;
1220             my $atom = $2;
1221             my $after = $';
1222
1223             $_ = $after;
1224             $last_type = '';
1225
1226             my $class_node = $struct->classed_node($current_class);
1227
1228             my $prefix = '';
1229             if ($req_ness) {
1230                 $prefix = ($req_ness =~ /^$$r{required_re}/) ? '' : '!';
1231             }
1232
1233             my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
1234
1235             if ($atom ne '' and !grep { $atom =~ /^\Q$_\E+$/ } ('&','|')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
1236 #                $class_node->add_phrase( $atom ) if ($atom =~ s/^$$r{required_re}//o);
1237
1238                 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $prefix, node => $class_node );
1239                 $struct->joiner( '&' );
1240             }
1241
1242             $last_type = '';
1243         } else {
1244             warn '  'x$recursing."Cannot parse: $_\n" if $self->debug;
1245             $_ = '';
1246         }
1247
1248         last unless ($_);
1249
1250     }
1251
1252     $struct = undef if 
1253         scalar(@{$struct->query_nodes}) == 0 &&
1254         scalar(@{$struct->filters}) == 0 &&
1255         !$struct->top_plan;
1256
1257     return $struct if !wantarray;
1258     return ($struct, $remainder);
1259 }
1260
1261 =head2 find_class_index
1262
1263     $index = $QParser->find_class_index($class, $query);
1264 =cut
1265
1266 sub find_class_index {
1267     my $class = shift;
1268     my $query = shift;
1269
1270     my ($class_part, @field_parts) = split '\|', $class;
1271     $class_part ||= $class;
1272
1273     for my $idx ( 0 .. scalar(@$query) - 1 ) {
1274         next unless ref($$query[$idx]);
1275         return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
1276     }
1277
1278     push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
1279     return -1;
1280 }
1281
1282 =head2 core_limit
1283
1284     $limit = $QParser->core_limit([$limit]);
1285
1286 Return and/or set the core_limit.
1287 =cut
1288
1289 sub core_limit {
1290     my $self = shift;
1291     my $l = shift;
1292     $self->{core_limit} = $l if ($l);
1293     return $self->{core_limit};
1294 }
1295
1296 =head2 superpage
1297
1298     $superpage = $QParser->superpage([$superpage]);
1299
1300 Return and/or set the superpage.
1301 =cut
1302
1303 sub superpage {
1304     my $self = shift;
1305     my $l = shift;
1306     $self->{superpage} = $l if ($l);
1307     return $self->{superpage};
1308 }
1309
1310 =head2 superpage_size
1311
1312     $size = $QParser->superpage_size([$size]);
1313
1314 Return and/or set the superpage size.
1315 =cut
1316
1317 sub superpage_size {
1318     my $self = shift;
1319     my $l = shift;
1320     $self->{superpage_size} = $l if ($l);
1321     return $self->{superpage_size};
1322 }
1323
1324
1325 #-------------------------------
1326 package QueryParser::_util;
1327
1328 # At this level, joiners are always & or |.  This is not
1329 # the external, configurable representation of joiners that
1330 # defaults to # && and ||.
1331 sub is_joiner {
1332     my $str = shift;
1333
1334     return (not ref $str and ($str eq '&' or $str eq '|'));
1335 }
1336
1337 sub default_joiner { '&' }
1338
1339 # 0 for different, 1 for the same.
1340 sub compare_abstract_atoms {
1341     my ($left, $right) = @_;
1342
1343     foreach (qw/prefix suffix content/) {
1344         no warnings;    # undef can stand in for '' here
1345         return 0 unless $left->{$_} eq $right->{$_};
1346     }
1347
1348     return 1;
1349 }
1350
1351 sub fake_abstract_atom_from_phrase {
1352     my $phrase = shift;
1353     my $neg = shift;
1354     my $qp_class = shift || 'QueryParser';
1355
1356     my $prefix = '"';
1357     if ($neg) {
1358         $prefix =
1359             $QueryParser::parser_config{$qp_class}{operators}{disallowed} .
1360             $prefix;
1361     }
1362
1363     return {
1364         "type" => "atom", "prefix" => $prefix, "suffix" => '"',
1365         "content" => $phrase
1366     }
1367 }
1368
1369 sub find_arrays_in_abstract {
1370     my ($hash) = @_;
1371
1372     my @arrays;
1373     foreach my $key (keys %$hash) {
1374         if (ref $hash->{$key} eq "ARRAY") {
1375             push @arrays, $hash->{$key};
1376             foreach (@{$hash->{$key}}) {
1377                 push @arrays, find_arrays_in_abstract($_);
1378             }
1379         }
1380     }
1381
1382     return @arrays;
1383 }
1384
1385 #-------------------------------
1386 package QueryParser::Canonicalize;  # not OO
1387 use Data::Dumper;
1388
1389 sub _abstract_query2str_filter {
1390     my $f = shift;
1391     my $qp_class = shift || 'QueryParser';
1392     my $qpconfig = $QueryParser::parser_config{$qp_class};
1393
1394     return sprintf(
1395         '%s%s(%s)',
1396         $f->{negate} ? $qpconfig->{operators}{disallowed} : "",
1397         $f->{name},
1398         join(",", @{$f->{args}})
1399     );
1400 }
1401
1402 sub _abstract_query2str_modifier {
1403     my $f = shift;
1404     my $qp_class = shift || 'QueryParser';
1405     my $qpconfig = $QueryParser::parser_config{$qp_class};
1406
1407     return $qpconfig->{operators}{modifier} . $f;
1408 }
1409
1410 sub _kid_list {
1411     my $children = shift;
1412     my $op = (keys %$children)[0];
1413     return @{$$children{$op}};
1414 }
1415
1416
1417 # This should produce an equivalent query to the original, given an
1418 # abstract_query.
1419 sub abstract_query2str_impl {
1420     my $abstract_query  = shift;
1421     my $depth = shift || 0;
1422
1423     my $qp_class ||= shift || 'QueryParser';
1424     my $force_qp_node = shift || 0;
1425     my $qpconfig = $QueryParser::parser_config{$qp_class};
1426
1427     my $fs = $qpconfig->{operators}{float_start};
1428     my $fe = $qpconfig->{operators}{float_end};
1429     my $gs = $qpconfig->{operators}{group_start};
1430     my $ge = $qpconfig->{operators}{group_end};
1431     my $and = $qpconfig->{operators}{and};
1432     my $or = $qpconfig->{operators}{or};
1433     my $ng = $qpconfig->{operators}{negated};
1434
1435     my $isnode = 0;
1436     my $negate = '';
1437     my $size = 0;
1438     my $q = "";
1439
1440     if (exists $abstract_query->{type}) {
1441         if ($abstract_query->{type} eq 'query_plan') {
1442             $q .= join(" ", map { _abstract_query2str_filter($_, $qp_class) } @{$abstract_query->{filters}}) if
1443                 exists $abstract_query->{filters};
1444
1445             $q .= ($q ? ' ' : '') . join(" ", map { _abstract_query2str_modifier($_, $qp_class) } @{$abstract_query->{modifiers}}) if
1446                 exists $abstract_query->{modifiers};
1447
1448             $size = _kid_list($abstract_query->{children});
1449             if ($abstract_query->{negate}) {
1450                 $isnode = 1;
1451                 $negate = $ng;
1452             }
1453             $isnode = 1 if ($size > 1 and ($force_qp_node or $depth));
1454             #warn "size: $size, depth: $depth, isnode: $isnode, AQ: ".Dumper($abstract_query);
1455         } elsif ($abstract_query->{type} eq 'node') {
1456             if ($abstract_query->{alias}) {
1457                 $q .= ($q ? ' ' : '') . $abstract_query->{alias};
1458                 $q .= "|$_" foreach @{$abstract_query->{alias_fields}};
1459             } else {
1460                 $q .= ($q ? ' ' : '') . $abstract_query->{class};
1461                 $q .= "|$_" foreach @{$abstract_query->{fields}};
1462             }
1463             $q .= ":";
1464             $isnode = 1;
1465         } elsif ($abstract_query->{type} eq 'atom') {
1466             my $add_space = $q ? 1 : 0;
1467             if ($abstract_query->{explicit_start}) {
1468                 $q .= ' ' if $add_space;
1469                 $q .= $gs;
1470                 $add_space = 0;
1471             }
1472             my $prefix = $abstract_query->{prefix} || '';
1473             $prefix = $qpconfig->{operators}{negated} if $prefix eq '!';
1474             $q .= ($add_space ? ' ' : '') . $prefix .
1475                 ($abstract_query->{content} // '') .
1476                 ($abstract_query->{suffix} || '');
1477             $q .= $ge if ($abstract_query->{explicit_end});
1478         } elsif ($abstract_query->{type} eq 'facet') {
1479             my $prefix = $abstract_query->{negate} ? $qpconfig->{operators}{disallowed} : '';
1480             $q .= ($q ? ' ' : '') . $prefix . $abstract_query->{name} . "[" .
1481                 join("][", @{$abstract_query->{values}}) . "]";
1482         }
1483     }
1484
1485     my $next_depth = int($size > 1);
1486
1487     if (exists $abstract_query->{children}) {
1488
1489         my $op = (keys(%{$abstract_query->{children}}))[0];
1490
1491         if ($abstract_query->{floating}) { # always the top node!
1492             my $sub_node = pop @{$abstract_query->{children}{$op}};
1493
1494             $abstract_query->{floating} = 0;
1495             $q = $fs . " " . abstract_query2str_impl($abstract_query,0,$qp_class, 1) . $fe. " ";
1496
1497             $abstract_query = $sub_node;
1498         }
1499
1500         if ($abstract_query && exists $abstract_query->{children}) {
1501             $op = (keys(%{$abstract_query->{children}}))[0];
1502             $q .= ($q ? ' ' : '') . join(
1503                 ($op eq '&' ? ' ' : " $or "),
1504                 map {
1505                     my $x = abstract_query2str_impl($_, $depth + $next_depth, $qp_class, $force_qp_node); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1506                 } @{$abstract_query->{children}{$op}}
1507             );
1508         }
1509     } elsif ($abstract_query->{'&'} or $abstract_query->{'|'}) {
1510         my $op = (keys(%{$abstract_query}))[0];
1511         $q .= ($q ? ' ' : '') . join(
1512             ($op eq '&' ? ' ' : " $or "),
1513             map {
1514                     my $x = abstract_query2str_impl($_, $depth + $next_depth, $qp_class, $force_qp_node); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1515             } @{$abstract_query->{$op}}
1516         );
1517     }
1518
1519     $q = "$gs$q$ge" if ($isnode);
1520     $q = $negate . $q if ($q);
1521
1522     return $q;
1523 }
1524
1525 #-------------------------------
1526 package QueryParser::query_plan;
1527 use Data::Dumper;
1528 $Data::Dumper::Indent = 0;
1529
1530 sub get_abstract_data {
1531     my $self = shift;
1532     my $key = shift;
1533     return $self->{abstract_data}{$key};
1534 }
1535
1536 sub set_abstract_data {
1537     my $self = shift;
1538     my $key = shift;
1539     my $value = shift;
1540     $self->{abstract_data}{$key} = $value;
1541 }
1542
1543 sub atoms_only {
1544     my $self = shift;
1545     return @{$self->filters} == 0 &&
1546             @{$self->modifiers} == 0 &&
1547             @{[map { @{$_->phrases} } grep { ref($_) && $_->isa('QueryParser::query_plan::node')} @{$self->query_nodes}]} == 0
1548     ;
1549 }
1550
1551 sub _identical {
1552     my( $left, $right ) = @_;
1553     return 0 if scalar @$left != scalar @$right;
1554     my %hash;
1555     @hash{ @$left, @$right } = ();
1556     return scalar keys %hash == scalar @$left;
1557 }
1558
1559 sub pullup {
1560     my $self = shift;
1561
1562     # burrow down until we our kids have no subqueries
1563     my $downlink_joiner;
1564     for my $qnode (@{ $self->query_nodes }) {
1565         $qnode->pullup() if (ref($qnode) && $qnode->can('pullup'));
1566     }
1567     warn "Entering pullup depth ". $self->plan_level . "\n" if $self->QueryParser->debug;
1568
1569     my $old_qnodes = $self->query_nodes;
1570     warn @$old_qnodes . " query nodes (plans, nodes) at pullup depth ". $self->plan_level . "\n"
1571         if $self->QueryParser->debug;
1572
1573     # Step 1: pull up subplan filter/facet/modifier nodes. These
1574     # will bubble up to the top of the plan tree.  Evergreen doesn't
1575     # support nested filter/facet/modifier constructs currently.
1576     for my $kid (@$old_qnodes) {
1577         if (ref($kid) and $kid->isa('QueryParser::query_plan')) {
1578             $self->add_filter($_) foreach @{$kid->filters};
1579             $self->add_facet($_) foreach @{$kid->facets};
1580             $self->add_modifier($_) foreach @{$kid->modifiers};
1581             $kid->{filters} = [];
1582             $kid->{facets} = [];
1583             $kid->{modifiers} = [];
1584         }
1585     }
1586
1587     # Step 2: Pull up ::nodes from subplans that only have nodes (no
1588     # nested subplans).  This is in preparation for adjacent node merge,
1589     # and because this is a depth-first recursion, we've already decided
1590     # if nested plans can be elided.
1591     my @new_nodes;
1592     while (my $kid = shift @$old_qnodes) {
1593         if (ref($kid) and $kid->isa('QueryParser::query_plan')) {
1594             my $kid_query_nodes = $kid->query_nodes;
1595             my @kid_notnodes = grep { ref($_) and !$_->isa('QueryParser::query_plan::node') } @$kid_query_nodes;
1596             my @kid_nodes = grep { ref($_) and $_->isa('QueryParser::query_plan::node') } @$kid_query_nodes;
1597             if (@kid_nodes and !@kid_notnodes) {
1598                 warn "pulling up nodes from nested plan at pullup depth ". $self->plan_level . "\n" if $self->QueryParser->debug;
1599                 push @new_nodes, map { $_->plan($self) if ref; $_ } @$kid_query_nodes;
1600                 next;
1601             }
1602         }
1603         push @new_nodes, $kid;
1604     }
1605
1606     # Step 3: Merge our adjacent ::nodes if they have the same requested_class.
1607     # This could miss merging aliased classes that are equiv, but that check
1608     # is more fiddly, and usually searches just use the class name.
1609     $old_qnodes = [@new_nodes];
1610     @new_nodes = ();
1611     while ( my $current_node = shift(@$old_qnodes) ) {
1612
1613         unless (@$old_qnodes) { # last node, no compression possible
1614             push @new_nodes, $current_node;
1615             last;
1616         }
1617
1618         my $current_joiner = shift(@$old_qnodes);
1619         my $next_node = shift(@$old_qnodes);
1620
1621         # if they're both nodes, see if we can merge them
1622         if ($current_node->isa('QueryParser::query_plan::node')
1623             and $next_node->isa('QueryParser::query_plan::node')
1624             and $current_node->requested_class eq $next_node->requested_class
1625             and $current_node->negate eq $next_node->negate
1626         ) {
1627             warn "merging RHS atoms into atom list for LHS with joiner $current_joiner\n" if $self->QueryParser->debug;
1628             push @{$current_node->query_atoms}, $current_joiner if @{$current_node->query_atoms};
1629             push @{$current_node->query_atoms}, map { if (ref($_)) { $_->{node} = $current_node }; $_ } @{$next_node->query_atoms};
1630             push @{$current_node->phrases}, @{$next_node->phrases};
1631             unshift @$old_qnodes, $current_node;
1632         } else {
1633             push @new_nodes, $current_node, $current_joiner;
1634             unshift @$old_qnodes, $next_node;
1635         }
1636     }
1637
1638     $self->{query} = \@new_nodes;
1639
1640     # Step 4: As soon as we can, apply the explicit markers directly
1641     # to ::atoms so that we retain that for canonicalization while
1642     # also clearing away useless explicit groupings.
1643     if ($self->explicit) {
1644         if (!grep { # we have no non-::node, non-joiner query nodes, we've become a same-class singlton
1645                 ref($_) and !$_->isa('QueryParser::query_plan::node')
1646             } @{$self->query_nodes}
1647             and 1 == grep { # and we have exactly one (possibly merged, above) ::node with at least one ::atom
1648                 ref($_) and $_->isa('QueryParser::query_plan::node')
1649             } @{$self->query_nodes}
1650         ) {
1651             warn "setting explicit flags on atoms that may later be pulled up, at depth". $self->plan_level . "\n"
1652                 if $self->QueryParser->debug;
1653             $self->query_nodes->[0]->query_atoms->[0]->explicit_start(1) if @{$self->query_nodes->[0]->query_atoms};
1654             $self->query_nodes->[0]->query_atoms->[-1]->explicit_end(1) if @{$self->query_nodes->[0]->query_atoms};
1655         } else { # otherwise, the explicit grouping is meaningless, toss it
1656             $self->explicit(0);
1657         }
1658     }
1659
1660     warn @new_nodes . " nodes at pullup depth ". $self->plan_level . " after compression\n" if $self->QueryParser->debug;
1661
1662     return $self;
1663 }
1664
1665 sub joiners {
1666     my $self = shift;
1667     my %list = map { ($_=>1) } grep {!ref($_)} @{$self->{query}};
1668     return keys %list;
1669 }
1670
1671 sub classes {
1672     my $self = shift;
1673     my %list = map {
1674         ($_->classname . '|' . join('|',sort($_->fields)) => 1)
1675     } grep {
1676         ref($_) and ref($_) =~ /::node$/
1677     } @{$self->{query}};
1678     return keys %list;
1679 }
1680
1681 sub QueryParser {
1682     my $self = shift;
1683     return undef unless ref($self);
1684     return $self->{QueryParser};
1685 }
1686
1687 sub new {
1688     my $pkg = shift;
1689     $pkg = ref($pkg) || $pkg;
1690     my %args = (abstract_data => {}, query => [], joiner => '&', @_);
1691
1692     return bless \%args => $pkg;
1693 }
1694
1695 sub new_node {
1696     my $self = shift;
1697     my $pkg = ref($self) || $self;
1698     my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
1699     $self->add_node( $node );
1700     return $node;
1701 }
1702
1703 sub new_facet {
1704     my $self = shift;
1705     my $pkg = ref($self) || $self;
1706     my $name = shift;
1707     my $args = shift;
1708     my $negate = shift;
1709
1710     my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
1711     $self->add_node( $node );
1712
1713     return $node;
1714 }
1715
1716 sub new_filter {
1717     my $self = shift;
1718     my $pkg = ref($self) || $self;
1719     my $name = shift;
1720     my $args = shift;
1721     my $negate = shift;
1722
1723     my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
1724     $self->add_filter( $node );
1725
1726     return $node;
1727 }
1728
1729
1730 sub _merge_filters {
1731     my $left_filter = shift;
1732     my $right_filter = shift;
1733     my $join = shift;
1734
1735     return undef unless $left_filter or $right_filter;
1736     return $right_filter unless $left_filter;
1737     return $left_filter unless $right_filter;
1738
1739     my $args = $left_filter->{args} || [];
1740
1741     if ($join eq '|') {
1742         push(@$args, @{$right_filter->{args}});
1743
1744     } else {
1745         # find the intersect values
1746         my %new_vals;
1747         map { $new_vals{$_} = 1 } @{$right_filter->{args} || []};
1748         $args = [ grep { $new_vals{$_} } @$args ];
1749     }
1750
1751     $left_filter->{args} = $args;
1752     return $left_filter;
1753 }
1754
1755 sub collapse_filters {
1756     my $self = shift;
1757     my $name = shift;
1758
1759     # start by merging any filters at this level.
1760     # like-level filters are always ORed together
1761
1762     my $cur_filter;
1763     my @cur_filters = grep {$_->name eq $name } @{ $self->filters };
1764     if (@cur_filters) {
1765         $cur_filter = shift @cur_filters;
1766         my $args = $cur_filter->{args} || [];
1767         $cur_filter = _merge_filters($cur_filter, $_, '|') for @cur_filters;
1768     }
1769
1770     # next gather the collapsed filters from sub-plans and 
1771     # merge them with our own
1772
1773     my @subquery = @{$self->{query}};
1774
1775     while (@subquery) {
1776         my $blob = shift @subquery;
1777         shift @subquery; # joiner
1778         next unless $blob->isa('QueryParser::query_plan');
1779         my $sub_filter = $blob->collapse_filters($name);
1780         $cur_filter = _merge_filters($cur_filter, $sub_filter, $self->joiner);
1781     }
1782
1783     if ($self->QueryParser->debug) {
1784         my @args = ($cur_filter and $cur_filter->{args}) ? @{$cur_filter->{args}} : ();
1785         warn "collapse_filters($name) => [@args]\n";
1786     }
1787
1788     return $cur_filter;
1789 }
1790
1791 sub find_filter {
1792     my $self = shift;
1793     my $needle = shift;;
1794     return undef unless ($needle);
1795
1796     my $filter = $self->collapse_filters($needle);
1797
1798     warn "find_filter($needle) => " . 
1799         (($filter and $filter->{args}) ? "@{$filter->{args}}" : '[]') . "\n" 
1800         if $self->QueryParser->debug;
1801
1802     return $filter ? ($filter) : ();
1803 }
1804
1805 sub find_modifier {
1806     my $self = shift;
1807     my $needle = shift;;
1808     return undef unless ($needle);
1809     return grep { $_->name eq $needle } @{ $self->modifiers };
1810 }
1811
1812 sub new_modifier {
1813     my $self = shift;
1814     my $pkg = ref($self) || $self;
1815     my $name = shift;
1816
1817     my $node = do{$pkg.'::modifier'}->new( $name );
1818     $self->add_modifier( $node );
1819
1820     return $node;
1821 }
1822
1823 sub classed_node {
1824     my $self = shift;
1825     my $requested_class = shift;
1826
1827     my $node;
1828     for my $n (@{$self->{query}}) {
1829         next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
1830         if ($n->requested_class eq $requested_class) {
1831             $node = $n;
1832             last;
1833         }
1834     }
1835
1836     if (!$node) {
1837         $node = $self->new_node;
1838         $node->requested_class( $requested_class );
1839     }
1840
1841     return $node;
1842 }
1843
1844 sub remove_last_node {
1845     my $self = shift;
1846     my $requested_class = shift;
1847
1848     my $old = pop(@{$self->query_nodes});
1849     pop(@{$self->query_nodes}) if (@{$self->query_nodes});
1850
1851     return $old;
1852 }
1853
1854 sub query_nodes {
1855     my $self = shift;
1856     return $self->{query};
1857 }
1858
1859 sub floating {
1860     my $self = shift;
1861     my $f = shift;
1862     $self->{floating} = $f if (defined $f);
1863     return $self->{floating};
1864 }
1865
1866 sub explicit {
1867     my $self = shift;
1868     my $f = shift;
1869     $self->{explicit} = $f if (defined $f);
1870     return $self->{explicit};
1871 }
1872
1873 sub add_node {
1874     my $self = shift;
1875     my $node = shift;
1876
1877     $self->{query} ||= [];
1878     if ($node) {
1879         push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
1880         push(@{$self->{query}}, $node);
1881     }
1882
1883     return $self;
1884 }
1885
1886 sub top_plan {
1887     my $self = shift;
1888
1889     return $self->{level} ? 0 : 1;
1890 }
1891
1892 sub plan_level {
1893     my $self = shift;
1894     my $level = shift;
1895
1896     if (defined $level) {
1897         $self->{level} = $level;
1898         for (@{$self->query_nodes}) {
1899             $_->plan_level($level + 1) if (ref and $_->isa('QueryParser::query_plan'));
1900         }
1901     }
1902             
1903     return $self->{level};
1904 }
1905
1906 sub joiner {
1907     my $self = shift;
1908     my $joiner = shift;
1909
1910     $self->{joiner} = $joiner if ($joiner);
1911     return $self->{joiner};
1912 }
1913
1914 sub modifiers {
1915     my $self = shift;
1916     $self->{modifiers} ||= [];
1917     return $self->{modifiers};
1918 }
1919
1920 sub add_modifier {
1921     my $self = shift;
1922     my $modifier = shift;
1923
1924     $self->{modifiers} ||= [];
1925     $self->{modifiers} = [ grep {$_->name ne $modifier->name} @{$self->{modifiers}} ];
1926
1927     push(@{$self->{modifiers}}, $modifier);
1928
1929     return $self;
1930 }
1931
1932 sub facets {
1933     my $self = shift;
1934     $self->{facets} ||= [];
1935     return $self->{facets};
1936 }
1937
1938 sub add_facet {
1939     my $self = shift;
1940     my $facet = shift;
1941
1942     $self->{facets} ||= [];
1943     $self->{facets} = [ grep {$_->name ne $facet->name} @{$self->{facets}} ];
1944
1945     push(@{$self->{facets}}, $facet);
1946
1947     return $self;
1948 }
1949
1950 sub filters {
1951     my $self = shift;
1952     $self->{filters} ||= [];
1953     return $self->{filters};
1954 }
1955
1956 sub add_filter {
1957     my $self = shift;
1958     my $filter = shift;
1959
1960     $self->{filters} ||= [];
1961
1962     push(@{$self->{filters}}, $filter);
1963
1964     return $self;
1965 }
1966
1967 sub negate {
1968     my $self = shift;
1969     my $negate = shift;
1970
1971     $self->{negate} = $negate if (defined $negate);
1972
1973     return $self->{negate};
1974 }
1975
1976 # %opts supports two options at this time:
1977 #   no_phrases :
1978 #       If true, do not do anything to the phrases
1979 #       fields on any discovered nodes.
1980 #   with_config :
1981 #       If true, also return the query parser config as part of the blob.
1982 #       This will get set back to 0 before recursion to avoid repetition.
1983 sub to_abstract_query {
1984     my $self = shift;
1985     my %opts = @_;
1986
1987     my $pkg = ref $self->QueryParser || $self->QueryParser;
1988
1989     my $abstract_query = {
1990         type => "query_plan",
1991         floating => $self->floating,
1992         level => $self->plan_level,
1993         filters => [map { $_->to_abstract_query } @{$self->filters}],
1994         modifiers => [map { $_->to_abstract_query } @{$self->modifiers}],
1995         negate => $self->negate
1996     };
1997
1998     if ($opts{with_config}) {
1999         $opts{with_config} = 0;
2000         $abstract_query->{config} = $QueryParser::parser_config{$pkg};
2001     }
2002
2003     my $kids = [];
2004
2005     my $prev_was_joiner = 0;
2006     for my $qnode (@{$self->query_nodes}) {
2007         # Remember: qnode can be a joiner string, a node, or another query_plan
2008
2009         if (QueryParser::_util::is_joiner($qnode)) {
2010             unless ($prev_was_joiner) {
2011                 if ($abstract_query->{children}) {
2012                     my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2013                     next if $open_joiner eq $qnode;
2014
2015                     my $oldroot = $abstract_query->{children};
2016                     $kids = [$oldroot];
2017                     $abstract_query->{children} = {$qnode => $kids};
2018                 } else {
2019                     $abstract_query->{children} = {$qnode => $kids};
2020                 }
2021             }
2022             $prev_was_joiner = 1;
2023         } elsif ($qnode) {
2024             if (my $next_kid = $qnode->to_abstract_query(%opts)) {
2025                 push @$kids, $qnode->to_abstract_query(%opts);
2026                 $prev_was_joiner = 0;
2027             }
2028         }
2029     }
2030
2031     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2032     $$abstract_query{additional_data} = $self->{abstract_data}
2033         if (keys(%{$self->{abstract_data}}));
2034
2035     return $abstract_query;
2036 }
2037
2038
2039 #-------------------------------
2040 package QueryParser::query_plan::node;
2041 use Data::Dumper;
2042 $Data::Dumper::Indent = 0;
2043
2044 sub effective_joiner {
2045     my $node = shift;
2046
2047     my @nodelist = @{$node->query_atoms};
2048     return $node->plan->joiner if (@nodelist == 1);
2049
2050     # gather the joiners
2051     my %joiners = ( '&' => 0, '|' => 0 );
2052     while (my $n = shift(@nodelist)) {
2053         next if ref($n); # only look at joiners
2054         $joiners{$n}++;
2055     }
2056
2057     if (!($joiners{'&'} > 0 and $joiners{'|'} > 0)) { # no mix of joiners
2058         return '|' if ($joiners{'|'});
2059         return '&';
2060     }
2061
2062     return undef;
2063 }
2064
2065 sub new {
2066     my $pkg = shift;
2067     $pkg = ref($pkg) || $pkg;
2068     my %args = @_;
2069
2070     return bless \%args => $pkg;
2071 }
2072
2073 sub new_atom {
2074     my $self = shift;
2075     my $pkg = ref($self) || $self;
2076     return do{$pkg.'::atom'}->new( @_ );
2077 }
2078
2079 sub requested_class { # also split into classname, fields and alias
2080     my $self = shift;
2081     my $class = shift;
2082
2083     if ($class) {
2084         my @afields;
2085         my (undef, $alias) = split '#', $class;
2086         if ($alias) {
2087             $class =~ s/#[^|]+//;
2088             ($alias, @afields) = split '\|', $alias;
2089         }
2090
2091         my @fields = @afields;
2092         my ($class_part, @field_parts) = split '\|', $class;
2093         for my $f (@field_parts) {
2094              push(@fields, $f) unless (grep { $f eq $_ } @fields);
2095         }
2096
2097         $class_part ||= $class;
2098
2099         $self->{requested_class} = $class;
2100         $self->{alias} = $alias if $alias;
2101         $self->{alias_fields} = \@afields if $alias;
2102         $self->{classname} = $class_part;
2103         $self->{fields} = \@fields;
2104     }
2105
2106     return $self->{requested_class};
2107 }
2108
2109 sub plan {
2110     my $self = shift;
2111     my $plan = shift;
2112
2113     $self->{plan} = $plan if ($plan);
2114     return $self->{plan};
2115 }
2116
2117 sub alias {
2118     my $self = shift;
2119     my $alias = shift;
2120
2121     $self->{alias} = $alias if ($alias);
2122     return $self->{alias};
2123 }
2124
2125 sub alias_fields {
2126     my $self = shift;
2127     my $alias = shift;
2128
2129     $self->{alias_fields} = $alias if ($alias);
2130     return $self->{alias_fields};
2131 }
2132
2133 sub classname {
2134     my $self = shift;
2135     my $class = shift;
2136
2137     $self->{classname} = $class if ($class);
2138     return $self->{classname};
2139 }
2140
2141 sub fields {
2142     my $self = shift;
2143     my @fields = @_;
2144
2145     $self->{fields} ||= [];
2146     $self->{fields} = \@fields if (@fields);
2147     return $self->{fields};
2148 }
2149
2150 sub phrases {
2151     my $self = shift;
2152     my @phrases = @_;
2153
2154     $self->{phrases} ||= [];
2155     $self->{phrases} = \@phrases if (@phrases);
2156     return $self->{phrases};
2157 }
2158
2159 sub add_phrase {
2160     my $self = shift;
2161     my $phrase = shift;
2162
2163     push(@{$self->phrases}, $phrase);
2164
2165     return $self;
2166 }
2167
2168 sub negate {
2169     my $self = shift;
2170     my $negate = shift;
2171
2172     $self->{negate} = $negate if (defined $negate);
2173
2174     return $self->{negate};
2175 }
2176
2177 sub query_atoms {
2178     my $self = shift;
2179     my @query_atoms = @_;
2180
2181     $self->{query_atoms} ||= [];
2182     $self->{query_atoms} = \@query_atoms if (@query_atoms);
2183     return $self->{query_atoms};
2184 }
2185
2186 sub add_fts_atom {
2187     my $self = shift;
2188     my $atom = shift;
2189
2190     if (!ref($atom)) {
2191         my $content = $atom;
2192         my @parts = @_;
2193
2194         $atom = $self->new_atom( content => $content, node => $self, @parts );
2195     }
2196
2197     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
2198     push(@{$self->query_atoms}, $atom);
2199
2200     return $self;
2201 }
2202
2203 sub add_dummy_atom {
2204     my $self = shift;
2205     my @parts = @_;
2206
2207     my $atom = $self->new_atom( node => $self, @parts, dummy => 1 );
2208
2209     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
2210     push(@{$self->query_atoms}, $atom);
2211
2212     return $self;
2213 }
2214
2215 # This will find up to one occurence of @$short_list within @$long_list, and
2216 # replace it with the single atom $replacement.
2217 sub replace_phrase_in_abstract_query {
2218     my ($self, $short_list, $long_list, $replacement) = @_;
2219
2220     my $success = 0;
2221     my @already = ();
2222     my $goal = scalar @$short_list;
2223
2224     for (my $i = 0; $i < scalar (@$long_list); $i++) {
2225         my $right = $long_list->[$i];
2226
2227         if (QueryParser::_util::compare_abstract_atoms(
2228             $short_list->[scalar @already], $right
2229         )) {
2230             push @already, $i;
2231         } elsif (scalar @already) {
2232             @already = ();
2233             next;
2234         }
2235
2236         if (scalar @already == $goal) {
2237             splice @$long_list, $already[0], scalar(@already), $replacement;
2238             $success = 1;
2239             last;
2240         }
2241     }
2242
2243     return $success;
2244 }
2245
2246 sub to_abstract_query {
2247     my $self = shift;
2248     my %opts = @_;
2249
2250     my $pkg = ref $self->plan->QueryParser || $self->plan->QueryParser;
2251
2252     my $abstract_query = {
2253         "type" => "node",
2254         "alias" => $self->alias,
2255         "alias_fields" => $self->alias_fields,
2256         "class" => $self->classname,
2257         "fields" => $self->fields
2258     };
2259
2260     $self->abstract_node_additions($abstract_query)
2261         if ($self->can('abstract_node_additions'));
2262
2263     my $kids = [];
2264
2265     my $prev_was_joiner = 0;
2266     for my $qatom (grep {!ref($_) or !$_->dummy} @{$self->query_atoms}) {
2267         if (QueryParser::_util::is_joiner($qatom)) {
2268             unless ($prev_was_joiner) {
2269                 if ($abstract_query->{children}) {
2270                     my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2271                     next if $open_joiner eq $qatom;
2272
2273                     my $oldroot = $abstract_query->{children};
2274                     $kids = [$oldroot];
2275                     $abstract_query->{children} = {$qatom => $kids};
2276                 } else {
2277                     $abstract_query->{children} = {$qatom => $kids};
2278                 }
2279             }
2280             $prev_was_joiner = 1;
2281         } else {
2282             push @$kids, $qatom->to_abstract_query;
2283             $prev_was_joiner = 0;
2284         }
2285     }
2286
2287     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2288
2289     if ($self->phrases and @{$self->phrases} and not $opts{no_phrases}) {
2290         my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2291         if ($open_joiner ne '&') {
2292             my $oldroot = $abstract_query->{children};
2293             $kids = [$oldroot];
2294             $abstract_query->{children} = {'&' => $kids};
2295         }
2296
2297         for my $phrase (@{$self->phrases}) {
2298             # Phrases appear duplication in a real QP tree, and we don't want
2299             # that duplication in our abstract query.  So for all our phrases,
2300             # break them into atoms as QP would, and remove any matching
2301             # sequences of atoms from our abstract query.
2302
2303             my $tmp_prefix = '';
2304             $tmp_prefix = $QueryParser::parser_config{$pkg}{operators}{disallowed} if ($self->{negate});
2305
2306             my $tmptree = $self->{plan}->{QueryParser}->new(query => $phrase)->parse->parse_tree;
2307             if ($tmptree) {
2308                 # For a well-behaved phrase, we should now have only one node
2309                 # in the $tmptree query plan, and that node should have an
2310                 # orderly list of atoms and joiners.
2311
2312                 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
2313                     my $tmplist;
2314
2315                     eval {
2316                         $tmplist = $tmptree->{query}->[0]->to_abstract_query(
2317                             no_phrases => 1
2318                         )->{children}->{'&'};
2319                     };
2320                     next if $@ or !ref($tmplist);
2321
2322                     $$tmplist[0]{prefix} = $tmp_prefix.'"';
2323                     $$tmplist[-1]{suffix} = '"';
2324                     push @{$abstract_query->{children}->{'&'}}, @$tmplist;
2325                 }
2326             }
2327         }
2328     }
2329
2330     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2331
2332     my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2333     return undef unless @{$abstract_query->{children}->{$open_joiner}};
2334
2335     return $abstract_query;
2336 }
2337
2338 #-------------------------------
2339 package QueryParser::query_plan::node::atom;
2340
2341 sub new {
2342     my $pkg = shift;
2343     $pkg = ref($pkg) || $pkg;
2344     my %args = @_;
2345
2346     return bless \%args => $pkg;
2347 }
2348
2349 sub node {
2350     my $self = shift;
2351     return undef unless (ref $self);
2352     return $self->{node};
2353 }
2354
2355 sub content {
2356     my $self = shift;
2357     return undef unless (ref $self);
2358     return $self->{content};
2359 }
2360
2361 sub prefix {
2362     my $self = shift;
2363     return undef unless (ref $self);
2364     return $self->{prefix};
2365 }
2366
2367 sub suffix {
2368     my $self = shift;
2369     return undef unless (ref $self);
2370     return $self->{suffix};
2371 }
2372
2373 sub explicit_start {
2374     my $self = shift;
2375     my $explicit_start = shift;
2376
2377     $self->{explicit_start} = $explicit_start if (defined $explicit_start);
2378
2379     return $self->{explicit_start};
2380 }
2381
2382 sub dummy {
2383     my $self = shift;
2384     my $dummy = shift;
2385
2386     $self->{dummy} = $dummy if (defined $dummy);
2387
2388     return $self->{dummy};
2389 }
2390
2391 sub explicit_end {
2392     my $self = shift;
2393     my $explicit_end = shift;
2394
2395     $self->{explicit_end} = $explicit_end if (defined $explicit_end);
2396
2397     return $self->{explicit_end};
2398 }
2399
2400 sub to_abstract_query {
2401     my ($self) = @_;
2402     
2403     return {
2404         (map { $_ => $self->$_ } qw/dummy prefix suffix content explicit_start explicit_end/),
2405         "type" => "atom"
2406     };
2407 }
2408 #-------------------------------
2409 package QueryParser::query_plan::filter;
2410
2411 sub new {
2412     my $pkg = shift;
2413     $pkg = ref($pkg) || $pkg;
2414     my %args = @_;
2415
2416     return bless \%args => $pkg;
2417 }
2418
2419 sub plan {
2420     my $self = shift;
2421     return $self->{plan};
2422 }
2423
2424 sub name {
2425     my $self = shift;
2426     return $self->{name};
2427 }
2428
2429 sub negate {
2430     my $self = shift;
2431     return $self->{negate};
2432 }
2433
2434 sub args {
2435     my $self = shift;
2436     return $self->{args};
2437 }
2438
2439 sub to_abstract_query {
2440     my ($self) = @_;
2441     
2442     return {
2443         map { $_ => $self->$_ } qw/name negate args/
2444     };
2445 }
2446
2447 #-------------------------------
2448 package QueryParser::query_plan::facet;
2449
2450 sub new {
2451     my $pkg = shift;
2452     $pkg = ref($pkg) || $pkg;
2453     my %args = @_;
2454
2455     return bless \%args => $pkg;
2456 }
2457
2458 sub plan {
2459     my $self = shift;
2460     return $self->{plan};
2461 }
2462
2463 sub name {
2464     my $self = shift;
2465     return $self->{name};
2466 }
2467
2468 sub negate {
2469     my $self = shift;
2470     return $self->{negate};
2471 }
2472
2473 sub values {
2474     my $self = shift;
2475     return $self->{'values'};
2476 }
2477
2478 sub to_abstract_query {
2479     my ($self) = @_;
2480
2481     return {
2482         (map { $_ => $self->$_ } qw/name negate values/),
2483         "type" => "facet"
2484     };
2485 }
2486
2487 #-------------------------------
2488 package QueryParser::query_plan::modifier;
2489
2490 sub new {
2491     my $pkg = shift;
2492     $pkg = ref($pkg) || $pkg;
2493     my $modifier = shift;
2494     my $negate = shift;
2495
2496     return bless { name => $modifier, negate => $negate } => $pkg;
2497 }
2498
2499 sub name {
2500     my $self = shift;
2501     return $self->{name};
2502 }
2503
2504 sub negate {
2505     my $self = shift;
2506     return $self->{negate};
2507 }
2508
2509 sub to_abstract_query {
2510     my ($self) = @_;
2511     
2512     return $self->name;
2513 }
2514 1;
2515