Update acq search operators
authorMike Rylander <mrylander@gmail.com>
Thu, 31 Oct 2019 19:23:22 +0000 (15:23 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Thu, 16 Jan 2020 21:38:28 +0000 (16:38 -0500)
Add __age (interval), __starts, and __ends operators.

Update __between to support __castdate modifier, for a more natural
comparison of dates entered by humans to timestamps stored in the
database.

Signed-off-by: Mike Rylander <mrylander@gmail.com>
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Search.pm

index c09fd35..e1ce5e6 100644 (file)
@@ -52,11 +52,12 @@ sub could_be_range {
 }
 
 sub castdate {
-    my ($value, $gte, $lte) = @_;
+    my ($value, $gte, $lte, $between) = @_;
 
     my $op = "=";
     $op = ">=" if $gte;
     $op = "<=" if $lte;
+    $op = "between" if $between;
 
     # avoid transforming a date if the match value is NULL.
     return {'=' => undef} if $op eq '=' and not $value;
@@ -76,7 +77,7 @@ sub prepare_acqlia_search_and {
         };
 
         # castdate not supported for acqlia fields: they're all type text
-        my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit);
+        my ($k, $v, $fuzzy, $between, $not, $starts, $ends) = breakdown_term($unit);
         my $point = $subquery->{"where"}->{"-and"};
         my $term_clause;
 
@@ -84,6 +85,10 @@ sub prepare_acqlia_search_and {
 
         if ($fuzzy and not ref $v) {
             push @$point, {"attr_value" => {"ilike" => "%" . $v . "%"}};
+        } elsif ($starts and not ref $v) {
+            push @$point, {"attr_value" => {"ilike" => $v . "%"}};
+        } elsif ($ends and not ref $v) {
+            push @$point, {"attr_value" => {"ilike" => "%" . $v}};
         } elsif ($between and could_be_range($v)) {
             push @$point, {"attr_value" => {"between" => $v}};
         } elsif (check_1d_max($v)) {
@@ -106,7 +111,7 @@ sub prepare_acqlia_search_or {
 
     foreach my $unit (@$acqlia) {
         # castdate not supported for acqlia fields: they're all type text
-        my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit);
+        my ($k, $v, $fuzzy, $between, $not, $starts, $ends) = breakdown_term($unit);
         my $term_clause;
         if ($fuzzy and not ref $v) {
             $term_clause = {
@@ -115,6 +120,20 @@ sub prepare_acqlia_search_or {
                     "attr_value" => {"ilike" => "%" . $v . "%"}
                 }
             };
+        } elsif ($starts and not ref $v) {
+            $term_clause = {
+                "-and" => {
+                    "definition" => $k,
+                    "attr_value" => {"ilike" => $v . "%"}
+                }
+            };
+        } elsif ($ends and not ref $v) {
+            $term_clause = {
+                "-and" => {
+                    "definition" => $k,
+                    "attr_value" => {"ilike" => "%" . $v}
+                }
+            };
         } elsif ($between and could_be_range($v)) {
             $term_clause = {
                 "-and" => {
@@ -143,9 +162,12 @@ sub breakdown_term {
         $term->{"__fuzzy"} ? 1 : 0,
         $term->{"__between"} ? 1 : 0,
         $term->{"__not"} ? 1 : 0,
+        $term->{"__starts"} ? 1 : 0,
+        $term->{"__ends"} ? 1 : 0,
         $term->{"__castdate"} ? 1 : 0,
         $term->{"__gte"} ? 1 : 0,
-        $term->{"__lte"} ? 1 : 0
+        $term->{"__lte"} ? 1 : 0,
+        $term->{"__age"} ? 1 : 0,
     );
 }
 
@@ -238,17 +260,39 @@ sub prepare_terms {
         $outer_clause->{$conj} = [] unless $outer_clause->{$conj};
         foreach my $unit (@{$terms->{$class}}) {
             my $special_clause;
-            my ($k, $v, $fuzzy, $between, $not, $castdate, $gte, $lte) =
+            my ($k, $v, $fuzzy, $between, $not, $starts, $ends, $castdate, $gte, $lte, $age) =
                 breakdown_term($unit);
 
             my $term_clause;
-            if ($fuzzy and not ref $v) {
+            if ($age and not ref $v) { # $v is expected to be parsed as an interval
+                $v =~ s/^\s*//;
+
+                my $op = $gte ? '>=' : '<=';
+                $term_clause = {$k = {$op => {transform => 'age', params => ['now'], value => '-' . $v}}};
+
+                # !!! NOTE: we invert $not because we have to compare to a /negative/
+                # interval, due to json_query restiction on function parameter order for
+                # transformed fields, so we flip the comparison.  Alternatively we could
+                # swap the GTE and LTE operators, but that would make the query harder
+                # to read and make diagnosing issues much more difficult.
+                $not = $not ? 0 : 1;
+
+            } elsif ($starts and not ref $v) {
+                $term_clause = {$k => {"ilike" => $v . "%"}};
+            } elsif ($ends and not ref $v) {
+                $term_clause = {$k => {"ilike" => "%" . $v}};
+            } elsif ($fuzzy and not ref $v) {
                 $term_clause = {$k => {"ilike" => "%" . $v . "%"}};
             } elsif ($between and could_be_range($v)) {
-                $term_clause = {$k => {"between" => $v}};
+                if ($castdate) {
+                    $v = castdate($v, 0, 0, $between);
+                    $term_clause = {$k => $v};
+                } else {
+                    $term_clause = {$k => {between => $v}};
+                }
             } elsif (check_1d_max($v)) {
                 if ($castdate) {
-                    $v = castdate($v, $gte, $lte) if $castdate;
+                    $v = castdate($v, $gte, $lte);
                 } elsif ($gte or $lte) {
                     my $op = $gte ? '>=' : '<=';
                     $v = {$op => $v};