package ETLp::Loader::OracleSQLLoader;

use MooseX::Declare;

=head1 NAME

ETLp::Loader::OracleSQLLoader - Load data using SQL*Loader

=head1 SYNOPSIS

This module will load data using Oracle's SQL*Loader utility. It is designed
to be used when:

    * The backend database is Oracle
    * The functionality is not provided by ETLp::Loader::CSV
    * Speed is of the essence, and features such as direct path or
      parallel loading are required.

For Example

    use ETLp::Loader::OracleSQLLoader;

    my $sqlldr = ETLp::Loader::OracleSQLLoader->new(
        userid           => 'scott/tiger',
        table            => 'scores',
        filename         => "$data_directory/$filename",
        specification    => $specification,
        mode             => 'truncate',
        controlfile      => "$outfile_directory/$filename.ctl",
        logfile          => "$outfile_directory/$filename.log",
        discardfile      => "$outfile_directory/$filename.dsc",
        badfile          => "$outfile_directory/$filename.bad",
    );
    
    my $recode = $sqlldr->load;
    
    if ($retcode) {
        print $sqlldr->error;
    }
    
=cut

class ETLp::Loader::OracleSQLLoader with ETLp::Role::Config {
    use ETLp::Exception;
    use ETLp::Types;
    
    has 'userid'           => (is => 'ro', isa => 'Str',  required => 1);
    has 'controlfile'      => (is => 'ro', isa => 'Str',  required => 1);
    has 'table'            => (is => 'ro', isa => 'Str',  required => 1);
    has 'filename'         => (is => 'ro', isa => 'Str',  required => 1);
    has 'specification'    => (is => 'ro', isa => 'Str',  required => 1);
    has 'logfile'          => (is => 'ro', isa => 'Maybe[Str]');
    has 'badfile'          => (is => 'ro', isa => 'Maybe[Str]');
    has 'discardfile'      => (is => 'ro', isa => 'Maybe[Str]');
    has 'keep_controlfile' => (is => 'ro', isa => 'Bool', default  => 0);
    has 'keep_logfile'     => (is => 'ro', isa => 'Bool', default  => 1);
    has 'keep_badfile'     => (is => 'ro', isa => 'Bool', default  => 1);
    has 'keep_discardfile' => (is => 'ro', isa => 'Bool', default  => 1);
    has 'parameters'       => (is => 'ro', isa => 'Maybe[HashRef]');
    has 'localize'         => (is => 'ro', isa => 'Bool', default  => 0);
    has 'mode' => (is => 'ro', isa => 'SQLLoaderMode', default => 'append');
    
    our @keywords = qw/
        discardmax
        skip
        load
        errors
        rows
        bindsize
        silent
        direct
        parfile
        parallel
        file
        skip_unusable_indexes
        skip_index_maintenance
        readsize
        external_table
        columnarrayrows
        streamsize
        multithreading
        resumable
        resumable_name
        resumable_timeout
    /;
    
=head1 METHODS

=head2 new

Creates a new ETLp::Loader::OracleSQLLoader object. Accespts the
following attributes:

    * userid. Required. The username and password of the user connecting
      to the database, in the form:
          o <user>/<password> or
          o <user>/<password>@<tns alias>
    * controlfile. Required. The name of the control file. The content
      will be generated by this module.
    * table. Required. The name of the table that the data will be loaded
      into
    * filename. Required. The file to be loaded.
    * mode. Optional. The type of load - append, truncate, insert or
      replace. Defaults to append
    * specification. Required. The specification of the incoming file and
      the processing. E.g.
      
            fields terminated by "," 
            (id, name, score, file_id constant 4)
            
