import package definitions, RT#32639
[freeside.git] / FS / FS / Record.pm
index 88e5411..a1c8776 100644 (file)
@@ -6,7 +6,8 @@ use vars qw( $AUTOLOAD @ISA @EXPORT_OK $DEBUG
              $conf $conf_encryption $money_char $lat_lower $lon_upper
              $me
              $nowarn_identical $nowarn_classload
-             $no_update_diff $no_history $no_check_foreign
+             $no_update_diff $no_history $qsearch_qualify_columns
+             $no_check_foreign
              @encrypt_payby
            );
 use Exporter;
@@ -38,7 +39,8 @@ use Tie::IxHash;
 #export dbdef for now... everything else expects to find it here
 @EXPORT_OK = qw(
   dbh fields hfields qsearch qsearchs dbdef jsearch
-  str2time_sql str2time_sql_closing regexp_sql not_regexp_sql concat_sql
+  str2time_sql str2time_sql_closing regexp_sql not_regexp_sql
+  concat_sql group_concat_sql
   midnight_sql
 );
 
@@ -49,6 +51,9 @@ $nowarn_identical = 0;
 $nowarn_classload = 0;
 $no_update_diff = 0;
 $no_history = 0;
+
+$qsearch_qualify_columns = 0;
+
 $no_check_foreign = 0;
 
 my $rsa_module;
@@ -125,6 +130,8 @@ FS::Record - Database record objects
     $error = $record->ut_floatn('column');
     $error = $record->ut_number('column');
     $error = $record->ut_numbern('column');
+    $error = $record->ut_decimal('column');
+    $error = $record->ut_decimaln('column');
     $error = $record->ut_snumber('column');
     $error = $record->ut_snumbern('column');
     $error = $record->ut_money('column');
@@ -739,72 +746,74 @@ sub _from_hashref {
   return @return;
 }
 
