File Coverage

blib/lib/Oracle/SQLLoader.pm
Criterion Covered Total %
statement 63 260 24.2
branch 19 132 14.3
condition 7 74 9.4
subroutine 11 33 33.3
pod 20 21 95.2
total 120 520 23.0


line stmt bran cond sub pod time code
1             # -*- mode: cperl -*-
2             # $Id: SQLLoader.pm,v 1.43 2005-07-28 03:10:23 ezra Exp $
3              
4             =head1 NAME
5              
6             Oracle::SQLLoader - object interface to Oracle's sqlldr
7              
8             =head1 DESCRIPTION
9              
10             B provides an object wrapper to the most commonly used
11             functionality of Oracle's SQL*Loader bulk loader utility. It tries to dwim
12             as best as possible by using defaults to all of the various sqlldr options.
13              
14             The module currently supports the loading of a single table from a single file.
15             The file can be either fixed length or delimited. For a delimited file load, just
16             add the names of the destination columns in the order the fields appears in the
17             data file and optionally supply a data type. For a fixed length load, supply the
18             destination column name; the combination of the field starting offset and field
19             length, or the field start and end offsets in the data file; and an optional
20             data type.
21              
22             Besides letting you skip the Oracle docs, the module provides a lot of useful
23             stats and return codes by parsing the sqlldr output.
24              
25              
26             =head1 SYNOPSIS
27              
28             use Oracle::SQLLoader qw/$CHAR $INT $DECIMAL $DATE/;
29              
30             ### load a simple comma-delimited file to a single table
31             $ldr = new Oracle::SQLLoader(
32             infile => '/tmp/test.dat',
33             terminated_by => ',',
34             username => $user,
35             password => $pass,
36             sid => $sid
37             );
38              
39             $ldr->addTable(table_name => 'test_table');
40             $ldr->addColumn(column_name => 'first_col');
41             $ldr->addColumn(column_name => 'second_col');
42             $ldr->addColumn(column_name => 'third_col');
43             $ldr->executeLoader() || warn "Problem executing sqlldr: $@\n";
44              
45             # stats
46             $skipped = $ldr->getNumberSkipped();
47              
48             $read = $ldr->getNumberRead();
49              
50             $rejects = $ldr->getNumberRejected();
51              
52             $discards = $ldr->getNumberDiscarded();
53              
54             $loads = $ldr->getNumberLoaded();
55              
56             $beginTS = $ldr->getLoadBegin();
57              
58             $endTS = $ldr->getLoadEnd();
59              
60             $runtimeSecs = $ldr->getElapsedSeconds();
61              
62             $secsOnCpu = $ldr->getCpuSeconds();
63              
64              
65              
66              
67             #### a fixed length example
68             $flldr = new Oracle::SQLLoader(
69             infile => '/tmp/test.fixed',
70             username => $user,
71             password => $pass,
72             );
73             $flldr->addTable(table_name => 'test_table');
74              
75             $flldr->addColumn(column_name => 'first_col',
76             field_offset => 0,
77             field_length => 4,
78             column_type => $INT);
79              
80             $flldr->addColumn(column_name => 'second_col',
81             field_offset => 4,
82             field_end => 9);
83              
84             $flldr->addColumn(column_name => 'third_col',
85             field_offset => 9,
86             field_end => 14,
87             column_type => $CHAR);
88              
89             $flldr->addColumn(column_name => 'timestamp',
90             field_offset => 9,
91             field_length => 13,
92             column_type => $DATE,
93             date_format => "YYYYMMDD HH24:MI");
94              
95             $flldr->executeLoader() || warn "Problem executing sqlldr: $@\n";
96              
97             # stats
98             $skipped = $ldr->getNumberSkipped();
99              
100             $read = $ldr->getNumberRead();
101              
102             $rejects = $ldr->getNumberRejected();
103              
104             $discards = $ldr->getNumberDiscarded();
105              
106             $loads = $ldr->getNumberLoaded();
107              
108             $beginTS = $ldr->getLoadBegin();
109              
110             $endTS = $ldr->getLoadEnd();
111              
112             $runtimeSecs = $ldr->getElapsedSeconds();
113              
114             $secsOnCpu = $ldr->getCpuSeconds();
115              
116              
117             =head1 AUTHOR
118              
119             Ezra Pagel
120              
121             =head1 CONTRIBUTIONS
122              
123             John Huckelba - fix for single record files scenario
124              
125             Craig Pearlman - added various fixes, most
126             importantly initializing and generating the options clause correctly.
127              
128             =head1 COPYRIGHT
129              
130             The Oracle::SQLLoader module is Copyright (c) 2006 Ezra Pagel.
131              
132             The Oracle::SQLLoader module is free software; you can redistribute it and/or
133             modify it under the same terms as Perl itself, either Perl version 5.8.4 or,
134             at your option, any later version of Perl 5 you may have available.
135              
136             Modifications to Oracle::SQLLoader are Copyright (c) 2006 Health Market Science,
137             though this code remains free under the terms of the original license.
138              
139             SQL*Loader is Copyright (c) 1982, 2002, Oracle Corporation. All rights reserved.
140              
141             =cut
142              
143             package Oracle::SQLLoader;
144              
145 8     8   33150 use IO::File;
  8         207476  
  8         1880  
146 8     8   84 use Carp;
  8         19  
  8         467  
147 8     8   48 use Config;
  8         20  
  8         380  
148 8     8   65 use Cwd;
  8         17  
  8         548  
149 8     8   44 use strict;
  8         13  
  8         293  
150 8     8   48 use Exporter;
  8         15  
  8         362  
151 8     8   14822 use Time::Local;
  8         23620  
  8         707  
152              
153 8         70867 use vars qw/@ISA
154             @EXPORT_OK
155             $VERSION
156             $CHAR
157             $INT
158             $DECIMAL
159             $DATE
160             $APPEND
161             $TRUNCATE
162             $REPLACE
163 8     8   68 $INSERT/;
  8         18  