    * logfile. Optional. The name of the log file
    * badfile. Optional. The name of the bad file.
    * discardfile . Optional. The name of the discardfile.
    * keep_controlfile. Optional. Whether to keep the control file
      following processing. Defaults to no (0).
    * keep_logfile. Optional. Whether to keep the log file
      following processing. Defaults to yes (1).
    * keep_badfile. Optional. Whether to keep the bad file
      following processing. Defaults to yes (1).
    * keep_discardfile. Optional. Whether to keep the discard file
      following processing. Defaults to yes (1).
    * localize. Optional. Whether to localize (fix for current OS), the
      data file's end-of-line delimiters. Defaults to no (0).
    * parameters. Optional. A hashref of SQL*Loader command-line
      parameters (excluding control, log, bad and discard)

=head2 run

Invoke the SQL*Loader run. Will return a code, that is an integer
between 0 and 3:

   * 0 - successful 
   * 1 - failed 
   * 2 - warn 
   * 3 - fatal
   
Examples

   * All rows loader successfully - 0 
   * All/some rows discarded      - 2 
   * All/some row rejected        - 2 
   * Discontinued load            - 2 
   * Command line/syntax errors   - 1 
   * Errors Fatal to Sql*Loader   - 1 
   * OS related errors            - 3

=cut
    
    method run {    
        # First, generate the controlfile
        open my $cf, '>', $self->controlfile;
        print $cf $self->controlfile_content;
        close $cf;
    
        my $command = "sqlldr userid=" . $self->userid;
        $command .= join(" ", @{$self->command});
        
        $self->{exe} = $command;
    
        my $sqlldr = ETLp::Utility::Command->new();
        my ($stdout, $retcode) = $sqlldr->run($command);
        
        $retcode = 0 unless defined $retcode;
    
        $self->{_output} = $stdout;
        $self->{_error}  = $sqlldr->errstr;
    
        $self->_cleanup;
    
        $self->logger->debug("Retcode: " . $retcode);
        #$retcode = $retcode >> 8;
        return $retcode;
    }
    
=head2 command

Return the constructed SQL*Loader command - the sqlldr invocation and the
user authentication information if not included

=cut

    method command {
        return $self->{_command};
    }

=head2 controlfile_content

Return the content of the controlfile

=cut

    method controlfile_content {
        return $self->{_controlfile_content};
    }

=head2 error

Return any error generated following the SQL*Loader run

=cut

    method error {
        return $self->{_error};
    }

=head2 output

Return the output from a SQL*Loader run

=cut

    method output {
        return $self->{_output};
        }
    
    # Remove files required for or generated by the SQL*Loader process unless
    # they should be kept
    method _cleanup {
        foreach my $filetype (qw/controlfile logfile badfile discardfile/) {
            my $keep = 'keep_' . $filetype;
            unless ($self->$keep) {
                $self->logger->debug("removing ". $self->$filetype);
                unlink $self->$filetype
                  || ETLpException->throw(error => "Cannot remove " .
                                          $self->filetype . ": $!");
            }
        }
    }

    # create the SQL*Loader controlfile
    method _build_controlfile {
        $self->{_controlfile_content} = 
            "LOAD DATA\n".
            "INFILE '".$self->filename."'\n".
            "INTO TABLE ".$self->table."\n".
            $self->mode."\n".
            $self->specification;
    }

    # Construct the SQL*Loader command
    method _build_command {
        my @command = " control=" . $self->controlfile;
    
        push @command, "log=" . $self->logfile         if $self->logfile;
        push @command, "bad=" . $self->badfile         if $self->badfile;
        push @command, "discard=" . $self->discardfile if $self->discardfile;
    
        if ($self->parameters) {
            foreach my $sqlldr_keyword (sort keys %{$self->parameters}) {
                unless (grep /^$sqlldr_keyword$/i, @keywords) {
                    ETLpException->throw("Invalid keyword: $sqlldr_keyword");
                }
                push @command,
                  "$sqlldr_keyword = " . $self->parameters->{$sqlldr_keyword};
            }
        }
    
        $self->{_command} = \@command;
    }
    
    method BUILD {
        if ($self->localize) {
            my $l = File::LocalizeNewlines->new;
            $l->localize($self->filename);
        }
            
        $self->_build_command;
        $self->_build_controlfile;
    }

}

=head1 LICENSE AND COPYRIGHT

Copyright 2010 Redbone Systems Ltd

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

The terms are in the LICENSE file that accompanies this application
    
=cut