-## makes this easier to read
-
 sub get_real_fields {
   my $table = shift;
   my $record = shift;
   my $real_fields = shift;
 
-   ## this huge map was previously inline, just broke it out to help read the qsearch method, should be optimized for readability
-      return ( 
-      map {
+  ## could be optimized more for readability
+  return ( 
+    map {
 
       my $op = '=';
       my $column = $_;
+      my $table_column = $qsearch_qualify_columns ? "$table.$column" : $column;
       my $type = dbdef->table($table)->column($column)->type;
       my $value = $record->{$column};
       $value = $value->{'value'} if ref($value);
-      if ( ref($record->{$_}) ) {
-        $op = $record->{$_}{'op'} if $record->{$_}{'op'};
+
+      if ( ref($record->{$column}) ) {
+        $op = $record->{$column}{'op'} if $record->{$column}{'op'};
         #$op = 'LIKE' if $op =~ /^ILIKE$/i && driver_name ne 'Pg';
         if ( uc($op) eq 'ILIKE' ) {
           $op = 'LIKE';
-          $record->{$_}{'value'} = lc($record->{$_}{'value'});
-          $column = "LOWER($_)";
+          $record->{$column}{'value'} = lc($record->{$column}{'value'});
+          $table_column = "LOWER($table_column)";
         }
-        $record->{$_} = $record->{$_}{'value'}
+        $record->{$column} = $record->{$column}{'value'}
       }
 
-      if ( ! defined( $record->{$_} ) || $record->{$_} eq '' ) {
+      if ( ! defined( $record->{$column} ) || $record->{$column} eq '' ) {
         if ( $op eq '=' ) {
           if ( driver_name eq 'Pg' ) {
             if ( $type =~ /(int|numeric|real|float4|(big)?serial)/i ) {
-              qq-( $column IS NULL )-;
+              qq-( $table_column IS NULL )-;
             } else {
-              qq-( $column IS NULL OR $column = '' )-;
+              qq-( $table_column IS NULL OR $table_column = '' )-;
             }
           } else {
-            qq-( $column IS NULL OR $column = "" )-;
+            qq-( $table_column IS NULL OR $table_column = "" )-;
           }
         } elsif ( $op eq '!=' ) {
           if ( driver_name eq 'Pg' ) {
             if ( $type =~ /(int|numeric|real|float4|(big)?serial)/i ) {
-              qq-( $column IS NOT NULL )-;
+              qq-( $table_column IS NOT NULL )-;
             } else {
-              qq-( $column IS NOT NULL AND $column != '' )-;
+              qq-( $table_column IS NOT NULL AND $table_column != '' )-;
             }
           } else {
-            qq-( $column IS NOT NULL AND $column != "" )-;
+            qq-( $table_column IS NOT NULL AND $table_column != "" )-;
           }
         } else {
           if ( driver_name eq 'Pg' ) {
-            qq-( $column $op '' )-;
+            qq-( $table_column $op '' )-;
           } else {
-            qq-( $column $op "" )-;
+            qq-( $table_column $op "" )-;
           }
         }
       } elsif ( $op eq '!=' ) {
-        qq-( $column IS NULL OR $column != ? )-;
+        qq-( $table_column IS NULL OR $table_column != ? )-;
       #if this needs to be re-enabled, it needs to use a custom op like
       #"APPROX=" or something (better name?, not '=', to avoid affecting other
       # searches
       #} elsif ( $op eq 'APPROX=' && _is_fs_float( $type, $value ) ) {
-      #  ( "$column <= ?", "$column >= ?" );
+      #  ( "$table_column <= ?", "$table_column >= ?" );
       } else {
-        "$column $op ?";
+        "$table_column $op ?";
       }
-    } @{ $real_fields } );  
+
+    } @{ $real_fields }
+  );  
 }
 
 =item by_key PRIMARY_KEY_VALUE
@@ -1581,7 +1590,7 @@ Table name (required).
 
 =item params
 
-Listref of field names for static fields.  They will be given values from the
+Arrayref of field names for static fields.  They will be given values from the
 PARAMS hashref and passed as a "params" hashref to batch_import.
 
 =item formats
@@ -1630,13 +1639,15 @@ format_types).
 
 =back
 
-PARAMS is a base64-encoded Storable string containing the POSTed data as
-a hash ref.  It normally contains at least one field, "uploaded files",
-generated by /elements/file-upload.html and containing the list of uploaded
-files.  Currently only supports a single file named "file".
+PARAMS is a hashref (or base64-encoded Storable hashref) containing the 
+POSTed data.  It must contain the field "uploaded files", generated by 
+/elements/file-upload.html and containing the list of uploaded files.
+Currently only supports a single file named "file".
 
 =cut
 
+# uploaded_files is kind of bizarre; fix that some time
+
 use Storable qw(thaw);
 use Data::Dumper;
 use MIME::Base64;
@@ -1647,7 +1658,13 @@ sub process_batch_import {
   my @pass_params = $opt->{params} ? @{ $opt->{params} } : ();
   my %formats = %{ $opt->{formats} };
 
-  my $param = thaw(decode_base64(shift));
+  my $param = shift;
+  # because some job-spawning code (JSRPC) pre-freezes the arguments,
+  # and then the 'frozen' attribute doesn't get set, and thus $job->args 
+  # doesn't know to thaw them, we have to do this everywhere.
+  if (!ref $param) {
+    $param = thaw(decode_base64($param));
+  }
   warn Dumper($param) if $DEBUG;
   
   my $files = $param->{'uploaded_files'}
@@ -1678,6 +1695,7 @@ sub process_batch_import {
     #?
     default_csv                => $opt->{default_csv},
     postinsert_callback        => $opt->{postinsert_callback},
+    insert_args_callback       => $opt->{insert_args_callback},
   );
 
   if ( $opt->{'batch_namecol'} ) {
@@ -1763,6 +1781,9 @@ sub batch_import {
   my $preinsert_callback = '';
   $preinsert_callback = $param->{'preinsert_callback'}
          if $param->{'preinsert_callback'};
+  my $insert_args_callback = '';
+  $insert_args_callback = $param->{'insert_args_callback'}
+         if $param->{'insert_args_callback'};
 
   if ( $param->{'format'} ) {
 
@@ -2072,7 +2093,12 @@ sub batch_import {
       next if exists $param->{skiprow} && $param->{skiprow};
     }
 
-    my $error = $record->insert;
+    my @insert_args = ();
+    if ( $insert_args_callback ) {
+      @insert_args = &{$insert_args_callback}($record, $param);
+    }
+
+    my $error = $record->insert(@insert_args);
 
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
@@ -2312,6 +2338,35 @@ sub ut_numbern {
   '';
 }
 
+=item ut_decimal COLUMN[, DIGITS]
+
+Check/untaint decimal numbers (up to DIGITS decimal places.  If there is an 
+error, returns the error, otherwise returns false.
+
+=item ut_decimaln COLUMN[, DIGITS]
+
+Check/untaint decimal numbers.  May be null.  If there is an error, returns
+the error, otherwise returns false.
+
+=cut
+
+sub ut_decimal {
+  my($self, $field, $digits) = @_;
+  $digits ||= '';
+  $self->getfield($field) =~ /^\s*(\d+(\.\d{0,$digits})?)\s*$/
+    or return "Illegal or empty (decimal) $field: ".$self->getfield($field);
+  $self->setfield($field, $1);
+  '';
+}
+
+sub ut_decimaln {
+  my($self, $field, $digits) = @_;
+  $self->getfield($field) =~ /^\s*(\d*(\.\d{0,$digits})?)\s*$/
+    or return "Illegal (decimal) $field: ".$self->getfield($field);
+  $self->setfield($field, $1);
+  '';
+}
+
 =item ut_money COLUMN
 
 Check/untaint monetary numbers.  May be negative.  Set to 0 if null.  If there
@@ -2456,8 +2511,8 @@ sub ut_alpha_lower {
 Check/untaint phone numbers.  May be null.  If there is an error, returns
 the error, otherwise returns false.
 
-Takes an optional two-letter ISO country code; without it or with unsupported
-countries, ut_phonen simply calls ut_alphan.
+Takes an optional two-letter ISO 3166-1 alpha-2 country code; without
+it or with unsupported countries, ut_phonen simply calls ut_alphan.
 
 =cut
 
@@ -3134,7 +3189,7 @@ sub scalar_sql {
   defined($scalar) ? $scalar : '';
 }
 
-=item count [ WHERE ]
+=item count [ WHERE [, PLACEHOLDER ...] ]
 
 Convenience method for the common case of "SELECT COUNT(*) FROM table", 
 with optional WHERE.  Must be called as method on a class with an 
@@ -3147,7 +3202,23 @@ sub count {
   my $table = $self->table or die 'count called on object of class '.ref($self);
   my $sql = "SELECT COUNT(*) FROM $table";
   $sql .= " WHERE $where" if $where;
-  $self->scalar_sql($sql);
+  $self->scalar_sql($sql, @_);
+}
+
+=item row_exists [ WHERE [, PLACEHOLDER ...] ]
+
+Convenience method for the common case of "SELECT 1 FROM table ... LIMIT 1"
+with optional (but almost always needed) WHERE.
+
+=cut
+
+sub row_exists {
+  my($self, $where) = (shift, shift);
+  my $table = $self->table or die 'row_exists called on object of class '.ref($self);
+  my $sql = "SELECT 1 FROM $table";
+  $sql .= " WHERE $where" if $where;
+  $sql .= " LIMIT 1";
+  $self->scalar_sql($sql, @_);
 }
 
 =back
@@ -3383,6 +3454,24 @@ sub concat_sql {
 
 }
 
+=item group_concat_sql COLUMN, DELIMITER
+
+Returns an SQL expression to concatenate an aggregate column, using 
+GROUP_CONCAT() for mysql and array_to_string() and array_agg() for Pg.
+
+=cut
+
+sub group_concat_sql {
+  my ($col, $delim) = @_;
+  $delim = dbh->quote($delim);
+  if ( driver_name() =~ /^mysql/i ) {
+    # DISTINCT(foo) is valid as $col
+    return "GROUP_CONCAT($col SEPARATOR $delim)";
+  } else {
+    return "array_to_string(array_agg($col), $delim)";
+  }
+}
+
 =item midnight_sql DATE
 
 Returns an SQL expression to convert DATE (a unix timestamp) to midnight