LP#1775958: Rework pullup mechanism to flatten more nested queries
[evergreen-equinox.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Storage / Driver / Pg / QueryParser.pm
index 749fc41..7668b94 100644 (file)
@@ -1252,31 +1252,31 @@ sub flatten {
 
                 my $node_rank = 'COALESCE(' . $node->rank . " * ${talias}.weight * 1000, 0.0)";
 
-                $from .= "\n" . ${spc} x 4 ."LEFT JOIN (\n"
+                my $search_cte = ",\n $talias AS (\n"
                       . ${spc} x 5 . "SELECT fe.*, fe_weight.weight, ${talias}_xq.tsq, ${talias}_xq.tsq_rank /* search */\n"
                       . ${spc} x 6 . "FROM  $table AS fe\n"
                       . ${spc} x 7 . "JOIN config.metabib_field AS fe_weight ON (fe_weight.id = fe.field)";
 
-                if ($node->dummy_count < @{$node->only_atoms} ) {
+                if ($node->dummy_count < @{$node->only_atoms} or @{$node->phrases}) {
                     $with .= ",\n     " if $with;
                     $with .= "${talias}_xq AS (SELECT ". $node->tsquery ." AS tsq,". $node->tsquery_rank ." AS tsq_rank )";
                     if ($node->combined_search) {
-                        $from .= "\n" . ${spc} x 7 . "JOIN $ctable AS com ON (com.record = fe.source";
+                        $search_cte .= "\n" . ${spc} x 7 . "JOIN $ctable AS com ON (com.record = fe.source";
                         if (@field_ids) {
-                            $from .= " AND com.metabib_field IN (" . join(',',@field_ids) . "))";
+                            $search_cte .= " AND com.metabib_field IN (" . join(',',@field_ids) . "))";
                         } else {
-                            $from .= " AND com.metabib_field IS NULL)";
+                            $search_cte .= " AND com.metabib_field IS NULL)";
                         }
-                        $from .= "\n" . ${spc} x 7 . "JOIN ${talias}_xq ON (com.index_vector @@ ${talias}_xq.tsq)";
+                        $search_cte .= "\n" . ${spc} x 7 . "JOIN ${talias}_xq ON (com.index_vector @@ ${talias}_xq.tsq)";
                     } else {
-                        $from .= "\n" . ${spc} x 7 . "JOIN ${talias}_xq ON (fe.index_vector @@ ${talias}_xq.tsq)";
+                        $search_cte .= "\n" . ${spc} x 7 . "JOIN ${talias}_xq ON (fe.index_vector @@ ${talias}_xq.tsq)";
                     }
                 } else {
-                    $from .= "\n" . ${spc} x 7 . ", (SELECT NULL::tsquery AS tsq, NULL::tsquery AS tsq_rank ) AS ${talias}_xq";
+                    $search_cte .= "\n" . ${spc} x 7 . ", (SELECT NULL::tsquery AS tsq, NULL::tsquery AS tsq_rank ) AS ${talias}_xq";
                 }
 
                 if (@field_ids) {
-                    $from .= "\n" . ${spc} x 6 . "WHERE fe_weight.id IN  (" .
+                    $search_cte .= "\n" . ${spc} x 6 . "WHERE fe_weight.id IN  (" .
                         join(',', @field_ids) . ")";
                 }
 
@@ -1313,7 +1313,7 @@ sub flatten {
 
                                 # NOTE: only real fields that match the (component) tsquery will
                                 #       get to contribute to and increased rank for the record.
-                                $from .= "\n" . ${spc} x 8 . "UNION ALL\n"
+                                $search_cte .= "\n" . ${spc} x 8 . "UNION ALL\n"
                                       . ${spc} x 5 . "SELECT fe.id, fe.source, fe.field, fe.value, fe.index_vector, "
                                       . "fe_weight.weight, ${talias}_xq.tsq, ${talias}_xq.tsq_rank /* virtual field addition */\n"
                                       . ${spc} x 6 . "FROM  $vtable AS fe\n"
@@ -1328,7 +1328,8 @@ sub flatten {
                     }
                 }
 
-                $from .= "\n" . ${spc} x 4 . ") AS $talias ON (m.source = ${talias}.source)";
+                $with .= $search_cte . ')';
+                $from .= "\n" . ${spc} x 4 . "LEFT JOIN $talias ON (m.source = ${talias}.source)";
 
                 my %used_bumps;
                 my @bumps;
@@ -1828,30 +1829,11 @@ sub buildSQL {
 
     my $classname = $self->node->classname;
 
-    return $self->sql("to_tsquery('$classname','')") if $self->{dummy};
+    return $self->sql("''::tsquery") if $self->{dummy};
 
     my $normalizers = $self->node->plan->QueryParser->query_normalizers( $classname );
-    my $fields = $self->node->fields;
-
-    my $lang;
-    my $filter = $self->node->plan->find_filter('preferred_language');
-    $lang ||= $filter->args->[0] if ($filter && $filter->args);
-    $lang ||= $self->node->plan->QueryParser->default_preferred_language;
-    my $ts_configs = [];
-
-    if (!@$fields) {
-        $ts_configs = $self->node->plan->QueryParser->class_ts_config($classname, $lang);
-    } else {
-        for my $field (@$fields) {
-            push @$ts_configs, @{$self->node->plan->QueryParser->field_ts_config($classname, $field, $lang)};
-        }
-    }
-    $ts_configs = [keys %{{map { $_ => 1 } @$ts_configs}}];
-
-    # Assume we want exact if none otherwise provided.
-    # Because we can reasonably expect this to exist
-    $ts_configs = ['simple'] unless (scalar @$ts_configs);
 
+    my $fields = $self->node->fields;
     $fields = $self->node->plan->QueryParser->search_fields->{$classname} if (!@$fields);
 
     my %norms;
@@ -1888,9 +1870,11 @@ sub buildSQL {
     $suffix_op = ":$suffix" if $suffix;
     $suffix_after = "|| '$suffix_op'" if $suffix;
 
+    my $ts_configs = $self->node->ts_configs;
+
     my @sql_set = ();
     for my $ts_config (@$ts_configs) {
-        push @sql_set, "to_tsquery('$ts_config', COALESCE(NULLIF($prefix '(' || btrim(regexp_replace($sql,E'(?:\\\\s+|:)','$suffix_op&','g'),'&|') $suffix_after || ')', '()'), ''))";
+        push @sql_set, "to_tsquery('$ts_config', COALESCE(NULLIF($prefix '(' || btrim(regexp_replace($sql,E'(?:\\\\s+|:)','$suffix_op&','g'),'&|') $suffix_after || ')', $prefix '()'), ''))";
     }
 
     $sql = join($joiner, @sql_set);
@@ -1905,6 +1889,36 @@ use base 'QueryParser::query_plan::node';
 use List::MoreUtils qw/uniq/;
 use Data::Dumper;
 
+sub ts_configs {
+    my $self = shift;
+    my $ts_configs = $self->{ts_configs} || [];
+
+    if (!@$ts_configs) {
+        my $classname = $self->classname;
+        my $lang;
+        my $filter = $self->plan->find_filter('preferred_language');
+        $lang ||= $filter->args->[0] if ($filter && $filter->args);
+        $lang ||= $self->plan->QueryParser->default_preferred_language;
+
+        my $fields = $self->fields;
+        if (!@$fields) {
+            $ts_configs = $self->plan->QueryParser->class_ts_config($classname, $lang);
+        } else {
+            for my $field (@$fields) {
+                push @$ts_configs, @{$self->plan->QueryParser->field_ts_config($classname, $field, $lang)};
+            }
+        }
+        $ts_configs = [keys %{{map { $_ => 1 } @$ts_configs}}];
+
+        # Assume we want exact if none otherwise provided.
+        # Because we can reasonably expect this to exist
+        $ts_configs = ['simple'] unless (scalar @$ts_configs);
+        $self->{ts_configs} = $ts_configs;
+    }
+
+    return $ts_configs;
+}
+
 sub abstract_node_additions {
     my $self = shift;
     my $aq = shift;
@@ -2063,7 +2077,10 @@ sub tsquery {
 
     for my $atom (@{$self->query_atoms}) {
         if (ref($atom)) {
-            $self->{tsquery} .= "\n" . ${spc} x 3 . $atom->sql;
+            $self->{tsquery} .= "\n" . ${spc} x 3;
+            $self->{tsquery} .= '(' if $atom->explicit_start;
+            $self->{tsquery} .= $atom->sql;
+            $self->{tsquery} .= ')' if $atom->explicit_end;
         } else {
             $self->{tsquery} .= $atom x 2;
         }
@@ -2072,12 +2089,11 @@ sub tsquery {
     # any phrases that are more than empty or all-whitespace
     if (my @phrases = grep { /\S+/ } @{$self->phrases}) {
         my $neg = $self->negate ? '!!' : '';
-        $self->{tsquery} = '('.$self->{tsquery}.') && ' .
-            join(' && ',
-                map {
-                    "${neg}phraseto_tsquery('simple', \$_$$\$$_\$_$$\$)"
-                } @phrases
-            );
+        $self->{tsquery} ||= "''::tsquery";
+        $self->{tsquery} .= ' && ' . join(
+            ' && ',
+            map { "${neg}phraseto_tsquery('simple', \$_$$\$$_\$_$$\$)" } @phrases
+        );
     }
 
     return $self->{tsquery};
@@ -2093,6 +2109,19 @@ sub tsquery_rank {
     }
     $self->{tsquery_rank} = join(' ||', @atomlines);
     $self->{tsquery_rank} = "''::tsquery" unless $self->{tsquery_rank};
+
+    # any non-negated phrases that are more than empty or all-whitespace
+    if (!$self->negate) {
+        if (my @phrases = grep { /\S+/ } @{$self->phrases}) {
+            for my $tsc (@{$self->ts_configs}) {
+                $self->{tsquery_rank} .= ' || ' . join(
+                    ' || ',
+                    map { "phraseto_tsquery('$tsc', \$_$$\$$_\$_$$\$)" } @phrases
+                );
+            }
+        }
+    }
+
     return $self->{tsquery_rank};
 }