164              
165              
166             $VERSION = '0.9';
167             @ISA = qw/Exporter/;
168             @EXPORT_OK = qw/$CHAR $INT $DECIMAL $DATE $APPEND $TRUNCATE $REPLACE $INSERT/;
169              
170             our %OPTS = map { $_, 1 } qw/bindsize columnarrayrows direct discardmax
171             errors load readsize resumable rows silent
172             skip streamsize/;
173              
174             our %BOOL_OPTS = map { $_, 1 } qw/direct multithreading parallel
175             skip_index_maintenance skip_unusable_indexes/;
176              
177             our %OPT_DEFAULTS;
178              
179             #initDefaults();
180              
181             ################################################################################
182              
183             =head1 API
184              
185             =cut
186              
187             ################################################################################
188              
189              
190             ################################################################################
191              
192             =head2 IMPORTABLE VARIABLES
193              
194             =over 2
195              
196             =item Column Data Types
197              
198             =over 2
199              
200             =item $CHAR - data to be interpreted as character. empty data is treated as null
201              
202             =item $INT - data to be interpreted as integer. empty data is treated as 0
203              
204             =item $DECIMAL - data to be interpreted as floating point. empty data is treated
205             as null
206              
207             =item $DATE - date fields. these want a valid format, but will default to DD-MON-YY
208              
209             =back
210              
211             =item Table Load Modes
212              
213             =over 2
214              
215             =item $APPEND - add new rows into the table
216              
217             =item $INSERT - adds rows into an empty table, or error out if the table has data
218              
219             =item $REPLACE - delete all existing rows from the table and then load
220              
221             =item $TRUNCATE - truncate the table (no rollback!) and then load
222              
223             =back
224              
225             =back
226              
227             =cut
228              
229             ################################################################################
230              
231             $CHAR = 'CHAR';
232             $INT = 'INTEGER EXTERNAL';
233             $DECIMAL = 'DECIMAL EXTERNAL';
234             $DATE = 'DATE';
235             $APPEND = 'APPEND';
236             $TRUNCATE = 'TRUNCATE';
237             $REPLACE = 'REPLACE';
238             $INSERT = 'INSERT';
239              
240              
241             # what's the name of the sqlldr executable?
242             my $SQLLDRBIN = $^O =~ /win32/i ? 'sqlldr.exe' : 'sqlldr';
243              
244              
245             # trial+error result codes
246             my %ERRORCODES = $^O =~ /win32/i ? (SUCCESS => 0,
247             ERROR => 3,
248             WARN => 2,
249             FATAL => 4) :
250             (SUCCESS => 0,
251             ERROR => 1,
252             WARN => 2,
253             FATAL => 3);
254              
255             # used to determine start/end epochs from logs
256             my %MONTHS = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5,
257             Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec=> 11);
258             my $DEBUG = 0;
259              
260              
261             ################################################################################
262              
263             =head2 PUBLIC METHODS
264              
265             =cut
266              
267             ################################################################################
268              
269              
270              
271              
272             ################################################################################
273              
274             =head3 B
275              
276             create a new Oracle::SQLLoader object
277              
278             =over 2
279              
280             =item mandatory arguments
281              
282             =over 2
283              
284             =item I - the name and full filesystem path to the input file
285              
286             =item I - the oracle username for this load
287              
288             =item I - the password for this username
289              
290             =back
291              
292             =item common arguments
293              
294             =over 2
295              
296             =item I - supply an explicit file name to use when
297             generating the control file; useful when I is set to false.
298              
299             =item I - if you're planning on loading a delimited file,
300             just provide the character used as a delimiter and Oracle::SQLLoader will
301             presume that it's a delimited load from here on out. if this option is not
302             supplied, a fixed length file format will be used.
303              
304             =item I - bypass parsing sql and write directly to the tablespace on
305             the filesystem. this is considerably faster than conventional loads, but can
306             only be performed on a local instance. if this argument is not supplied,
307             conventional load will be used.
308              
309             =item I - one of the above Load Modes: $APPEND, $INSERT, $REPLACE,
310             $TRUNCATE (default $APPEND)
311              
312             =back
313              
314             =item other optional arguments - you can ususally accept the defaults and
315             pretend that these don't exist.
316              
317             =over 2
318              
319             =item I - byte size of conventional load buffer; also sets
320             I (default 256000)
321              
322             =item I - number of rows to buffer for direct loads
323             (default 5000)
324              
325             =item I - the maximum number of errors to accept before failing
326             the load (default 50)
327              
328             =item I - the maximum number of records to load (default ALL)
329              
330             =item I - the number of rows to load before committing (default 64)
331              
332             =item I - the number of records to allow to be skipped (default 0)
333              
334             =item I - for direct loads, don't rebuild indexes (default false)
335              
336             =item I - skips rebuilding unusable indexes (default false)
337              
338             =item I - (direct) the byte size of the load stream (default 256000)
339              
340             =item I -
341              
342             =item I -
343              
344             =item I -
345              
346             =item I - do your offsets start at position 0 or 1
347              
348             =item I - are there some sort of enclosing characters, double-quotes perhaps?
349              
350             =item I - want to leave the load files on disk, maybe for
351             testing/auditing?
352              
353             =back
354              
355             =back
356              
357             =cut
358              
359             ################################################################################
360             sub new {
361 7     7 1 8576 my ($class, %args) = @_;
362              
363 7 50       48 croak __PACKAGE__."::new: missing mandatory argument 'infile'"
364             unless exists $args{'infile'};
365              
366 7         21 my $self = {};
367 7         25 bless ($self, $class);
368              
369 7 50       576 if ($^O =~ /win32/i) {
370 0         0 $self->{'_OSTYPE'} = 'WIN';
371             }
372              
373 7         58 $self->_initDefaults(%args);
374              
375 0         0 return $self;
376             } # sub new
377              
378              
379              
380              
381              
382             ###############################################################################
383              
384             =head3 B
385              
386             identify a table to be loaded. multiple adds with the same table name clobbers
387             any old definition.
388              
389             =over 2
390              
391             =item mandatory arguments
392              
393             =over 2
394              
395             =item I - the name of the table to load
396              
397             =back
398              
399             =item optional arguments
400              
401             =over 2
402              
403             =item I
404              
405             =item I
406              
407             =item I
408              
409             =item I
410              
411             =item I
412              
413             =back
414              
415             =back
416              
417             =cut
418              
419             ###############################################################################
420             sub addTable {
421 0     0 1 0 my $self = shift;
422              
423 0 0       0 croak __PACKAGE__."::addTable(): need name/value pairs" unless $#_ % 2;
424              
425 0         0 my %args = @_;
426 0 0       0 croak __PACKAGE__."::addTable: missing table name"
427             unless $args{'table_name'};
428 0         0 $self->{'_cfg_tables'}{$args{'table_name'}} = \%args;
429 0         0 $self->{'_last_table'} = $args{'table_name'};
430             } # sub addTable
431              
432              
433              
434              
435              
436             ###############################################################################
437              
438             =head3 B
439              
440             add a column to be loaded
441              
442             =over 2
443              
444             =item mandatory arguments for all load types
445              
446             =over 2
447              
448             =item I - the name of the column to be loaded
449              
450             =back
451              
452             =item mandatory arguments for fixed length load types
453              
454             =over 2
455              
456             =item I - starting position of the data in the input file
457              
458             =item and one of:
459              
460             =over 2
461              
462             =item I - the ending position of the data in the input file
463              
464             =item I - the length of the field, measured from field_offset
465              
466             =back
467              
468             =back
469              
470             =item optional arguments for all load types
471              
472             =over 2
473              
474             =item I -
475             the name of the table that this column belongs to. if no table name is
476             specified, default is the last known table name. if no previous table name
477             exists, croak.
478              
479             =item I -
480             $CHAR, $INT, $DECIMAL, or $DATE; defaults to $CHAR
481              
482             =item I -
483             the TO_DATE format for a $DATE column; defaults to "DD-MON-YY"
484              
485             =item I -
486             on occassion, it's useful to specify the length of the field; for some reason,
487             this is required when loading large strings (e.g. CHAR(3000))
488              
489             =back
490              
491             =back
492              
493             =cut
494              
495             ###############################################################################
496             sub addColumn {
497 0     0 1 0 my $self = shift;
498 0         0 my %args = @_;
499 0   0     0 my $table = $args{'table_name'} || $self->{'_last_table'};
500 0 0       0 croak __PACKAGE__."::addColumn: missing table name"
501             unless $table;
502 0 0       0 croak __PACKAGE__."::addColumn: missing column name"
503             unless $args{'column_name'};
504              
505             # if this isn't a delimited file, then we'll need offsets and lengths for
506             # each column to parse
507 0 0       0 if (not $self->{'_cfg_global'}{'terminated_by'}) {
508 0 0 0     0 croak __PACKAGE__."::addColumn: fixed length file fields require offset ".
      0        
509             "and length or end" unless (exists $args{'field_offset'} &&
510             (exists $args{'field_length'} ||
511             exists $args{'field_end'})
512             );
513             # sqlldr offsets start 1
514 0 0       0 if ($self->{_cfg_global}{'offset_from'} == 0) {
515 0         0 $args{'field_offset'} += 1;
516 0 0       0 $args{'field_end'} += 1 if exists $args{'field_end'};
517             }
518              
519              
520 0 0       0 if (exists $args{'field_length'}) {
521 0         0 $args{'field_end'} = $args{'field_offset'} + $args{'field_length'};
522             }
523              
524              
525 0         0 $args{'position_spec'} = "POSITION(".
526             "$args{'field_offset'}-$args{'field_end'}) ";
527              
528              
529             # may as well clean up
530 0         0 delete $args{'field_length'};
531 0         0 delete $args{'field_offset'};
532 0         0 delete $args{'field_end'};
533             }
534              
535              
536             # control files default to character;
537             # so the external numeric types mean that there are strings, but that
538             # they should be treated as numbers, including defaulting to 0, not null
539 0   0     0 $args{'column_type'} = $args{'column_type'} || $CHAR;
540              
541 0 0       0 $args{'column_length'} = $args{'column_length'} ?
542             "($args{'column_length'})" : '';
543 0         0 $args{'column_type'} .= $args{'column_length'};
544              
545             # and should we just warn and use the default format? probably not; i'd hade
546             # to load a bunch of bad date w/out knowing about it.
547 0 0       0 if ($args{'column_type'} eq $DATE) {
548 0   0     0 $args{'date_format'} = $args{'date_format'} || "DD-MON-YY";
549 0         0 $args{'column_type'} =
550             "\"TO_DATE(:$args{'column_name'},'$args{'date_format'}')\"";
551             }
552              
553 0         0 push @{$self->{'_cfg_tables'}{$table}{'columns'}}, \%args;
  0         0  
554              
555             } # sub addColumn
556              
557              
558              
559              
560             ################################################################################
561              
562             =head3 B
563              
564             generate a control file and execute an sqlldr job. this is a blocking call. if
565             you don't care about load statistics, you can always fork it off. returns 1 if
566             sqlldr ran successfully, 0 if there were errors or warnings
567              
568             =cut
569              
570             ################################################################################
571             sub executeLoader {
572 0     0 1 0 my $self = shift;
573              
574 0         0 $self->generateControlfile();
575             # if ($self->{'_OSTYPE'} ne 'WIN') {
576 0         0 my $exe = $ENV{'ORACLE_HOME'}."/bin/$SQLLDRBIN";
577 0         0 my $cmd = "$exe control=$self->{'_control_file'} ".
578             "userid=$self->{'_cfg_global'}{'userid'} ".
579             "log=$self->{'_cfg_global'}{'logfile'} 2>&1";
580              
581 0         0 my $output = `$cmd`;
582 0         0 my $exitval = $? / 256;
583              
584 0         0 $self->checkLogfile();
585              
586             #-- if ($exitval == $ERRORCODES{'SUCCESS'} ||
587             #-- $exitval == $ERRORCODES{'WARN'}) {
588             #-- $self->checkLogfile();
589             #--
590 0 0       0 if ($self->{'_cleanup'}) {
591 0   0     0 my $ctlFile = $self->{'_cfg_global'}{'control_file'} ||
592             $self->{'_cfg_global'}{'infile'} . ".ctl";
593 0         0 unlink $ctlFile;
594 0         0 unlink $self->{'_cfg_global'}{'badfile'};
595 0         0 unlink $self->{'_cfg_global'}{'discardfile'};
596 0         0 unlink $self->{'_cfg_global'}{'logfile'};
597             }
598              
599 0         0 return !$exitval;
600             } # sub executeLoader
601              
602              
603              
604              
605              
606             ################################################################################
607              
608             =head3 B
609              
610             parse an sqlldr logfile and be store results in object status
611              
612             =over 2
613              
614             =item optional arguments
615              
616             =over 2
617              
618             =item $logfile - the file to parse; defaults to the object's current logfile
619              
620             =back
621              
622             =back
623              
624             =cut
625              
626             ################################################################################
627             sub checkLogfile {
628 0     0 1 0 my $self = shift;
629 0   0     0 my $logfile = shift || $self->{'_cfg_global'}{'logfile'};
630              
631 0         0 my $log = new IO::File "< $logfile";
632 0 0       0 if (! defined $log) {
633 0 0       0 carp "checkLogfile(): failed to open file $logfile : $!\n" if $DEBUG;
634 0         0 $self->{'_stats'}{'skipped'} = undef;
635 0         0 $self->{'_stats'}{'read'} = undef;
636 0         0 $self->{'_stats'}{'rejected'} = undef;
637 0         0 $self->{'_stats'}{'discarded'} = undef;
638 0         0 $self->{'_stats'}{'loaded'} = undef;
639 0         0 return undef;
640             }
641              
642             # skip the first line, check the second for the SQL*Loader declaration
643 0         0 my $line = <$log>;
644 0         0 $line = <$log>;
645              
646 0 0       0 unless ($line =~ /^SQL\*Loader/) {
647 0         0 carp __PACKAGE__."::checkLoadLogfile: $logfile does not appear to be a ".
648             "valid sqlldr log file. returning";
649 0         0 return undef;
650             }
651              
652 0         0 while (<$log>) {
653 0         0 chomp;
654 0 0       0 if (/Total logical records skipped:\s+(\d+)/) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
655 0         0 $self->{'_stats'}{'skipped'} = $1;
656             }
657              
658             # presume that additional lines have error messages
659             elsif (/^SQL\*Loader/) {
660 0         0 push (@{$self->{'_stats'}->{'errors'}},$_);
  0         0  
661             }
662             elsif (/Total logical records read:\s+(\d+)/) {
663 0         0 $self->{'_stats'}{'read'} = $1;
664             }
665             elsif (/Total logical records rejected:\s+(\d+)/) {
666 0         0 $self->{'_stats'}{'rejected'} = $1;
667             }
668             elsif (/Total logical records discarded:\s+(\d+)/) {
669 0         0 $self->{'_stats'}{'discarded'} = $1;
670             }
671             elsif (/(\d+) Rows? successfully loaded\./) {
672 0         0 $self->{'_stats'}{'loaded'} = $1;
673             }
674             elsif (/Record\s\d+:\s+Rejected\s+\-\s+/) {
675             # grab the next line and add it to the last known rejection
676 0         0 my $errMsg = <$log>;
677 0         0 chomp $errMsg;
678 0         0 $errMsg =~ s/\s+$//g;
679 0         0 $self->{'_stats'}{'last_reject_message'} = $errMsg;
680             }
681             elsif(/Run began on (\w+)\s(\w+)\s(\d\d)\s(\d\d):(\d\d):(\d\d)\s+(\d{4})/) {
682 0         0 my ($dow,$mon,$dom,$hr,$min,$sec,$yr) = ($1,$2,$3,$4,$5,$6,$7);
683 0         0 $yr -= 1900;
684 0         0 $mon = $MONTHS{$mon};
685 0         0 $self->{'_stats'}{'run_begin'} = timelocal($sec,$min,$hr,$dom,$mon,$yr);
686             }
687             elsif(/Run ended on (\w+)\s(\w+)\s(\d\d)\s(\d\d):(\d\d):(\d\d)\s+(\d{4})/) {
688 0         0 my ($dow,$mon,$dom,$hr,$min,$sec,$yr) = ($1,$2,$3,$4,$5,$6,$7);
689 0         0 $yr -= 1900;
690 0         0 $mon = $MONTHS{$mon};
691 0         0 $self->{'_stats'}{'run_end'} = timelocal($sec,$min,$hr,$dom,$mon,$yr);
692             }
693             elsif(/Elapsed time was:\s+(\d+):(\d{2}):(\d{2})\.\d{2}/) {
694             # i'm assuming that this is hh::mm::ss.ms
695 0         0 $self->{'_stats'}{'elapsed_seconds'} = (3600 * $1) + (60 * $2) + $3;
696             }
697             elsif(/CPU time was:\s+(\d+):(\d{2}):(\d{2})\.\d{2}/) {
698 0         0 $self->{'_stats'}{'cpu_seconds'} = (3600 * $1) + (60 * $2) + $3;
699             }
700             # what to do w/ trashed indexes, force rebuild?
701             elsif (/index\s(\w+\.\w+)\swas made unusable/) {
702             }
703             }
704              
705 0   0     0 $self->{'_stats'}{'skipped'} ||= 0;
706 0   0     0 $self->{'_stats'}{'read'} ||= 0;
707 0   0     0 $self->{'_stats'}{'rejected'} ||= 0;
708 0   0     0 $self->{'_stats'}{'discarded'} ||= 0;
709 0   0     0 $self->{'_stats'}{'loaded'} ||= 0;
710 0   0     0 $self->{'_stats'}{'run_begin'} ||= 0;
711 0   0     0 $self->{'_stats'}{'run_end'} ||= time;
712 0   0     0 $self->{'_stats'}{'elapsed_seconds'} ||= 0;
713 0   0     0 $self->{'_stats'}{'cpu_seconds'} ||= 0;
714              
715 0         0 $log->close;
716             } # sub checkLoadLogfile
717              
718              
719              
720             ###############################################################################
721              
722             =head2 STATUS METHODS
723              
724             =cut
725              
726             ###############################################################################
727              
728              
729              
730              
731             ###############################################################################
732              
733             =head3 B
734              
735             returns the number of records skipped , or undef if no stats are known
736              
737             =cut
738              
739             ###############################################################################
740             sub getNumberSkipped {
741 0     0 1 0 $_[0]->{'_stats'}{'skipped'};
742             }
743              
744              
745              
746             ###############################################################################
747              
748             =head3 B
749              
750             returns the number of read from all input files, or undef if no stats are known
751              
752             =cut
753              
754             ###############################################################################
755             sub getNumberRead {
756 0     0 1 0 $_[0]->{'_stats'}{'read'};
757             }
758              
759              
760              
761             ###############################################################################
762              
763             =head3 B
764              
765             returns the number of records rejected, or undef if no stats are known
766              
767             =cut
768              
769             ###############################################################################
770             sub getNumberRejected {
771 0     0 1 0 $_[0]->{'_stats'}{'rejected'};
772             }
773              
774              
775              
776             ###############################################################################
777              
778             =head3 B
779              
780             returns the number of records discarded, or undef if no stats are known
781              
782             =cut
783              
784             ###############################################################################
785             sub getNumberDiscarded {
786 0     0 1 0 $_[0]->{'_stats'}{'discarded'};
787             }
788              
789              
790              
791             ###############################################################################
792              
793             =head3 B
794              
795             returns the number of records successfully loaded, or undef if no stats are
796             known
797              
798             =cut
799              
800             ###############################################################################
801             sub getNumberLoaded {
802 0     0 1 0 $_[0]->{'_stats'}{'loaded'};
803             }
804              
805              
806              
807             ###############################################################################
808              
809             =head3 B
810              
811             returns the last known rejection message, if any
812              
813             =cut
814              
815             ###############################################################################
816             sub getLastRejectMessage {
817 0     0 1 0 $_[0]->{'_stats'}{'last_reject_message'};
818             }
819              
820              
821             ###############################################################################
822              
823             =head3 B
824              
825             the time that the job began represented as epoch timestamp
826              
827             =cut
828              
829             ###############################################################################
830             sub getLoadBegin {
831 0     0 1 0 $_[0]->{'_stats'}{'run_begin'};
832             }
833              
834             ###############################################################################
835              
836             =head3 B
837              
838             the time that the job finished represented as epoch timestamp
839              
840             =cut
841              
842             ###############################################################################
843             sub getLoadEnd {
844 0     0 1 0 $_[0]->{'_stats'}{'run_end'};
845             }
846              
847              
848             ###############################################################################
849              
850             =head3 B
851              
852             returns the number if seconds elapsed during load
853              
854             =cut
855              
856             ###############################################################################
857             sub getElapsedSeconds {
858 0     0 1 0 $_[0]->{'_stats'}{'elapsed_seconds'};
859             }
860              
861              
862              
863             ###############################################################################
864              
865             =head3 B
866              
867             returns the number if seconds on cpu during load
868              
869             =cut
870              
871             ###############################################################################
872             sub getCpuSeconds {
873 0     0 1 0 $_[0]->{'_stats'}{'cpu_seconds'};
874             }
875              
876              
877             ###############################################################################
878              
879             =head3 B
880              
881             returns a listref of any SQL*Loader-specific error codes and messages that were
882             reported in the load logs, e.g the instance is down (SQL*Loader-128),
883             the table does not exist (SQL*Loader-941), or the username/password was wrong
884             (SQL*Loader-101). see the 'SQL*Loader Messages' section of your Oracle docs
885              
886             =cut
887              
888             ###############################################################################
889             sub getErrors {
890 0     0 1 0 my $self = shift;
891 0         0 return $self->{'_stats'}{'errors'};
892             } # getErrors
893              
894              
895              
896             ###############################################################################
897              
898             =head3 B
899              
900             returns the last known SQL*Loader error code and message, or an empty string
901              
902             =cut
903              
904             ###############################################################################
905             sub getLastError {
906 0     0 1 0 my $self = shift;
907 0 0       0 if (scalar @{$self->{'_stats'}{'errors'}}) {
  0         0  
908 0         0 return $self->{'_stats'}{'errors'}->[$#{$self->{'_stats'}{'errors'}}];
  0         0  
909             }
910 0         0 return '';
911             } # getErrors
912              
913              
914              
915              
916              
917              
918              
919              
920              
921             ###############################################################################
922              
923             =head2 B
924              
925             =cut
926              
927             ###############################################################################
928              
929              
930              
931              
932             ###############################################################################
933              
934             =head3 B
935              
936             based on the current configuration options, generate a control file
937              
938             =cut
939              
940             ###############################################################################
941             sub generateControlfile {
942 0     0 1 0 my $self = shift;
943              
944 0   0     0 my $ctlFile = $self->{'_cfg_global'}{'control_file'} ||
945             $self->{'_cfg_global'}{'infile'} . ".ctl";
946              
947 0         0 my $fh = new IO::File;
948 0 0 0     0 carp __PACKAGE__."::generateControlfile: file $ctlFile already exists\n"
949             if -e $ctlFile && $DEBUG;
950              
951 0 0       0 if (! $fh->open("> $ctlFile")) {
952 0         0 croak __PACKAGE__."::generateControlfile: failed to opern file $ctlFile: $!\n";
953             }
954              
955             # the SQL*Loader reference says that control files are basically three
956             # sections:
957             # * Session-wide information
958             # - Global options such as bindsize, rows, records to skip, and so on
959             # - INFILE clauses to specify where the input data is located
960             # - Data to be loaded
961              
962             # * Table and field-list information
963             # * Input data (optional section)
964              
965              
966              
967 0         0 $self->{'_control_text'} =
968             $self->_generateSessionClause().
969             $self->_generateTablesClause().
970             $self->_generateDataClause();
971              
972 0         0 print $fh $self->{'_control_text'};
973 0         0 $fh->close;
974              
975 0         0 $self->{'_control_file'} = $ctlFile;
976              
977 0         0 return 1;
978             } # sub generateControlFile
979              
980              
981              
982              
983              
984             ################################################################################
985              
986             =head2 UTILITY METHODS
987              
988             =cut
989              
990             ################################################################################
991              
992              
993             ################################################################################
994              
995             =head3 B
996              
997             searches ORACLE_HOME and PATH environment variables for an executable program.
998             returns the full path and file name of the first match, or undef if not found.
999             can be invoked as a class or instance method.
1000              
1001             Oracle::SQLLoader->findProgram('sqlldr')
1002             or
1003             $ldr->findProgram('sqlldr.exe')
1004              
1005             =over 2
1006              
1007             =item mandatory arguments
1008              
1009             =over 2
1010              
1011             =item $executable - the name of the program to search for
1012              
1013             =back
1014              
1015             =back
1016              
1017             =cut
1018              
1019             ################################################################################
1020             sub findProgram {
1021 1     1 1 30 my $argclass = shift;
1022 1         3 my $exe = shift;
1023 1   33     9 my $class = ref($argclass) || $argclass;
1024              
1025 1 50       4 if (exists $ENV{'ORACLE_HOME'}) {
1026 0 0       0 return "$ENV{'ORACLE_HOME'}/bin/$exe"
1027             if -x "$ENV{'ORACLE_HOME'}/bin/$exe";
1028             }
1029              
1030 1         37 foreach (split($Config{'path_sep'}, $ENV{'PATH'})){
1031 7 50       140 return "$_/$exe" if -x "$_/$exe";
1032             }
1033 1         9 return undef;
1034             } # sub findProgram
1035              
1036              
1037              
1038             ################################################################################
1039              
1040             =head3 B
1041              
1042             ensure that ORACLE_HOME is set and that the sqlldr binary is present and
1043             executable. can be invoked as a class or instance method.
1044              
1045             Oracle::SQLLoader->findProgram('sqlldr')
1046             or
1047             $ldr->findProgram('sqlldr.exe')
1048              
1049             =cut
1050              
1051             ################################################################################
1052             sub checkEnvironment {
1053 0     0 1 0 my $argclass = shift;
1054 0   0     0 my $class = ref($argclass) || $argclass;
1055              
1056 0 0       0 carp __PACKAGE__."::checkEnvironment: no ORACLE_HOME environment variable set"
1057             unless $ENV{'ORACLE_HOME'};
1058 0 0       0 carp __PACKAGE__."::checkEnvironment: no ORACLE_SID environment variable set"
1059             unless $ENV{'ORACLE_SID'};
1060 0 0       0 carp __PACKAGE__."::checkEnvironment: sqlldr doesn't exist or isn't executable"
1061             unless ($class->findProgram($SQLLDRBIN));
1062             } # sub checkEnvironment
1063              
1064              
1065              
1066              
1067             ################################################################################
1068              
1069             =head2 PRIVATE METHODS
1070              
1071             =cut
1072              
1073             ################################################################################
1074              
1075              
1076             ################################################################################
1077             # setup sane defaults
1078             ################################################################################
1079             sub _initDefaults {
1080 7     7   19 my $self = shift;
1081 7         31 my %args = @_;
1082              
1083              
1084             # _cfg_global
1085 7 50       62 if ($args{'infile'} eq '*') {
1086             # so we're loading inline data; that means that we don't have a sane
1087             # default for any of the other file options.
1088 0 0       0 $args{'badfile'} ? $self->{'_cfg_global'} = $args{'badfile'} :
1089             croak __PACKAGE__,"::_initDefaults: can't guess badfile with inline data";
1090              
1091 0 0       0 $args{'discardfile'} ? $self->{'_cfg_global'} = $args{'discardfile'} :
1092             croak __PACKAGE__,"::_initDefaults: can't guess discardfile with inline data";
1093              
1094 0 0       0 $args{'logfile'} ? $self->{'_cfg_global'} = $args{'logfile'} :
1095             croak __PACKAGE__,"::_initDefaults: can't guess logfile with inline data";
1096              
1097             }
1098             else {
1099 7   33     166 $self->{'_cfg_global'}{'badfile'} = $args{'badfile'} ||
1100             $args{'infile'} . '.bad';
1101 7   33     71 $self->{'_cfg_global'}{'discardfile'} = $args{'discardfile'} ||
1102             $args{'infile'} . '.discard';
1103 7   33     66 $self->{'_cfg_global'}{'logfile'} = $args{'logfile'} ||
1104             $args{'infile'} . '.log';
1105             }
1106              
1107 7         30 $self->{'_cfg_global'}{'infile'} = $args{'infile'};
1108              
1109              
1110             # only accept legal keys as config values
1111 7         239 foreach my $key (keys %OPTS) {
1112 84 100       188 $self->{'_cfg_global'}{$key} = $args{$key} if exists $args{$key};
1113             }
1114              
1115 7         43 foreach my $key (keys %BOOL_OPTS) {
1116 35 100       94 if (exists $args{$key}) {
1117 1 50       10 if ($args{$key} =~ /0|false/i) {
    50          
1118 0         0 $self->{'_cfg_global'}{$key} = 'false';
1119             }
1120             elsif ($args{$key} =~ /1|true/i) {
1121 1         4 $self->{'_cfg_global'}{$key} = 'true';
1122             }
1123             else {
1124 0         0 carp __PACKAGE__,"::_initDefaults: invalid value \"$args{$key}\"".
1125             " for option \"$key\"";
1126             }
1127             }
1128             }
1129              
1130              
1131              
1132             # fix $recordLength, var $bytes
1133 7   50     60 $self->{'_cfg_global'}{'recfmt'} = $args{'recfmt'} || '';
1134              
1135             # end of stream terminator. don't bother with defaulting to \n
1136 7   50     68 $self->{'_cfg_global'}{'eol'} = $args{'eol'} || '';
1137              
1138             # delimiter?
1139 7         21 $self->{'_cfg_global'}{'terminated_by'} = $args{'terminated_by'};
1140              
1141             # if not, it's fixed length; do offsets start at position 0 or 1?
1142 7 50       39 $self->{_cfg_global}{'offset_from'} = exists $args{'offset_from'} ?
1143             $args{'offset_from'} : 0;
1144              
1145             # are there some sort of enclosing characters, double-quotes perhaps?
1146 7         22 $self->{'_cfg_global'}{'enclosed_by'} = $args{'enclosed_by'};
1147              
1148              
1149             # handle 0 or 'false', 1 or 'true'
1150 7 100       33 if (exists $args{'direct'}) {
1151 1 50 33     7 if (!$args{'direct'} || $args{'direct'} =~ /false/i) {
1152 0         0 $self->{'_cfg_global'}{'direct'} = 'false';
1153             }
1154             else {
1155 1         3 $self->{'_cfg_global'}{'direct'} = 'true';
1156             }
1157             }
1158             else {
1159 6         22 $self->{'_cfg_global'}{'direct'} = $OPT_DEFAULTS{'direct'};
1160             }
1161              
1162             # default to 'all'
1163 7 50       41 $self->{'_cfg_global'}{'nullcols'} = $args{'nullcols'} ? 'trailing nullcols' : '';
1164              
1165             # default to shutup
1166 7 50       33 $self->{'_cfg_global'}{'silent'} = $args{'silent'} ? $args{'silent'} : 'header,feedback';
1167             # 'silent=header,feedback,errors,discards,partitions';
1168              
1169              
1170             # figure out if we've got username and password arguments. if not, check
1171             # ORACLE_USERID for it and see if it's a 'scott/tiger@sid' format
1172 7 50       51 if ($args{'username'}) {
    50          
1173 0 0       0 if (exists $args{'password'}) {
1174              
1175 0         0 my $sid;
1176 0 0       0 if (exists $args{'sid'}) {
    0          
1177 0         0 $sid = $args{'sid'};
1178             }
1179             elsif(exists $ENV{'ORACLE_SID'}) {
1180 0         0 $sid = $ENV{'ORACLE_SID'};
1181             }
1182             else {
1183 0         0 croak __PACKAGE__,"::_initDefaults(): must include sid argument if no ".
1184             "ORACLE_SID environment variable is set";
1185             }
1186 0         0 $self->{'_cfg_global'}{'userid'} =
1187             $args{'username'} . "/" .
1188             $args{'password'} . "\@$sid";
1189             }
1190             else {
1191 0         0 croak __PACKAGE__,"::_initDefaults(): must include password with ".
1192             "username option";
1193             }
1194             }
1195             # missing auth info. let's see if ORACLE_USERID holds anything useful
1196             elsif ($ENV{'ORACLE_USERID'}) {
1197 0 0       0 if (($self->{'_cfg_global'}{'username'},
1198             $self->{'_cfg_global'}{'password'},
1199             $self->{'_cfg_global'}{'sid'}) =
1200             ($ENV{'ORACLE_USERID'} =~ (/(\w+)\/(\w+)[\@(\w+)]?/))) {
1201             # great, got a match
1202             }
1203             else {
1204 0         0 croak __PACKAGE__,"::_initDefaults: no username argument supplied and ".
1205             "ORACLE_USERID environment variable does not contain valid account info";
1206             }
1207             }
1208             else {
1209 7         4410 croak __PACKAGE__,"::_initDefaults: no username argument supplied and ".
1210             "ORACLE_USERID environment variable does not contain valid account info";
1211             }
1212              
1213             # default the load mode to append
1214 0   0       $self->{'_cfg_global'}{'loadmode'} = $args{'loadmode'} || $APPEND;
1215              
1216             # cache the last table
1217 0           undef $self->{'_last_table'};
1218              
1219             # do we want to cleanup after ourselves, or leave the files around for
1220             # testing or auditing?
1221 0 0         $self->{'_cleanup'} = exists $args{'cleanup'} ? $args{'cleanup'} : 1;
1222              
1223              
1224              
1225             # finally, initialize any stats we're interested in
1226 0           $self->{'_stats'}{'skipped'} = undef;
1227 0           $self->{'_stats'}{'read'} = undef;
1228 0           $self->{'_stats'}{'rejected'} = undef;
1229 0           $self->{'_stats'}{'discarded'} = undef;
1230 0           $self->{'_stats'}{'loaded'} = undef;
1231 0           $self->{'_stats'}{'errors'} = [];
1232              
1233             } # sub _initDefaults
1234              
1235              
1236              
1237             ###############################################################################
1238              
1239             =head3 B<_generateSessionClause()>
1240              
1241             generate the session-wide information for a control file
1242              
1243             =cut
1244              
1245             ###############################################################################
1246             sub _generateSessionClause {
1247 0     0     my $self = shift;
1248 0           my $cfg = $self->{'_cfg_global'};
1249 0   0       $cfg->{'fixed'} ||= '';
1250              
1251              
1252             # TBD
1253             #-- RESUMABLE = {TRUE | FALSE}
1254             #-- RESUMABLE_NAME = 'text string'
1255             #-- RESUMABLE_TIMEOUT = n
1256             #-- SKIP_INDEX_MAINTENANCE = {TRUE | FALSE}
1257             #-- SKIP_UNUSABLE_INDEXES = {TRUE | FALSE}
1258              
1259              
1260 0           my $text = <
1261             OPTIONS (
1262             SILENT=(\U$cfg->{'silent'}\E)
1263              
1264             )
1265             LOAD DATA
1266             INFILE '$cfg->{'infile'}' $cfg->{'fixed'}
1267             BADFILE '$cfg->{'badfile'}'
1268             DISCARDFILE '$cfg->{'discardfile'}'
1269             $cfg->{'loadmode'}
1270             EndText
1271              
1272 0           chomp $text; # remove extra \n
1273              
1274 0           return $text;
1275             } # sub _generateSessionClause
1276              
1277              
1278             ###############################################################################
1279              
1280             =head3 B<_generateTablesClause()>
1281              
1282             generate table and column information for a control file
1283              
1284             =cut
1285              
1286             ###############################################################################
1287             sub _generateTablesClause {
1288 0     0     my $self = shift;
1289 0           my $tableClause;
1290 0 0         if (not $self->{'_cfg_tables'}) {
1291 0           croak __PACKAGE__."::_generateTablesClause: no tables defined";
1292             }
1293              
1294 0           foreach my $table (keys %{$self->{'_cfg_tables'}}) {
  0            
1295              
1296 0           my $cfg = $self->{'_cfg_tables'}{$table};
1297 0   0       $cfg->{'when_clauses'} ||= '';
1298              
1299              
1300 0           $tableClause = "\nINTO TABLE $table $cfg->{'when_clauses'} ";
1301 0 0         if ($self->{'_cfg_global'}{'terminated_by'}) {
1302 0           $tableClause .= "\nfields terminated by '".
1303             $self->{'_cfg_global'}{'terminated_by'} ."'";
1304             }
1305              
1306 0 0         if ($self->{'_cfg_global'}{'enclosed_by'}) {
1307 0           $tableClause .= "\noptionally enclosed by '".
1308             $self->{'_cfg_global'}{'enclosed_by'}. "'";
1309             }
1310              
1311 0 0         if ($self->{'_cfg_global'}{'nullcols'}) {
1312 0           $tableClause .= "\ntrailing nullcols ";
1313             }
1314 0           $tableClause .= " (\n";
1315              
1316              
1317             # "$cfg->{'continue_clauses'} ".
1318              
1319 0           my @colDefs;
1320 0           foreach my $def (@{$self->{'_cfg_tables'}{$table}{'columns'}}) {
  0            
1321 0           my $colClause;
1322              
1323 0           $colClause .= $def->{'column_name'} . " ";
1324 0 0         $colClause .= $def->{'position_spec'} . " " if $def->{'position_spec'};
1325 0           $colClause .= $def->{'column_type'}. " ";
1326 0 0         $colClause .= $def->{'nullif_clause'}. " " if $def->{'nullif_clause'};
1327 0 0         $colClause .= $def->{'terminated_clause'}. " " if $def->{'terminated_clause'};
1328 0 0         $colClause .= $def->{'transform_clause'}. " " if $def->{'transform_clause'};
1329 0           $colClause =~ s/\s+$//g;
1330 0           push @colDefs, "\t$colClause";
1331             }
1332              
1333 0           $tableClause .= join(",\n", @colDefs);
1334 0           $tableClause .= "\n)";
1335             }
1336              
1337              
1338             # after the table clause, we can include optional delimiter or enclosure specs
1339              
1340 0           return $tableClause;
1341             } # sub _generateTablesClause
1342              
1343              
1344              
1345              
1346             ###############################################################################
1347              
1348             =head3 B<_generateDataClause()>
1349              
1350             generate any input data for a control file
1351              
1352             =cut
1353              
1354             ###############################################################################
1355             sub _generateDataClause {
1356 0     0     my $self = shift;
1357 0           return '';
1358             } # sub _generateDataClause
1359              
1360              
1361              
1362             ###############################################################################
1363              
1364             =head3 B
1365              
1366             the loader defaults are almost directly from the sqlldr usage dumps
1367              
1368             =cut
1369              
1370             ###############################################################################
1371             sub unused_initDefaults {
1372              
1373 0     0 0   %OPT_DEFAULTS = (
1374             bindsize => 256000,
1375             columnarrayrows => 5000,
1376             direct => 'false',
1377             # discardmax => 'all',
1378             errors => 50,
1379             # load => 'all',
1380             multithreading => 'false',
1381             parallel => 'false',
1382             parfile => '',
1383             readsize => 0,
1384             resumable => 'false',
1385             resumable_name => 'text string',
1386             resumable_timeout => 0,
1387             rows_direct => 'all',
1388             rows_conventional => 64,
1389             rows => 64,
1390             skip => 0,
1391             skip_index_maintenance => 'false',
1392             skip_unusable_indexes => 'false',
1393             streamsize => 256000,
1394             silent => '',
1395             file => '',
1396             );
1397              
1398              
1399 0           my %optDescrip = (
1400             bad => 'Bad file name',
1401             data => 'Data file name',
1402             discard => 'Discard file name',
1403             discardmax => 'Number of discards to allow',
1404             skip => 'Number of logical records to skip',
1405             load => 'Number of logical records to load',
1406             errors => 'Number of errors to allow',
1407             rows => 'Number of rows in conventional path bind array '.
1408             'or between direct path data saves',
1409             bindsize => 'Size of conventional path bind array in bytes',
1410             silent => 'Suppress messages during run (header,feedback,'.
1411             'errors,discards,partitions)',
1412             direct => 'use direct path',
1413             parfile => 'parameter file: name of file that contains '.
1414             'parameter specifications',
1415             parallel => 'do parallel load',
1416             file => 'File to allocate extents from',
1417             );
1418             } # sub initDescriptions
1419              
1420              
1421              
1422              
1423              
1424             1;