| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package DBIx::BulkUtil; | 
| 2 |  |  |  |  |  |  |  | 
| 3 | 1 |  |  | 1 |  | 14136 | use DBI; | 
|  | 1 |  |  |  |  | 11499 |  | 
|  | 1 |  |  |  |  | 40 |  | 
| 4 | 1 |  |  | 1 |  | 4 | use Carp qw(confess); | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 40 |  | 
| 5 |  |  |  |  |  |  |  | 
| 6 | 1 |  |  | 1 |  | 3 | use strict; | 
|  | 1 |  |  |  |  | 4 |  | 
|  | 1 |  |  |  |  | 13 |  | 
| 7 | 1 |  |  | 1 |  | 2 | use warnings; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 1037 |  | 
| 8 |  |  |  |  |  |  |  | 
| 9 |  |  |  |  |  |  | our $VERSION = '0.05'; | 
| 10 |  |  |  |  |  |  |  | 
| 11 |  |  |  |  |  |  | # Override this | 
| 12 |  |  |  |  |  |  | sub passwd { | 
| 13 | 0 |  |  | 0 | 1 |  | return ''; | 
| 14 |  |  |  |  |  |  | } | 
| 15 |  |  |  |  |  |  |  | 
| 16 |  |  |  |  |  |  | # Override this | 
| 17 |  |  |  |  |  |  | sub user { | 
| 18 | 0 |  |  | 0 | 0 |  | return ''; | 
| 19 |  |  |  |  |  |  | } | 
| 20 |  |  |  |  |  |  |  | 
| 21 |  |  |  |  |  |  | { | 
| 22 |  |  |  |  |  |  | my @connect_options = qw( | 
| 23 |  |  |  |  |  |  | Server | 
| 24 |  |  |  |  |  |  | Database | 
| 25 |  |  |  |  |  |  | Env | 
| 26 |  |  |  |  |  |  | Type | 
| 27 |  |  |  |  |  |  | User | 
| 28 |  |  |  |  |  |  | Password | 
| 29 |  |  |  |  |  |  | DataDir | 
| 30 |  |  |  |  |  |  | ConnectMethod | 
| 31 |  |  |  |  |  |  | RetryCount | 
| 32 |  |  |  |  |  |  | RetryMinutes | 
| 33 |  |  |  |  |  |  | BulkLogin | 
| 34 |  |  |  |  |  |  | NoBlankNull | 
| 35 |  |  |  |  |  |  | Silent | 
| 36 |  |  |  |  |  |  | NoCharset | 
| 37 |  |  |  |  |  |  | NoServer | 
| 38 |  |  |  |  |  |  | Dsl | 
| 39 |  |  |  |  |  |  | DslOptions | 
| 40 |  |  |  |  |  |  | DateFormat | 
| 41 |  |  |  |  |  |  | DatetimeFormat | 
| 42 |  |  |  |  |  |  | DatetimeTzFormat | 
| 43 |  |  |  |  |  |  | ); | 
| 44 |  |  |  |  |  |  | my %is_valid; | 
| 45 |  |  |  |  |  |  | $is_valid{$_}++ for @connect_options; | 
| 46 |  |  |  |  |  |  |  | 
| 47 |  |  |  |  |  |  | sub _options_valid { | 
| 48 | 0 |  |  | 0 |  |  | my $class = shift; | 
| 49 | 0 |  |  |  |  |  | my %opts = @_; | 
| 50 | 0 |  |  |  |  |  | for my $opt (keys %opts) { | 
| 51 | 0 | 0 |  |  |  |  | return $opt if !$is_valid{$opt}; | 
| 52 |  |  |  |  |  |  | } | 
| 53 | 0 |  |  |  |  |  | return; | 
| 54 |  |  |  |  |  |  | } | 
| 55 |  |  |  |  |  |  | } | 
| 56 |  |  |  |  |  |  |  | 
| 57 |  |  |  |  |  |  | # Override this to set server, db, env, type based on whatever | 
| 58 |  |  |  |  |  |  | sub env2db { | 
| 59 | 0 |  |  | 0 | 0 |  | my ($self,$args) = @_; | 
| 60 |  |  |  |  |  |  |  | 
| 61 | 0 | 0 | 0 |  |  |  | $args->{Type} ||= (!$args->{Server} && $args->{Database} ) ? 'Oracle' : 'Sybase'; | 
|  |  |  | 0 |  |  |  |  | 
| 62 | 0 | 0 |  |  |  |  | if ( $args->{Type} eq 'SybaseIQ' ) { | 
| 63 | 0 |  |  |  |  |  | $args->{IsIQ}++; | 
| 64 | 0 |  |  |  |  |  | $args->{Type} = 'Sybase' | 
| 65 |  |  |  |  |  |  | } | 
| 66 |  |  |  |  |  |  | } | 
| 67 |  |  |  |  |  |  |  | 
| 68 |  |  |  |  |  |  | sub connect { | 
| 69 | 0 |  |  | 0 | 1 |  | my $class = shift; | 
| 70 |  |  |  |  |  |  |  | 
| 71 |  |  |  |  |  |  | # Use HandleError sub instead of more straightforward RaiseError | 
| 72 |  |  |  |  |  |  | # attribute because Sybase 1.09 does not include line numbers in its | 
| 73 |  |  |  |  |  |  | # RaiseError die messages. And a stacktrace is usually more helpful | 
| 74 |  |  |  |  |  |  | # anyway. | 
| 75 | 0 |  |  |  |  |  | my $dbi_opts = { | 
| 76 |  |  |  |  |  |  | ChopBlanks  => 1, | 
| 77 |  |  |  |  |  |  | AutoCommit  => 1, | 
| 78 |  |  |  |  |  |  | PrintError  => 0, | 
| 79 |  |  |  |  |  |  | RaiseError  => 1, | 
| 80 |  |  |  |  |  |  | LongReadLen => 1_024 * 1_024, | 
| 81 |  |  |  |  |  |  | }; | 
| 82 |  |  |  |  |  |  |  | 
| 83 | 0 | 0 | 0 |  |  |  | if ( @_ and ref($_[-1]) ) { | 
| 84 | 0 |  |  |  |  |  | my $tmp_opts = pop @_; | 
| 85 | 0 |  |  |  |  |  | @$dbi_opts{keys %$tmp_opts} = values %$tmp_opts; | 
| 86 |  |  |  |  |  |  | } | 
| 87 | 0 |  |  |  |  |  | my $bad_opt = $class->_options_valid(@_); | 
| 88 | 0 | 0 |  |  |  |  | die "Invalid option $bad_opt to ${class}->connect" if $bad_opt; | 
| 89 |  |  |  |  |  |  |  | 
| 90 |  |  |  |  |  |  | # TODO: Log or Output option? | 
| 91 | 0 |  |  |  |  |  | my %args = @_; | 
| 92 | 0 |  |  |  |  |  | my $fh; | 
| 93 | 0 | 0 |  |  |  |  | open($fh, ">", "/dev/null") if $args{Silent}; | 
| 94 | 0 | 0 |  |  |  |  | my $stdout = $args{Silent} ? $fh : \*STDOUT; | 
| 95 | 0 |  |  |  |  |  | local *STDOUT = $stdout; | 
| 96 |  |  |  |  |  |  |  | 
| 97 | 0 |  | 0 |  |  |  | my $connect = $args{ConnectMethod} || 'connect'; | 
| 98 |  |  |  |  |  |  |  | 
| 99 | 0 |  |  |  |  |  | $class->env2db( \%args ); | 
| 100 |  |  |  |  |  |  |  | 
| 101 |  |  |  |  |  |  | my @dsl_args = $args{Dsl} | 
| 102 | 0 |  |  |  |  |  | ?  ref($args{Dsl}) ? @{$args{Dsl}} : $args{Dsl} | 
| 103 | 0 | 0 |  |  |  |  | : (); | 
|  |  | 0 |  |  |  |  |  | 
| 104 |  |  |  |  |  |  |  | 
| 105 | 0 |  |  |  |  |  | my $type     = $args{Type}; | 
| 106 | 0 |  |  |  |  |  | my $database = $args{Database}; | 
| 107 | 0 |  | 0 |  |  |  | my $server   = $args{Server} || ''; | 
| 108 |  |  |  |  |  |  |  | 
| 109 | 0 | 0 |  |  |  |  | if (!@dsl_args) { | 
| 110 | 0 | 0 |  |  |  |  | if ( $type eq 'Sybase' ) { | 
|  |  | 0 |  |  |  |  |  | 
| 111 |  |  |  |  |  |  | # Server-side charset is iso, need to specify it as client-side charset | 
| 112 |  |  |  |  |  |  | # or else we get utf8 to iso charset conversion error when database handle is cloned | 
| 113 |  |  |  |  |  |  | # (which happens automatically when you need multiple active statement handles). | 
| 114 | 0 | 0 |  |  |  |  | push @dsl_args, "server=$server" unless $args{NoServer}; | 
| 115 | 0 | 0 |  |  |  |  | push @dsl_args, 'charset=iso_1' unless $args{NoCharset}; | 
| 116 | 0 | 0 |  |  |  |  | push @dsl_args, 'bulkLogin=1' if $args{BulkLogin}; | 
| 117 |  |  |  |  |  |  | } elsif ( $type eq 'Oracle' ) { | 
| 118 | 0 | 0 |  |  |  |  | push @dsl_args, $database unless $args{NoServer}; | 
| 119 |  |  |  |  |  |  | } else { | 
| 120 | 0 |  |  |  |  |  | die "Unable to connect to database type $type"; | 
| 121 |  |  |  |  |  |  | } | 
| 122 |  |  |  |  |  |  | } | 
| 123 |  |  |  |  |  |  |  | 
| 124 |  |  |  |  |  |  | # For Xtra Dsl options | 
| 125 |  |  |  |  |  |  | push @dsl_args, ref($args{DslOptions}) | 
| 126 | 0 |  |  |  |  |  | ? @{$args{DslOptions}} : $args{DslOptions} | 
| 127 | 0 | 0 |  |  |  |  | if $args{DslOptions}; | 
|  |  | 0 |  |  |  |  |  | 
| 128 |  |  |  |  |  |  |  | 
| 129 | 0 |  |  |  |  |  | my $dsl = "dbi:$type:"; | 
| 130 | 0 |  |  |  |  |  | $dsl .= join( ";", @dsl_args ); | 
| 131 |  |  |  |  |  |  |  | 
| 132 | 0 |  | 0 |  |  |  | my $user     = $args{User}     || $class->user(\%args); | 
| 133 | 0 |  | 0 |  |  |  | my $passwd   = $args{Password} || $class->passwd(\%args); | 
| 134 |  |  |  |  |  |  |  | 
| 135 | 0 |  |  |  |  |  | my $dbh; | 
| 136 | 0 |  | 0 |  |  |  | my $retry = int($args{RetryCount} || 0); | 
| 137 |  |  |  |  |  |  |  | 
| 138 | 0 |  | 0 |  |  |  | my $retry_seconds = 60 * ($args{RetryMinutes} || 10); | 
| 139 | 0 | 0 |  |  |  |  | $retry_seconds    = 60 * 10 if $retry_seconds < 0; | 
| 140 |  |  |  |  |  |  |  | 
| 141 | 0 | 0 |  |  |  |  | my $conn_name = ($type eq 'Sybase') ? $server : $database; | 
| 142 | 0 |  |  |  |  |  | while (1) { | 
| 143 |  |  |  |  |  |  |  | 
| 144 | 0 |  |  |  |  |  | print "Connecting to $conn_name\n"; | 
| 145 | 0 |  |  |  |  |  | $dbh = eval { DBI->$connect($dsl, $user, $passwd, $dbi_opts) }; | 
|  | 0 |  |  |  |  |  |  | 
| 146 | 0 |  |  |  |  |  | my $err = $@; | 
| 147 | 0 | 0 |  |  |  |  | unless ($dbh) { | 
| 148 | 0 | 0 |  |  |  |  | die $err unless $retry-- > 0; | 
| 149 | 0 |  |  |  |  |  | print "Unable to connect to $conn_name. Will retry in $retry_seconds seconds"; | 
| 150 | 0 |  |  |  |  |  | sleep $retry_seconds; | 
| 151 | 0 |  |  |  |  |  | redo; | 
| 152 |  |  |  |  |  |  | } | 
| 153 |  |  |  |  |  |  |  | 
| 154 |  |  |  |  |  |  | # Make selected Sybase database the current database | 
| 155 |  |  |  |  |  |  | # And make date formats consistent | 
| 156 | 0 | 0 |  |  |  |  | if ( $type eq 'Sybase' ) { | 
|  |  | 0 |  |  |  |  |  | 
| 157 |  |  |  |  |  |  | # Switch database after connect so that we get a helpful error message | 
| 158 |  |  |  |  |  |  | # and an error instead of a warning | 
| 159 | 0 | 0 |  |  |  |  | if ($database) { | 
| 160 | 0 |  |  |  |  |  | print "Using $database database\n"; | 
| 161 | 0 |  |  |  |  |  | my $result = eval { $dbh->do("USE $database") }; | 
|  | 0 |  |  |  |  |  |  | 
| 162 | 0 |  |  |  |  |  | my $err = $@; | 
| 163 | 0 | 0 |  |  |  |  | unless ($result) { | 
| 164 | 0 | 0 |  |  |  |  | die $err unless $retry-- > 0; | 
| 165 | 0 |  |  |  |  |  | $dbh->disconnect(); | 
| 166 | 0 |  |  |  |  |  | print "Unable to use database $database on $server. Will retry in $retry_seconds seconds\n"; | 
| 167 | 0 |  |  |  |  |  | sleep $retry_seconds; | 
| 168 | 0 |  |  |  |  |  | redo; | 
| 169 |  |  |  |  |  |  | } | 
| 170 |  |  |  |  |  |  | }; | 
| 171 | 0 |  | 0 |  |  |  | $dbh->{syb_date_fmt} = $args{DateFormat} || 'ISO'; | 
| 172 |  |  |  |  |  |  |  | 
| 173 | 0 | 0 | 0 |  |  |  | $dbh->do("set temporary option Load_ZeroLength_AsNULL = 'ON'") if $args{IsIQ} and !$args{NoBlankNull}; | 
| 174 |  |  |  |  |  |  | } elsif ( $type eq 'Oracle' ) { | 
| 175 |  |  |  |  |  |  | # Fractions on Oracle "DATE" format not allowed | 
| 176 | 0 |  | 0 |  |  |  | my $date_fmt        = $args{DateFormat}       || 'YYYY-MM-DD HH24:MI:SS'; | 
| 177 | 0 |  | 0 |  |  |  | my $datetime_fmt    = $args{DatetimeFormat}   || 'YYYY-MM-DD HH24:MI:SS.FF'; | 
| 178 | 0 |  | 0 |  |  |  | my $datetime_tz_fmt = $args{DatetimeTzFormat} || $args{DatetimeFormat} || $datetime_fmt; | 
| 179 | 0 |  |  |  |  |  | $_ = $dbh->quote($_) for $date_fmt, $datetime_fmt, $datetime_tz_fmt; | 
| 180 | 0 |  |  |  |  |  | $dbh->do("alter session set nls_date_format=$date_fmt"); | 
| 181 | 0 |  |  |  |  |  | $dbh->do("alter session set nls_timestamp_format=$datetime_fmt"); | 
| 182 | 0 |  |  |  |  |  | $dbh->do("alter session set nls_timestamp_tz_format=$datetime_tz_fmt"); | 
| 183 |  |  |  |  |  |  | } | 
| 184 | 0 |  |  |  |  |  | last; | 
| 185 |  |  |  |  |  |  | } | 
| 186 |  |  |  |  |  |  |  | 
| 187 |  |  |  |  |  |  | # We do not want stack trace on connect so that we do not expose password | 
| 188 |  |  |  |  |  |  | # But everywhere else it is useful | 
| 189 | 0 |  |  |  |  |  | $dbh->{RaiseError} = 0; | 
| 190 | 0 |  |  | 0 |  |  | $dbh->{HandleError} = sub { confess $_[0] }; | 
|  | 0 |  |  |  |  |  |  | 
| 191 | 0 | 0 |  |  |  |  | return $dbh unless wantarray; | 
| 192 | 0 |  |  |  |  |  | my $util = DBIx::BulkUtil::Obj->new($dbh, $passwd, \%args); | 
| 193 | 0 |  |  |  |  |  | return $dbh, $util; | 
| 194 |  |  |  |  |  |  | } | 
| 195 |  |  |  |  |  |  |  | 
| 196 |  |  |  |  |  |  | # Just set the connect method and call connect() | 
| 197 |  |  |  |  |  |  | sub connect_cached { | 
| 198 | 0 |  |  | 0 | 1 |  | my $class = shift; | 
| 199 | 0 |  |  |  |  |  | my @args = $class->override({ConnectMethod => 'connect_cached'}, @_); | 
| 200 | 0 |  |  |  |  |  | $class->connect(@args); | 
| 201 |  |  |  |  |  |  | } | 
| 202 |  |  |  |  |  |  |  | 
| 203 |  |  |  |  |  |  | sub syb_connect { | 
| 204 | 0 |  |  | 0 | 1 |  | my $class = shift; | 
| 205 | 0 |  |  |  |  |  | my @args = $class->override({ Type => 'Sybase' }, @_); | 
| 206 | 0 |  |  |  |  |  | return $class->connect(@args); | 
| 207 |  |  |  |  |  |  | } | 
| 208 |  |  |  |  |  |  |  | 
| 209 |  |  |  |  |  |  | sub syb_connect_cached { | 
| 210 | 0 |  |  | 0 | 0 |  | my $class = shift; | 
| 211 | 0 |  |  |  |  |  | my @args = $class->override({ Type => 'Sybase' }, @_); | 
| 212 | 0 |  |  |  |  |  | return $class->connect_cached(@args); | 
| 213 |  |  |  |  |  |  | } | 
| 214 |  |  |  |  |  |  |  | 
| 215 |  |  |  |  |  |  | sub ora_connect { | 
| 216 | 0 |  |  | 0 | 1 |  | my $class = shift; | 
| 217 | 0 |  |  |  |  |  | my @args = $class->override({ Type => 'Oracle' }, @_); | 
| 218 | 0 |  |  |  |  |  | return $class->connect(@args); | 
| 219 |  |  |  |  |  |  | } | 
| 220 |  |  |  |  |  |  |  | 
| 221 |  |  |  |  |  |  | sub ora_connect_cached { | 
| 222 | 0 |  |  | 0 | 1 |  | my $class = shift; | 
| 223 | 0 |  |  |  |  |  | my @args = $class->override({ Type => 'Oracle' }, @_); | 
| 224 | 0 |  |  |  |  |  | return $class->connect_cached(@args); | 
| 225 |  |  |  |  |  |  | } | 
| 226 |  |  |  |  |  |  |  | 
| 227 |  |  |  |  |  |  | sub iq_connect { | 
| 228 | 0 |  |  | 0 | 1 |  | my $class = shift; | 
| 229 | 0 |  |  |  |  |  | my @args = $class->override({ Type => 'SybaseIQ' }, @_); | 
| 230 | 0 |  |  |  |  |  | return $class->connect(@args); | 
| 231 |  |  |  |  |  |  | } | 
| 232 |  |  |  |  |  |  |  | 
| 233 |  |  |  |  |  |  | sub iq_connect_cached { | 
| 234 | 0 |  |  | 0 | 1 |  | my $class = shift; | 
| 235 | 0 |  |  |  |  |  | my @args = $class->override({ Type => 'SybaseIQ' }, @_); | 
| 236 | 0 |  |  |  |  |  | return $class->connect_cached(@args); | 
| 237 |  |  |  |  |  |  | } | 
| 238 |  |  |  |  |  |  |  | 
| 239 |  |  |  |  |  |  |  | 
| 240 |  |  |  |  |  |  | # Overriden connect args need to be spliced in before any dbi options | 
| 241 |  |  |  |  |  |  | sub override { | 
| 242 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 243 | 0 |  |  |  |  |  | my ($ovr, @args) = @_; | 
| 244 | 0 | 0 |  |  |  |  | if ( (@args % 2) == 0 ) { | 
| 245 | 0 |  |  |  |  |  | return @args, %$ovr; | 
| 246 |  |  |  |  |  |  | } | 
| 247 | 0 |  |  |  |  |  | my $dbi_opts = pop @args; | 
| 248 | 0 | 0 | 0 |  |  |  | die "Last argument to connect must be hash reference" unless $dbi_opts and ref($dbi_opts); | 
| 249 | 0 |  |  |  |  |  | return @args, %$ovr, $dbi_opts; | 
| 250 |  |  |  |  |  |  | } | 
| 251 |  |  |  |  |  |  |  | 
| 252 |  |  |  |  |  |  | package DBIx::BulkUtil::Obj; | 
| 253 |  |  |  |  |  |  |  | 
| 254 |  |  |  |  |  |  | our $BCP_DELIMITER = '|'; | 
| 255 |  |  |  |  |  |  |  | 
| 256 | 1 |  |  | 1 |  | 554 | use Memoize qw(memoize); | 
|  | 1 |  |  |  |  | 1562 |  | 
|  | 1 |  |  |  |  | 45 |  | 
| 257 | 1 |  |  | 1 |  | 4 | use Carp qw(confess); | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 641 |  | 
| 258 |  |  |  |  |  |  |  | 
| 259 |  |  |  |  |  |  | sub new { | 
| 260 | 0 |  |  | 0 |  |  | my ( $class, $dbh, $passwd, $args ) = @_; | 
| 261 | 0 |  |  |  |  |  | my $type = $dbh->{Driver}{Name}; | 
| 262 | 0 | 0 |  |  |  |  | if ($type eq 'Sybase') { | 
| 263 | 0 |  |  |  |  |  | (my $version = $dbh->{syb_server_version_string}) =~ s|/.*||; | 
| 264 | 0 | 0 |  |  |  |  | $type = 'SybaseIQ' if $version =~ /IQ/; | 
| 265 |  |  |  |  |  |  | } | 
| 266 | 0 | 0 |  |  |  |  | $class =~ s/::Obj$// or die "Invalid class $class"; | 
| 267 |  |  |  |  |  |  |  | 
| 268 | 0 |  |  |  |  |  | $class .= "::" . $type; | 
| 269 | 0 |  |  |  |  |  | return $class->util($dbh, $passwd, $args); | 
| 270 |  |  |  |  |  |  | } | 
| 271 |  |  |  |  |  |  |  | 
| 272 |  |  |  |  |  |  | sub util { | 
| 273 | 0 |  |  | 0 |  |  | my $class = shift; | 
| 274 | 0 |  |  |  |  |  | my ($dbh, $pw, $args) = @_; | 
| 275 | 0 | 0 |  |  |  |  | confess "Must use subclass of this package" if __PACKAGE__ eq $class; | 
| 276 | 0 |  |  |  |  |  | my %util_args; | 
| 277 |  |  |  |  |  |  |  | 
| 278 | 0 | 0 | 0 |  |  |  | if ( $args and ref($args) ) { | 
| 279 | 0 | 0 |  |  |  |  | $util_args{NoBlankNull} = 1 if $args->{NoBlankNull}; | 
| 280 |  |  |  |  |  |  | } | 
| 281 |  |  |  |  |  |  |  | 
| 282 |  |  |  |  |  |  | # Prevent dbh from disconnecting after fork in child processes | 
| 283 | 0 |  |  |  |  |  | my $dbh_pid = $$; | 
| 284 | 0 | 0 |  | 0 |  |  | my $release = DBIx::BulkUtil::Release->new(sub { $dbh->{InactiveDestroy} = 1 if $dbh_pid != $$ }); | 
|  | 0 |  |  |  |  |  |  | 
| 285 | 0 |  |  |  |  |  | bless { DBH => $dbh, PASSWORD => $pw, DELIMITER => $BCP_DELIMITER, RELEASE => $release, %util_args }, $class; | 
| 286 |  |  |  |  |  |  | } | 
| 287 |  |  |  |  |  |  |  | 
| 288 | 0 |  |  | 0 |  |  | sub dbh { $_[0]->{DBH} } | 
| 289 |  |  |  |  |  |  |  | 
| 290 |  |  |  |  |  |  | sub get { | 
| 291 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 292 | 0 |  |  |  |  |  | my $select = shift; | 
| 293 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 294 | 0 |  |  |  |  |  | my @result = $dbh->selectrow_array( $self->row_select($select) ); | 
| 295 | 0 | 0 |  |  |  |  | return $result[0] if @result == 1; | 
| 296 | 0 |  |  |  |  |  | return @result; | 
| 297 |  |  |  |  |  |  | } | 
| 298 |  |  |  |  |  |  |  | 
| 299 |  |  |  |  |  |  | sub exec_sp { | 
| 300 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 301 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 302 | 0 |  |  |  |  |  | $dbh->do($self->sp_sql(@_)); | 
| 303 |  |  |  |  |  |  | } | 
| 304 |  |  |  |  |  |  |  | 
| 305 |  |  |  |  |  |  | sub bcp_out { | 
| 306 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 307 | 0 |  |  |  |  |  | my $opts = {}; | 
| 308 | 0 | 0 |  |  |  |  | if (ref $_[-1]) { | 
| 309 | 0 |  |  |  |  |  | $opts = pop @_; | 
| 310 |  |  |  |  |  |  | } | 
| 311 | 0 |  |  |  |  |  | my ( $table, $file ) = @_; | 
| 312 | 0 |  | 0 |  |  |  | $file ||= "$table.bcp"; | 
| 313 |  |  |  |  |  |  |  | 
| 314 | 0 |  | 0 |  |  |  | my $delimiter = $opts->{Delimiter}  || $self->{DELIMITER}; | 
| 315 | 0 |  | 0 |  |  |  | my $row_delim = $opts->{RowDelimiter} || $/; | 
| 316 | 0 | 0 |  |  |  |  | my @esc       = ( escape_char => $opts->{EscapeChar} ) if $opts->{EscapeChar}; | 
| 317 |  |  |  |  |  |  |  | 
| 318 |  |  |  |  |  |  | # Default to no quote char to be more compatible w/Sybase | 
| 319 | 0 | 0 |  |  |  |  | my @quote_char = $opts->{QuoteFields} ? () : ( quote_char => undef, escape_char => undef ); | 
| 320 |  |  |  |  |  |  |  | 
| 321 |  |  |  |  |  |  | # TODO: Give up on Text::CSV ?? | 
| 322 | 0 |  |  |  |  |  | my $csv; | 
| 323 | 0 | 0 |  |  |  |  | if ( length($delimiter) == 1 ) { | 
| 324 | 0 |  |  |  |  |  | require Text::CSV_XS; | 
| 325 | 0 |  |  |  |  |  | $csv = Text::CSV_XS->new({ | 
| 326 |  |  |  |  |  |  | binary => 1, | 
| 327 |  |  |  |  |  |  | eol => $row_delim, | 
| 328 |  |  |  |  |  |  | sep_char => $delimiter, | 
| 329 |  |  |  |  |  |  | @esc, | 
| 330 |  |  |  |  |  |  | @quote_char, | 
| 331 |  |  |  |  |  |  | }); | 
| 332 |  |  |  |  |  |  | } | 
| 333 |  |  |  |  |  |  |  | 
| 334 | 0 | 0 |  |  |  |  | my $col_list = $opts->{Columns} ? $opts->{Columns} : "*"; | 
| 335 |  |  |  |  |  |  |  | 
| 336 |  |  |  |  |  |  | # Only for HP? | 
| 337 |  |  |  |  |  |  | #local $ENV{NLS_LANG} = "AMERICAN_AMERICA.WE8ROMAN8"; | 
| 338 |  |  |  |  |  |  |  | 
| 339 | 0 |  | 0 |  |  |  | my $enc_opt = $opts->{Encoding} || ''; | 
| 340 | 0 |  |  |  |  |  | my $db_type = $self->type(); | 
| 341 | 0 | 0 |  |  |  |  | if ( $db_type eq 'Oracle' ) { | 
| 342 |  |  |  |  |  |  |  | 
| 343 | 0 | 0 |  |  |  |  | my $partition = ( $table =~ s/:(\w+)$// ) ? $1 : ''; | 
| 344 | 0 |  | 0 |  |  |  | my $nls_lang = $ENV{NLS_LANG} || ''; | 
| 345 | 0 | 0 |  |  |  |  | if ( $nls_lang =~ /utf8/i ) { | 
| 346 | 0 |  | 0 |  |  |  | $enc_opt ||= 'utf8'; | 
| 347 |  |  |  |  |  |  | } | 
| 348 | 0 | 0 |  |  |  |  | if ( $col_list eq '*' ) { | 
| 349 | 0 |  |  |  |  |  | my @col_list; | 
| 350 | 0 |  |  |  |  |  | my $col_info = $self->column_info($table); | 
| 351 | 0 |  |  |  |  |  | my $list = $col_info->{LIST}; | 
| 352 | 0 |  |  |  |  |  | my $col_map = $col_info->{MAP}; | 
| 353 | 0 |  |  |  |  |  | my $xml_cnt; | 
| 354 | 0 |  |  |  |  |  | for my $col (@$list) { | 
| 355 | 0 | 0 |  |  |  |  | if ( $col_map->{$col}{TYPE_NAME} eq 'XMLTYPE' ) { | 
| 356 | 0 |  |  |  |  |  | $xml_cnt++; | 
| 357 | 0 |  |  |  |  |  | push @col_list, "XMLType.getclobval($col)"; | 
| 358 | 0 |  |  |  |  |  | next; | 
| 359 |  |  |  |  |  |  | } | 
| 360 | 0 |  |  |  |  |  | push @col_list, $col; | 
| 361 |  |  |  |  |  |  | } | 
| 362 | 0 | 0 |  |  |  |  | if ($xml_cnt) { | 
| 363 | 0 |  |  |  |  |  | $col_list = join(",", @col_list); | 
| 364 |  |  |  |  |  |  | } | 
| 365 |  |  |  |  |  |  | } | 
| 366 | 0 | 0 |  |  |  |  | $table = "$table PARTITION ($partition)" if $partition; | 
| 367 |  |  |  |  |  |  | } | 
| 368 | 0 | 0 |  |  |  |  | my $enc = $enc_opt ? ":encoding($enc_opt)" : ''; | 
| 369 |  |  |  |  |  |  |  | 
| 370 | 0 | 0 |  |  |  |  | open(my $fh, ">$enc", $file) or confess "Can not write to $file: $!"; | 
| 371 | 0 |  |  |  |  |  | my $sql = "SELECT $col_list FROM $table\n"; | 
| 372 | 0 | 0 |  |  |  |  | $sql .= $opts->{Filter} if $opts->{Filter}; | 
| 373 |  |  |  |  |  |  |  | 
| 374 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 375 | 0 |  |  |  |  |  | my $sth = $dbh->prepare($sql); | 
| 376 | 0 | 0 |  |  |  |  | $sth->{ChopBlanks} = 0 unless $opts->{TrimBlanks}; | 
| 377 | 0 |  |  |  |  |  | $sth->execute(); | 
| 378 | 0 | 0 |  |  |  |  | if ($opts->{Header}) { | 
| 379 | 0 | 0 |  |  |  |  | if ($csv) { | 
| 380 | 0 |  |  |  |  |  | $csv->print($fh, $sth->{NAME_lc}); | 
| 381 |  |  |  |  |  |  | } else { | 
| 382 | 0 |  |  |  |  |  | print $fh join( $delimiter, @{$sth->{NAME_lc}} ), $row_delim; | 
|  | 0 |  |  |  |  |  |  | 
| 383 |  |  |  |  |  |  | } | 
| 384 |  |  |  |  |  |  | } | 
| 385 |  |  |  |  |  |  |  | 
| 386 | 0 |  |  |  |  |  | my $cnt = 0; | 
| 387 | 0 |  |  |  |  |  | while ( my $row = $sth->fetchrow_arrayref() ) { | 
| 388 | 1 |  |  | 1 |  | 4 | no warnings 'uninitialized'; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 115 |  | 
| 389 | 0 | 0 |  |  |  |  | if ($csv) { | 
| 390 | 0 |  |  |  |  |  | $csv->print($fh, $row); | 
| 391 |  |  |  |  |  |  | } else { | 
| 392 | 0 |  |  |  |  |  | print $fh join($delimiter, @$row), $row_delim; | 
| 393 |  |  |  |  |  |  | } | 
| 394 | 0 |  |  |  |  |  | $cnt++; | 
| 395 |  |  |  |  |  |  | } | 
| 396 | 0 |  |  |  |  |  | close $fh; | 
| 397 | 0 |  |  |  |  |  | return $cnt; | 
| 398 |  |  |  |  |  |  | } | 
| 399 |  |  |  |  |  |  |  | 
| 400 |  |  |  |  |  |  | { | 
| 401 | 1 |  |  | 1 |  | 4 | no warnings 'once'; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 2809 |  | 
| 402 |  |  |  |  |  |  | *select2file = \&bcp_out; | 
| 403 |  |  |  |  |  |  | } | 
| 404 |  |  |  |  |  |  |  | 
| 405 |  |  |  |  |  |  | sub bcp_file { | 
| 406 | 0 |  |  | 0 |  |  | my ($self, $file_in, $file_out) = @_; | 
| 407 | 0 |  |  |  |  |  | my $opts = {}; | 
| 408 | 0 | 0 |  |  |  |  | if (ref $_[-1]) { | 
| 409 | 0 |  |  |  |  |  | $opts = pop @_; | 
| 410 |  |  |  |  |  |  | } | 
| 411 |  |  |  |  |  |  |  | 
| 412 | 0 |  | 0 |  |  |  | my $delimiter = $opts->{Delimiter}  || $self->{DELIMITER}; | 
| 413 | 0 |  | 0 |  |  |  | my $esc       = $opts->{EscapeChar} || "\\"; | 
| 414 |  |  |  |  |  |  |  | 
| 415 | 0 | 0 |  |  |  |  | my @quote_char = $opts->{QuoteFields} ? () : ( quote_char => undef ); | 
| 416 | 0 |  |  |  |  |  | require Text::CSV_XS; | 
| 417 | 0 |  |  |  |  |  | my $csv = Text::CSV_XS->new({ | 
| 418 |  |  |  |  |  |  | binary => 1, | 
| 419 |  |  |  |  |  |  | eol => $/, | 
| 420 |  |  |  |  |  |  | sep_char => $delimiter, | 
| 421 |  |  |  |  |  |  | escape_char => $esc, | 
| 422 |  |  |  |  |  |  | @quote_char, | 
| 423 |  |  |  |  |  |  | }); | 
| 424 |  |  |  |  |  |  |  | 
| 425 | 0 | 0 |  |  |  |  | open(my $in_fh,  "<", $file_in)  or die "Err: $!"; | 
| 426 | 0 | 0 |  |  |  |  | open(my $out_fh, ">", $file_out) or die "Err: $!"; | 
| 427 | 0 |  |  |  |  |  | my $hdr = $csv->getline($in_fh); | 
| 428 | 0 |  |  |  |  |  | $csv->column_names($hdr); | 
| 429 | 0 | 0 |  |  |  |  | my @drop_cols = $opts->{DropCols} ? @{$opts->{DropCols}} : (); | 
|  | 0 |  |  |  |  |  |  | 
| 430 | 0 |  |  |  |  |  | my %drop; $drop{$_}++ for @drop_cols; | 
|  | 0 |  |  |  |  |  |  | 
| 431 |  |  |  |  |  |  |  | 
| 432 |  |  |  |  |  |  | my @cols = | 
| 433 | 0 |  |  |  |  |  | $opts->{KeepCols} ? @{$opts->{KeepCols}} | 
| 434 | 0 | 0 |  |  |  |  | : @drop_cols        ? grep !$drop{$_}, @$hdr | 
|  |  | 0 |  |  |  |  |  | 
| 435 |  |  |  |  |  |  | : @$hdr; | 
| 436 | 0 |  |  |  |  |  | my %hdr_idx; @hdr_idx{@$hdr} = 0..$#$hdr; | 
|  | 0 |  |  |  |  |  |  | 
| 437 |  |  |  |  |  |  |  | 
| 438 | 0 | 0 |  |  |  |  | $csv->print($out_fh, [@$hdr[@hdr_idx{@cols}]]) if $opts->{Header}; | 
| 439 | 0 |  |  |  |  |  | while ( my $row = $csv->getline_hr($in_fh) ) { | 
| 440 | 0 |  |  |  |  |  | $csv->print($out_fh, [@$row{@cols}]); | 
| 441 |  |  |  |  |  |  | } | 
| 442 | 0 |  |  |  |  |  | close $in_fh; | 
| 443 | 0 |  |  |  |  |  | close $out_fh; | 
| 444 |  |  |  |  |  |  | } | 
| 445 |  |  |  |  |  |  |  | 
| 446 |  |  |  |  |  |  | sub add_header { | 
| 447 | 0 |  |  | 0 |  |  | my ($self, $table, $file, $opts) = @_; | 
| 448 | 0 |  | 0 |  |  |  | $opts ||= {}; | 
| 449 |  |  |  |  |  |  |  | 
| 450 | 0 |  |  |  |  |  | my $cols; | 
| 451 | 0 | 0 | 0 |  |  |  | if ( $opts->{Header} || $opts->{Columns} ) { | 
| 452 |  |  |  |  |  |  | my $sel_str = | 
| 453 |  |  |  |  |  |  | $opts->{Columns} | 
| 454 |  |  |  |  |  |  | ?  ref($opts->{Columns}) | 
| 455 | 0 |  |  |  |  |  | ? join(",", @{$opts->{Columns}}) | 
| 456 |  |  |  |  |  |  | : $opts->{Columns} | 
| 457 | 0 | 0 |  |  |  |  | : '*'; | 
|  |  | 0 |  |  |  |  |  | 
| 458 | 0 |  |  |  |  |  | my $sth = $self->{DBH}->prepare("SELECT $sel_str FROM $table WHERE 1=0"); | 
| 459 | 0 |  |  |  |  |  | $sth->execute(); | 
| 460 | 0 |  |  |  |  |  | $cols = $sth->{NAME_lc}; | 
| 461 | 0 |  |  |  |  |  | $sth->finish(); | 
| 462 |  |  |  |  |  |  | } | 
| 463 |  |  |  |  |  |  |  | 
| 464 | 0 | 0 |  |  |  |  | return $self->add_quotes($table, $file, $cols, $opts) if $opts->{QuoteFields}; | 
| 465 |  |  |  |  |  |  |  | 
| 466 |  |  |  |  |  |  | # If quotes are not required, this is more efficient | 
| 467 |  |  |  |  |  |  | # I doubt anyone uses either option anyway | 
| 468 |  |  |  |  |  |  | # but highly doubt anyone uses the quoting | 
| 469 | 0 |  |  |  |  |  | require File::Copy; | 
| 470 | 0 |  | 0 |  |  |  | my $d = $opts->{Delimiter} || $self->{DELIMITER}; | 
| 471 | 0 |  | 0 |  |  |  | local $/ = $opts->{RowDelimiter} || "\n"; | 
| 472 |  |  |  |  |  |  |  | 
| 473 | 0 | 0 |  |  |  |  | open(my $fh, ">", "$file.bak") or die "Failed to open $file.bak: $!"; | 
| 474 |  |  |  |  |  |  |  | 
| 475 |  |  |  |  |  |  | # Unbuffer the filehandle for printing header | 
| 476 |  |  |  |  |  |  | # because File::Copy uses unbuffered syswrite | 
| 477 |  |  |  |  |  |  | # $fh->flush() after the print would also work depending on | 
| 478 |  |  |  |  |  |  | # version of perl and whether IO::Handle is loaded | 
| 479 | 0 |  |  |  |  |  | for ( select $fh ) { $| = 1; select $_ } | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 480 | 0 |  |  |  |  |  | print $fh join($d, @$cols), $/; | 
| 481 |  |  |  |  |  |  |  | 
| 482 | 0 | 0 |  |  |  |  | File::Copy::copy($file, $fh) or die "Failed to copy $file to $file.bak: $!"; | 
| 483 | 0 |  |  |  |  |  | close $fh; | 
| 484 |  |  |  |  |  |  |  | 
| 485 | 0 |  |  |  |  |  | return "$file.bak"; | 
| 486 |  |  |  |  |  |  | } | 
| 487 |  |  |  |  |  |  |  | 
| 488 |  |  |  |  |  |  | sub add_quotes { | 
| 489 | 0 |  |  | 0 |  |  | my ($self, $table, $file, $cols, $opts) = @_; | 
| 490 | 0 |  | 0 |  |  |  | my $d = $opts->{Delimiter} || $self->{DELIMITER}; | 
| 491 | 0 |  |  |  |  |  | my $dre = quotemeta($d); | 
| 492 |  |  |  |  |  |  |  | 
| 493 | 0 |  |  |  |  |  | local ($_, $., $ARGV, *ARGV); | 
| 494 | 0 |  |  |  |  |  | local ( $^I, @ARGV ) = ( '.bak', $file ); | 
| 495 | 0 |  | 0 |  |  |  | local $/ = $opts->{RowDelimiter} || "\n"; | 
| 496 | 0 |  |  |  |  |  | my $done; | 
| 497 | 0 |  |  |  |  |  | while ( <> ) { | 
| 498 | 0 | 0 | 0 |  |  |  | print join($d, @$cols), $/ if !$done++ && $opts->{Header}; | 
| 499 |  |  |  |  |  |  |  | 
| 500 | 0 | 0 |  |  |  |  | if ($opts->{QuoteFields}) { | 
| 501 | 0 |  |  |  |  |  | chomp; | 
| 502 | 0 |  |  |  |  |  | my @fields = split /$dre/; | 
| 503 | 0 |  | 0 |  |  |  | /\s/ and $_ = qq("$_") for @fields; | 
| 504 | 0 |  |  |  |  |  | $_ = join($d, @fields) . $/; | 
| 505 |  |  |  |  |  |  | } | 
| 506 | 0 |  |  |  |  |  | print; | 
| 507 |  |  |  |  |  |  | } | 
| 508 | 0 |  |  |  |  |  | return "$file.bak"; | 
| 509 |  |  |  |  |  |  | } | 
| 510 |  |  |  |  |  |  |  | 
| 511 |  |  |  |  |  |  | sub type { | 
| 512 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 513 | 0 |  |  |  |  |  | return $self->{DBH}{Driver}{Name}; | 
| 514 |  |  |  |  |  |  | } | 
| 515 |  |  |  |  |  |  |  | 
| 516 |  |  |  |  |  |  | # Because of Sybase and its stupid mixed case column names, | 
| 517 |  |  |  |  |  |  | # we need to be able to find the actual cased name for a given | 
| 518 |  |  |  |  |  |  | # uncased column name. | 
| 519 |  |  |  |  |  |  | # Just pray that there are not two columns with the same name | 
| 520 |  |  |  |  |  |  | # in the same table that are differently cased. | 
| 521 |  |  |  |  |  |  | memoize('column_info'); | 
| 522 |  |  |  |  |  |  |  | 
| 523 |  |  |  |  |  |  | sub column_info { | 
| 524 |  |  |  |  |  |  | my $self = shift; | 
| 525 |  |  |  |  |  |  | my $table = shift; | 
| 526 |  |  |  |  |  |  |  | 
| 527 |  |  |  |  |  |  | my $schema; | 
| 528 |  |  |  |  |  |  | my $dbtype = $self->type(); | 
| 529 |  |  |  |  |  |  | my ($tmp_db, $curr_db) = (undef,''); | 
| 530 |  |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 531 |  |  |  |  |  |  | my %col_dflt; | 
| 532 |  |  |  |  |  |  | if ( $dbtype eq 'Oracle' ) { | 
| 533 |  |  |  |  |  |  | $table = uc($table); | 
| 534 |  |  |  |  |  |  | if ( $table =~ /^(\w+)\.(\w+)$/ ) { | 
| 535 |  |  |  |  |  |  | ($schema, $table) = ($1,$2); | 
| 536 |  |  |  |  |  |  | } else { $schema = $self->curr_schema() } | 
| 537 |  |  |  |  |  |  | } elsif ( $dbtype eq 'Sybase' ) { | 
| 538 |  |  |  |  |  |  |  | 
| 539 |  |  |  |  |  |  | $tmp_db = $curr_db = $self->curr_db(); | 
| 540 |  |  |  |  |  |  |  | 
| 541 |  |  |  |  |  |  | if ( $table =~ /^#/ ) { | 
| 542 |  |  |  |  |  |  | $table = $self->temp_table_name($table); | 
| 543 |  |  |  |  |  |  | } | 
| 544 |  |  |  |  |  |  |  | 
| 545 |  |  |  |  |  |  | if ( $table =~ /^(?:(\w+)\.)?(\w*)\.(#?\w+)$/ ) { | 
| 546 |  |  |  |  |  |  | ($tmp_db, $schema, $table) = ($1,$2,$3); | 
| 547 |  |  |  |  |  |  | $schema ||= undef; | 
| 548 |  |  |  |  |  |  |  | 
| 549 |  |  |  |  |  |  | # We can only get column info on the current database | 
| 550 |  |  |  |  |  |  | $dbh->do("USE $tmp_db") if defined($tmp_db) and $tmp_db ne $curr_db; | 
| 551 |  |  |  |  |  |  | } | 
| 552 |  |  |  |  |  |  |  | 
| 553 |  |  |  |  |  |  | $schema ||= '%'; | 
| 554 |  |  |  |  |  |  |  | 
| 555 |  |  |  |  |  |  | # Sybase gets metadata through a (under the DBD hood) stored proc, but does not return defaults. | 
| 556 |  |  |  |  |  |  | # So get defaults here. | 
| 557 |  |  |  |  |  |  | my $sth = $dbh->prepare( sprintf( $self->default_sql(), $table ) ); | 
| 558 |  |  |  |  |  |  | $sth->execute(); | 
| 559 |  |  |  |  |  |  | $sth->bind_columns( \my ( $col_name, $default ) ); | 
| 560 |  |  |  |  |  |  | while ( $sth->fetch ) { | 
| 561 |  |  |  |  |  |  | $col_dflt{$col_name} .= $default; | 
| 562 |  |  |  |  |  |  | } | 
| 563 |  |  |  |  |  |  | } | 
| 564 |  |  |  |  |  |  |  | 
| 565 |  |  |  |  |  |  | my $sth = $self->{DBH}->column_info($tmp_db, $schema, $table, '%'); | 
| 566 |  |  |  |  |  |  | my @names = @{$sth->{NAME_uc}}; | 
| 567 |  |  |  |  |  |  | my %row; $sth->bind_columns(\@row{@names}); | 
| 568 |  |  |  |  |  |  | my @list; | 
| 569 |  |  |  |  |  |  | my %col_map; | 
| 570 |  |  |  |  |  |  | my $col_cnt = 0; | 
| 571 |  |  |  |  |  |  | while ( $sth->fetch() ) { | 
| 572 |  |  |  |  |  |  |  | 
| 573 |  |  |  |  |  |  | # Data is probably in order, but we are not guaranteed | 
| 574 |  |  |  |  |  |  | # So assign by index instead of pushing to array if possible | 
| 575 |  |  |  |  |  |  | # IQ does not have ORDINAL_POSITION so fall back to select order | 
| 576 |  |  |  |  |  |  | my $idx = defined($row{ORDINAL_POSITION}) ? $row{ORDINAL_POSITION}-1 : $col_cnt; | 
| 577 |  |  |  |  |  |  | $col_cnt++; | 
| 578 |  |  |  |  |  |  |  | 
| 579 |  |  |  |  |  |  | my $name = lc($row{COLUMN_NAME}); | 
| 580 |  |  |  |  |  |  | $list[$idx] = $name; | 
| 581 |  |  |  |  |  |  | ($row{COLUMN_DEF} = $col_dflt{$name}) =~ s/^default\s*//i if defined($col_dflt{$name}) and !defined($row{COLUMN_DEF}); | 
| 582 |  |  |  |  |  |  | $col_map{$name} = { %row }; | 
| 583 |  |  |  |  |  |  | } | 
| 584 |  |  |  |  |  |  | $dbh->do("USE $curr_db") if defined($tmp_db) and $tmp_db ne $curr_db; | 
| 585 |  |  |  |  |  |  |  | 
| 586 |  |  |  |  |  |  | return unless $col_cnt; | 
| 587 |  |  |  |  |  |  | my %col_info = ( | 
| 588 |  |  |  |  |  |  | LIST => \@list, | 
| 589 |  |  |  |  |  |  | MAP  => \%col_map, | 
| 590 |  |  |  |  |  |  | ); | 
| 591 |  |  |  |  |  |  | return \%col_info; | 
| 592 |  |  |  |  |  |  | } | 
| 593 |  |  |  |  |  |  |  | 
| 594 |  |  |  |  |  |  | sub last_chg_list { | 
| 595 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 596 | 0 |  |  |  |  |  | my ($table, $columns) = @_; | 
| 597 |  |  |  |  |  |  |  | 
| 598 |  |  |  |  |  |  | # Determine if last_chg_user, last_chg_date need to be updated | 
| 599 | 0 |  |  |  |  |  | my %chg_field = (last_chg_user => 1, last_chg_date => 1); | 
| 600 | 0 |  |  |  |  |  | delete $chg_field{$_} for map lc, @$columns; | 
| 601 | 0 |  |  |  |  |  | my %chg_cols; | 
| 602 | 0 | 0 |  |  |  |  | if (%chg_field) { | 
| 603 |  |  |  |  |  |  | # Are chg columns in table | 
| 604 | 0 |  |  |  |  |  | my $col_info = $self->column_info($table); | 
| 605 | 0 |  |  |  |  |  | my $col_map = $col_info->{MAP}; | 
| 606 | 0 |  |  |  |  |  | for my $c (keys %chg_field) { | 
| 607 | 0 | 0 |  |  |  |  | $chg_cols{$c} = $col_map->{$c}{COLUMN_SIZE} if $col_map->{$c}; | 
| 608 |  |  |  |  |  |  | } | 
| 609 |  |  |  |  |  |  | } | 
| 610 |  |  |  |  |  |  |  | 
| 611 | 0 |  |  |  |  |  | return %chg_cols; | 
| 612 |  |  |  |  |  |  | } | 
| 613 |  |  |  |  |  |  |  | 
| 614 |  |  |  |  |  |  | sub key_columns { | 
| 615 | 0 |  |  | 0 |  |  | my ($self, $table) = @_; | 
| 616 |  |  |  |  |  |  |  | 
| 617 | 0 |  |  |  |  |  | my $pk = $self->primary_key($table); | 
| 618 | 0 | 0 |  |  |  |  | return $pk if $pk; | 
| 619 |  |  |  |  |  |  |  | 
| 620 | 0 |  |  |  |  |  | my $idx = $self->index_info($table); | 
| 621 | 0 | 0 |  |  |  |  | return unless $idx; | 
| 622 |  |  |  |  |  |  |  | 
| 623 |  |  |  |  |  |  | # Look for unique indexes with suffixes uk, pk, or key | 
| 624 | 0 |  |  |  |  |  | my ($pk_name) = sort grep /(?i)(?:[pu]k|key)\d*$/, keys %$idx; | 
| 625 | 0 | 0 |  |  |  |  | return $idx->{$pk_name} if $pk_name; | 
| 626 |  |  |  |  |  |  |  | 
| 627 | 0 |  |  |  |  |  | my ($idx_name) = sort keys %$idx; | 
| 628 | 0 |  |  |  |  |  | return $idx->{$idx_name}; | 
| 629 |  |  |  |  |  |  | } | 
| 630 |  |  |  |  |  |  |  | 
| 631 |  |  |  |  |  |  | sub upd_columns { | 
| 632 | 0 |  |  | 0 |  |  | my ($self, $table, $key_cols) = @_; | 
| 633 |  |  |  |  |  |  |  | 
| 634 | 0 |  |  |  |  |  | my $col_data = $self->column_info($table)->{LIST}; | 
| 635 | 0 |  | 0 |  |  |  | $key_cols ||= $self->key_columns($table); | 
| 636 | 0 | 0 |  |  |  |  | return unless $key_cols; | 
| 637 |  |  |  |  |  |  |  | 
| 638 | 0 |  |  |  |  |  | my %is_key_col; $is_key_col{$_}++ for @$key_cols; | 
|  | 0 |  |  |  |  |  |  | 
| 639 | 0 |  |  |  |  |  | return [ grep !$is_key_col{$_}, @$col_data ]; | 
| 640 |  |  |  |  |  |  | } | 
| 641 |  |  |  |  |  |  |  | 
| 642 |  |  |  |  |  |  | sub delete { | 
| 643 | 0 |  |  | 0 |  |  | my ($self, $table, $where) = @_; | 
| 644 |  |  |  |  |  |  |  | 
| 645 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 646 | 0 |  |  |  |  |  | my $sql = "DELETE FROM $table"; | 
| 647 | 0 | 0 |  |  |  |  | $sql .= " WHERE $where" if $where; | 
| 648 |  |  |  |  |  |  |  | 
| 649 | 0 |  |  |  |  |  | my $rows = $dbh->do($sql) + 0; | 
| 650 |  |  |  |  |  |  |  | 
| 651 | 0 |  |  |  |  |  | print "$rows rows deleted\n"; | 
| 652 | 0 |  |  |  |  |  | return $rows; | 
| 653 |  |  |  |  |  |  | } | 
| 654 |  |  |  |  |  |  |  | 
| 655 |  |  |  |  |  |  | # Execute sql with retry on deadlocks | 
| 656 |  |  |  |  |  |  | sub execute { | 
| 657 | 0 |  |  | 0 |  |  | my ($self,$sth,@args) = @_; | 
| 658 |  |  |  |  |  |  |  | 
| 659 |  |  |  |  |  |  | # We can pass a sql statement or a sth | 
| 660 | 0 | 0 |  |  |  |  | $sth = $self->{DBH}->prepare($sth) if !ref($sth); | 
| 661 |  |  |  |  |  |  |  | 
| 662 | 0 |  |  |  |  |  | my $retry = 5; | 
| 663 | 0 |  |  |  |  |  | for (1..$retry) { | 
| 664 | 0 |  |  |  |  |  | my $status = eval { $sth->execute(@args) }; | 
|  | 0 |  |  |  |  |  |  | 
| 665 | 0 | 0 |  |  |  |  | return $status if $status; | 
| 666 | 0 | 0 |  |  |  |  | confess $@ unless $@ =~ /deadlock/i; | 
| 667 | 0 |  |  |  |  |  | print "Deadlock detected on retry $_ of 5\n"; | 
| 668 | 0 | 0 |  |  |  |  | sleep 2 if $_ < $retry; | 
| 669 |  |  |  |  |  |  | } | 
| 670 | 0 |  |  |  |  |  | confess $@; | 
| 671 |  |  |  |  |  |  | } | 
| 672 |  |  |  |  |  |  |  | 
| 673 |  |  |  |  |  |  | sub ora_date_fmt { | 
| 674 |  |  |  |  |  |  |  | 
| 675 |  |  |  |  |  |  | # Not very OO-ish but allow calling the Oracle date mask routine | 
| 676 |  |  |  |  |  |  | # From any generic utility object | 
| 677 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 678 | 0 |  |  |  |  |  | DBIx::BulkUtil::Oracle->date_mask(@_); | 
| 679 |  |  |  |  |  |  |  | 
| 680 |  |  |  |  |  |  | } | 
| 681 |  |  |  |  |  |  |  | 
| 682 |  |  |  |  |  |  | sub strptime_fmt { | 
| 683 | 0 |  |  | 0 |  |  | my ($class, $str, $fmt) = @_; | 
| 684 | 0 |  | 0 |  |  |  | $fmt ||= DBIx::BulkUtil::Oracle->date_mask($str); | 
| 685 | 0 | 0 |  |  |  |  | return undef unless $fmt; | 
| 686 | 0 |  |  |  |  |  | for ($fmt) { | 
| 687 | 0 |  |  |  |  |  | s/MONTH/%B/; | 
| 688 | 0 |  |  |  |  |  | s/MON/%b/; | 
| 689 | 0 |  |  |  |  |  | s/MM/%m/; | 
| 690 | 0 |  |  |  |  |  | s/DD/%d/; | 
| 691 | 0 |  |  |  |  |  | s/YYYY/%Y/; | 
| 692 | 0 |  |  |  |  |  | s/YY/%y/; | 
| 693 | 0 |  |  |  |  |  | s/RRRR/%Y/; | 
| 694 | 0 |  |  |  |  |  | s/RR/%y/; | 
| 695 | 0 |  |  |  |  |  | s/HH24/%H/; | 
| 696 | 0 |  |  |  |  |  | s/HH(?:12)?/%I/; | 
| 697 | 0 |  |  |  |  |  | s/MI/%M/; | 
| 698 | 0 |  |  |  |  |  | s/SS/%S/; | 
| 699 | 0 |  |  |  |  |  | s/AM/%p/; | 
| 700 | 0 |  |  |  |  |  | s/DY/%a/; | 
| 701 | 0 |  |  |  |  |  | s/DAY/%a/; | 
| 702 | 0 |  |  |  |  |  | s/TZD/%Z/; | 
| 703 | 0 |  |  |  |  |  | s/TZH.TZD/%z/; | 
| 704 | 0 |  |  |  |  |  | s/"(.)"/$1/g; | 
| 705 |  |  |  |  |  |  | } | 
| 706 | 0 |  |  |  |  |  | return $fmt; | 
| 707 |  |  |  |  |  |  | } | 
| 708 |  |  |  |  |  |  |  | 
| 709 |  |  |  |  |  |  | sub blk_prepare { | 
| 710 | 0 |  |  | 0 |  |  | my ($self, $table, %args) = @_; | 
| 711 | 0 |  | 0 |  |  |  | my $blk_opts = $args{BlkOpts} || {}; | 
| 712 | 0 |  | 0 |  |  |  | my $commit   = $args{CommitSize} || 1000; | 
| 713 | 0 |  |  |  |  |  | my $con      = $args{Constants}; | 
| 714 |  |  |  |  |  |  |  | 
| 715 | 0 | 0 |  |  |  |  | my $col_info = $self->column_info($table) or confess "Table $table does not exist"; | 
| 716 | 0 |  |  |  |  |  | my @col_list = @{$col_info->{LIST}}; | 
|  | 0 |  |  |  |  |  |  | 
| 717 | 0 |  |  |  |  |  | my $arg_len = @col_list; | 
| 718 |  |  |  |  |  |  |  | 
| 719 | 0 |  |  |  |  |  | my $col_cnt = @col_list; | 
| 720 | 0 |  |  |  |  |  | my $sql = "INSERT INTO $table VALUES (" . join(",", ("?") x $col_cnt) . ")"; | 
| 721 | 0 |  |  |  |  |  | my $type = $self->type(); | 
| 722 | 0 | 0 |  |  |  |  | my @blk_opts = ($type eq 'Sybase') | 
| 723 |  |  |  |  |  |  | ? { syb_bcp_attribs => $blk_opts } | 
| 724 |  |  |  |  |  |  | : (); | 
| 725 |  |  |  |  |  |  |  | 
| 726 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 727 | 0 |  |  |  |  |  | my $sth = $dbh->prepare($sql, @blk_opts); | 
| 728 |  |  |  |  |  |  |  | 
| 729 | 0 |  |  |  |  |  | my ($exec_f,$commit_f,$finish_f); | 
| 730 | 0 |  |  |  |  |  | my @ex_arg_list = (undef) x @col_list; | 
| 731 | 0 |  |  |  |  |  | my $cnt = 0; | 
| 732 | 0 | 0 |  |  |  |  | if ($con) { | 
| 733 | 0 |  |  |  |  |  | my %const  = %$con; | 
| 734 | 0 |  |  |  |  |  | my @c_list   = keys %const; | 
| 735 |  |  |  |  |  |  |  | 
| 736 | 0 |  |  |  |  |  | my %col_pos; | 
| 737 | 0 |  |  |  |  |  | @col_pos{@col_list} = 0..$#col_list; | 
| 738 | 0 |  |  |  |  |  | my %const_pos; | 
| 739 | 0 |  |  |  |  |  | @const_pos{@c_list} = delete @col_pos{@c_list}; | 
| 740 | 0 |  |  |  |  |  | $arg_len = keys %col_pos; | 
| 741 |  |  |  |  |  |  |  | 
| 742 |  |  |  |  |  |  | # Create arg array for execute method | 
| 743 |  |  |  |  |  |  | # Set constants and create sub for all but constant args | 
| 744 | 0 |  |  |  |  |  | @ex_arg_list[@const_pos{@c_list}] = @const{@c_list}; | 
| 745 | 0 |  |  |  |  |  | my @non_const = sort { $a <=> $b } values %col_pos; | 
|  | 0 |  |  |  |  |  |  | 
| 746 | 0 | 0 |  |  |  |  | $sth->{HandleError} = undef if $type eq 'Oracle'; | 
| 747 |  |  |  |  |  |  |  | 
| 748 | 0 | 0 |  |  |  |  | if ($type eq 'Sybase') { | 
| 749 | 0 |  |  | 0 |  |  | $exec_f   = sub { @ex_arg_list[@non_const] = @_; $sth->execute(@ex_arg_list) }; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 750 | 0 |  |  | 0 |  |  | $commit_f = sub { $dbh->commit() }; | 
|  | 0 |  |  |  |  |  |  | 
| 751 | 0 |  |  | 0 |  |  | $finish_f = sub { $dbh->commit(); $sth->finish(); $sth = undef }; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 752 |  |  |  |  |  |  | } else { | 
| 753 | 0 |  |  | 0 |  |  | $exec_f   = sub { my $i=0; push @{$ex_arg_list[$_]}, $_[$i++] for @non_const }; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 754 |  |  |  |  |  |  | $commit_f = sub { | 
| 755 | 0 |  |  | 0 |  |  | my ($t,$r) = $sth->execute_array({ ArrayTupleStatus => \my @status }, @ex_arg_list); | 
| 756 | 0 | 0 |  |  |  |  | unless (defined $t) { | 
| 757 | 0 |  |  |  |  |  | for my $i (0..$#status) { | 
| 758 | 0 | 0 |  |  |  |  | next unless ref $status[$i]; | 
| 759 | 0 | 0 |  |  |  |  | my @row = map { ref($ex_arg_list[$_]) ? qq('$ex_arg_list[$_][$i]') : $ex_arg_list[$_] } 0..$#ex_arg_list; | 
|  | 0 |  |  |  |  |  |  | 
| 760 | 0 |  |  |  |  |  | confess "Error: [$status[$i][1]] inserting [".join(",", @row)."]"; | 
| 761 |  |  |  |  |  |  | } | 
| 762 |  |  |  |  |  |  | } | 
| 763 | 0 |  |  |  |  |  | $_ = [] for @ex_arg_list[@non_const]; | 
| 764 | 0 |  |  |  |  |  | $r; | 
| 765 | 0 |  |  |  |  |  | }; | 
| 766 | 0 | 0 |  | 0 |  |  | $finish_f = sub { $commit_f->() if $cnt > 0 }; | 
|  | 0 |  |  |  |  |  |  | 
| 767 |  |  |  |  |  |  | } | 
| 768 |  |  |  |  |  |  | } else { | 
| 769 | 0 | 0 |  |  |  |  | if ($type eq 'Sybase') { | 
| 770 | 0 |  |  | 0 |  |  | $exec_f   = sub { $sth->execute(@_); '0E0' }; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 771 | 0 |  |  | 0 |  |  | $commit_f = sub { $dbh->commit(); $cnt }; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 772 | 0 | 0 |  | 0 |  |  | $finish_f = sub { $dbh->commit(); $sth->finish(); $sth = undef; ( $cnt > 0 ) ? $cnt : '0E0' }; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 773 |  |  |  |  |  |  | } else { | 
| 774 | 0 |  |  | 0 |  |  | $exec_f   = sub { my $i=0; push @{$ex_arg_list[$_]}, $_[$_] for 0..$#ex_arg_list; return '0E0' }; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 775 |  |  |  |  |  |  | $commit_f = sub { | 
| 776 | 0 |  |  | 0 |  |  | my ($t,$r) = $sth->execute_array({ ArrayTupleStatus => \my @status }, @ex_arg_list); | 
| 777 | 0 | 0 |  |  |  |  | unless (defined $t) { | 
| 778 | 0 |  |  |  |  |  | for my $i (0..$#status) { | 
| 779 | 0 | 0 |  |  |  |  | next unless ref $status[$i]; | 
| 780 | 0 |  |  |  |  |  | my @row = map { qq('$ex_arg_list[$_][$i]') } 0..$#ex_arg_list; | 
|  | 0 |  |  |  |  |  |  | 
| 781 | 0 |  |  |  |  |  | confess "Error: [$status[$i][1]] inserting [".join(",", @row)."]"; | 
| 782 |  |  |  |  |  |  | } | 
| 783 |  |  |  |  |  |  | } | 
| 784 | 0 |  |  |  |  |  | $_ = [] for @ex_arg_list; | 
| 785 | 0 |  |  |  |  |  | $r; | 
| 786 | 0 |  |  |  |  |  | }; | 
| 787 | 0 | 0 |  | 0 |  |  | $finish_f = sub { ( $cnt > 0 ) ? $commit_f->() : '0E0' }; | 
|  | 0 |  |  |  |  |  |  | 
| 788 |  |  |  |  |  |  | } | 
| 789 |  |  |  |  |  |  | } | 
| 790 |  |  |  |  |  |  |  | 
| 791 | 0 |  |  |  |  |  | bless { | 
| 792 |  |  |  |  |  |  | CNT => \$cnt, | 
| 793 |  |  |  |  |  |  | COMMIT_SIZE => $commit, | 
| 794 |  |  |  |  |  |  | EXEC_FUNC   => $exec_f, | 
| 795 |  |  |  |  |  |  | COMMIT_FUNC => $commit_f, | 
| 796 |  |  |  |  |  |  | FINISH_FUNC => $finish_f, | 
| 797 |  |  |  |  |  |  | ARG_LEN     => $arg_len, | 
| 798 |  |  |  |  |  |  | }, "DBIx::BulkUtil::BLK"; | 
| 799 |  |  |  |  |  |  | } | 
| 800 |  |  |  |  |  |  |  | 
| 801 |  |  |  |  |  |  | sub prepare { | 
| 802 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 803 | 0 |  |  |  |  |  | my %opt = @_; | 
| 804 | 0 |  |  |  |  |  | my $table = $opt{Table}; | 
| 805 | 0 |  |  |  |  |  | my $sql   = $opt{Sql}; | 
| 806 | 0 |  |  |  |  |  | my $columns = $opt{Columns}; | 
| 807 | 0 |  |  |  |  |  | my $href   = $opt{BindHash}; | 
| 808 | 0 |  |  |  |  |  | my $aref   = $opt{BindArray}; | 
| 809 |  |  |  |  |  |  | my $by_name = | 
| 810 |  |  |  |  |  |  | defined($opt{ByName})        ? $opt{ByName} | 
| 811 | 0 | 0 | 0 |  |  |  | : ( !$href && !$aref )         ? 0 | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 812 |  |  |  |  |  |  | : ($self->type() eq 'Sybase' ) ? 0 | 
| 813 |  |  |  |  |  |  | : 1; | 
| 814 | 0 | 0 | 0 |  |  |  | confess "Can not supply both BindHash and BindArray" if $href && $aref; | 
| 815 | 0 | 0 | 0 |  |  |  | confess "Can not use BindHash or BindArray without ByName" if ( $href || $aref ) && !$by_name; | 
|  |  |  | 0 |  |  |  |  | 
| 816 |  |  |  |  |  |  |  | 
| 817 | 0 | 0 | 0 |  |  |  | confess "Must supply Table or Sql to prepare" unless $table || $sql; | 
| 818 | 0 | 0 | 0 |  |  |  | confess "Can not supply both Table and Sql to prepare" if $table && $sql; | 
| 819 |  |  |  |  |  |  |  | 
| 820 | 0 |  |  |  |  |  | my $dflt_col = eval { | 
| 821 | 0 | 0 | 0 |  |  |  | $columns ||= $self->column_info($table)->{LIST} if $table; | 
| 822 | 0 |  |  |  |  |  | 1; | 
| 823 |  |  |  |  |  |  | }; | 
| 824 | 0 | 0 |  |  |  |  | confess "Table $table not found in datbase" unless $dflt_col; | 
| 825 |  |  |  |  |  |  |  | 
| 826 | 0 | 0 | 0 |  |  |  | if ( $columns && @$columns ) { | 
| 827 |  |  |  |  |  |  |  | 
| 828 |  |  |  |  |  |  | # A little overkill to get a nicely formatted SQL statement | 
| 829 | 0 | 0 |  |  |  |  | my $c_sep  = ( @$columns > 5 ) ? "\n" : ''; | 
| 830 | 0 |  |  |  |  |  | my $cnt; | 
| 831 | 0 | 0 |  | 0 |  |  | my $c_ind  = ( @$columns > 5 ) ? sub { ' ' } : sub { $cnt++ ? ' ' : '' }; | 
|  | 0 | 0 |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 832 |  |  |  |  |  |  |  | 
| 833 | 0 |  |  |  |  |  | my $h_cnt; | 
| 834 | 0 | 0 |  | 0 |  |  | my $h_ind  = ( @$columns > 5 ) ? sub { ' ' } : sub { $h_cnt++ ? ' ' : '' }; | 
|  | 0 | 0 |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 835 |  |  |  |  |  |  |  | 
| 836 | 0 | 0 |  |  |  |  | my $v_sep  = $by_name ? ( @$columns > 5 ) ? "\n" : '' : ''; | 
|  |  | 0 |  |  |  |  |  | 
| 837 | 0 | 0 |  | 0 |  |  | my $hold   = $by_name ? sub { $h_ind->() . ":$_" } : sub { "?" }; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 838 |  |  |  |  |  |  |  | 
| 839 | 0 |  | 0 |  |  |  | $sql ||= sprintf("INSERT INTO $table ($c_sep%s$c_sep) VALUES ($v_sep%s$v_sep)", | 
| 840 |  |  |  |  |  |  | join(",$c_sep", map $c_ind->() . $_, @$columns), | 
| 841 |  |  |  |  |  |  | join(",$v_sep", map $hold->(), @$columns), | 
| 842 |  |  |  |  |  |  | ); | 
| 843 |  |  |  |  |  |  | } | 
| 844 | 0 |  |  |  |  |  | print "Preparing: $sql\n"; | 
| 845 | 0 |  |  |  |  |  | my $sth = $self->dbh->prepare($sql); | 
| 846 |  |  |  |  |  |  |  | 
| 847 | 0 | 0 |  |  |  |  | if ($href) { | 
|  |  | 0 |  |  |  |  |  | 
| 848 | 0 |  |  |  |  |  | $sth->bind_param_inout( ":$_" => \$href->{$_}, 0 ) for @$columns; | 
| 849 |  |  |  |  |  |  | } elsif ($aref) { | 
| 850 | 0 |  |  |  |  |  | $sth->bind_param_inout( ":$columns->[$_]" => \$aref->[$_], 0 ) for 0..$#$columns; | 
| 851 |  |  |  |  |  |  | } | 
| 852 |  |  |  |  |  |  |  | 
| 853 | 0 |  |  |  |  |  | return $sth; | 
| 854 |  |  |  |  |  |  |  | 
| 855 |  |  |  |  |  |  | } | 
| 856 |  |  |  |  |  |  |  | 
| 857 |  |  |  |  |  |  | sub prepare_upd { | 
| 858 |  |  |  |  |  |  |  | 
| 859 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 860 |  |  |  |  |  |  |  | 
| 861 | 0 |  |  |  |  |  | my %args = @_; | 
| 862 |  |  |  |  |  |  |  | 
| 863 | 0 |  | 0 |  |  |  | my $table = $args{Table} || die "Must supply Table option"; | 
| 864 |  |  |  |  |  |  |  | 
| 865 | 0 |  |  |  |  |  | my $col_info = $self->column_info($table); | 
| 866 | 0 |  | 0 |  |  |  | my $col_list = $args{Columns} || $col_info->{LIST}; | 
| 867 |  |  |  |  |  |  |  | 
| 868 | 0 |  | 0 |  |  |  | my $key_cols = $args{KeyCols} || $self->key_columns($table); | 
| 869 | 0 |  | 0 |  |  |  | my $upd_cols = $args{UpdCols} || $self->upd_columns($table); | 
| 870 |  |  |  |  |  |  |  | 
| 871 | 0 |  |  |  |  |  | my $sql = < | 
| 872 |  |  |  |  |  |  | UPDATE $table | 
| 873 |  |  |  |  |  |  | SET | 
| 874 | 0 |  |  |  |  |  | @{[ join( ",\n  ", map "$_ = ?", @$upd_cols )]} | 
| 875 |  |  |  |  |  |  | WHERE | 
| 876 | 0 |  |  |  |  |  | @{[ join( " AND\n  ", map "$_ = ?", @$key_cols )]} | 
| 877 |  |  |  |  |  |  | SQL | 
| 878 | 0 |  |  |  |  |  | print "Preparing: $sql\n"; | 
| 879 | 0 |  |  |  |  |  | my $sth = $self->{DBH}->prepare($sql); | 
| 880 |  |  |  |  |  |  |  | 
| 881 | 0 |  |  |  |  |  | my %col_pos; | 
| 882 | 0 |  |  |  |  |  | my $cnt = 0; | 
| 883 | 0 |  |  |  |  |  | $col_pos{$_} = $cnt++ for @$col_list; | 
| 884 |  |  |  |  |  |  |  | 
| 885 | 0 |  |  |  |  |  | my @sth_pos; | 
| 886 | 0 |  |  |  |  |  | push @sth_pos, $col_pos{$_} for @$upd_cols, @$key_cols; | 
| 887 |  |  |  |  |  |  |  | 
| 888 |  |  |  |  |  |  | return sub { | 
| 889 | 0 | 0 |  | 0 |  |  | unless (@_) { | 
| 890 | 0 |  |  |  |  |  | $sth->finish(); | 
| 891 | 0 |  |  |  |  |  | undef $sth; | 
| 892 | 0 |  |  |  |  |  | undef @sth_pos; | 
| 893 | 0 |  |  |  |  |  | return; | 
| 894 |  |  |  |  |  |  | } | 
| 895 | 0 |  |  |  |  |  | $sth->execute(@_[@sth_pos]); | 
| 896 |  |  |  |  |  |  | } | 
| 897 | 0 |  |  |  |  |  | } | 
| 898 |  |  |  |  |  |  |  | 
| 899 | 0 |  |  | 0 |  |  | sub is_iq { 0 } | 
| 900 |  |  |  |  |  |  |  | 
| 901 |  |  |  |  |  |  | package DBIx::BulkUtil::BLK; | 
| 902 |  |  |  |  |  |  |  | 
| 903 | 1 |  |  | 1 |  | 5 | use Carp qw(confess); | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 148 |  | 
| 904 |  |  |  |  |  |  |  | 
| 905 |  |  |  |  |  |  | sub execute { | 
| 906 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 907 | 0 | 0 |  |  |  |  | unless (@_ == $self->{ARG_LEN}) { | 
| 908 | 0 |  |  |  |  |  | my $arg_cnt = @_; | 
| 909 | 0 |  |  |  |  |  | confess "Execute argument count $arg_cnt must be $self->{ARG_LEN}"; | 
| 910 |  |  |  |  |  |  | } | 
| 911 | 0 |  |  |  |  |  | my $f = $self->{ARG_FUNC}; | 
| 912 | 0 |  |  |  |  |  | my $rows = $self->{EXEC_FUNC}->(@_); | 
| 913 | 0 |  |  |  |  |  | my $cnt = $self->{CNT}; | 
| 914 | 0 | 0 |  |  |  |  | if ( ++$$cnt >= $self->{COMMIT_SIZE} ) { | 
| 915 | 0 |  |  |  |  |  | $rows = $self->{COMMIT_FUNC}->(); | 
| 916 | 0 |  |  |  |  |  | $$cnt = 0; | 
| 917 |  |  |  |  |  |  | } | 
| 918 | 0 |  |  |  |  |  | return $rows; | 
| 919 |  |  |  |  |  |  | } | 
| 920 |  |  |  |  |  |  |  | 
| 921 |  |  |  |  |  |  | sub finish { | 
| 922 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 923 | 0 |  |  |  |  |  | $self->{FINISH_FUNC}->(); | 
| 924 |  |  |  |  |  |  | } | 
| 925 |  |  |  |  |  |  |  | 
| 926 |  |  |  |  |  |  | package DBIx::BulkUtil::Sybase; | 
| 927 |  |  |  |  |  |  |  | 
| 928 | 1 |  |  | 1 |  | 3 | use Carp qw(confess); | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 1360 |  | 
| 929 |  |  |  |  |  |  |  | 
| 930 |  |  |  |  |  |  | our @ISA = qw(DBIx::BulkUtil::Obj); | 
| 931 |  |  |  |  |  |  |  | 
| 932 | 0 |  |  | 0 |  |  | sub now { 'getdate()' }; | 
| 933 |  |  |  |  |  |  |  | 
| 934 |  |  |  |  |  |  | sub add { | 
| 935 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 936 | 0 |  |  |  |  |  | my $date = shift; | 
| 937 | 0 |  |  |  |  |  | my $n = shift; | 
| 938 | 0 |  |  |  |  |  | my $unit = shift; | 
| 939 | 0 |  |  |  |  |  | my $new_date = "dateadd( $unit, $n, $date)"; | 
| 940 | 0 | 0 |  |  |  |  | return $new_date unless @_; | 
| 941 | 0 |  |  |  |  |  | return $self->add( $new_date, @_ ); | 
| 942 |  |  |  |  |  |  | } | 
| 943 |  |  |  |  |  |  |  | 
| 944 |  |  |  |  |  |  | sub diff { | 
| 945 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 946 | 0 |  |  |  |  |  | my $date1 = shift; | 
| 947 | 0 |  |  |  |  |  | my $date2 = shift; | 
| 948 | 0 |  |  |  |  |  | my $unit = shift; | 
| 949 | 0 |  |  |  |  |  | my $new_date = "datediff( $unit, $date1, $date2)"; | 
| 950 | 0 |  |  |  |  |  | return $new_date; | 
| 951 |  |  |  |  |  |  | } | 
| 952 |  |  |  |  |  |  |  | 
| 953 |  |  |  |  |  |  | sub row_select { | 
| 954 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 955 | 0 |  |  |  |  |  | my $sel = shift; | 
| 956 | 0 |  |  |  |  |  | return "select $sel"; | 
| 957 |  |  |  |  |  |  | } | 
| 958 |  |  |  |  |  |  |  | 
| 959 |  |  |  |  |  |  | sub sp_sth { | 
| 960 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 961 | 0 |  |  |  |  |  | my $sth = $self->{DBH}->prepare($self->sp_sql(@_)); | 
| 962 | 0 |  |  |  |  |  | $sth->execute(); | 
| 963 | 0 |  |  |  |  |  | return $sth; | 
| 964 |  |  |  |  |  |  | } | 
| 965 |  |  |  |  |  |  |  | 
| 966 |  |  |  |  |  |  | sub sp_sql { | 
| 967 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 968 | 0 |  |  |  |  |  | my ($stored_proc, @args) = @_; | 
| 969 | 0 |  |  |  |  |  | return "exec " . join(" ", $stored_proc, join(",", map {$self->{DBH}->quote($_)} grep !/^:cursor$/, @args)); | 
|  | 0 |  |  |  |  |  |  | 
| 970 |  |  |  |  |  |  | } | 
| 971 |  |  |  |  |  |  |  | 
| 972 |  |  |  |  |  |  | # This is trivial in Sybase, but a necessary function for Oracle | 
| 973 |  |  |  |  |  |  | # and so makes this portably compatible | 
| 974 |  |  |  |  |  |  | sub to_datetime { | 
| 975 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 976 | 0 |  |  |  |  |  | my $date = shift; | 
| 977 |  |  |  |  |  |  |  | 
| 978 | 0 |  |  |  |  |  | return "'$date'"; | 
| 979 |  |  |  |  |  |  | } | 
| 980 |  |  |  |  |  |  |  | 
| 981 |  |  |  |  |  |  | sub bcp_in { | 
| 982 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 983 | 0 | 0 |  |  |  |  | my $optref = (ref $_[-1]) ? pop @_ : {}; | 
| 984 | 0 |  |  |  |  |  | my %opts = %$optref; | 
| 985 |  |  |  |  |  |  |  | 
| 986 | 0 |  |  |  |  |  | my ( $table, $file, $dir ) = @_; | 
| 987 | 0 | 0 |  |  |  |  | my $partition = ( $table =~ s/(:\d+)$// ) ? $1 : ''; | 
| 988 |  |  |  |  |  |  |  | 
| 989 | 0 |  | 0 |  |  |  | $file ||= "$table.bcp"; | 
| 990 | 0 |  | 0 |  |  |  | $dir  ||= 'in'; | 
| 991 |  |  |  |  |  |  |  | 
| 992 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 993 | 0 |  |  |  |  |  | my $db = $dbh->{Name}; | 
| 994 | 0 | 0 |  |  |  |  | $db =~ /server=(\w+)/ or confess "Can't determine server for bcp"; | 
| 995 | 0 |  |  |  |  |  | my $server = $1; | 
| 996 | 0 |  |  |  |  |  | my $database = $self->curr_db(); | 
| 997 |  |  |  |  |  |  |  | 
| 998 | 0 |  |  |  |  |  | my $user      = $dbh->{Username}; | 
| 999 | 0 |  | 0 |  |  |  | my $delimiter = $opts{Delimiter} || $self->{DELIMITER}; | 
| 1000 | 0 |  | 0 |  |  |  | my $row_delimiter = $opts{RowDelimiter} || "\n"; | 
| 1001 | 0 |  | 0 |  |  |  | my $commit_size   = $opts{CommitSize} || 1000; | 
| 1002 |  |  |  |  |  |  |  | 
| 1003 | 0 | 0 | 0 |  |  |  | my $bcp_table = | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 1004 |  |  |  |  |  |  | (!$database or $table =~ /^\w+\.\w*\.\w+$/) ? $table | 
| 1005 |  |  |  |  |  |  | : ($table =~ /^\w+$/)                         ? "$database..$table" | 
| 1006 |  |  |  |  |  |  | : ($table =~ /^\w*\.\w+$/)                    ? "$database.$table" | 
| 1007 |  |  |  |  |  |  | : confess "Can not determine database for bcp"; | 
| 1008 |  |  |  |  |  |  |  | 
| 1009 | 0 |  |  |  |  |  | $bcp_table .= $partition; | 
| 1010 |  |  |  |  |  |  |  | 
| 1011 |  |  |  |  |  |  | # Simulate Oracle sqlldr Append/Replace/Truncate | 
| 1012 | 0 |  |  |  |  |  | my $id_cnt; | 
| 1013 | 0 | 0 |  |  |  |  | if ( $dir eq 'in' ) { | 
| 1014 | 0 |  | 0 |  |  |  | my $mode = $opts{Action} || "A"; | 
| 1015 | 0 | 0 |  |  |  |  | if ( $mode eq 'T' ) { | 
|  |  | 0 |  |  |  |  |  | 
| 1016 | 0 |  |  |  |  |  | my $sql = "TRUNCATE TABLE $bcp_table"; | 
| 1017 | 0 |  |  |  |  |  | print "Executing: $sql\n"; | 
| 1018 | 0 |  |  |  |  |  | $dbh->do($sql); | 
| 1019 |  |  |  |  |  |  | } elsif ($mode eq 'R') { | 
| 1020 | 0 |  |  |  |  |  | $self->delete($bcp_table, '', $commit_size); | 
| 1021 |  |  |  |  |  |  | } | 
| 1022 | 0 | 0 |  |  |  |  | confess "BCP file $file does not exist" unless -f $file; | 
| 1023 |  |  |  |  |  |  |  | 
| 1024 |  |  |  |  |  |  | # Save some work | 
| 1025 |  |  |  |  |  |  | # checking underscore ok, we just did -f above | 
| 1026 | 0 | 0 |  |  |  |  | unless ( -s _ ) { | 
| 1027 | 0 |  |  |  |  |  | print "$file is empty. Skipping bcp\n"; | 
| 1028 |  |  |  |  |  |  |  | 
| 1029 |  |  |  |  |  |  | # Make any log file parsers happy | 
| 1030 | 0 |  |  |  |  |  | print "0 rows copied\n"; | 
| 1031 | 0 |  |  |  |  |  | return 0; | 
| 1032 |  |  |  |  |  |  | } | 
| 1033 |  |  |  |  |  |  |  | 
| 1034 |  |  |  |  |  |  | # All this to decide whether or not to use '-E' | 
| 1035 |  |  |  |  |  |  | # Only use '-E' if there is an identity column | 
| 1036 |  |  |  |  |  |  | # And GenerateId is false | 
| 1037 | 0 | 0 |  |  |  |  | unless ( $opts{GenerateId} ) { | 
| 1038 | 0 |  |  |  |  |  | my $col_info = $self->column_info($table); | 
| 1039 | 0 |  |  |  |  |  | my $col_map = $col_info->{MAP}; | 
| 1040 | 0 | 0 |  |  |  |  | if ($col_map) { | 
| 1041 | 0 |  |  |  |  |  | for my $c ( values %$col_map ) { | 
| 1042 | 0 | 0 | 0 |  |  |  | ++$id_cnt and last if $c->{TYPE_NAME} =~ /identity/; | 
| 1043 |  |  |  |  |  |  | } | 
| 1044 |  |  |  |  |  |  | } | 
| 1045 |  |  |  |  |  |  | } | 
| 1046 |  |  |  |  |  |  | } | 
| 1047 |  |  |  |  |  |  |  | 
| 1048 | 0 | 0 |  |  |  |  | my ($action,$to_from) = ($dir eq 'in') ? ('Loading', 'from') : ('Exporting', 'to'); | 
| 1049 | 0 |  |  |  |  |  | print "$action $server/$bcp_table $to_from $file\n"; | 
| 1050 |  |  |  |  |  |  |  | 
| 1051 | 0 |  |  |  |  |  | my (@max_err_opt, @commit_opt, @header_opt, @id_opt); | 
| 1052 | 0 |  | 0 |  |  |  | my $max_err_cnt = $opts{MaxErrors} || 0; | 
| 1053 | 0 | 0 |  |  |  |  | if ( $dir eq 'in' ) { | 
| 1054 | 0 |  |  |  |  |  | @max_err_opt = (-m => $max_err_cnt); | 
| 1055 | 0 |  |  |  |  |  | @commit_opt  = (-b => $commit_size); | 
| 1056 | 0 | 0 |  |  |  |  | @header_opt  = (-F => $opts{Header}+1) if $opts{Header}; | 
| 1057 | 0 | 0 |  |  |  |  | @id_opt = "-E" if $id_cnt; | 
| 1058 |  |  |  |  |  |  | } | 
| 1059 |  |  |  |  |  |  |  | 
| 1060 | 0 |  | 0 |  |  |  | my $keep_temp = $opts{KeepTempFiles} || $opts{Debug}; | 
| 1061 | 0 |  | 0 |  |  |  | my $in_temp_dir = $opts{TempDir}     || $opts{Debug}; | 
| 1062 | 0 |  |  |  |  |  | my $temp_dir; | 
| 1063 | 0 | 0 | 0 |  |  |  | $temp_dir = $opts{TempDir} || "." if $in_temp_dir; | 
| 1064 |  |  |  |  |  |  |  | 
| 1065 | 0 |  |  |  |  |  | require File::Temp; | 
| 1066 | 0 | 0 |  |  |  |  | my @temp_dir = $in_temp_dir ? (DIR => $temp_dir) : (); | 
| 1067 | 0 | 0 |  |  |  |  | my @unlink  = $keep_temp ? (UNLINK => 0) : (); | 
| 1068 | 0 |  |  |  |  |  | my $error_file = File::Temp->new( | 
| 1069 |  |  |  |  |  |  | TEMPLATE => "${table}_XXXXX", | 
| 1070 |  |  |  |  |  |  | SUFFIX   => ".err", | 
| 1071 |  |  |  |  |  |  | @temp_dir, @unlink, | 
| 1072 |  |  |  |  |  |  | ); | 
| 1073 | 0 |  |  |  |  |  | chmod(0664, $error_file->filename()); | 
| 1074 | 0 |  |  |  |  |  | $error_file->close(); | 
| 1075 |  |  |  |  |  |  |  | 
| 1076 | 0 | 0 |  |  |  |  | my @packet_size = $opts{PacketSize} ? ( -A => $opts{PacketSize} ) : (); | 
| 1077 | 0 | 0 |  |  |  |  | my @passthru    = $opts{PassThru}   ? @{$opts{PassThru}} : (); | 
|  | 0 |  |  |  |  |  |  | 
| 1078 |  |  |  |  |  |  |  | 
| 1079 | 0 |  |  |  |  |  | my ( $fmt_file, $tmp_fmt_file ); | 
| 1080 | 0 | 0 | 0 |  |  |  | if ( $opts{FormatFile} ) { | 
|  |  | 0 | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 1081 | 0 |  |  |  |  |  | $fmt_file = $opts{FormatFile}; | 
| 1082 | 0 |  |  |  |  |  | } elsif ( ( $opts{ColumnList} && $opts{ColumnList} ) || ( $opts{Filler} && @{$opts{Filler}} ) ) { | 
| 1083 |  |  |  |  |  |  | ($tmp_fmt_file,$fmt_file) = $self->mk_fmt_file( | 
| 1084 |  |  |  |  |  |  | Table          => $table, | 
| 1085 |  |  |  |  |  |  | Delimiter      => $delimiter, | 
| 1086 |  |  |  |  |  |  | RowDelimiter   => $row_delimiter, | 
| 1087 |  |  |  |  |  |  | ColumnList     => $opts{ColumnList}, | 
| 1088 |  |  |  |  |  |  | Filler         => $opts{Filler}, | 
| 1089 |  |  |  |  |  |  | TempDir        => $opts{TempDir}, | 
| 1090 |  |  |  |  |  |  | FormatFileName => $opts{FormatFileName}, | 
| 1091 | 0 |  |  |  |  |  | KeepTempFiles  => $keep_temp, | 
| 1092 |  |  |  |  |  |  | ); | 
| 1093 |  |  |  |  |  |  | } | 
| 1094 | 0 | 0 |  |  |  |  | my @fmt_file_opt = $fmt_file ? ( -f => $fmt_file ) : '-c'; | 
| 1095 |  |  |  |  |  |  |  | 
| 1096 |  |  |  |  |  |  | # UTF-8 doesn't work on HP - default is roman8 on HP | 
| 1097 |  |  |  |  |  |  | # Should probably make '-J' some kind of option, with maybe | 
| 1098 |  |  |  |  |  |  | # a map of OS types and default values. But leave that for | 
| 1099 |  |  |  |  |  |  | # a later date. | 
| 1100 | 0 |  |  |  |  |  | my @cmd = ( bcp => $bcp_table, $dir, $file, | 
| 1101 |  |  |  |  |  |  | -U => $user, | 
| 1102 |  |  |  |  |  |  | #-J => "utf8", | 
| 1103 |  |  |  |  |  |  | -S => $server, | 
| 1104 |  |  |  |  |  |  | -t => $delimiter, | 
| 1105 |  |  |  |  |  |  | -r => $row_delimiter, | 
| 1106 |  |  |  |  |  |  | -e => $error_file->filename(), | 
| 1107 |  |  |  |  |  |  | @header_opt, | 
| 1108 |  |  |  |  |  |  | @id_opt, | 
| 1109 |  |  |  |  |  |  | @commit_opt, | 
| 1110 |  |  |  |  |  |  | @max_err_opt, | 
| 1111 |  |  |  |  |  |  | @packet_size, | 
| 1112 |  |  |  |  |  |  | @passthru, | 
| 1113 |  |  |  |  |  |  | @fmt_file_opt, | 
| 1114 |  |  |  |  |  |  | ); | 
| 1115 | 0 |  |  |  |  |  | print "Executing: @cmd\n"; | 
| 1116 | 0 |  |  |  |  |  | push @cmd, -P => $self->{PASSWORD}; | 
| 1117 | 0 | 0 |  |  |  |  | open(my $fh, "-|", @cmd) or confess "Can't exec bcp: $!"; | 
| 1118 |  |  |  |  |  |  |  | 
| 1119 | 0 |  |  |  |  |  | my ($rows, $failed, $partially_failed); | 
| 1120 | 0 |  |  |  |  |  | local ($_, $.); | 
| 1121 |  |  |  |  |  |  |  | 
| 1122 | 0 |  |  |  |  |  | my $err_cnt = my $c_lib_err_cnt = my $srvr_err_cnt = 0; | 
| 1123 | 0 |  |  |  |  |  | while (<$fh>) { | 
| 1124 | 0 |  |  |  |  |  | print; | 
| 1125 | 0 | 0 |  |  |  |  | if ( /^(Server|C[TS]LIB) Message/ ) { | 
| 1126 | 0 |  |  |  |  |  | my $msg_type = $1; | 
| 1127 | 0 | 0 |  |  |  |  | if ( $msg_type eq 'CSLIB' ) { | 
|  |  | 0 |  |  |  |  |  | 
| 1128 | 0 | 0 |  |  |  |  | if ( m|/N(\d+)| ) { | 
| 1129 |  |  |  |  |  |  | # Sybase says truncation is not an error, so we will too | 
| 1130 |  |  |  |  |  |  | # Or else we might get > 1 error on the same row | 
| 1131 | 0 | 0 |  |  |  |  | unless ( $1 == 36 ) { | 
| 1132 | 0 |  |  |  |  |  | $err_cnt++; | 
| 1133 | 0 |  |  |  |  |  | $c_lib_err_cnt++; | 
| 1134 |  |  |  |  |  |  | } | 
| 1135 |  |  |  |  |  |  | } | 
| 1136 |  |  |  |  |  |  | } elsif ( $msg_type eq 'CTLIB' ) { | 
| 1137 | 0 |  |  |  |  |  | $err_cnt++; | 
| 1138 | 0 |  |  |  |  |  | $c_lib_err_cnt++; | 
| 1139 |  |  |  |  |  |  | } else { | 
| 1140 |  |  |  |  |  |  | # On server errors the whole batch is an error | 
| 1141 | 0 | 0 |  |  |  |  | if ( /\s(\d+)/ ) { | 
| 1142 |  |  |  |  |  |  | # Ignore 'slow bcp' warning | 
| 1143 | 0 | 0 |  |  |  |  | unless ( $1 == 4852 ) { | 
| 1144 | 0 |  |  |  |  |  | $err_cnt += $commit_size; | 
| 1145 | 0 |  |  |  |  |  | $srvr_err_cnt += $commit_size; | 
| 1146 |  |  |  |  |  |  | } | 
| 1147 |  |  |  |  |  |  | } else { | 
| 1148 | 0 |  |  |  |  |  | $err_cnt += $commit_size; | 
| 1149 | 0 |  |  |  |  |  | $srvr_err_cnt += $commit_size; | 
| 1150 |  |  |  |  |  |  | } | 
| 1151 |  |  |  |  |  |  | } | 
| 1152 |  |  |  |  |  |  | } | 
| 1153 | 0 | 0 |  |  |  |  | $rows = $1 if /^(\d+) rows copied/; | 
| 1154 |  |  |  |  |  |  |  | 
| 1155 |  |  |  |  |  |  | # failed or partially failed | 
| 1156 | 0 | 0 |  |  |  |  | if ( /^bcp copy in ((?:partially )?)failed/ ) { | 
| 1157 | 0 | 0 |  |  |  |  | $partially_failed++ if $1; | 
| 1158 | 0 |  |  |  |  |  | $failed++; | 
| 1159 |  |  |  |  |  |  | } | 
| 1160 |  |  |  |  |  |  | } | 
| 1161 |  |  |  |  |  |  |  | 
| 1162 |  |  |  |  |  |  | # "NaN" (literally "NaN") to numeric errors | 
| 1163 |  |  |  |  |  |  | # do not show up on STDOUT. | 
| 1164 |  |  |  |  |  |  | # So we may as well search the err file to count | 
| 1165 |  |  |  |  |  |  | # all CSLIB and CTLIB errors. | 
| 1166 |  |  |  |  |  |  | # Truncation errors do not show up in file, so we | 
| 1167 |  |  |  |  |  |  | # don't have to filter them out as we would if we | 
| 1168 |  |  |  |  |  |  | # were parsing STDOUT. | 
| 1169 | 0 |  |  |  |  |  | my $err_file_cnt = 0; | 
| 1170 | 0 | 0 |  |  |  |  | open(my $err_h, "<", $error_file->filename()) or die "Failed to open $error_file: $!"; | 
| 1171 | 0 |  |  |  |  |  | while (<$err_h>) { | 
| 1172 | 0 | 0 |  |  |  |  | $err_file_cnt++ if /^#@ Row \d+: Not transferred/; | 
| 1173 |  |  |  |  |  |  | } | 
| 1174 | 0 |  |  |  |  |  | close $err_h; | 
| 1175 |  |  |  |  |  |  |  | 
| 1176 | 0 | 0 |  |  |  |  | if ( $err_file_cnt > $c_lib_err_cnt ) { | 
| 1177 | 0 |  |  |  |  |  | $err_cnt += $err_file_cnt - $c_lib_err_cnt; | 
| 1178 |  |  |  |  |  |  | } | 
| 1179 |  |  |  |  |  |  |  | 
| 1180 |  |  |  |  |  |  | # BCP 11.x,12.x returns meaningful exit status | 
| 1181 |  |  |  |  |  |  | # 10.x does not (returns 0 even on errors) | 
| 1182 | 0 |  |  |  |  |  | my $close_success = close $fh; | 
| 1183 |  |  |  |  |  |  |  | 
| 1184 | 0 | 0 |  |  |  |  | unless ($close_success) { | 
| 1185 | 0 |  |  |  |  |  | my $exit_stat = $? >> 8; | 
| 1186 | 0 |  |  |  |  |  | my $exit_sig  = $? & 127; | 
| 1187 | 0 |  |  |  |  |  | my $exit_core = $? & 128; | 
| 1188 |  |  |  |  |  |  |  | 
| 1189 |  |  |  |  |  |  | # bcp will exit with non-zero status on any 'Server' error, | 
| 1190 |  |  |  |  |  |  | # but not on 'CSLIB' errors unless 'CSLIB' error count exceeds max. | 
| 1191 | 0 | 0 |  |  |  |  | if ( $exit_stat != 0 ) { | 
| 1192 | 0 | 0 |  |  |  |  | if ( $dir eq 'in' ) { | 
| 1193 |  |  |  |  |  |  |  | 
| 1194 |  |  |  |  |  |  | # Some of this may seem unneccessary, but Sybase bcp is | 
| 1195 |  |  |  |  |  |  | # horribly inconsistent. | 
| 1196 |  |  |  |  |  |  |  | 
| 1197 |  |  |  |  |  |  | # Exceeded the error count | 
| 1198 | 0 | 0 |  |  |  |  | confess "BCP error - max error count ($max_err_cnt) exceeded - bcp returned status $exit_stat: $!" | 
| 1199 |  |  |  |  |  |  | if $err_cnt > $max_err_cnt; | 
| 1200 |  |  |  |  |  |  |  | 
| 1201 |  |  |  |  |  |  | # The load was aborted before bcp indicated that it finished | 
| 1202 | 0 | 0 | 0 |  |  |  | confess "BCP error - bcp aborted [$exit_stat]: $!" | 
| 1203 |  |  |  |  |  |  | if !defined($rows) and !$failed; | 
| 1204 |  |  |  |  |  |  |  | 
| 1205 |  |  |  |  |  |  | # BCP failed - even if we allow some errors on a small file, if zero rows are copied | 
| 1206 |  |  |  |  |  |  | # then call it a total failure. | 
| 1207 | 0 | 0 | 0 |  |  |  | confess "BCP error - bcp failed [$exit_stat]: $!" | 
| 1208 |  |  |  |  |  |  | if $failed and !$partially_failed; | 
| 1209 |  |  |  |  |  |  |  | 
| 1210 |  |  |  |  |  |  | } else { | 
| 1211 | 0 |  |  |  |  |  | confess "BCP error - bcp returned status $exit_stat: $!"; | 
| 1212 |  |  |  |  |  |  | } | 
| 1213 |  |  |  |  |  |  | } | 
| 1214 |  |  |  |  |  |  |  | 
| 1215 | 0 | 0 |  |  |  |  | confess "BCP error - bcp recieved signal $exit_sig"      if $exit_sig > 0; | 
| 1216 | 0 | 0 |  |  |  |  | confess "BCP error - bcp coredumped"                     if $exit_core; | 
| 1217 |  |  |  |  |  |  | } | 
| 1218 |  |  |  |  |  |  |  | 
| 1219 |  |  |  |  |  |  | # Will miss error count exceeded error on 10.x | 
| 1220 |  |  |  |  |  |  | # But will catch other errors if load is aborted | 
| 1221 |  |  |  |  |  |  | # Or no rows are loaded. | 
| 1222 | 0 | 0 |  |  |  |  | confess "BCP error - no rows copied" if !defined($rows); | 
| 1223 |  |  |  |  |  |  |  | 
| 1224 |  |  |  |  |  |  | # CTLIB errors do not cause non-zero exit - so catch them here | 
| 1225 | 0 | 0 |  |  |  |  | confess "BCP error - max error count ($max_err_cnt) exceeded" if $err_cnt > $max_err_cnt; | 
| 1226 | 0 |  | 0 |  |  |  | $rows ||= 0; | 
| 1227 | 0 |  |  |  |  |  | return $rows; | 
| 1228 |  |  |  |  |  |  | } | 
| 1229 |  |  |  |  |  |  |  | 
| 1230 |  |  |  |  |  |  | { | 
| 1231 | 1 |  |  | 1 |  | 4 | no warnings 'once'; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 3597 |  | 
| 1232 |  |  |  |  |  |  | *bcp = \&bcp_in; | 
| 1233 |  |  |  |  |  |  | } | 
| 1234 |  |  |  |  |  |  |  | 
| 1235 |  |  |  |  |  |  | sub mk_fmt_file { | 
| 1236 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 1237 | 0 |  |  |  |  |  | my %opts = @_; | 
| 1238 |  |  |  |  |  |  |  | 
| 1239 | 0 |  | 0 |  |  |  | my $table = $opts{Table} || die "Table required for mk_fmt_file"; | 
| 1240 | 0 |  |  |  |  |  | my $col_info = $self->column_info($table); | 
| 1241 | 0 |  |  |  |  |  | my $db_col_list = $col_info->{LIST}; | 
| 1242 | 0 |  |  |  |  |  | my %is_db_column; | 
| 1243 | 0 |  |  |  |  |  | $is_db_column{$_}++ for @$db_col_list; | 
| 1244 | 0 |  |  |  |  |  | my %is_filler; | 
| 1245 | 0 | 0 |  |  |  |  | if ( $opts{Filler} ) { | 
| 1246 | 0 |  |  |  |  |  | $is_filler{lc($_)}++ for @{$opts{Filler}}; | 
|  | 0 |  |  |  |  |  |  | 
| 1247 |  |  |  |  |  |  | } | 
| 1248 |  |  |  |  |  |  |  | 
| 1249 | 0 |  |  |  |  |  | my ($tmp_fmt_file,$fmt_file); | 
| 1250 | 0 | 0 |  |  |  |  | if ( $opts{FormatFileName} ) { | 
| 1251 | 0 |  |  |  |  |  | $fmt_file = $opts{FormatFileName}; | 
| 1252 |  |  |  |  |  |  | } else { | 
| 1253 | 0 |  |  |  |  |  | require File::Temp; | 
| 1254 | 0 |  | 0 |  |  |  | my $keep_temp = $opts{KeepTempFiles} || $opts{Debug}; | 
| 1255 | 0 |  | 0 |  |  |  | my $in_temp_dir = $opts{TempDir}     || $opts{Debug}; | 
| 1256 | 0 |  |  |  |  |  | my $temp_dir; | 
| 1257 |  |  |  |  |  |  |  | 
| 1258 | 0 | 0 | 0 |  |  |  | $temp_dir = $opts{TempDir} || "." if $in_temp_dir; | 
| 1259 | 0 | 0 |  |  |  |  | my @temp_dir = $in_temp_dir ? (DIR => $temp_dir) : (); | 
| 1260 | 0 | 0 | 0 |  |  |  | my @unlink  = ( $keep_temp || !defined(wantarray) ) ? (UNLINK => 0) : (); | 
| 1261 | 0 |  |  |  |  |  | $tmp_fmt_file = File::Temp->new( | 
| 1262 |  |  |  |  |  |  | TEMPLATE => "${table}_XXXXX", | 
| 1263 |  |  |  |  |  |  | SUFFIX   => ".fmt", | 
| 1264 |  |  |  |  |  |  | @temp_dir, @unlink, | 
| 1265 |  |  |  |  |  |  | ); | 
| 1266 | 0 |  |  |  |  |  | $fmt_file = $tmp_fmt_file->filename(); | 
| 1267 | 0 |  |  |  |  |  | chmod(0664, $tmp_fmt_file); | 
| 1268 | 0 |  |  |  |  |  | $tmp_fmt_file->close(); | 
| 1269 |  |  |  |  |  |  | } | 
| 1270 |  |  |  |  |  |  |  | 
| 1271 | 0 |  | 0 |  |  |  | my $delim = $opts{Delimiter} || "|"; | 
| 1272 | 0 |  | 0 |  |  |  | my $row_delim = $opts{RowDelimiter} || "\n"; | 
| 1273 |  |  |  |  |  |  |  | 
| 1274 |  |  |  |  |  |  | # Need escaped text in fmt file | 
| 1275 |  |  |  |  |  |  | # for CR/LF | 
| 1276 | 0 |  |  |  |  |  | for ($delim,$row_delim) { | 
| 1277 | 0 |  |  |  |  |  | s/\n/\\n/g; | 
| 1278 | 0 |  |  |  |  |  | s/\r/\\r/g; | 
| 1279 |  |  |  |  |  |  | } | 
| 1280 |  |  |  |  |  |  |  | 
| 1281 |  |  |  |  |  |  | my @col_list = ( $opts{ColumnList} && @{$opts{ColumnList}} ) | 
| 1282 | 0 |  |  |  |  |  | ? @{$opts{ColumnList}} | 
| 1283 | 0 | 0 | 0 |  |  |  | : @{$col_info->{LIST}}; | 
|  | 0 |  |  |  |  |  |  | 
| 1284 |  |  |  |  |  |  |  | 
| 1285 | 0 |  |  |  |  |  | my $ncols = @col_list; | 
| 1286 | 0 | 0 |  |  |  |  | open( my $fh, ">", $fmt_file ) or confess "Failed to open $fmt_file: $!"; | 
| 1287 | 0 |  |  |  |  |  | print $fh "10.0\n"; | 
| 1288 | 0 |  |  |  |  |  | print $fh "$ncols\n"; | 
| 1289 |  |  |  |  |  |  |  | 
| 1290 | 0 |  |  |  |  |  | my $col_map = $col_info->{MAP}; | 
| 1291 | 0 |  |  |  |  |  | for my $i (1..$ncols) { | 
| 1292 | 0 |  |  |  |  |  | my $name = $col_list[$i-1]; | 
| 1293 | 0 | 0 |  |  |  |  | my $d = ( $i == $ncols ) ? $row_delim : $delim; | 
| 1294 | 0 |  |  |  |  |  | my @row = ($i, 'SYBCHAR', 0); | 
| 1295 | 0 | 0 |  |  |  |  | if ($is_filler{lc($name)}) { | 
|  |  | 0 |  |  |  |  |  | 
| 1296 | 0 |  |  |  |  |  | push @row, 0, qq["$d"], 0; | 
| 1297 |  |  |  |  |  |  | } elsif ($is_db_column{lc($name)}) { | 
| 1298 | 0 |  |  |  |  |  | my $info = $col_map->{lc($name)}; | 
| 1299 |  |  |  |  |  |  |  | 
| 1300 |  |  |  |  |  |  | # Native Sybase date format size is 26 though metadata says 23 | 
| 1301 |  |  |  |  |  |  | # For numbers, add extra for decimal | 
| 1302 |  |  |  |  |  |  | my $size = | 
| 1303 |  |  |  |  |  |  | ( $info->{TYPE_NAME} =~ /date/ ) ? 26 | 
| 1304 |  |  |  |  |  |  | : ( $info->{TYPE_NAME} =~ /char|text/ ) ? $info->{COLUMN_SIZE} | 
| 1305 | 0 | 0 |  |  |  |  | : $info->{COLUMN_SIZE} + 1; | 
|  |  | 0 |  |  |  |  |  | 
| 1306 | 0 |  |  |  |  |  | push @row, $size, qq["$d"], $info->{ORDINAL_POSITION}, $name; | 
| 1307 | 0 |  |  |  |  |  | } else { confess "$name is neither a db nor filler column" } | 
| 1308 | 0 |  |  |  |  |  | print $fh join("\t", @row), "\n"; | 
| 1309 |  |  |  |  |  |  | } | 
| 1310 |  |  |  |  |  |  |  | 
| 1311 | 0 |  |  |  |  |  | close $fh; | 
| 1312 |  |  |  |  |  |  |  | 
| 1313 |  |  |  |  |  |  |  | 
| 1314 |  |  |  |  |  |  | # Also return temp object so it will not be cleaned up yet | 
| 1315 |  |  |  |  |  |  | return | 
| 1316 | 0 | 0 |  |  |  |  | wantarray ?  ($tmp_fmt_file, $fmt_file) | 
|  |  | 0 |  |  |  |  |  | 
| 1317 |  |  |  |  |  |  | : $tmp_fmt_file ? $tmp_fmt_file | 
| 1318 |  |  |  |  |  |  | : $fmt_file; | 
| 1319 |  |  |  |  |  |  |  | 
| 1320 |  |  |  |  |  |  | } | 
| 1321 |  |  |  |  |  |  |  | 
| 1322 |  |  |  |  |  |  | sub bcp_out { | 
| 1323 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 1324 | 0 |  |  |  |  |  | my @opts; | 
| 1325 | 0 | 0 |  |  |  |  | if (ref $_[-1]) { | 
| 1326 | 0 |  |  |  |  |  | @opts = pop @_; | 
| 1327 |  |  |  |  |  |  | } | 
| 1328 | 0 |  |  |  |  |  | my ($table, $file) = @_; | 
| 1329 | 0 |  | 0 |  |  |  | $file ||= "$table.bcp"; | 
| 1330 |  |  |  |  |  |  |  | 
| 1331 | 0 | 0 | 0 |  |  |  | my $scratchdb = @opts ? $opts[0]{TempDb} || 'scratchdb' : 'scratchdb'; | 
| 1332 |  |  |  |  |  |  |  | 
| 1333 |  |  |  |  |  |  | # Sybase rounds money columns, need to bcp a view of it | 
| 1334 |  |  |  |  |  |  | # if any exist. | 
| 1335 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 1336 |  |  |  |  |  |  |  | 
| 1337 |  |  |  |  |  |  | # Need to save current db in case view is created | 
| 1338 | 0 |  |  |  |  |  | my $curr_db = $self->curr_db(); | 
| 1339 | 0 |  |  |  |  |  | my $view = $self->mk_view($table, @opts); | 
| 1340 |  |  |  |  |  |  |  | 
| 1341 | 0 |  | 0 |  |  |  | my $rows = eval { $self->bcp($view || $table, $file, 'out', @opts) }; | 
|  | 0 |  |  |  |  |  |  | 
| 1342 | 0 | 0 |  |  |  |  | unless (defined $rows) { | 
| 1343 | 0 |  |  |  |  |  | my $err = $@; | 
| 1344 | 0 | 0 |  |  |  |  | if ($view) { | 
| 1345 | 0 |  |  |  |  |  | warn "BCP error detected - dropping view $view\n"; | 
| 1346 | 0 |  |  |  |  |  | my $result = eval { $dbh->do("DROP VIEW $view") }; | 
|  | 0 |  |  |  |  |  |  | 
| 1347 | 0 | 0 |  |  |  |  | warn "Unable to drop view $view: $@" unless $result; | 
| 1348 | 0 | 0 | 0 |  |  |  | $dbh->do("USE $curr_db") if !$self->is_iq() and $curr_db; | 
| 1349 |  |  |  |  |  |  | } | 
| 1350 | 0 |  |  |  |  |  | confess $err; | 
| 1351 |  |  |  |  |  |  | } | 
| 1352 |  |  |  |  |  |  |  | 
| 1353 | 0 | 0 |  |  |  |  | if ($view) { | 
| 1354 | 0 |  |  |  |  |  | print "Dropping view $view\n"; | 
| 1355 | 0 |  |  |  |  |  | $dbh->do("DROP VIEW $view"); | 
| 1356 | 0 | 0 | 0 |  |  |  | $dbh->do("USE $curr_db") if !$self->is_iq() and $curr_db; | 
| 1357 |  |  |  |  |  |  | } | 
| 1358 | 0 | 0 | 0 |  |  |  | if ( !@opts or !$opts[0]{NoFix} ) { | 
| 1359 | 0 |  |  |  |  |  | my $bak = eval { $self->fix_bcp_file($file, @opts) }; | 
|  | 0 |  |  |  |  |  |  | 
| 1360 | 0 | 0 |  |  |  |  | if ( $bak ) { | 
| 1361 | 0 |  |  |  |  |  | unlink $bak; | 
| 1362 |  |  |  |  |  |  | } else { | 
| 1363 | 0 |  |  |  |  |  | warn "Error processing $file. BCP file in $file.bak: $@\n"; | 
| 1364 | 0 |  |  |  |  |  | return; | 
| 1365 |  |  |  |  |  |  | } | 
| 1366 |  |  |  |  |  |  | } | 
| 1367 | 0 | 0 | 0 |  |  |  | if ( @opts and ( $opts[0]{Header} || $opts[0]{QuoteFields} ) ) { | 
|  |  |  | 0 |  |  |  |  | 
| 1368 | 0 |  |  |  |  |  | my $bak = eval { $self->add_header($table, $file, @opts) }; | 
|  | 0 |  |  |  |  |  |  | 
| 1369 | 0 | 0 |  |  |  |  | if ( $bak ) { | 
| 1370 | 0 |  |  |  |  |  | unlink $bak; | 
| 1371 |  |  |  |  |  |  | } else { | 
| 1372 | 0 |  |  |  |  |  | warn "Error post processing $file. BCP file in $file.bak: $@\n"; | 
| 1373 | 0 |  |  |  |  |  | return; | 
| 1374 |  |  |  |  |  |  | } | 
| 1375 |  |  |  |  |  |  | } | 
| 1376 | 0 |  |  |  |  |  | return $rows; | 
| 1377 |  |  |  |  |  |  | } | 
| 1378 |  |  |  |  |  |  |  | 
| 1379 |  |  |  |  |  |  | sub mk_view { | 
| 1380 |  |  |  |  |  |  |  | 
| 1381 | 0 |  |  | 0 |  |  | my ($self,$table) = @_; | 
| 1382 | 0 |  |  |  |  |  | my @opts; | 
| 1383 | 0 | 0 |  |  |  |  | @opts = pop @_ if ref $_[-1]; | 
| 1384 | 0 | 0 | 0 |  |  |  | my $scratchdb = @opts ? $opts[0]{TempDb} || 'scratchdb' : 'scratchdb'; | 
| 1385 |  |  |  |  |  |  |  | 
| 1386 |  |  |  |  |  |  | # Sybase rounds money columns, need to bcp a view of it | 
| 1387 |  |  |  |  |  |  | # if any exist. | 
| 1388 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 1389 |  |  |  |  |  |  |  | 
| 1390 | 0 |  |  |  |  |  | my $col_info = $self->column_info($table); | 
| 1391 |  |  |  |  |  |  |  | 
| 1392 |  |  |  |  |  |  | # Columns might be a string from a SELECT clause | 
| 1393 |  |  |  |  |  |  | # Or it might be an arrayref of columns | 
| 1394 |  |  |  |  |  |  | my $col_list = ( @opts && $opts[0]{Columns} ) | 
| 1395 |  |  |  |  |  |  | ? $opts[0]{Columns} | 
| 1396 | 0 | 0 | 0 |  |  |  | : $col_info->{LIST}; | 
| 1397 |  |  |  |  |  |  |  | 
| 1398 | 0 |  |  |  |  |  | my $col_map  = $col_info->{MAP}; | 
| 1399 |  |  |  |  |  |  |  | 
| 1400 | 0 |  |  |  |  |  | my @columns; | 
| 1401 | 0 |  |  |  |  |  | my $money_cnt = 0; | 
| 1402 |  |  |  |  |  |  |  | 
| 1403 | 0 |  |  |  |  |  | my $column_str; | 
| 1404 | 0 | 0 |  |  |  |  | if ( ref $col_list ) { | 
| 1405 | 0 |  |  |  |  |  | for my $name (@$col_list) { | 
| 1406 | 0 |  |  |  |  |  | my $col_name = $name; | 
| 1407 | 0 | 0 |  |  |  |  | if ( my $info = $col_map->{$name} ) { | 
| 1408 | 0 |  |  |  |  |  | my $type = $info->{TYPE_NAME}; | 
| 1409 | 0 |  |  |  |  |  | $col_name = $info->{COLUMN_NAME}; | 
| 1410 | 0 | 0 |  |  |  |  | if ($type =~ /money/) { | 
| 1411 | 0 |  |  |  |  |  | $money_cnt++; | 
| 1412 | 0 | 0 |  |  |  |  | my $len = ($type =~ /small/) ? 10 : 19; | 
| 1413 | 0 |  |  |  |  |  | $col_name = "convert(decimal($len,4), $col_name) $col_name"; | 
| 1414 |  |  |  |  |  |  | } | 
| 1415 |  |  |  |  |  |  | } | 
| 1416 | 0 |  |  |  |  |  | push @columns, $col_name; | 
| 1417 |  |  |  |  |  |  | } | 
| 1418 | 0 |  |  |  |  |  | $column_str = join ",", @columns; | 
| 1419 |  |  |  |  |  |  | } else { | 
| 1420 | 0 |  |  |  |  |  | $column_str = $col_list; | 
| 1421 |  |  |  |  |  |  | } | 
| 1422 |  |  |  |  |  |  |  | 
| 1423 | 0 | 0 | 0 |  |  |  | return if $money_cnt==0 and !$opts[0]{Filter} and !$opts[0]{Columns}; | 
|  |  |  | 0 |  |  |  |  | 
| 1424 |  |  |  |  |  |  |  | 
| 1425 | 0 |  |  |  |  |  | my ($view, $db_view); | 
| 1426 |  |  |  |  |  |  |  | 
| 1427 | 0 |  |  |  |  |  | my $curr_db = $self->curr_db(); | 
| 1428 | 0 | 0 | 0 |  |  |  | if ( !$curr_db and $table =~ /^(\w+)\.\w*\.\w+$/ ) { | 
| 1429 | 0 |  |  |  |  |  | $curr_db = $1; | 
| 1430 |  |  |  |  |  |  | } | 
| 1431 | 0 | 0 |  |  |  |  | confess "Can not determine database" unless $curr_db; | 
| 1432 |  |  |  |  |  |  |  | 
| 1433 | 0 | 0 | 0 |  |  |  | my $base_table = | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 1434 |  |  |  |  |  |  | (!$curr_db or $table =~ /^\w+\.\w*\.\w+$/)  ? $table | 
| 1435 |  |  |  |  |  |  | : ($table =~ /^\w+$/)                         ? "$curr_db..$table" | 
| 1436 |  |  |  |  |  |  | : ($table =~ /^\w*\.\w+$/)                    ? "$curr_db.$table" | 
| 1437 |  |  |  |  |  |  | : confess "Can not determine database for view"; | 
| 1438 |  |  |  |  |  |  |  | 
| 1439 | 0 |  |  |  |  |  | ( my $tmp_view = $base_table ) =~ s/.*\.//; | 
| 1440 | 0 | 0 |  |  |  |  | $tmp_view = substr($tmp_view, 0, 19) if length($tmp_view) > 19; | 
| 1441 |  |  |  |  |  |  |  | 
| 1442 | 0 | 0 |  |  |  |  | $dbh->do("USE $scratchdb") unless $self->is_iq(); | 
| 1443 |  |  |  |  |  |  |  | 
| 1444 | 0 |  |  |  |  |  | my $cnt; | 
| 1445 | 0 |  |  |  |  |  | while (1) { | 
| 1446 | 0 |  |  |  |  |  | my ($sec, $min, $hr) = localtime; | 
| 1447 | 0 |  |  |  |  |  | my $id = sprintf("%05d%02d%02d%02d", $$, $hr, $min, $sec); | 
| 1448 |  |  |  |  |  |  |  | 
| 1449 | 0 |  |  |  |  |  | $view = "${tmp_view}${id}"; | 
| 1450 | 0 | 0 |  |  |  |  | $db_view = $self->is_iq() ? $view : "$scratchdb..$view"; | 
| 1451 | 0 |  |  |  |  |  | my $sql = sprintf( | 
| 1452 |  |  |  |  |  |  | "CREATE VIEW %s AS SELECT %s FROM %s", | 
| 1453 |  |  |  |  |  |  | $view, | 
| 1454 |  |  |  |  |  |  | $column_str, | 
| 1455 |  |  |  |  |  |  | $base_table, | 
| 1456 |  |  |  |  |  |  | ); | 
| 1457 | 0 | 0 | 0 |  |  |  | $sql .= " $opts[0]{Filter}" if @opts && $opts[0]{Filter}; | 
| 1458 | 0 |  |  |  |  |  | print "Creating view $db_view\n"; | 
| 1459 | 0 |  |  |  |  |  | print "Executing: $sql\n"; | 
| 1460 | 0 |  |  |  |  |  | my $result = eval { $dbh->do($sql) }; | 
|  | 0 |  |  |  |  |  |  | 
| 1461 | 0 | 0 |  |  |  |  | return $view if $result; | 
| 1462 | 0 | 0 |  |  |  |  | confess $@ unless $@ =~ /already an object/; | 
| 1463 | 0 |  |  |  |  |  | $cnt++; | 
| 1464 | 0 | 0 |  |  |  |  | confess "Too many retries trying to create view $db_view. Aborting" | 
| 1465 |  |  |  |  |  |  | if $cnt > 20; | 
| 1466 | 0 |  |  |  |  |  | print "View $db_view already exists, retrying #$cnt..."; | 
| 1467 | 0 |  |  |  |  |  | sleep 2; | 
| 1468 |  |  |  |  |  |  | } | 
| 1469 |  |  |  |  |  |  |  | 
| 1470 |  |  |  |  |  |  | } | 
| 1471 |  |  |  |  |  |  |  | 
| 1472 |  |  |  |  |  |  | # Fix native date format from Sybase bcp out | 
| 1473 |  |  |  |  |  |  | {   my %mons = qw( Jan 1 Feb 2 Mar 3 Apr 4 May 5 Jun 6 Jul 7 Aug 8 Sep 9 Oct 10 Nov 11 Dec 12 ); | 
| 1474 |  |  |  |  |  |  | my $mon_str = join '|', keys %mons; | 
| 1475 |  |  |  |  |  |  | my $mon_re = qr/$mon_str/; | 
| 1476 |  |  |  |  |  |  |  | 
| 1477 |  |  |  |  |  |  | sub fix_bcp_file { | 
| 1478 | 0 |  |  | 0 |  |  | my ( $self, $file ) = @_; | 
| 1479 | 0 |  |  |  |  |  | my $opts = {}; | 
| 1480 | 0 | 0 |  |  |  |  | if (ref $_[-1]) { | 
| 1481 | 0 |  |  |  |  |  | $opts = pop @_; | 
| 1482 |  |  |  |  |  |  | } | 
| 1483 | 0 |  | 0 |  |  |  | my $delimiter = $opts->{Delimiter} || $self->{DELIMITER} || '|'; | 
| 1484 | 0 |  |  |  |  |  | my $dre = quotemeta($delimiter); | 
| 1485 | 0 |  |  |  |  |  | local ($_, $., $ARGV, *ARGV); | 
| 1486 | 0 |  |  |  |  |  | local ( $^I, @ARGV ) = ( '.bak', $file ); | 
| 1487 | 0 |  | 0 |  |  |  | local $/ = $opts->{RowDelimiter} || $/; | 
| 1488 | 0 |  |  |  |  |  | while ( <> ) { | 
| 1489 | 0 |  |  |  |  |  | 1 while s!(^|$dre)($mon_re)\s{1,2}(\d{1,2})\s(\d{4})\s\s?(\d\d?):(\d\d):(\d\d):(\d{3})([AP])M($dre|$/)! | 
| 1490 |  |  |  |  |  |  | $1 . | 
| 1491 |  |  |  |  |  |  | sprintf( '%04d-%02d-%02d %02d:%02d:%02d.%03d', | 
| 1492 |  |  |  |  |  |  | $4, | 
| 1493 | 0 | 0 | 0 |  |  |  | $mons{ $2 }, | 
|  |  | 0 | 0 |  |  |  |  | 
| 1494 |  |  |  |  |  |  | $3, | 
| 1495 |  |  |  |  |  |  | ( $9 eq 'P' && $5 < 12) ? $5 + 12 : ( $9 eq 'A' && $5 == 12 ) ? 0 : $5, | 
| 1496 |  |  |  |  |  |  | $6, | 
| 1497 |  |  |  |  |  |  | $7, | 
| 1498 |  |  |  |  |  |  | $8 ) . | 
| 1499 |  |  |  |  |  |  | $10 | 
| 1500 |  |  |  |  |  |  | !eg; | 
| 1501 | 0 |  |  |  |  |  | 1 while s!(^|$dre)($mon_re)\s{1,2}(\d{1,2})\s(\d{4})\s\s?(\d\d?):(\d\d)([AP])M($dre|$/)! | 
| 1502 |  |  |  |  |  |  | $1 . | 
| 1503 |  |  |  |  |  |  | sprintf( '%04d-%02d-%02d %02d:%02d', | 
| 1504 |  |  |  |  |  |  | $4, | 
| 1505 | 0 | 0 | 0 |  |  |  | $mons{ $2 }, | 
|  |  | 0 | 0 |  |  |  |  | 
| 1506 |  |  |  |  |  |  | $3, | 
| 1507 |  |  |  |  |  |  | ( $7 eq 'P' && $5 < 12) ? $5 + 12 : ( $7 eq 'A' && $5 == 12 ) ? 0 : $5, | 
| 1508 |  |  |  |  |  |  | $6 ) . | 
| 1509 |  |  |  |  |  |  | $8 | 
| 1510 |  |  |  |  |  |  | !eg; | 
| 1511 | 0 |  |  |  |  |  | 1 while s!(^|$dre)($mon_re)\s{1,2}(\d{1,2})\s(\d{4})($dre|$/)! | 
| 1512 |  |  |  |  |  |  | $1 . | 
| 1513 |  |  |  |  |  |  | sprintf( '%04d-%02d-%02d', | 
| 1514 |  |  |  |  |  |  | $4, | 
| 1515 | 0 |  |  |  |  |  | $mons{ $2 }, | 
| 1516 |  |  |  |  |  |  | $3 ) . | 
| 1517 |  |  |  |  |  |  | $5 | 
| 1518 |  |  |  |  |  |  | !eg; | 
| 1519 | 0 |  |  |  |  |  | print; | 
| 1520 |  |  |  |  |  |  | } | 
| 1521 | 0 |  |  |  |  |  | return "$file.bak"; | 
| 1522 |  |  |  |  |  |  | } | 
| 1523 |  |  |  |  |  |  | } | 
| 1524 |  |  |  |  |  |  |  | 
| 1525 |  |  |  |  |  |  | { | 
| 1526 |  |  |  |  |  |  | my %type_map = ( 'V' => 'V', 'P' => 'P', 'U' => 'T' ); | 
| 1527 |  |  |  |  |  |  |  | 
| 1528 |  |  |  |  |  |  | sub obj_type { | 
| 1529 | 0 |  |  | 0 |  |  | my ( $self, $name ) = @_; | 
| 1530 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 1531 | 0 |  |  |  |  |  | my $qname = $dbh->quote($name); | 
| 1532 | 0 |  |  |  |  |  | my ( $type ) = $dbh->selectrow_array("select type from sysobjects where name = $qname"); | 
| 1533 | 0 | 0 |  |  |  |  | return unless $type; | 
| 1534 | 0 |  | 0 |  |  |  | return $type_map{$type} || confess "Don't know about type $type for object $name"; | 
| 1535 |  |  |  |  |  |  | } | 
| 1536 |  |  |  |  |  |  | } | 
| 1537 |  |  |  |  |  |  |  | 
| 1538 |  |  |  |  |  |  | sub curr_db { | 
| 1539 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 1540 |  |  |  |  |  |  |  | 
| 1541 | 0 |  |  |  |  |  | $self->get('db_name()'); | 
| 1542 |  |  |  |  |  |  | } | 
| 1543 |  |  |  |  |  |  |  | 
| 1544 | 0 |  |  | 0 |  |  | sub curr_schema { undef } | 
| 1545 |  |  |  |  |  |  |  | 
| 1546 |  |  |  |  |  |  | { | 
| 1547 |  |  |  |  |  |  |  | 
| 1548 |  |  |  |  |  |  | # Can get errors in some databases if you don't add dbo to everything | 
| 1549 |  |  |  |  |  |  | my $sql_t = < | 
| 1550 |  |  |  |  |  |  | SELECT | 
| 1551 |  |  |  |  |  |  | dbo.sysindexes.name, | 
| 1552 |  |  |  |  |  |  | index_col(object_name(dbo.sysindexes.id), dbo.sysindexes.indid, dbo.syscolumns.colid) col_name | 
| 1553 |  |  |  |  |  |  | FROM dbo.sysindexes, dbo.syscolumns | 
| 1554 |  |  |  |  |  |  | WHERE dbo.sysindexes.id = dbo.syscolumns.id | 
| 1555 |  |  |  |  |  |  | AND dbo.syscolumns.colid <= dbo.sysindexes.keycnt | 
| 1556 |  |  |  |  |  |  | AND dbo.sysindexes.id = object_id(%s) | 
| 1557 |  |  |  |  |  |  | SQL | 
| 1558 |  |  |  |  |  |  |  | 
| 1559 |  |  |  |  |  |  | sub index_info { | 
| 1560 | 0 |  |  | 0 |  |  | my ( $self, $table, $all_indexes ) = @_; | 
| 1561 |  |  |  |  |  |  |  | 
| 1562 | 0 |  |  |  |  |  | my ($tmp_db, $curr_db) = (undef,''); | 
| 1563 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 1564 |  |  |  |  |  |  |  | 
| 1565 | 0 |  |  |  |  |  | my $schema = ''; | 
| 1566 | 0 |  |  |  |  |  | $tmp_db = $curr_db = $self->curr_db(); | 
| 1567 |  |  |  |  |  |  |  | 
| 1568 | 0 | 0 |  |  |  |  | if ( $table =~ /^(?:(\w+)\.)?(\w*)\.(\w+)$/ ) { | 
| 1569 | 0 |  |  |  |  |  | ($tmp_db, $schema, $table) = ($1,$2,$3); | 
| 1570 | 0 |  |  |  |  |  | $table = "$schema.$table"; | 
| 1571 |  |  |  |  |  |  |  | 
| 1572 |  |  |  |  |  |  | # We can only get info on the current database | 
| 1573 | 0 | 0 | 0 |  |  |  | if ( defined($tmp_db) and $tmp_db ne $curr_db ) { | 
| 1574 | 0 |  |  |  |  |  | $dbh->do("USE $tmp_db"); | 
| 1575 |  |  |  |  |  |  | } | 
| 1576 |  |  |  |  |  |  | } | 
| 1577 |  |  |  |  |  |  |  | 
| 1578 | 0 |  |  |  |  |  | my $sql = sprintf $sql_t, $dbh->quote($table); | 
| 1579 | 0 | 0 |  |  |  |  | $sql .= "AND dbo.sysindexes.status & 2 = 2\n" unless $all_indexes; | 
| 1580 | 0 |  |  |  |  |  | $sql .= "ORDER BY dbo.syscolumns.colid\n"; | 
| 1581 | 0 |  |  |  |  |  | my $sth = $dbh->prepare($sql); | 
| 1582 | 0 |  |  |  |  |  | $sth->execute(); | 
| 1583 | 0 |  |  |  |  |  | my @col_names = @{$sth->{NAME_lc}}; | 
|  | 0 |  |  |  |  |  |  | 
| 1584 | 0 |  |  |  |  |  | my %row; $sth->bind_columns(\@row{@col_names}); | 
|  | 0 |  |  |  |  |  |  | 
| 1585 | 0 |  |  |  |  |  | my %ind; | 
| 1586 | 0 |  |  |  |  |  | while ($sth->fetch()) { | 
| 1587 | 0 | 0 |  |  |  |  | if ( $row{col_name} ) { | 
| 1588 | 0 |  |  |  |  |  | push @{$ind{$row{name}}}, lc($row{col_name}); | 
|  | 0 |  |  |  |  |  |  | 
| 1589 |  |  |  |  |  |  | } | 
| 1590 |  |  |  |  |  |  | } | 
| 1591 |  |  |  |  |  |  |  | 
| 1592 | 0 | 0 | 0 |  |  |  | $dbh->do("USE $curr_db") if defined($tmp_db) and $tmp_db ne $curr_db; | 
| 1593 |  |  |  |  |  |  |  | 
| 1594 | 0 | 0 |  |  |  |  | return unless %ind; | 
| 1595 | 0 |  |  |  |  |  | return \%ind; | 
| 1596 |  |  |  |  |  |  | } | 
| 1597 |  |  |  |  |  |  |  | 
| 1598 |  |  |  |  |  |  | } | 
| 1599 |  |  |  |  |  |  |  | 
| 1600 |  |  |  |  |  |  | sub primary_key { | 
| 1601 | 0 |  |  | 0 |  |  | my ( $self, $table ) = @_; | 
| 1602 | 0 |  |  |  |  |  | my $schema; | 
| 1603 | 0 |  |  |  |  |  | my ($tmp_db, $curr_db) = (undef,''); | 
| 1604 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 1605 |  |  |  |  |  |  |  | 
| 1606 | 0 |  |  |  |  |  | $tmp_db = $curr_db = $self->curr_db(); | 
| 1607 |  |  |  |  |  |  |  | 
| 1608 | 0 | 0 |  |  |  |  | if ( $table =~ /^(?:(\w+)\.)?(\w*)\.(\w+)$/ ) { | 
| 1609 | 0 |  |  |  |  |  | ($tmp_db, $schema, $table) = ($1,$2,$3); | 
| 1610 | 0 |  | 0 |  |  |  | $schema ||= undef; | 
| 1611 |  |  |  |  |  |  |  | 
| 1612 |  |  |  |  |  |  | # We can only get column info on the current database | 
| 1613 | 0 | 0 | 0 |  |  |  | $dbh->do("USE $tmp_db") if defined($tmp_db) and $tmp_db ne $curr_db; | 
| 1614 |  |  |  |  |  |  | } | 
| 1615 |  |  |  |  |  |  |  | 
| 1616 | 0 |  |  |  |  |  | my @pk = $self->{DBH}->primary_key($tmp_db, $schema, $table); | 
| 1617 |  |  |  |  |  |  |  | 
| 1618 | 0 | 0 | 0 |  |  |  | $dbh->do("USE $curr_db") if defined($tmp_db) and $tmp_db ne $curr_db; | 
| 1619 |  |  |  |  |  |  |  | 
| 1620 | 0 | 0 |  |  |  |  | return unless @pk; | 
| 1621 |  |  |  |  |  |  |  | 
| 1622 | 0 |  |  |  |  |  | return \@pk; | 
| 1623 |  |  |  |  |  |  | } | 
| 1624 |  |  |  |  |  |  |  | 
| 1625 |  |  |  |  |  |  | { | 
| 1626 |  |  |  |  |  |  |  | 
| 1627 |  |  |  |  |  |  | my $del_sql = < | 
| 1628 |  |  |  |  |  |  | DELETE %s | 
| 1629 |  |  |  |  |  |  | FROM %s d, %s s | 
| 1630 |  |  |  |  |  |  | WHERE %s | 
| 1631 |  |  |  |  |  |  | SQL | 
| 1632 |  |  |  |  |  |  |  | 
| 1633 |  |  |  |  |  |  | my $ins_sql = < | 
| 1634 |  |  |  |  |  |  | SELECT %s | 
| 1635 |  |  |  |  |  |  | FROM %s | 
| 1636 |  |  |  |  |  |  | SQL | 
| 1637 |  |  |  |  |  |  |  | 
| 1638 |  |  |  |  |  |  | sub merge { | 
| 1639 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 1640 | 0 |  |  |  |  |  | my %args = @_; | 
| 1641 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 1642 |  |  |  |  |  |  |  | 
| 1643 | 0 |  |  |  |  |  | my $table     = lc($args{Table}); | 
| 1644 | 0 |  |  |  |  |  | my $stg_table = lc($args{StgTable}); | 
| 1645 |  |  |  |  |  |  |  | 
| 1646 | 0 |  |  |  |  |  | my $tbl_info = $self->column_info($table); | 
| 1647 | 0 |  |  |  |  |  | my $tbl_map  = $tbl_info->{MAP}; | 
| 1648 |  |  |  |  |  |  |  | 
| 1649 | 0 |  |  |  |  |  | my $stg_info = $self->column_info($stg_table); | 
| 1650 | 0 |  |  |  |  |  | my $stg_map  = $stg_info->{MAP}; | 
| 1651 |  |  |  |  |  |  |  | 
| 1652 | 0 |  |  |  |  |  | my %stg_has; $stg_has{$_}++ for @{$stg_info->{LIST}}; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 1653 |  |  |  |  |  |  |  | 
| 1654 | 0 | 0 | 0 |  |  |  | my $key_col_ref = ($args{KeyCols} && @{$args{KeyCols}}) ? $args{KeyCols} : $self->key_columns($table); | 
| 1655 | 0 | 0 | 0 |  |  |  | my $upd_col_ref = ($args{UpdCols} && @{$args{UpdCols}}) ? $args{UpdCols} : $self->upd_columns($table, $key_col_ref); | 
| 1656 |  |  |  |  |  |  |  | 
| 1657 | 0 |  |  |  |  |  | my @key_cols = map $tbl_map->{lc($_)}{COLUMN_NAME}, @$key_col_ref; | 
| 1658 | 0 |  |  |  |  |  | my %is_key_col; | 
| 1659 | 0 |  |  |  |  |  | $is_key_col{$_}++ for map lc, @$key_col_ref; | 
| 1660 | 0 |  |  |  |  |  | my @upd_cols = map $tbl_map->{lc($_)}{COLUMN_NAME}, @$upd_col_ref; | 
| 1661 | 0 |  |  |  |  |  | my %is_upd_col; | 
| 1662 | 0 |  |  |  |  |  | $is_upd_col{$_}++ for map lc, @$upd_col_ref; | 
| 1663 |  |  |  |  |  |  |  | 
| 1664 | 0 |  |  |  |  |  | my %tmp_col_map; | 
| 1665 | 0 | 0 |  |  |  |  | %tmp_col_map = map lc, %{$args{ColMap}} if $args{ColMap}; | 
|  | 0 |  |  |  |  |  |  | 
| 1666 |  |  |  |  |  |  |  | 
| 1667 |  |  |  |  |  |  | # Column map for upd statement, which must map the correct case | 
| 1668 |  |  |  |  |  |  | # to the correct case. | 
| 1669 | 0 |  |  |  |  |  | my %col_map = map {( $_ => ( | 
| 1670 |  |  |  |  |  |  | $tmp_col_map{lc($_)} | 
| 1671 |  |  |  |  |  |  | ? $stg_map->{lc($tmp_col_map{lc($_)})}{COLUMN_NAME} | 
| 1672 |  |  |  |  |  |  | : $stg_map->{lc($_)}{COLUMN_NAME} | 
| 1673 | 0 | 0 |  |  |  |  | ))} @key_cols, @upd_cols; | 
| 1674 |  |  |  |  |  |  |  | 
| 1675 |  |  |  |  |  |  | # Correctly cased field list for bcp select from stage table statement | 
| 1676 |  |  |  |  |  |  | # Either it's in the explicit column map, or it's a key or upd column | 
| 1677 |  |  |  |  |  |  | # with the same name as the target table, | 
| 1678 |  |  |  |  |  |  | # or it can be last_chg_user or date | 
| 1679 |  |  |  |  |  |  | my @fields   = map { | 
| 1680 |  |  |  |  |  |  | ($_ eq 'last_chg_user' && !$stg_has{last_chg_user}) ? 'suser_name()' | 
| 1681 |  |  |  |  |  |  | : ($_ eq 'last_chg_date' && !$stg_has{last_chg_date}) ? 'getdate()' | 
| 1682 |  |  |  |  |  |  | : $tmp_col_map{$_} ? $stg_has{$tmp_col_map{$_}} ? $stg_map->{$tmp_col_map{$_}}{COLUMN_NAME} : $tmp_col_map{$_} | 
| 1683 |  |  |  |  |  |  | : ( $is_key_col{$_} || $is_upd_col{$_} ) ? $stg_has{$_} ? $stg_map->{$_}{COLUMN_NAME} : () | 
| 1684 |  |  |  |  |  |  | : $stg_map->{$_} ? $stg_map->{$_}{COLUMN_NAME} | 
| 1685 | 0 | 0 | 0 |  |  |  | : confess "Failed to map target column $table.$_" | 
|  |  | 0 | 0 |  |  |  |  | 
|  |  | 0 | 0 |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 1686 | 0 |  |  |  |  |  | } @{$tbl_info->{LIST}}; | 
|  | 0 |  |  |  |  |  |  | 
| 1687 | 0 |  |  |  |  |  | my $field_str     = join(",", @fields); | 
| 1688 |  |  |  |  |  |  |  | 
| 1689 | 0 |  | 0 |  |  |  | my $key_col_str  = join("\nAND ", map "d.$_=s.".($col_map{$_}||$_), @key_cols); | 
| 1690 |  |  |  |  |  |  |  | 
| 1691 | 0 |  |  |  |  |  | my $del_merge_sql = sprintf($del_sql, | 
| 1692 |  |  |  |  |  |  | $table, | 
| 1693 |  |  |  |  |  |  | $table, $stg_table, | 
| 1694 |  |  |  |  |  |  | $key_col_str, | 
| 1695 |  |  |  |  |  |  | ); | 
| 1696 | 0 |  |  |  |  |  | print("Executing: $del_merge_sql\n"); | 
| 1697 |  |  |  |  |  |  |  | 
| 1698 | 0 | 0 |  |  |  |  | unless ($args{NoExec}) { | 
| 1699 | 0 |  |  |  |  |  | my $del_rows = $dbh->do($del_merge_sql) + 0; | 
| 1700 | 0 |  |  |  |  |  | print("$del_rows rows deleted from $table\n\n"); | 
| 1701 |  |  |  |  |  |  | } | 
| 1702 |  |  |  |  |  |  |  | 
| 1703 | 0 |  |  |  |  |  | my $ins_merge_sql = sprintf($ins_sql, | 
| 1704 |  |  |  |  |  |  | $field_str, | 
| 1705 |  |  |  |  |  |  | $stg_table, | 
| 1706 |  |  |  |  |  |  | ); | 
| 1707 | 0 |  |  |  |  |  | print("Inserting to $table: $ins_merge_sql\n"); | 
| 1708 |  |  |  |  |  |  |  | 
| 1709 | 0 | 0 |  |  |  |  | return 1 if $args{NoExec}; | 
| 1710 |  |  |  |  |  |  |  | 
| 1711 | 0 | 0 | 0 |  |  |  | my $ins_rows = ( $args{NoBCP} or ($stg_table =~ /^#/) ) | 
| 1712 |  |  |  |  |  |  | ? $dbh->do("INSERT INTO $table\n$ins_merge_sql") + 0 | 
| 1713 |  |  |  |  |  |  | : $self->bcp_sql($table, $ins_merge_sql) + 0; | 
| 1714 | 0 |  |  |  |  |  | print("$ins_rows rows inserted to $table\n\n"); | 
| 1715 |  |  |  |  |  |  |  | 
| 1716 | 0 |  |  |  |  |  | return 1; | 
| 1717 |  |  |  |  |  |  | } | 
| 1718 |  |  |  |  |  |  | } | 
| 1719 |  |  |  |  |  |  |  | 
| 1720 |  |  |  |  |  |  | # This merge is destructive to the staging table | 
| 1721 |  |  |  |  |  |  | # Only 'new' rows will be left in the staging table | 
| 1722 |  |  |  |  |  |  | { | 
| 1723 |  |  |  |  |  |  |  | 
| 1724 |  |  |  |  |  |  | my $upd_sql = < | 
| 1725 |  |  |  |  |  |  | UPDATE %s | 
| 1726 |  |  |  |  |  |  | SET %s | 
| 1727 |  |  |  |  |  |  | FROM %s d,%s s | 
| 1728 |  |  |  |  |  |  | WHERE %s | 
| 1729 |  |  |  |  |  |  | SQL | 
| 1730 |  |  |  |  |  |  |  | 
| 1731 |  |  |  |  |  |  | my $del_sql = < | 
| 1732 |  |  |  |  |  |  | DELETE %s | 
| 1733 |  |  |  |  |  |  | FROM %s s, %s d | 
| 1734 |  |  |  |  |  |  | WHERE %s | 
| 1735 |  |  |  |  |  |  | SQL | 
| 1736 |  |  |  |  |  |  |  | 
| 1737 |  |  |  |  |  |  | my $ins_sql = < | 
| 1738 |  |  |  |  |  |  | SELECT %s | 
| 1739 |  |  |  |  |  |  | FROM %s | 
| 1740 |  |  |  |  |  |  | SQL | 
| 1741 |  |  |  |  |  |  |  | 
| 1742 |  |  |  |  |  |  | sub merge2 { | 
| 1743 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 1744 | 0 |  |  |  |  |  | my %args = @_; | 
| 1745 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 1746 |  |  |  |  |  |  |  | 
| 1747 | 0 |  |  |  |  |  | my $table     = lc($args{Table}); | 
| 1748 | 0 |  |  |  |  |  | my $stg_table = lc($args{StgTable}); | 
| 1749 |  |  |  |  |  |  |  | 
| 1750 | 0 |  |  |  |  |  | my $tbl_info = $self->column_info($table); | 
| 1751 | 0 |  |  |  |  |  | my $tbl_map  = $tbl_info->{MAP}; | 
| 1752 |  |  |  |  |  |  |  | 
| 1753 | 0 |  |  |  |  |  | my $stg_info = $self->column_info($stg_table); | 
| 1754 | 0 |  |  |  |  |  | my $stg_map  = $stg_info->{MAP}; | 
| 1755 | 0 |  |  |  |  |  | my %stg_has; $stg_has{$_}++ for @{$stg_info->{LIST}}; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 1756 |  |  |  |  |  |  |  | 
| 1757 | 0 | 0 | 0 |  |  |  | my $key_col_ref = ($args{KeyCols} && @{$args{KeyCols}}) ? $args{KeyCols} : $self->key_columns($table); | 
| 1758 | 0 | 0 | 0 |  |  |  | my $upd_col_ref = ($args{UpdCols} && @{$args{UpdCols}}) ? $args{UpdCols} : $self->upd_columns($table); | 
| 1759 |  |  |  |  |  |  |  | 
| 1760 | 0 |  |  |  |  |  | my @key_cols = map $tbl_map->{lc($_)}{COLUMN_NAME}, @$key_col_ref; | 
| 1761 | 0 |  |  |  |  |  | my %is_key_col; | 
| 1762 | 0 |  |  |  |  |  | $is_key_col{$_}++ for map lc, @$key_col_ref; | 
| 1763 | 0 |  |  |  |  |  | my @upd_cols = map $tbl_map->{lc($_)}{COLUMN_NAME}, @$upd_col_ref; | 
| 1764 | 0 |  |  |  |  |  | my %is_upd_col; | 
| 1765 | 0 |  |  |  |  |  | $is_upd_col{$_}++ for map lc, @$upd_col_ref; | 
| 1766 |  |  |  |  |  |  |  | 
| 1767 | 0 |  |  |  |  |  | my %tmp_col_map; | 
| 1768 | 0 | 0 |  |  |  |  | %tmp_col_map = map lc, %{$args{ColMap}} if $args{ColMap}; | 
|  | 0 |  |  |  |  |  |  | 
| 1769 |  |  |  |  |  |  |  | 
| 1770 |  |  |  |  |  |  | # Column map for upd statement, which must map the correct case | 
| 1771 |  |  |  |  |  |  | # to the correct case. | 
| 1772 | 0 |  |  |  |  |  | my %col_map = map {( $_ => ( | 
| 1773 |  |  |  |  |  |  | $tmp_col_map{lc($_)} | 
| 1774 |  |  |  |  |  |  | ? $stg_map->{lc($tmp_col_map{lc($_)})}{COLUMN_NAME} | 
| 1775 |  |  |  |  |  |  | : $stg_map->{lc($_)}{COLUMN_NAME} | 
| 1776 | 0 | 0 |  |  |  |  | ))} @key_cols, @upd_cols; | 
| 1777 |  |  |  |  |  |  |  | 
| 1778 |  |  |  |  |  |  | # Correctly cased field list for bcp select from stage table statement | 
| 1779 |  |  |  |  |  |  | # Either it's in the explicit column map, or it's a key or upd column | 
| 1780 |  |  |  |  |  |  | # with the same name as the target table, | 
| 1781 |  |  |  |  |  |  | # or it can be last_chg_user or date | 
| 1782 |  |  |  |  |  |  | my @fields   = map { | 
| 1783 |  |  |  |  |  |  | ($_ eq 'last_chg_user' && !$stg_has{last_chg_user}) ? 'suser_name()' | 
| 1784 |  |  |  |  |  |  | : ($_ eq 'last_chg_date' && !$stg_has{last_chg_date}) ? 'getdate()' | 
| 1785 |  |  |  |  |  |  | : $tmp_col_map{$_} ? $stg_map->{$tmp_col_map{$_}}{COLUMN_NAME} | 
| 1786 |  |  |  |  |  |  | : ( $is_key_col{$_} || $is_upd_col{$_} ) ? $stg_map->{$_}{COLUMN_NAME} | 
| 1787 |  |  |  |  |  |  | : $stg_map->{$_}   ? $stg_map->{$_}{COLUMN_NAME} | 
| 1788 | 0 | 0 | 0 |  |  |  | : confess "Failed to map target column $table.$_" | 
|  |  | 0 | 0 |  |  |  |  | 
|  |  | 0 | 0 |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 1789 | 0 |  |  |  |  |  | } @{$tbl_info->{LIST}}; | 
|  | 0 |  |  |  |  |  |  | 
| 1790 | 0 |  |  |  |  |  | my $field_str     = join(",", @fields); | 
| 1791 |  |  |  |  |  |  |  | 
| 1792 | 0 |  | 0 |  |  |  | my $key_col_str  = join("\nAND ", map "d.$_=s.".($col_map{$_}||$_), @key_cols); | 
| 1793 | 0 |  | 0 |  |  |  | my $upd_col_str  = join(",", map "$_=s.".($col_map{$_}||$_), @upd_cols); | 
| 1794 |  |  |  |  |  |  |  | 
| 1795 |  |  |  |  |  |  | # Determine if last_chg_user, last_chg_date need to be updated | 
| 1796 | 0 |  |  |  |  |  | my %chg_col = $self->last_chg_list($table, \@fields); | 
| 1797 | 0 |  |  |  |  |  | for my $col ( sort { $b cmp $a } keys %chg_col ) { | 
|  | 0 |  |  |  |  |  |  | 
| 1798 | 0 | 0 |  |  |  |  | $upd_col_str .= ",$col=".( ($col eq 'last_chg_user') ? 'suser_name()' : 'getdate()'); | 
| 1799 |  |  |  |  |  |  | } | 
| 1800 |  |  |  |  |  |  |  | 
| 1801 | 0 | 0 |  |  |  |  | unless ($args{InsertOnly}) { | 
| 1802 | 0 |  |  |  |  |  | my $upd_merge_sql = sprintf($upd_sql, | 
| 1803 |  |  |  |  |  |  | $table, | 
| 1804 |  |  |  |  |  |  | $upd_col_str, | 
| 1805 |  |  |  |  |  |  | $table, $stg_table, | 
| 1806 |  |  |  |  |  |  | $key_col_str, | 
| 1807 |  |  |  |  |  |  | ); | 
| 1808 | 0 |  |  |  |  |  | print("Executing: $upd_merge_sql\n"); | 
| 1809 |  |  |  |  |  |  |  | 
| 1810 | 0 | 0 |  |  |  |  | unless ($args{NoExec}) { | 
| 1811 | 0 |  |  |  |  |  | my $upd_rows = $dbh->do($upd_merge_sql) + 0; | 
| 1812 | 0 |  |  |  |  |  | print("$upd_rows rows updated in $table\n\n"); | 
| 1813 |  |  |  |  |  |  | } | 
| 1814 |  |  |  |  |  |  | } | 
| 1815 |  |  |  |  |  |  |  | 
| 1816 | 0 |  |  |  |  |  | my $del_merge_sql = sprintf($del_sql, | 
| 1817 |  |  |  |  |  |  | $stg_table, | 
| 1818 |  |  |  |  |  |  | $stg_table, $table, | 
| 1819 |  |  |  |  |  |  | $key_col_str, | 
| 1820 |  |  |  |  |  |  | ); | 
| 1821 | 0 |  |  |  |  |  | print("Executing: $del_merge_sql\n"); | 
| 1822 |  |  |  |  |  |  |  | 
| 1823 | 0 | 0 |  |  |  |  | unless ($args{NoExec}) { | 
| 1824 | 0 |  |  |  |  |  | my $del_rows = $dbh->do($del_merge_sql) + 0; | 
| 1825 | 0 |  |  |  |  |  | print("$del_rows rows deleted from $stg_table\n\n"); | 
| 1826 |  |  |  |  |  |  | } | 
| 1827 |  |  |  |  |  |  |  | 
| 1828 | 0 |  |  |  |  |  | my $ins_merge_sql = sprintf($ins_sql, | 
| 1829 |  |  |  |  |  |  | $field_str, | 
| 1830 |  |  |  |  |  |  | $stg_table, | 
| 1831 |  |  |  |  |  |  | ); | 
| 1832 | 0 |  |  |  |  |  | print("Inserting to $table: $ins_merge_sql\n"); | 
| 1833 |  |  |  |  |  |  |  | 
| 1834 | 0 | 0 |  |  |  |  | return 1 if $args{NoExec}; | 
| 1835 | 0 |  |  |  |  |  | my $ins_rows = $self->bcp_sql($table, $ins_merge_sql) + 0; | 
| 1836 | 0 |  |  |  |  |  | print("$ins_rows rows inserted to $table\n\n"); | 
| 1837 |  |  |  |  |  |  |  | 
| 1838 | 0 |  |  |  |  |  | return 1; | 
| 1839 |  |  |  |  |  |  | } | 
| 1840 |  |  |  |  |  |  | } | 
| 1841 |  |  |  |  |  |  |  | 
| 1842 |  |  |  |  |  |  | # BCP (via sqsh) the results of a sql select statement into a table | 
| 1843 |  |  |  |  |  |  | sub bcp_sql { | 
| 1844 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 1845 | 0 |  |  |  |  |  | my ($table,$sql) = @_; | 
| 1846 |  |  |  |  |  |  |  | 
| 1847 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 1848 | 0 |  |  |  |  |  | my $db = $dbh->{Name}; | 
| 1849 | 0 | 0 |  |  |  |  | $db =~ /server=(\w+)/ or confess "Can't determine server for bcp"; | 
| 1850 | 0 |  |  |  |  |  | my $server = $1; | 
| 1851 | 0 |  |  |  |  |  | my $database = $self->curr_db(); | 
| 1852 |  |  |  |  |  |  |  | 
| 1853 | 0 |  |  |  |  |  | my $user      = $dbh->{Username}; | 
| 1854 | 0 | 0 | 0 |  |  |  | my $bcp_table = | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 1855 |  |  |  |  |  |  | (!$database or $table =~ /^\w+\.\w*\.\w+$/) ? $table | 
| 1856 |  |  |  |  |  |  | : ($table =~ /^\w+$/)                         ? "$database..$table" | 
| 1857 |  |  |  |  |  |  | : ($table =~ /^\w*\.\w+$/)                    ? "$database.$table" | 
| 1858 |  |  |  |  |  |  | : confess "Can not determine database for sqsh/bcp"; | 
| 1859 |  |  |  |  |  |  |  | 
| 1860 | 0 |  |  |  |  |  | local $ENV{SQSH} = "-U $dbh->{Username} -P $self->{PASSWORD}"; | 
| 1861 | 0 |  |  |  |  |  | my $pid = open(my $fh, "-|"); | 
| 1862 | 0 | 0 |  |  |  |  | confess "Can't fork: $!" unless defined $pid; | 
| 1863 | 0 | 0 |  |  |  |  | unless ($pid) { | 
| 1864 |  |  |  |  |  |  |  | 
| 1865 |  |  |  |  |  |  | # sqsh needs library path set - make sure it is set | 
| 1866 |  |  |  |  |  |  | # Don't know where it is in generic environment, or best way | 
| 1867 |  |  |  |  |  |  | # to universally set this, or even if this is necessary in general... | 
| 1868 |  |  |  |  |  |  | # local $ENV{LD_LIBRARY_PATH} = '/path/to/sybase/OCS-12_5/lib'; | 
| 1869 | 0 |  |  |  |  |  | my @cmd = (sqsh => -S => $server, -D => $database); | 
| 1870 | 0 |  |  |  |  |  | my $sqsh_fh; | 
| 1871 |  |  |  |  |  |  |  | 
| 1872 |  |  |  |  |  |  | # sqsh outputs to stderr | 
| 1873 | 0 |  |  |  |  |  | open(STDERR, ">&STDOUT"); | 
| 1874 | 0 | 0 |  |  |  |  | unless ( open($sqsh_fh, "|-", @cmd) ) { | 
| 1875 | 0 |  |  |  |  |  | warn "Unable to exec @cmd: $!"; | 
| 1876 | 0 |  |  |  |  |  | exit(1); | 
| 1877 |  |  |  |  |  |  | } | 
| 1878 | 0 |  |  |  |  |  | print $sqsh_fh "$sql\n"; | 
| 1879 | 0 |  |  |  |  |  | print $sqsh_fh "\\bcp -b 1000 $bcp_table\n"; | 
| 1880 |  |  |  |  |  |  |  | 
| 1881 | 0 |  |  |  |  |  | my $status = close $sqsh_fh; | 
| 1882 | 0 | 0 |  |  |  |  | exit($status ? 0 : 1); | 
| 1883 |  |  |  |  |  |  | } | 
| 1884 | 0 |  |  |  |  |  | my $rows; | 
| 1885 | 0 |  |  |  |  |  | local ($_, $.); | 
| 1886 | 0 |  |  |  |  |  | my $cnt; | 
| 1887 | 0 |  |  |  |  |  | while (<$fh>) { | 
| 1888 | 0 | 0 |  |  |  |  | if (/^Batch successfully bulk-copied/) { | 
| 1889 | 0 |  |  |  |  |  | $cnt += 1000; | 
| 1890 | 0 | 0 |  |  |  |  | print "$cnt: $_" unless $cnt % 10_000; | 
| 1891 | 0 |  |  |  |  |  | next; | 
| 1892 |  |  |  |  |  |  | } | 
| 1893 | 0 |  |  |  |  |  | print; | 
| 1894 | 0 | 0 |  |  |  |  | $rows = $1 if /^\s*(\d+) rows copied/; | 
| 1895 |  |  |  |  |  |  | } | 
| 1896 | 0 |  |  |  |  |  | my $close_status = close $fh; | 
| 1897 | 0 | 0 |  |  |  |  | confess "SQSH BCP error - no rows copied"    unless defined $rows; | 
| 1898 | 0 | 0 |  |  |  |  | confess "SQSH BCP error - $rows rows copied" unless $close_status; | 
| 1899 |  |  |  |  |  |  |  | 
| 1900 |  |  |  |  |  |  | # Return true value | 
| 1901 | 0 |  |  |  |  |  | return $rows; | 
| 1902 |  |  |  |  |  |  | } | 
| 1903 |  |  |  |  |  |  |  | 
| 1904 |  |  |  |  |  |  | # SQL to return table column defaults | 
| 1905 |  |  |  |  |  |  | { | 
| 1906 |  |  |  |  |  |  | my $sql = < | 
| 1907 |  |  |  |  |  |  | SELECT c.name, d.text | 
| 1908 |  |  |  |  |  |  | FROM dbo.syscolumns c, dbo.syscomments d | 
| 1909 |  |  |  |  |  |  | WHERE c.id = object_id('%s') | 
| 1910 |  |  |  |  |  |  | AND c.cdefault = d.id | 
| 1911 |  |  |  |  |  |  | AND d.texttype = 0 | 
| 1912 |  |  |  |  |  |  | SQL | 
| 1913 |  |  |  |  |  |  |  | 
| 1914 | 0 |  |  | 0 |  |  | sub default_sql { return $sql } | 
| 1915 |  |  |  |  |  |  | } | 
| 1916 |  |  |  |  |  |  |  | 
| 1917 |  |  |  |  |  |  | # Changed for Sybase v12 and multiple tempdbs | 
| 1918 |  |  |  |  |  |  | sub temp_table_name { | 
| 1919 | 0 |  |  | 0 |  |  | my ($self, $name) = @_; | 
| 1920 |  |  |  |  |  |  |  | 
| 1921 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 1922 | 0 |  |  |  |  |  | my ($spid) = $dbh->selectrow_array('select @@spid'); | 
| 1923 | 0 |  |  |  |  |  | print "SPid: $spid\n"; | 
| 1924 | 0 |  |  |  |  |  | my $who = $dbh->selectrow_hashref("exec sp_who '$spid'"); | 
| 1925 | 0 |  | 0 |  |  |  | my $tempdb = $who->{tempdbname} || 'tempdb'; | 
| 1926 | 0 |  |  |  |  |  | print "TempDb: $tempdb\n"; | 
| 1927 | 0 |  |  |  |  |  | my ($id) = $dbh->selectrow_array("select object_id('$tempdb..$name')"); | 
| 1928 | 0 |  |  |  |  |  | print "ID: $id\n"; | 
| 1929 | 0 |  |  |  |  |  | my ($real_name) = $dbh->selectrow_array("select object_name($id, db_id('$tempdb'))"); | 
| 1930 | 0 |  |  |  |  |  | print "RealName: $real_name\n"; | 
| 1931 | 0 |  |  |  |  |  | return "$tempdb..$real_name"; | 
| 1932 |  |  |  |  |  |  | } | 
| 1933 |  |  |  |  |  |  |  | 
| 1934 |  |  |  |  |  |  | sub delete { | 
| 1935 | 0 |  |  | 0 |  |  | my ($self, $table, $where, $limit) = @_; | 
| 1936 |  |  |  |  |  |  |  | 
| 1937 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 1938 | 0 |  | 0 |  |  |  | $dbh->{syb_rowcount} = $limit || 1000; | 
| 1939 |  |  |  |  |  |  |  | 
| 1940 | 0 |  |  |  |  |  | my $sql = "DELETE FROM $table"; | 
| 1941 | 0 | 0 |  |  |  |  | $sql .= " WHERE $where" if $where; | 
| 1942 |  |  |  |  |  |  |  | 
| 1943 | 0 |  |  |  |  |  | my ($rows, $tot_rows); | 
| 1944 | 0 |  |  |  |  |  | my ($err, $err_msg); | 
| 1945 |  |  |  |  |  |  |  | 
| 1946 | 0 |  |  |  |  |  | print "Executing: $sql\n"; | 
| 1947 | 0 |  |  |  |  |  | do { | 
| 1948 |  |  |  |  |  |  |  | 
| 1949 | 0 |  |  |  |  |  | $rows = eval { $dbh->do($sql) }; | 
|  | 0 |  |  |  |  |  |  | 
| 1950 | 0 | 0 |  |  |  |  | unless ($rows) { | 
| 1951 | 0 |  |  |  |  |  | $err_msg = $@; | 
| 1952 | 0 |  |  |  |  |  | $err++; | 
| 1953 | 0 |  |  |  |  |  | $rows = 0; | 
| 1954 |  |  |  |  |  |  | } | 
| 1955 |  |  |  |  |  |  |  | 
| 1956 | 0 |  |  |  |  |  | $tot_rows += $rows; | 
| 1957 | 0 | 0 |  |  |  |  | print "Deleted $tot_rows rows\n" if $rows > 0; | 
| 1958 |  |  |  |  |  |  |  | 
| 1959 |  |  |  |  |  |  | } while $rows > 0; | 
| 1960 |  |  |  |  |  |  |  | 
| 1961 | 0 |  |  |  |  |  | $dbh->{syb_rowcount} = 0; | 
| 1962 |  |  |  |  |  |  |  | 
| 1963 | 0 | 0 |  |  |  |  | confess $err_msg if $err; | 
| 1964 |  |  |  |  |  |  |  | 
| 1965 | 0 |  |  |  |  |  | print "$tot_rows rows deleted from $table\n"; | 
| 1966 | 0 |  |  |  |  |  | return $tot_rows; | 
| 1967 |  |  |  |  |  |  | } | 
| 1968 |  |  |  |  |  |  |  | 
| 1969 |  |  |  |  |  |  |  | 
| 1970 |  |  |  |  |  |  | package DBIx::BulkUtil::SybaseIQ; | 
| 1971 |  |  |  |  |  |  |  | 
| 1972 | 1 |  |  | 1 |  | 5 | use Carp qw(confess); | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 45 |  | 
| 1973 | 1 |  |  | 1 |  | 3 | use Cwd qw(abs_path); | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 805 |  | 
| 1974 |  |  |  |  |  |  |  | 
| 1975 |  |  |  |  |  |  | our @ISA = qw(DBIx::BulkUtil::Sybase); | 
| 1976 |  |  |  |  |  |  |  | 
| 1977 |  |  |  |  |  |  | { | 
| 1978 |  |  |  |  |  |  |  | 
| 1979 |  |  |  |  |  |  | my $sql = < | 
| 1980 |  |  |  |  |  |  | LOAD TABLE %s | 
| 1981 |  |  |  |  |  |  | ( | 
| 1982 |  |  |  |  |  |  | %s | 
| 1983 |  |  |  |  |  |  | ) | 
| 1984 |  |  |  |  |  |  | FROM | 
| 1985 |  |  |  |  |  |  | %s | 
| 1986 |  |  |  |  |  |  | QUOTES OFF | 
| 1987 |  |  |  |  |  |  | ESCAPES OFF | 
| 1988 |  |  |  |  |  |  | SQL | 
| 1989 |  |  |  |  |  |  |  | 
| 1990 |  |  |  |  |  |  | sub bcp_in { | 
| 1991 | 0 |  |  | 0 |  |  | my $self  = shift; | 
| 1992 | 0 |  |  |  |  |  | my $table = shift; | 
| 1993 |  |  |  |  |  |  |  | 
| 1994 | 0 | 0 |  |  |  |  | my $opts = (ref $_[-1]) ? pop @_ : {}; | 
| 1995 |  |  |  |  |  |  |  | 
| 1996 | 0 |  |  |  |  |  | my @files = @_; | 
| 1997 |  |  |  |  |  |  |  | 
| 1998 | 0 | 0 |  |  |  |  | push @files, "$table.bcp" unless @files; | 
| 1999 |  |  |  |  |  |  |  | 
| 2000 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 2001 |  |  |  |  |  |  |  | 
| 2002 | 0 |  | 0 |  |  |  | my $delimiter = $opts->{Delimiter} || $self->{DELIMITER}; | 
| 2003 | 0 |  | 0 |  |  |  | my $row_delimiter = $opts->{RowDelimiter} || "\n"; | 
| 2004 |  |  |  |  |  |  |  | 
| 2005 | 0 |  |  |  |  |  | my $id_cnt; | 
| 2006 | 0 |  | 0 |  |  |  | my $mode = $opts->{Action} || "A"; | 
| 2007 | 0 | 0 |  |  |  |  | if ( $mode eq 'T' ) { | 
|  |  | 0 |  |  |  |  |  | 
| 2008 | 0 |  |  |  |  |  | my $sql = "TRUNCATE TABLE $table"; | 
| 2009 | 0 |  |  |  |  |  | print "Executing: $sql\n"; | 
| 2010 | 0 |  |  |  |  |  | $dbh->do($sql); | 
| 2011 |  |  |  |  |  |  | } elsif ($mode eq 'R') { | 
| 2012 | 0 |  |  |  |  |  | my $sql = "DELETE FROM $table"; | 
| 2013 | 0 |  |  |  |  |  | print "Executing: $sql\n"; | 
| 2014 | 0 |  |  |  |  |  | $dbh->do($sql); | 
| 2015 |  |  |  |  |  |  | } | 
| 2016 |  |  |  |  |  |  |  | 
| 2017 | 0 |  |  |  |  |  | my @bcp_list; | 
| 2018 | 0 |  |  |  |  |  | for my $file (@files) { | 
| 2019 | 0 | 0 |  |  |  |  | confess "BCP file $file does not exist" unless -f $file; | 
| 2020 | 0 | 0 |  |  |  |  | unless ( -s _ ) { | 
| 2021 | 0 |  |  |  |  |  | print "$file is empty. Skipping ...\n"; | 
| 2022 | 0 |  |  |  |  |  | next; | 
| 2023 |  |  |  |  |  |  | } | 
| 2024 | 0 |  |  |  |  |  | push @bcp_list, $file; | 
| 2025 |  |  |  |  |  |  | } | 
| 2026 |  |  |  |  |  |  |  | 
| 2027 | 0 | 0 |  |  |  |  | unless ( @bcp_list ) { | 
| 2028 | 0 |  |  |  |  |  | print "All files are empty. Skipping bcp of $table\n"; | 
| 2029 |  |  |  |  |  |  |  | 
| 2030 |  |  |  |  |  |  | # Make any log file parsers happy | 
| 2031 | 0 |  |  |  |  |  | print "0 rows copied\n"; | 
| 2032 | 0 |  |  |  |  |  | return 0; | 
| 2033 |  |  |  |  |  |  | } | 
| 2034 |  |  |  |  |  |  |  | 
| 2035 | 0 |  |  |  |  |  | my $info = $self->column_info($table); | 
| 2036 | 0 | 0 | 0 |  |  |  | my $col_list = ( $opts->{ColumnList} && @{$opts->{ColumnList}} ) ? $opts->{ColumnList} : $info->{LIST}; | 
| 2037 | 0 | 0 |  |  |  |  | my @filler = $opts->{Filler} ? @{$opts->{Filler}} : (); | 
|  | 0 |  |  |  |  |  |  | 
| 2038 | 0 |  |  |  |  |  | my %is_filler; | 
| 2039 | 0 |  |  |  |  |  | $is_filler{$_}++ for @filler; | 
| 2040 |  |  |  |  |  |  |  | 
| 2041 |  |  |  |  |  |  | # Convert empty string to NULL | 
| 2042 |  |  |  |  |  |  | # Should be default but we don't want to break existing apps | 
| 2043 | 0 | 0 |  |  |  |  | my $null_blanks = $self->{NoBlankNull} ? ' NULL(BLANKS)' : ''; | 
| 2044 |  |  |  |  |  |  |  | 
| 2045 |  |  |  |  |  |  | # Columns that we will let default to the schema default | 
| 2046 | 0 |  | 0 |  |  |  | my $dflt = $opts->{Default} || []; | 
| 2047 | 0 |  |  |  |  |  | my %dflt; $dflt{$_}++ for @$dflt; | 
|  | 0 |  |  |  |  |  |  | 
| 2048 |  |  |  |  |  |  |  | 
| 2049 | 0 |  | 0 |  |  |  | my $constant = $opts->{Constants} || {}; | 
| 2050 |  |  |  |  |  |  |  | 
| 2051 | 0 |  | 0 |  |  |  | my @list = grep !defined($constant->{$_})&&!$dflt{$_}, @$col_list; | 
| 2052 | 0 |  |  |  |  |  | my $last_col = $list[-1]; | 
| 2053 |  |  |  |  |  |  |  | 
| 2054 |  |  |  |  |  |  | # It is best to explicitly put the row delimiter on the last column | 
| 2055 |  |  |  |  |  |  | my $load_sql = sprintf( | 
| 2056 |  |  |  |  |  |  | $sql, | 
| 2057 |  |  |  |  |  |  | $table, | 
| 2058 |  |  |  |  |  |  | join( ",\n", map { | 
| 2059 |  |  |  |  |  |  | defined($constant->{$_}) ? qq( [$_] DEFAULT '$constant->{$_}') | 
| 2060 |  |  |  |  |  |  | : ( $_ ne $last_col ) | 
| 2061 |  |  |  |  |  |  | ? $is_filler{$_} ? qq( FILLER('$delimiter')) : qq( [$_] '$delimiter'$null_blanks) | 
| 2062 |  |  |  |  |  |  | : ( $opts->{TrailingDelimiter} ) | 
| 2063 |  |  |  |  |  |  | ? $is_filler{$_} ? qq( FILLER('$delimiter$row_delimiter')) : qq( [$_] '$delimiter$row_delimiter'$null_blanks) | 
| 2064 | 0 | 0 |  |  |  |  | : $is_filler{$_} ? qq( FILLER('$row_delimiter')) : qq( [$_] '$row_delimiter'$null_blanks) | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 2065 |  |  |  |  |  |  | } grep !$dflt{$_}, @$col_list), | 
| 2066 | 0 |  |  |  |  |  | join( ",\n  ", map { "'". abs_path($_) . "'" } @bcp_list), | 
|  | 0 |  |  |  |  |  |  | 
| 2067 |  |  |  |  |  |  | ); | 
| 2068 |  |  |  |  |  |  |  | 
| 2069 | 0 | 0 |  |  |  |  | $load_sql .= "SKIP $opts->{Header}\n" if $opts->{Header}; | 
| 2070 |  |  |  |  |  |  |  | 
| 2071 |  |  |  |  |  |  | # '0' indicates unlimited errors to IQ, but will be skipped here since '0' is false | 
| 2072 |  |  |  |  |  |  | # That's okay, '00' might work (it is 'true' and == 0). | 
| 2073 | 0 | 0 |  |  |  |  | $load_sql .= "IGNORE CONSTRAINT ALL $opts->{MaxErrors}\n" if $opts->{MaxErrors}; | 
| 2074 |  |  |  |  |  |  |  | 
| 2075 | 0 |  |  |  |  |  | my $db = $dbh->{Name}; | 
| 2076 | 0 | 0 |  |  |  |  | $db =~ /server=(\w+)/ or confess "Can't determine server for bcp"; | 
| 2077 | 0 |  |  |  |  |  | my $server = $1; | 
| 2078 | 0 |  |  |  |  |  | my $database = $self->curr_db(); | 
| 2079 |  |  |  |  |  |  |  | 
| 2080 | 0 |  |  |  |  |  | print "Loading $server/$database/$table\n"; | 
| 2081 | 0 |  |  |  |  |  | print "Executing: $load_sql\n"; | 
| 2082 | 0 |  |  |  |  |  | my $rows = $dbh->do($load_sql) + 0; | 
| 2083 | 0 |  |  |  |  |  | print "$rows rows copied\n"; | 
| 2084 | 0 |  |  |  |  |  | return $rows; | 
| 2085 |  |  |  |  |  |  | } | 
| 2086 |  |  |  |  |  |  | } | 
| 2087 |  |  |  |  |  |  |  | 
| 2088 |  |  |  |  |  |  | { | 
| 2089 |  |  |  |  |  |  | my $sql = < | 
| 2090 |  |  |  |  |  |  | SELECT cname, default_value | 
| 2091 |  |  |  |  |  |  | FROM sys.syscolumns | 
| 2092 |  |  |  |  |  |  | WHERE tname = '%s' | 
| 2093 |  |  |  |  |  |  | AND default_value IS NOT NULL | 
| 2094 |  |  |  |  |  |  | SQL | 
| 2095 |  |  |  |  |  |  |  | 
| 2096 | 0 |  |  | 0 |  |  | sub default_sql { return $sql } | 
| 2097 |  |  |  |  |  |  | } | 
| 2098 |  |  |  |  |  |  |  | 
| 2099 |  |  |  |  |  |  | # Because SybaseIQ can not do sqsh | 
| 2100 |  |  |  |  |  |  | sub bcp_sql { | 
| 2101 | 0 |  |  | 0 |  |  | my ($self, $table, $sql) = @_; | 
| 2102 | 0 |  |  |  |  |  | my $do_sql = "INSERT INTO $table\n$sql"; | 
| 2103 | 0 |  |  |  |  |  | $self->{DBH}->do("INSERT INTO $table\n$sql"); | 
| 2104 |  |  |  |  |  |  | } | 
| 2105 |  |  |  |  |  |  |  | 
| 2106 | 0 |  |  | 0 |  |  | sub is_iq {1} | 
| 2107 |  |  |  |  |  |  |  | 
| 2108 |  |  |  |  |  |  | sub index_info { | 
| 2109 | 0 |  |  | 0 |  |  | my ( $self, $table, $all_indexes ) = @_; | 
| 2110 |  |  |  |  |  |  |  | 
| 2111 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 2112 |  |  |  |  |  |  |  | 
| 2113 | 0 |  |  |  |  |  | my $sql = "exec sp_iqindex [$table]"; | 
| 2114 | 0 |  |  |  |  |  | my $sth = $dbh->prepare($sql); | 
| 2115 | 0 |  |  |  |  |  | $sth->execute(); | 
| 2116 | 0 |  |  |  |  |  | my @col_names = @{$sth->{NAME_lc}}; | 
|  | 0 |  |  |  |  |  |  | 
| 2117 | 0 |  |  |  |  |  | my %row; $sth->bind_columns(\@row{@col_names}); | 
|  | 0 |  |  |  |  |  |  | 
| 2118 | 0 |  |  |  |  |  | my %ind; | 
| 2119 | 0 |  |  |  |  |  | while ($sth->fetch()) { | 
| 2120 | 0 | 0 | 0 |  |  |  | next if !$all_indexes and $row{unique_index} ne 'Y'; | 
| 2121 | 0 |  |  |  |  |  | $ind{$row{index_name}} = [ split /,/, $row{column_name} ]; | 
| 2122 |  |  |  |  |  |  | } | 
| 2123 |  |  |  |  |  |  |  | 
| 2124 | 0 | 0 |  |  |  |  | return unless %ind; | 
| 2125 | 0 |  |  |  |  |  | return \%ind; | 
| 2126 |  |  |  |  |  |  | } | 
| 2127 |  |  |  |  |  |  |  | 
| 2128 |  |  |  |  |  |  | package DBIx::BulkUtil::Oracle; | 
| 2129 |  |  |  |  |  |  |  | 
| 2130 | 1 |  |  | 1 |  | 12 | use Carp qw(confess); | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 37 |  | 
| 2131 | 1 |  |  | 1 |  | 3 | use Cwd  qw(abs_path); | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 4959 |  | 
| 2132 |  |  |  |  |  |  |  | 
| 2133 |  |  |  |  |  |  | our @ISA = qw(DBIx::BulkUtil::Obj); | 
| 2134 |  |  |  |  |  |  |  | 
| 2135 | 0 |  |  | 0 |  |  | sub now { 'systimestamp' } | 
| 2136 |  |  |  |  |  |  |  | 
| 2137 |  |  |  |  |  |  | sub add { | 
| 2138 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 2139 | 0 |  |  |  |  |  | my $date = shift; | 
| 2140 | 0 |  |  |  |  |  | while (my ( $n, $unit ) = splice( @_, 0, 2 ) ) { | 
| 2141 | 0 |  |  |  |  |  | $date .= " + numtodsinterval( $n, '$unit' )"; | 
| 2142 |  |  |  |  |  |  | } | 
| 2143 | 0 |  |  |  |  |  | return $date; | 
| 2144 |  |  |  |  |  |  | } | 
| 2145 |  |  |  |  |  |  |  | 
| 2146 |  |  |  |  |  |  | { | 
| 2147 |  |  |  |  |  |  | my %intervals = ( | 
| 2148 |  |  |  |  |  |  | year   => '/ 365', | 
| 2149 |  |  |  |  |  |  | month  => '/ 30', | 
| 2150 |  |  |  |  |  |  | hour   => '* 24', | 
| 2151 |  |  |  |  |  |  | minute => '* 24 * 60', | 
| 2152 |  |  |  |  |  |  | second => '* 24 * 60 * 60', | 
| 2153 |  |  |  |  |  |  | ); | 
| 2154 |  |  |  |  |  |  |  | 
| 2155 |  |  |  |  |  |  | sub diff { | 
| 2156 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 2157 | 0 |  |  |  |  |  | my $date1 = shift; | 
| 2158 | 0 |  |  |  |  |  | my $date2 = shift; | 
| 2159 | 0 |  |  |  |  |  | my $unit = shift; | 
| 2160 | 0 |  |  |  |  |  | my $diff_str = "$date2 - $date1"; | 
| 2161 | 0 | 0 |  |  |  |  | if (my $str = $intervals{$unit}) { | 
| 2162 | 0 |  |  |  |  |  | $diff_str = "($diff_str) $str"; | 
| 2163 |  |  |  |  |  |  | } | 
| 2164 | 0 |  |  |  |  |  | return "trunc($diff_str)"; | 
| 2165 |  |  |  |  |  |  | } | 
| 2166 |  |  |  |  |  |  | } | 
| 2167 |  |  |  |  |  |  |  | 
| 2168 |  |  |  |  |  |  | # This is necessary when you want to use a literal | 
| 2169 |  |  |  |  |  |  | # date in a datetime calculation | 
| 2170 |  |  |  |  |  |  | sub to_datetime { | 
| 2171 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 2172 | 0 |  |  |  |  |  | my $date = shift; | 
| 2173 |  |  |  |  |  |  |  | 
| 2174 | 0 |  |  |  |  |  | return "to_timestamp('$date', 'YYYY-MM-DD HH24:MI:SS.FF')"; | 
| 2175 |  |  |  |  |  |  | } | 
| 2176 |  |  |  |  |  |  |  | 
| 2177 |  |  |  |  |  |  | # Don't need this with new version of DBI/DBD | 
| 2178 |  |  |  |  |  |  | #sub to_char { | 
| 2179 |  |  |  |  |  |  | #  my $self = shift; | 
| 2180 |  |  |  |  |  |  | #  my $date = shift; | 
| 2181 |  |  |  |  |  |  | #  return "to_char($date, 'YYYY-MM-DD HH24:MI:SS')"; | 
| 2182 |  |  |  |  |  |  | #} | 
| 2183 |  |  |  |  |  |  | # | 
| 2184 |  |  |  |  |  |  | #sub fmt { return $_[1] } | 
| 2185 |  |  |  |  |  |  |  | 
| 2186 |  |  |  |  |  |  | sub row_select { | 
| 2187 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 2188 | 0 |  |  |  |  |  | my $sel = shift; | 
| 2189 | 0 |  |  |  |  |  | return "select $sel from dual"; | 
| 2190 |  |  |  |  |  |  | } | 
| 2191 |  |  |  |  |  |  |  | 
| 2192 |  |  |  |  |  |  | sub sp_sth { | 
| 2193 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 2194 | 0 |  |  |  |  |  | my $sth = $self->{DBH}->prepare($self->sp_sql(@_)); | 
| 2195 | 0 |  |  |  |  |  | $sth->bind_param_inout(":cursor", \my $sth2, 0, { ora_type => DBD::Oracle::ORA_RSET() }); | 
| 2196 | 0 |  |  |  |  |  | $sth->execute(); | 
| 2197 | 0 |  |  |  |  |  | return $sth2; | 
| 2198 |  |  |  |  |  |  | } | 
| 2199 |  |  |  |  |  |  |  | 
| 2200 |  |  |  |  |  |  | sub sp_sql { | 
| 2201 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 2202 | 0 |  |  |  |  |  | my ($stored_proc, @args) = @_; | 
| 2203 |  |  |  |  |  |  | return | 
| 2204 |  |  |  |  |  |  | "BEGIN\n$stored_proc(" . | 
| 2205 | 0 | 0 |  |  |  |  | join(",", map { /^:cursor$/ ? $_ : $self->{DBH}->quote($_) } @args) . | 
|  | 0 |  |  |  |  |  |  | 
| 2206 |  |  |  |  |  |  | ");\nEND;\n"; | 
| 2207 |  |  |  |  |  |  | } | 
| 2208 |  |  |  |  |  |  |  | 
| 2209 |  |  |  |  |  |  | { | 
| 2210 |  |  |  |  |  |  |  | 
| 2211 |  |  |  |  |  |  | my %action_map = ( | 
| 2212 |  |  |  |  |  |  | A => "APPEND", | 
| 2213 |  |  |  |  |  |  | R => "REPLACE", | 
| 2214 |  |  |  |  |  |  | T => "TRUNCATE", | 
| 2215 |  |  |  |  |  |  | ); | 
| 2216 |  |  |  |  |  |  |  | 
| 2217 |  |  |  |  |  |  | sub bcp_in { | 
| 2218 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 2219 | 0 |  |  |  |  |  | my $opts = {}; | 
| 2220 | 0 | 0 |  |  |  |  | if (ref $_[-1]) { | 
| 2221 | 0 |  |  |  |  |  | $opts = pop @_; | 
| 2222 |  |  |  |  |  |  | } | 
| 2223 | 0 |  | 0 |  |  |  | my $action_opt = uc($opts->{Action} || "A"); | 
| 2224 |  |  |  |  |  |  |  | 
| 2225 | 0 |  |  |  |  |  | my ( $table, @files ) = @_; | 
| 2226 |  |  |  |  |  |  |  | 
| 2227 | 0 | 0 |  |  |  |  | my $partition = ( $table =~ s/:(\w+)$// ) ? $1 : ''; | 
| 2228 |  |  |  |  |  |  |  | 
| 2229 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 2230 |  |  |  |  |  |  |  | 
| 2231 | 0 |  |  |  |  |  | my $stdin = $opts->{Stdin}; | 
| 2232 | 0 | 0 | 0 |  |  |  | @files = "$table.bcp" if !@files && !$stdin; | 
| 2233 |  |  |  |  |  |  |  | 
| 2234 | 0 |  |  |  |  |  | my $has_stdin; | 
| 2235 | 0 |  |  |  |  |  | for my $file (@files) { | 
| 2236 | 0 | 0 |  |  |  |  | if ( $file eq "-" ) { | 
| 2237 | 0 |  |  |  |  |  | $has_stdin++; | 
| 2238 | 0 |  |  |  |  |  | next; | 
| 2239 |  |  |  |  |  |  | } | 
| 2240 | 0 | 0 |  |  |  |  | confess "BCP file $file does not exist" unless -f $file; | 
| 2241 |  |  |  |  |  |  | } | 
| 2242 |  |  |  |  |  |  |  | 
| 2243 | 0 | 0 | 0 |  |  |  | if ( $has_stdin && !$stdin ) { | 
|  |  | 0 | 0 |  |  |  |  | 
| 2244 | 0 |  |  |  |  |  | $stdin = \*STDIN; | 
| 2245 |  |  |  |  |  |  | } elsif ( $stdin && !$has_stdin ) { | 
| 2246 | 0 |  |  |  |  |  | push @files, "-"; | 
| 2247 |  |  |  |  |  |  | } | 
| 2248 |  |  |  |  |  |  |  | 
| 2249 |  |  |  |  |  |  | # Save some work, skip load on empty file | 
| 2250 |  |  |  |  |  |  | # Let sqlldr do a heavy handed truncate or delete | 
| 2251 |  |  |  |  |  |  | # if that is the chosen action | 
| 2252 | 0 | 0 |  |  |  |  | my @bcp_files = grep { $_ eq "-" or -s } @files; | 
|  | 0 |  |  |  |  |  |  | 
| 2253 |  |  |  |  |  |  |  | 
| 2254 | 0 | 0 |  |  |  |  | if ( !@bcp_files ) { | 
| 2255 | 0 | 0 |  |  |  |  | if ( $action_opt eq 'A') { | 
| 2256 | 0 |  |  |  |  |  | print "$files[0],... is empty. Skipping sqlldr\n"; | 
| 2257 |  |  |  |  |  |  |  | 
| 2258 |  |  |  |  |  |  | # Make any log file parsers happy | 
| 2259 | 0 |  |  |  |  |  | print "0 Rows successfully loaded\n"; | 
| 2260 | 0 |  |  |  |  |  | return 0; | 
| 2261 |  |  |  |  |  |  | } | 
| 2262 |  |  |  |  |  |  |  | 
| 2263 |  |  |  |  |  |  | # Need some files if we run sqlldr | 
| 2264 | 0 |  |  |  |  |  | @bcp_files = @files; | 
| 2265 |  |  |  |  |  |  | } | 
| 2266 | 0 |  |  |  |  |  | require File::Temp; | 
| 2267 |  |  |  |  |  |  |  | 
| 2268 | 0 |  | 0 |  |  |  | my $constants = $opts->{Constants} || {}; | 
| 2269 | 0 |  |  |  |  |  | my %const = map { uc($_) => $constants->{$_} } keys %$constants; | 
|  | 0 |  |  |  |  |  |  | 
| 2270 |  |  |  |  |  |  |  | 
| 2271 | 0 |  | 0 |  |  |  | my $sizes = $opts->{CharSizes} || {}; | 
| 2272 | 0 |  |  |  |  |  | my %char_sizes = map { uc($_) => $sizes->{$_} } keys %$sizes; | 
|  | 0 |  |  |  |  |  |  | 
| 2273 |  |  |  |  |  |  |  | 
| 2274 | 0 |  | 0 |  |  |  | my $keep_temp = $opts->{KeepTempFiles} || $opts->{Debug}; | 
| 2275 | 0 |  | 0 |  |  |  | my $in_temp_dir = $opts->{TempDir}     || $opts->{Debug}; | 
| 2276 | 0 |  |  |  |  |  | my $temp_dir; | 
| 2277 | 0 | 0 | 0 |  |  |  | $temp_dir = $opts->{TempDir} || "." if $in_temp_dir; | 
| 2278 |  |  |  |  |  |  |  | 
| 2279 | 0 | 0 |  |  |  |  | my @temp_dir = $in_temp_dir ? (DIR => $temp_dir) : (); | 
| 2280 | 0 | 0 |  |  |  |  | my @unlink  = $keep_temp ? (UNLINK => 0) : (); | 
| 2281 | 0 |  |  |  |  |  | my $ctl_fh = File::Temp->new( | 
| 2282 |  |  |  |  |  |  | TEMPLATE => "${table}_XXXXX", | 
| 2283 |  |  |  |  |  |  | SUFFIX   => ".ctl", | 
| 2284 |  |  |  |  |  |  | @temp_dir, @unlink, | 
| 2285 |  |  |  |  |  |  | ); | 
| 2286 | 0 |  |  |  |  |  | chmod(0664, $ctl_fh->filename()); | 
| 2287 | 0 |  |  |  |  |  | my $bad_fh = File::Temp->new( | 
| 2288 |  |  |  |  |  |  | TEMPLATE => "${table}_XXXXX", | 
| 2289 |  |  |  |  |  |  | SUFFIX   => ".bad", | 
| 2290 |  |  |  |  |  |  | @temp_dir, @unlink, | 
| 2291 |  |  |  |  |  |  | ); | 
| 2292 | 0 |  |  |  |  |  | chmod(0664, $bad_fh->filename()); | 
| 2293 | 0 |  |  |  |  |  | my $log_fh = File::Temp->new( | 
| 2294 |  |  |  |  |  |  | TEMPLATE => "${table}_XXXXX", | 
| 2295 |  |  |  |  |  |  | SUFFIX   => ".log", | 
| 2296 |  |  |  |  |  |  | @temp_dir, @unlink, | 
| 2297 |  |  |  |  |  |  | ); | 
| 2298 | 0 |  |  |  |  |  | chmod(0664, $log_fh->filename()); | 
| 2299 | 0 | 0 |  |  |  |  | my $prm_fh = $stdin ? File::Temp->new( | 
| 2300 |  |  |  |  |  |  | TEMPLATE => "${table}_XXXXX", | 
| 2301 |  |  |  |  |  |  | SUFFIX   => ".prm", | 
| 2302 |  |  |  |  |  |  | @temp_dir, | 
| 2303 |  |  |  |  |  |  | ) : undef; | 
| 2304 |  |  |  |  |  |  |  | 
| 2305 |  |  |  |  |  |  | # NLS date format env variable does not work | 
| 2306 |  |  |  |  |  |  | # for sqlldr. | 
| 2307 |  |  |  |  |  |  | # So we must determine date fields and | 
| 2308 |  |  |  |  |  |  | # specify the format in the control file. | 
| 2309 | 0 |  |  |  |  |  | my $db = $self->{DBH}->{Name}; | 
| 2310 | 0 |  |  |  |  |  | my $user = $dbh->{Username}; | 
| 2311 | 0 |  |  |  |  |  | my ($schema, $tbl_name) = split /\./, uc($table); | 
| 2312 | 0 | 0 |  |  |  |  | if (!$tbl_name) { | 
| 2313 | 0 |  |  |  |  |  | $tbl_name = $schema; | 
| 2314 | 0 |  |  |  |  |  | $schema = $self->curr_schema(); | 
| 2315 |  |  |  |  |  |  | } | 
| 2316 |  |  |  |  |  |  |  | 
| 2317 | 0 |  |  |  |  |  | my $sth = $dbh->column_info(undef, $schema, $tbl_name, undef); | 
| 2318 | 0 |  |  |  |  |  | my @info_names = @{$sth->{NAME_uc}}; | 
|  | 0 |  |  |  |  |  |  | 
| 2319 | 0 |  |  |  |  |  | my %row; $sth->bind_columns(\@row{@info_names}); | 
|  | 0 |  |  |  |  |  |  | 
| 2320 | 0 |  |  |  |  |  | my (@columns, %is_date, %char_sz, %is_lob); | 
| 2321 | 0 | 0 |  |  |  |  | print "ColumnName  Type Size\n" if $opts->{Debug}; | 
| 2322 | 0 | 0 |  |  |  |  | print "----------------\n" if $opts->{Debug}; | 
| 2323 | 0 |  |  |  |  |  | while ($sth->fetch()) { | 
| 2324 | 0 |  |  |  |  |  | push @columns, $row{COLUMN_NAME}; | 
| 2325 | 0 | 0 |  |  |  |  | print "$row{COLUMN_NAME}\t$row{TYPE_NAME}\t$row{COLUMN_SIZE}\n" if $opts->{Debug}; | 
| 2326 | 0 | 0 |  |  |  |  | $char_sz{$row{COLUMN_NAME}} = exists($char_sizes{$row{COLUMN_NAME}}) ? $char_sizes{$row{COLUMN_NAME}} : $row{COLUMN_SIZE} if $row{TYPE_NAME} =~ /CHAR/; | 
|  |  | 0 |  |  |  |  |  | 
| 2327 | 0 | 0 |  |  |  |  | $char_sz{$row{COLUMN_NAME}} = exists($char_sizes{$row{COLUMN_NAME}}) ? $char_sizes{$row{COLUMN_NAME}} : 20_000_000, $is_lob{$row{COLUMN_NAME}} = 1 if $row{TYPE_NAME} =~ /TEXT|LOB|XML/; | 
|  |  | 0 |  |  |  |  |  | 
| 2328 | 0 | 0 |  |  |  |  | $is_date{$row{COLUMN_NAME}} = $1 if $row{TYPE_NAME} =~ /(DATE|TIMESTAMP)/; | 
| 2329 |  |  |  |  |  |  | } | 
| 2330 | 0 | 0 |  |  |  |  | confess("Table $schema.$tbl_name not found in database $db") unless @columns; | 
| 2331 |  |  |  |  |  |  |  | 
| 2332 |  |  |  |  |  |  | # Find date formats in file, remove constants from column list | 
| 2333 | 0 |  |  |  |  |  | my %date_fmt; | 
| 2334 |  |  |  |  |  |  | my @file_columns = grep !defined($const{$_}), | 
| 2335 | 0 | 0 | 0 |  |  |  | ( ( $opts->{ColumnList} && @{$opts->{ColumnList}} ) ? ( map uc, @{$opts->{ColumnList}} ) : @columns ); | 
|  | 0 |  |  |  |  |  |  | 
| 2336 | 0 | 0 |  |  |  |  | if (%is_date) { | 
| 2337 |  |  |  |  |  |  | # We don't want to sample rows from stdin | 
| 2338 | 0 |  |  |  |  |  | my @real_files = grep { $_ ne "-" } @files; | 
|  | 0 |  |  |  |  |  |  | 
| 2339 | 0 | 0 |  |  |  |  | %date_fmt = $self->date_masks_from_file( \@real_files, \@file_columns, \%is_date, $opts) | 
| 2340 |  |  |  |  |  |  | if @real_files; | 
| 2341 |  |  |  |  |  |  | } | 
| 2342 |  |  |  |  |  |  |  | 
| 2343 | 0 | 0 |  |  |  |  | my $row_delim_str = $opts->{RowDelimiter} ? qq("str '$opts->{RowDelimiter}'"\n) : ''; | 
| 2344 |  |  |  |  |  |  |  | 
| 2345 | 0 |  | 0 |  |  |  | my $delimiter = $opts->{Delimiter} || $self->{DELIMITER}; | 
| 2346 | 0 |  | 0 |  |  |  | my $action = $action_map{$action_opt} || "APPEND"; | 
| 2347 | 0 |  |  |  |  |  | my $direct_load_pre  = ''; | 
| 2348 | 0 |  |  |  |  |  | my $direct_load_post = ''; | 
| 2349 |  |  |  |  |  |  |  | 
| 2350 | 0 |  |  |  |  |  | my $sqlldr_opts = ''; | 
| 2351 | 0 |  | 0 |  |  |  | my $max_errors = $opts->{MaxErrors} || 0; | 
| 2352 | 0 |  |  |  |  |  | $sqlldr_opts .= "ERRORS=$max_errors"; | 
| 2353 | 0 | 0 |  |  |  |  | $sqlldr_opts .= ", SKIP=$opts->{Header}" if $opts->{Header}; | 
| 2354 |  |  |  |  |  |  |  | 
| 2355 | 0 | 0 |  |  |  |  | if ($opts->{DirectPath}) { | 
| 2356 | 0 | 0 |  |  |  |  | my $parallel = ( uc($opts->{DirectPath}) eq 'P' ) ?  ", PARALLEL=TRUE" : ''; | 
| 2357 | 0 |  |  |  |  |  | $direct_load_pre = "OPTIONS(DIRECT=TRUE$parallel, ROWS=1000000, $sqlldr_opts)\nUNRECOVERABLE\n"; | 
| 2358 | 0 |  |  |  |  |  | $direct_load_post = "REENABLE DISABLED_CONSTRAINTS\n"; | 
| 2359 |  |  |  |  |  |  | } else { | 
| 2360 | 0 |  | 0 |  |  |  | my $commit_rows = $opts->{CommitSize} || 2000; | 
| 2361 | 0 |  |  |  |  |  | $direct_load_pre = "OPTIONS (ROWS=$commit_rows, BINDSIZE=5000000, READSIZE=20970000, $sqlldr_opts)\n"; | 
| 2362 |  |  |  |  |  |  | } | 
| 2363 |  |  |  |  |  |  | my $default_date_fmt = | 
| 2364 |  |  |  |  |  |  | $opts->{SybaseDateFmt} ? 'MON DD YYYY HH12:MI:SS:FF3AM' | 
| 2365 |  |  |  |  |  |  | : $opts->{DateFormat}    ? $opts->{DateFormat} | 
| 2366 | 0 | 0 |  |  |  |  | : 'YYYY-MM-DD HH24:MI:SS.FF3' | 
|  |  | 0 |  |  |  |  |  | 
| 2367 |  |  |  |  |  |  | ; | 
| 2368 | 0 |  |  |  |  |  | for ( keys %is_date ) { | 
| 2369 | 0 |  | 0 |  |  |  | $date_fmt{$_} ||= $default_date_fmt; | 
| 2370 | 0 | 0 |  |  |  |  | $is_date{$_} = 'TIMESTAMP' if $date_fmt{$_} =~ /FF|TZ[DHMR]/; | 
| 2371 |  |  |  |  |  |  | } | 
| 2372 |  |  |  |  |  |  | my $quote_str = $opts->{QuoteFields} | 
| 2373 | 0 | 0 |  |  |  |  | ? qq( OPTIONALLY ENCLOSED BY '"') | 
| 2374 |  |  |  |  |  |  | : '' | 
| 2375 |  |  |  |  |  |  | ; | 
| 2376 | 0 | 0 |  |  |  |  | if ( $opts->{LoadWhen} ) { | 
| 2377 | 0 |  |  |  |  |  | $direct_load_post .= "WHEN $opts->{LoadWhen}\n"; | 
| 2378 |  |  |  |  |  |  | } | 
| 2379 |  |  |  |  |  |  |  | 
| 2380 | 0 |  |  |  |  |  | my $nls_str = ''; | 
| 2381 | 0 | 0 |  |  |  |  | $nls_str  = "CHARACTERSET $opts->{NLSLang}" if $opts->{NLSLang}; | 
| 2382 | 0 | 0 |  |  |  |  | $nls_str .= " LENGTH SEMANTICS $opts->{Semantics}" if $opts->{Semantics}; | 
| 2383 | 0 | 0 |  |  |  |  | $nls_str .= "\n" if $nls_str; | 
| 2384 |  |  |  |  |  |  |  | 
| 2385 | 0 |  |  |  |  |  | my %sybase_type; | 
| 2386 | 0 | 0 |  |  |  |  | @sybase_type{@file_columns} = @{$opts->{SybaseTypes}} if $opts->{SybaseTypes}; | 
|  | 0 |  |  |  |  |  |  | 
| 2387 |  |  |  |  |  |  | # Logic for trimming or preserving blanks on char/varchar columns | 
| 2388 |  |  |  |  |  |  | my $blank_control = sub { | 
| 2389 | 0 |  |  | 0 |  |  | my $size = $char_sz{$_}; | 
| 2390 |  |  |  |  |  |  | return "  $_ CHAR($size) PRESERVE BLANKS" | 
| 2391 | 0 | 0 | 0 |  |  |  | if $opts->{PreserveBlanks} or $size == 1; | 
| 2392 | 0 | 0 |  |  |  |  | if ( $opts->{SybaseTypes} ) { | 
| 2393 |  |  |  |  |  |  | # On the off chance a Sybase char column becomes an Oracle BLOB | 
| 2394 | 0 | 0 |  |  |  |  | return qq[  $_ CHAR($size)] if $is_lob{$_}; | 
| 2395 | 0 | 0 |  |  |  |  | return qq[  $_ CHAR($size) "NVL(RTRIM(:$_),' ')"] if $sybase_type{$_} eq 'char'; | 
| 2396 |  |  |  |  |  |  | } else { | 
| 2397 | 0 | 0 |  |  |  |  | return qq[  $_ CHAR($size)] if $is_lob{$_}; | 
| 2398 | 0 | 0 |  |  |  |  | return qq[  $_ CHAR($size) "NVL(RTRIM(:$_),' ')"] if $opts->{TrimBlanks}; | 
| 2399 |  |  |  |  |  |  | } | 
| 2400 | 0 |  |  |  |  |  | return "  $_ CHAR($size)"; | 
| 2401 | 0 |  |  |  |  |  | }; | 
| 2402 |  |  |  |  |  |  |  | 
| 2403 | 0 |  | 0 |  |  |  | my $field_ref = $opts->{FieldRef}  || {}; | 
| 2404 |  |  |  |  |  |  | my %field_ref = map { | 
| 2405 | 0 |  |  |  |  |  | my $col = $_; | 
|  | 0 |  |  |  |  |  |  | 
| 2406 | 0 |  |  |  |  |  | my $tmp = $field_ref->{$col}; | 
| 2407 | 0 | 0 |  |  |  |  | my $v = ( $tmp =~ s/^~// ) ? "POSITION $tmp" : qq("$tmp"); | 
| 2408 | 0 |  |  |  |  |  | uc($col) => $v; | 
| 2409 |  |  |  |  |  |  | } keys %$field_ref; | 
| 2410 |  |  |  |  |  |  |  | 
| 2411 |  |  |  |  |  |  | # Field ref columns that don't reference themselves | 
| 2412 |  |  |  |  |  |  | # will be considered similar to constant columns, but they must come | 
| 2413 |  |  |  |  |  |  | # last, otherwise column alignment will be off | 
| 2414 | 0 |  |  |  |  |  | my %field_ref_const; | 
| 2415 | 0 |  |  |  |  |  | for ( keys %field_ref ) { | 
| 2416 | 0 | 0 |  |  |  |  | next if $field_ref{$_} =~ /:$_\b/i; | 
| 2417 | 0 |  |  |  |  |  | $field_ref_const{$_}++; | 
| 2418 |  |  |  |  |  |  | } | 
| 2419 | 0 |  |  |  |  |  | @columns = ( $opts->{ColumnList} && @{$opts->{ColumnList}} ) ? map uc($_), @{$opts->{ColumnList}} : ( | 
| 2420 | 0 | 0 | 0 |  |  |  | (grep !$field_ref_const{$_}, @columns), | 
| 2421 |  |  |  |  |  |  | keys %field_ref_const, | 
| 2422 |  |  |  |  |  |  | ); | 
| 2423 | 0 |  |  |  |  |  | my %is_filler; | 
| 2424 | 0 | 0 |  |  |  |  | if ( $opts->{Filler} ) { | 
| 2425 | 0 |  |  |  |  |  | $is_filler{uc($_)}++ for @{$opts->{Filler}}; | 
|  | 0 |  |  |  |  |  |  | 
| 2426 |  |  |  |  |  |  | } | 
| 2427 |  |  |  |  |  |  |  | 
| 2428 | 0 |  |  |  |  |  | my $file_str = join(",", @bcp_files); | 
| 2429 | 0 |  |  |  |  |  | my $sqlldr_file_str = join("\n", map "INFILE '$_'", @bcp_files); | 
| 2430 | 0 |  |  |  |  |  | my $disp_table = my $sqlldr_table = $table; | 
| 2431 |  |  |  |  |  |  |  | 
| 2432 | 0 | 0 |  |  |  |  | if ($partition) { | 
| 2433 | 0 |  |  |  |  |  | $sqlldr_table .= " PARTITION ($partition)"; | 
| 2434 | 0 |  |  |  |  |  | $disp_table   .= ":$partition"; | 
| 2435 |  |  |  |  |  |  | } | 
| 2436 |  |  |  |  |  |  |  | 
| 2437 |  |  |  |  |  |  | # Default charset is roman8 on HP | 
| 2438 |  |  |  |  |  |  | # Must set it here | 
| 2439 |  |  |  |  |  |  | printf $ctl_fh | 
| 2440 |  |  |  |  |  |  | $direct_load_pre. | 
| 2441 |  |  |  |  |  |  | "LOAD DATA\n". | 
| 2442 |  |  |  |  |  |  | #"CHARACTERSET WE8ROMAN8\n". | 
| 2443 |  |  |  |  |  |  | $nls_str. | 
| 2444 |  |  |  |  |  |  | "%s\n". | 
| 2445 |  |  |  |  |  |  | $row_delim_str. | 
| 2446 |  |  |  |  |  |  | "INTO TABLE %s %s\n". | 
| 2447 |  |  |  |  |  |  | $direct_load_post. | 
| 2448 |  |  |  |  |  |  | qq(FIELDS TERMINATED BY '$delimiter'$quote_str\n). | 
| 2449 |  |  |  |  |  |  | "TRAILING NULLCOLS\n". | 
| 2450 |  |  |  |  |  |  | "(\n%s\n)\n", | 
| 2451 |  |  |  |  |  |  | $sqlldr_file_str, | 
| 2452 |  |  |  |  |  |  | $sqlldr_table, | 
| 2453 |  |  |  |  |  |  | $action, | 
| 2454 |  |  |  |  |  |  | join(",\n", map { | 
| 2455 | 0 |  |  |  |  |  | ( | 
| 2456 |  |  |  |  |  |  | exists($const{$_}) ? qq[  $_ CONSTANT '$const{$_}'] | 
| 2457 |  |  |  |  |  |  | : exists($is_filler{$_}) ? qq[  $_ FILLER] | 
| 2458 |  |  |  |  |  |  | : exists($field_ref{$_}) ? qq[  $_ $field_ref{$_}] | 
| 2459 |  |  |  |  |  |  | : $is_date{$_} ? "  $_ $is_date{$_} '$date_fmt{$_}'" | 
| 2460 | 0 | 0 |  |  |  |  | : $char_sz{$_} ? $blank_control->() | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 2461 |  |  |  |  |  |  | : "  $_" | 
| 2462 |  |  |  |  |  |  | ) | 
| 2463 |  |  |  |  |  |  | } @columns); | 
| 2464 | 0 | 0 |  |  |  |  | if ($prm_fh) { | 
| 2465 | 0 |  |  |  |  |  | print $prm_fh "userid=$user/$self->{PASSWORD}\@$db\n"; | 
| 2466 | 0 |  |  |  |  |  | close $prm_fh; | 
| 2467 |  |  |  |  |  |  | } | 
| 2468 |  |  |  |  |  |  |  | 
| 2469 | 0 |  |  |  |  |  | $_->close() for $ctl_fh, $bad_fh, $log_fh; | 
| 2470 |  |  |  |  |  |  |  | 
| 2471 | 0 |  |  |  |  |  | print "Loading $db..$disp_table from $file_str\n"; | 
| 2472 |  |  |  |  |  |  |  | 
| 2473 | 0 |  |  |  |  |  | my $ctl_file = $ctl_fh->filename(); | 
| 2474 | 0 |  |  |  |  |  | my $bad_file = $bad_fh->filename(); | 
| 2475 | 0 |  |  |  |  |  | my $log_file = $log_fh->filename(); | 
| 2476 | 0 | 0 |  |  |  |  | my $prm_file = $prm_fh ? $prm_fh->filename() : undef; | 
| 2477 | 0 | 0 |  |  |  |  | if ($keep_temp) { | 
| 2478 | 0 |  |  |  |  |  | print "SqlldrControlFile: ", abs_path($ctl_file), "\n"; | 
| 2479 | 0 |  |  |  |  |  | print "SqlldrBadRowFile : ", abs_path($bad_file), "\n"; | 
| 2480 | 0 |  |  |  |  |  | print "SqlldrLogFile    : ", abs_path($log_file), "\n"; | 
| 2481 |  |  |  |  |  |  | } | 
| 2482 | 0 |  |  |  |  |  | local $ENV{NLS_DATE_FORMAT} = 'YYYY-MM-DD HH24:MI:SS'; | 
| 2483 | 0 |  |  |  |  |  | local $ENV{NLS_TIMESTAMP_FORMAT} = 'YYYY-MM-DD HH24:MI:SS.FF'; | 
| 2484 | 0 |  |  |  |  |  | local $ENV{NLS_TIMESTAMP_TZ_FORMAT} = 'YYYY-MM-DD HH24:MI:SS.FF'; | 
| 2485 |  |  |  |  |  |  |  | 
| 2486 | 0 |  |  |  |  |  | my @prm_opt; | 
| 2487 | 0 | 0 |  |  |  |  | @prm_opt = "parfile=$prm_file" if $prm_file; | 
| 2488 | 0 |  |  |  |  |  | my @cmd = ( | 
| 2489 |  |  |  |  |  |  | sqlldr => | 
| 2490 |  |  |  |  |  |  | "control=$ctl_file", | 
| 2491 |  |  |  |  |  |  | "log=$log_file", | 
| 2492 |  |  |  |  |  |  | "bad=$bad_file", | 
| 2493 |  |  |  |  |  |  | @prm_opt, | 
| 2494 |  |  |  |  |  |  | "silent=(header,discards,feedback,partitions)", | 
| 2495 |  |  |  |  |  |  | ); | 
| 2496 | 0 | 0 | 0 |  |  |  | print "Executing: @cmd\n" if $opts->{Debug} || $opts->{NoExec}; | 
| 2497 | 0 | 0 |  |  |  |  | return "@cmd" if $opts->{NoExec}; | 
| 2498 |  |  |  |  |  |  |  | 
| 2499 | 0 |  |  |  |  |  | my $close_success; | 
| 2500 |  |  |  |  |  |  |  | 
| 2501 |  |  |  |  |  |  | # We could do this either way with IPC::Run | 
| 2502 |  |  |  |  |  |  | # But lets not require it unless necessary. | 
| 2503 | 0 | 0 |  |  |  |  | if ($stdin) { | 
| 2504 | 0 |  |  |  |  |  | require IPC::Run; | 
| 2505 |  |  |  |  |  |  |  | 
| 2506 | 0 |  |  |  |  |  | $close_success = IPC::Run::run( \@cmd, '<', $stdin ); | 
| 2507 |  |  |  |  |  |  | } else { | 
| 2508 |  |  |  |  |  |  | # Hide user/passwd from ps | 
| 2509 | 0 | 0 |  |  |  |  | open(my $cmd_fh, "|-", @cmd) or confess "Could not exec sqlldr: $!"; | 
| 2510 | 0 |  |  |  |  |  | print $cmd_fh "$user/$self->{PASSWORD}\@$db\n"; | 
| 2511 |  |  |  |  |  |  |  | 
| 2512 |  |  |  |  |  |  | # We don't want to exit right away on failure | 
| 2513 |  |  |  |  |  |  | # We want to see the log file and bad record if any | 
| 2514 | 0 |  |  |  |  |  | $close_success = close $cmd_fh; | 
| 2515 |  |  |  |  |  |  | } | 
| 2516 |  |  |  |  |  |  |  | 
| 2517 |  |  |  |  |  |  | # We don't want to exit right away on failure | 
| 2518 |  |  |  |  |  |  | # We want to see the log file and bad record if any | 
| 2519 | 0 |  |  |  |  |  | my $exit_stat = $? >> 8; | 
| 2520 | 0 |  |  |  |  |  | my $exit_sig  = $? & 127; | 
| 2521 | 0 |  |  |  |  |  | my $exit_core = $? & 128; | 
| 2522 |  |  |  |  |  |  |  | 
| 2523 |  |  |  |  |  |  | # We have a limit of one rejected row. If we have a bad row | 
| 2524 |  |  |  |  |  |  | # we'll just include it in the error. | 
| 2525 |  |  |  |  |  |  | # Oops thats no longer true now that we have a MaxErrors option | 
| 2526 |  |  |  |  |  |  | # Just show the first bad row if we allow > 1 error | 
| 2527 | 0 |  |  |  |  |  | my $bad_row; | 
| 2528 | 0 | 0 |  |  |  |  | if ( -s $bad_file ) { | 
| 2529 | 0 | 0 |  |  |  |  | if ( $max_errors > 0 ) { | 
| 2530 | 0 |  |  |  |  |  | local ($_, $.); | 
| 2531 | 0 |  | 0 |  |  |  | local $/ = $opts->{RowDelimiter} || "\n"; | 
| 2532 | 0 | 0 |  |  |  |  | open(my $fh, "<", $bad_file) or confess "Can't open sqlldr reject file $bad_file: $!"; | 
| 2533 | 0 |  |  |  |  |  | $bad_row = <$fh>; | 
| 2534 | 0 |  |  |  |  |  | close $fh; | 
| 2535 |  |  |  |  |  |  | } else { | 
| 2536 | 0 |  |  |  |  |  | warn "sqlldr error loading $file_str into $disp_table on row:\n"; | 
| 2537 | 0 |  |  |  |  |  | $bad_row = `cat $bad_file`; | 
| 2538 |  |  |  |  |  |  | } | 
| 2539 |  |  |  |  |  |  | } | 
| 2540 | 0 | 0 |  |  |  |  | open(my $fh, "<", $log_file) or confess "Can't open sqlldr log $log_file: $!"; | 
| 2541 | 0 |  |  |  |  |  | print "Opened $log_file\n"; | 
| 2542 | 0 |  |  |  |  |  | local ($_, $.); | 
| 2543 | 0 |  |  |  |  |  | my ( $rows, $error_rows, $failed_rows, $null_rows, $error_msg, $discontinued, $dp_errors ); | 
| 2544 |  |  |  |  |  |  |  | 
| 2545 |  |  |  |  |  |  | # Only save first 1000 errors | 
| 2546 | 0 |  |  |  |  |  | my $err_cnt = 0; | 
| 2547 | 0 |  |  |  |  |  | while (<$fh>) { | 
| 2548 | 0 |  |  |  |  |  | print; | 
| 2549 | 0 | 0 |  |  |  |  | if ( /^\s*(\d+)/ ) { | 
| 2550 | 0 |  |  |  |  |  | my $tmp_rows = $1; | 
| 2551 | 0 | 0 |  |  |  |  | $rows        = $tmp_rows if /successfully loaded/; | 
| 2552 | 0 | 0 |  |  |  |  | $error_rows  = $tmp_rows if /not loaded due to data errors/; | 
| 2553 | 0 | 0 |  |  |  |  | $failed_rows = $tmp_rows if /not loaded because all WHEN clauses/; | 
| 2554 | 0 | 0 |  |  |  |  | $null_rows   = $tmp_rows if /not loaded because all fields were null/; | 
| 2555 | 0 |  |  |  |  |  | next; | 
| 2556 |  |  |  |  |  |  | } | 
| 2557 | 0 | 0 |  |  |  |  | if ( /^Record \d+: Rejected/ ) { | 
| 2558 | 0 | 0 |  |  |  |  | $error_msg .= $_ if $err_cnt < 1000; | 
| 2559 | 0 |  |  |  |  |  | next; | 
| 2560 |  |  |  |  |  |  | } | 
| 2561 | 0 | 0 |  |  |  |  | if ( /^(?:SQL\*Loader|ORA)-\d+:/ ) { | 
| 2562 | 0 | 0 |  |  |  |  | $error_msg .= $_ if ++$err_cnt <= 1000; | 
| 2563 | 0 | 0 |  |  |  |  | $discontinued++ if /discontinued|aborted/; | 
| 2564 | 0 |  |  |  |  |  | next; | 
| 2565 |  |  |  |  |  |  | } | 
| 2566 |  |  |  |  |  |  |  | 
| 2567 |  |  |  |  |  |  | # Catch direct path errors | 
| 2568 | 0 | 0 |  |  |  |  | if ( /was not re-(?:enabled|validated)/ ) { | 
| 2569 |  |  |  |  |  |  | # These errors do not cause non-zero exit status | 
| 2570 | 0 |  |  |  |  |  | $dp_errors++; | 
| 2571 | 0 | 0 |  |  |  |  | $error_msg .= $_ if $err_cnt < 1000; | 
| 2572 | 0 |  |  |  |  |  | next; | 
| 2573 |  |  |  |  |  |  | } | 
| 2574 | 0 | 0 |  |  |  |  | if ( /^index \S+ was made unusable/ ) { | 
| 2575 | 0 |  |  |  |  |  | $dp_errors++; | 
| 2576 | 0 | 0 |  |  |  |  | $error_msg .= $_ if ++$err_cnt <= 1000; | 
| 2577 | 0 |  |  |  |  |  | next; | 
| 2578 |  |  |  |  |  |  | } | 
| 2579 |  |  |  |  |  |  |  | 
| 2580 |  |  |  |  |  |  | } | 
| 2581 | 0 |  |  |  |  |  | close $fh; | 
| 2582 |  |  |  |  |  |  |  | 
| 2583 | 0 | 0 | 0 |  |  |  | if (!$close_success or $dp_errors) { | 
| 2584 | 0 |  | 0 |  |  |  | $error_msg ||= ''; | 
| 2585 | 0 | 0 | 0 |  |  |  | if ( $exit_stat != 0 or $dp_errors ) { | 
| 2586 | 0 | 0 | 0 |  |  |  | if ( $exit_stat == 2 or $dp_errors ) { | 
| 2587 |  |  |  |  |  |  | # Exit status 2 is just a warning | 
| 2588 |  |  |  |  |  |  | # But we should consider it an error if we exceeded the max errors allowed | 
| 2589 |  |  |  |  |  |  | # Or if load was discontinued for any reason | 
| 2590 |  |  |  |  |  |  | # Or for any direct path errors | 
| 2591 | 0 | 0 |  |  |  |  | my $first = ($max_errors > 0) ? 'first ' : ''; | 
| 2592 | 0 | 0 |  |  |  |  | confess "sqlldr exited with status $exit_stat [$error_msg]" if $dp_errors; | 
| 2593 | 0 | 0 |  |  |  |  | confess "sqlldr exited with status $exit_stat [$error_msg] - ${first}rejected record:[$bad_row]" if $error_rows > $max_errors; | 
| 2594 | 0 | 0 |  |  |  |  | confess "sqlldr exited with status $exit_stat [$error_msg]" if $discontinued; | 
| 2595 |  |  |  |  |  |  | } else { | 
| 2596 | 0 |  |  |  |  |  | confess "sqlldr exited with status $exit_stat [$error_msg]"; | 
| 2597 |  |  |  |  |  |  | } | 
| 2598 |  |  |  |  |  |  | } | 
| 2599 | 0 | 0 |  |  |  |  | confess "sqlldr received signal $exit_sig [$error_msg]" if $exit_sig > 0; | 
| 2600 | 0 | 0 |  |  |  |  | confess "sqlldr coredumped [$error_msg]"                if $exit_core; | 
| 2601 |  |  |  |  |  |  | } | 
| 2602 | 0 |  |  |  |  |  | return $rows; | 
| 2603 |  |  |  |  |  |  | } | 
| 2604 |  |  |  |  |  |  | } | 
| 2605 |  |  |  |  |  |  |  | 
| 2606 |  |  |  |  |  |  | # Dummy method for compatibility with Sybase | 
| 2607 |  |  |  | 0 |  |  | sub mk_view { } | 
| 2608 |  |  |  |  |  |  |  | 
| 2609 |  |  |  |  |  |  | sub date_masks_from_file { | 
| 2610 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 2611 | 0 |  |  |  |  |  | my ($files, $columns, $is_date, $opts) = @_; | 
| 2612 |  |  |  |  |  |  |  | 
| 2613 | 0 | 0 | 0 |  |  |  | return unless $is_date and %$is_date; | 
| 2614 |  |  |  |  |  |  |  | 
| 2615 | 0 |  | 0 |  |  |  | $opts ||= {}; | 
| 2616 |  |  |  |  |  |  |  | 
| 2617 | 0 |  | 0 |  |  |  | my $sample_rows = $opts->{DateSampleRows} || 1000; | 
| 2618 | 0 |  | 0 |  |  |  | my $d = $opts->{Delimiter} || $self->{DELIMITER}; | 
| 2619 | 0 |  |  |  |  |  | my $rd = $opts->{RowDelimiter}; | 
| 2620 | 0 |  | 0 |  |  |  | my $year_mask = $opts->{Year2Mask} || 'YY'; | 
| 2621 |  |  |  |  |  |  |  | 
| 2622 | 0 |  |  |  |  |  | local ($., $_, $ARGV, *ARGV); | 
| 2623 | 0 | 0 |  |  |  |  | local $/ = $rd if $rd; | 
| 2624 | 0 |  |  |  |  |  | local @ARGV = @$files; | 
| 2625 |  |  |  |  |  |  |  | 
| 2626 | 0 |  |  |  |  |  | my $row_cnt; | 
| 2627 | 0 |  |  |  |  |  | my (%remaining, %got); | 
| 2628 | 0 |  |  |  |  |  | $remaining{$_}++ for keys %$is_date; | 
| 2629 |  |  |  |  |  |  |  | 
| 2630 | 0 |  |  |  |  |  | my %fmt; | 
| 2631 | 0 |  | 0 |  |  |  | my $dc_fmt = $opts->{DateColumnFmt} || {}; | 
| 2632 | 0 |  |  |  |  |  | for my $col ( keys %$dc_fmt ) { | 
| 2633 | 0 |  |  |  |  |  | my $c = uc($col); | 
| 2634 | 0 |  |  |  |  |  | $fmt{$c} = $dc_fmt->{$col}; | 
| 2635 | 0 |  |  |  |  |  | delete $remaining{$c} | 
| 2636 |  |  |  |  |  |  | } | 
| 2637 |  |  |  |  |  |  |  | 
| 2638 | 0 |  |  |  |  |  | my %row; | 
| 2639 | 0 |  |  |  |  |  | while (<>) { | 
| 2640 | 0 | 0 | 0 |  |  |  | next if $opts->{Header} and $. <= $opts->{Header}; | 
| 2641 | 0 |  |  |  |  |  | chomp; | 
| 2642 | 0 | 0 |  |  |  |  | @row{@$columns} = $opts->{QuoteFields} ? split_quoted( $_, $d ) : split /\Q$d/; | 
| 2643 | 0 |  |  |  |  |  | for (keys %remaining) { | 
| 2644 | 0 | 0 |  |  |  |  | if ( $row{$_} ) { | 
| 2645 | 0 |  |  |  |  |  | delete $remaining{$_}; | 
| 2646 | 0 |  |  |  |  |  | $got{$_} = $row{$_}; | 
| 2647 | 0 | 0 |  |  |  |  | last if !%remaining; | 
| 2648 |  |  |  |  |  |  | } | 
| 2649 |  |  |  |  |  |  | } | 
| 2650 |  |  |  |  |  |  |  | 
| 2651 |  |  |  |  |  |  | # If we haven't found values by now, give up | 
| 2652 | 0 | 0 |  |  |  |  | last if ++$row_cnt >= $sample_rows; | 
| 2653 |  |  |  |  |  |  | } | 
| 2654 |  |  |  |  |  |  |  | 
| 2655 | 0 |  |  |  |  |  | $fmt{$_} = $self->date_mask($got{$_}, $year_mask) for keys %got; | 
| 2656 |  |  |  |  |  |  |  | 
| 2657 | 0 |  |  |  |  |  | return %fmt; | 
| 2658 |  |  |  |  |  |  | } | 
| 2659 |  |  |  |  |  |  |  | 
| 2660 |  |  |  |  |  |  | # If we allow quoted fields, need to split correctly and | 
| 2661 |  |  |  |  |  |  | # handle embedded quotes and delimiters | 
| 2662 |  |  |  |  |  |  | sub split_quoted { | 
| 2663 | 0 |  |  | 0 |  |  | my ($line,$d) = @_; | 
| 2664 | 0 |  |  |  |  |  | my @result; | 
| 2665 | 0 |  |  |  |  |  | while ( $line =~ s/\A("?)((?:""|.)*?)\1(\Q$d\E|\z)//s ) { | 
| 2666 | 0 |  |  |  |  |  | my ( $q, $s,$got_d ) = ( $1, $2, $3 ); | 
| 2667 | 0 | 0 |  |  |  |  | $s =~ s/""/"/g if $q; | 
| 2668 | 0 |  |  |  |  |  | push @result, $s; | 
| 2669 | 0 | 0 |  |  |  |  | last if length($got_d) == 0; | 
| 2670 |  |  |  |  |  |  | } | 
| 2671 | 0 |  |  |  |  |  | return @result; | 
| 2672 |  |  |  |  |  |  | } | 
| 2673 |  |  |  |  |  |  |  | 
| 2674 |  |  |  |  |  |  | { | 
| 2675 |  |  |  |  |  |  |  | 
| 2676 |  |  |  |  |  |  | my @mon    =  qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ); | 
| 2677 |  |  |  |  |  |  | my $mon_str = join("|", @mon); | 
| 2678 |  |  |  |  |  |  | my $mon_re = qr/(?i)$mon_str/; | 
| 2679 |  |  |  |  |  |  | my @months =  qw( January February March April May June July August September October November December ); | 
| 2680 |  |  |  |  |  |  | my $month_str = join("|", @months); | 
| 2681 |  |  |  |  |  |  | my $month_re = qr/(?i)$month_str/; | 
| 2682 |  |  |  |  |  |  | my @days = qw( Mon Tue Wed Thu Fri Sat Sun ); | 
| 2683 |  |  |  |  |  |  | my $day_str = join("|", @days); | 
| 2684 |  |  |  |  |  |  | my $day_re  = qr/(?i)$day_str/; | 
| 2685 |  |  |  |  |  |  |  | 
| 2686 |  |  |  |  |  |  | sub date_mask { | 
| 2687 | 0 |  |  | 0 |  |  | my ($self, $str, $year2mask) = @_; | 
| 2688 | 0 | 0 |  |  |  |  | return unless $str; | 
| 2689 | 0 |  |  |  |  |  | local $_ = $str; | 
| 2690 |  |  |  |  |  |  |  | 
| 2691 | 0 |  |  |  |  |  | my $fmt = ''; | 
| 2692 | 0 |  | 0 |  |  |  | $year2mask ||= 'YY'; | 
| 2693 |  |  |  |  |  |  |  | 
| 2694 |  |  |  |  |  |  | # YYYY-MM-DD or YYYYMMDD | 
| 2695 | 0 | 0 |  |  |  |  | if ( s/^\d{4}(\D?)\d\d(\D?)\d\d// ) { | 
| 2696 | 0 |  |  |  |  |  | $fmt .= "YYYY${1}MM${2}DD"; | 
| 2697 | 0 |  |  |  |  |  | $fmt .= time_mask(); | 
| 2698 |  |  |  |  |  |  | #die "Can not determine date mask for $str ($fmt)" if length($_); | 
| 2699 | 0 | 0 |  |  |  |  | return if length($_); | 
| 2700 | 0 |  |  |  |  |  | return $fmt; | 
| 2701 |  |  |  |  |  |  | } | 
| 2702 |  |  |  |  |  |  |  | 
| 2703 |  |  |  |  |  |  | # Allow day abbreviation (Mon Tue etc.) | 
| 2704 | 0 | 0 |  |  |  |  | $fmt .= "DY " if s/^$day_re\s+//; | 
| 2705 |  |  |  |  |  |  |  | 
| 2706 |  |  |  |  |  |  | # Jan 23 2010 | 
| 2707 | 0 | 0 |  |  |  |  | if ( s/^$mon_re\s+\d+// ) { | 
| 2708 | 0 |  |  |  |  |  | my $end_year; | 
| 2709 | 0 |  |  |  |  |  | $fmt .= "MON DD"; | 
| 2710 | 0 | 0 |  |  |  |  | if ( s/^\s\d{4}// ) { | 
|  |  | 0 |  |  |  |  |  | 
| 2711 | 0 |  |  |  |  |  | $fmt .= " YYYY"; | 
| 2712 |  |  |  |  |  |  | } elsif ( s/\s+\d{4}$// ) { | 
| 2713 | 0 |  |  |  |  |  | $end_year++; | 
| 2714 |  |  |  |  |  |  | } else { | 
| 2715 |  |  |  |  |  |  | #die "Can not determine date mask for $str ($fmt)"; | 
| 2716 | 0 |  |  |  |  |  | return; | 
| 2717 |  |  |  |  |  |  | } | 
| 2718 | 0 |  |  |  |  |  | $fmt .= time_mask(); | 
| 2719 |  |  |  |  |  |  |  | 
| 2720 |  |  |  |  |  |  | #die "Can not determine date mask for $str ($fmt)" if length($_); | 
| 2721 | 0 | 0 |  |  |  |  | return if length($_); | 
| 2722 | 0 | 0 |  |  |  |  | $fmt .= " YYYY" if $end_year; | 
| 2723 | 0 |  |  |  |  |  | return $fmt; | 
| 2724 |  |  |  |  |  |  | } | 
| 2725 |  |  |  |  |  |  |  | 
| 2726 |  |  |  |  |  |  | # January 23, 2010 | 
| 2727 | 0 | 0 |  |  |  |  | if ( s/^$month_re\s+\d+// ) { | 
| 2728 | 0 |  |  |  |  |  | my $end_year; | 
| 2729 | 0 |  |  |  |  |  | $fmt .= "MONTH DD"; | 
| 2730 | 0 | 0 |  |  |  |  | if ( s/^(\W?)\s\d{4}// ) { | 
|  |  | 0 |  |  |  |  |  | 
| 2731 | 0 |  |  |  |  |  | my $comma = $1; | 
| 2732 | 0 |  |  |  |  |  | $fmt .= "$comma YYYY"; | 
| 2733 |  |  |  |  |  |  | } elsif ( s/\s+\d{4}$// ) { | 
| 2734 | 0 |  |  |  |  |  | $end_year++; | 
| 2735 |  |  |  |  |  |  | } else { | 
| 2736 |  |  |  |  |  |  | #die "Can not determine date mask for $str ($fmt)"; | 
| 2737 | 0 |  |  |  |  |  | return; | 
| 2738 |  |  |  |  |  |  | } | 
| 2739 | 0 |  |  |  |  |  | $fmt .= time_mask(); | 
| 2740 |  |  |  |  |  |  |  | 
| 2741 |  |  |  |  |  |  | #die "Can not determine date mask for $str ($fmt)" if length($_); | 
| 2742 | 0 | 0 |  |  |  |  | return if length($_); | 
| 2743 | 0 | 0 |  |  |  |  | $fmt .= " YYYY" if $end_year; | 
| 2744 | 0 |  |  |  |  |  | return $fmt; | 
| 2745 |  |  |  |  |  |  | } | 
| 2746 |  |  |  |  |  |  |  | 
| 2747 |  |  |  |  |  |  | # 02-Jan-2010 | 
| 2748 | 0 | 0 |  |  |  |  | if ( s/^\d\d?(\D?)$mon_re(\D?)\d{4}// ) { | 
| 2749 | 0 |  |  |  |  |  | $fmt .= "DD${1}MON${2}YYYY"; | 
| 2750 | 0 |  |  |  |  |  | $fmt .= time_mask(); | 
| 2751 |  |  |  |  |  |  | #die "Can not determine date mask for $str ($fmt)" if length($_); | 
| 2752 | 0 | 0 |  |  |  |  | return if length($_); | 
| 2753 | 0 |  |  |  |  |  | return $fmt; | 
| 2754 |  |  |  |  |  |  | } | 
| 2755 |  |  |  |  |  |  |  | 
| 2756 |  |  |  |  |  |  | # 02-Jan-10 | 
| 2757 | 0 | 0 |  |  |  |  | if ( s/^\d\d?(\D?)$mon_re(\D?)\d\d?// ) { | 
| 2758 | 0 |  |  |  |  |  | $fmt .= "DD${1}MON${2}$year2mask"; | 
| 2759 | 0 |  |  |  |  |  | $fmt .= time_mask(); | 
| 2760 |  |  |  |  |  |  | #die "Can not determine date mask for $str ($fmt)" if length($_); | 
| 2761 | 0 | 0 |  |  |  |  | return if length($_); | 
| 2762 | 0 |  |  |  |  |  | return $fmt; | 
| 2763 |  |  |  |  |  |  | } | 
| 2764 |  |  |  |  |  |  |  | 
| 2765 |  |  |  |  |  |  | # MM/DD/YYYY | 
| 2766 | 0 | 0 |  |  |  |  | if ( s|^\d\d?(\D)\d\d?(\D)\d{4}|| ) { | 
| 2767 | 0 |  |  |  |  |  | $fmt .= "MM${1}DD${2}YYYY"; | 
| 2768 | 0 |  |  |  |  |  | $fmt .= time_mask(); | 
| 2769 |  |  |  |  |  |  | #die "Can not determine date mask for $str ($fmt)" if length($_); | 
| 2770 | 0 | 0 |  |  |  |  | return if length($_); | 
| 2771 | 0 |  |  |  |  |  | return $fmt; | 
| 2772 |  |  |  |  |  |  | } | 
| 2773 |  |  |  |  |  |  |  | 
| 2774 |  |  |  |  |  |  | #die "Failure to determine date mask for $str"; | 
| 2775 | 0 |  |  |  |  |  | return; | 
| 2776 |  |  |  |  |  |  | } | 
| 2777 |  |  |  |  |  |  | } | 
| 2778 |  |  |  |  |  |  |  | 
| 2779 |  |  |  |  |  |  | # Operates on and modifies current $_ | 
| 2780 |  |  |  |  |  |  | sub time_mask { | 
| 2781 | 0 |  |  | 0 |  |  | my $fmt = ''; | 
| 2782 | 0 | 0 |  |  |  |  | if ( s/^(\D?)[\s\d]\d// ) { | 
| 2783 | 0 |  |  |  |  |  | my $sep = $1; | 
| 2784 | 0 | 0 |  |  |  |  | $sep = qq("$sep") if $sep =~ /\S/; | 
| 2785 | 0 |  |  |  |  |  | $fmt .= "${sep}HH"; | 
| 2786 | 0 | 0 |  |  |  |  | $fmt .= /[AP]M\b/i ? "12" : "24"; | 
| 2787 | 0 | 0 |  |  |  |  | if ( s/^(\D)\d\d// ) { | 
| 2788 | 0 |  |  |  |  |  | $fmt .= "${1}MI"; | 
| 2789 | 0 | 0 |  |  |  |  | if ( s/^(\D)\d\d// ) { | 
| 2790 | 0 |  |  |  |  |  | $fmt .= "${1}SS"; | 
| 2791 | 0 | 0 |  |  |  |  | if ( s/^(\D)(\d+)// ) { | 
| 2792 | 0 |  |  |  |  |  | $fmt .= $1 . "FF" . length($2); | 
| 2793 |  |  |  |  |  |  | } | 
| 2794 |  |  |  |  |  |  | } | 
| 2795 |  |  |  |  |  |  | } | 
| 2796 | 0 | 0 |  |  |  |  | if ( s/^(\s?)[AP]M// ) { | 
| 2797 | 0 |  |  |  |  |  | $fmt .= "${1}AM"; | 
| 2798 |  |  |  |  |  |  | } | 
| 2799 | 0 | 0 |  |  |  |  | if ( s/^(\s*)\w{2,3}T//i ) { | 
| 2800 | 0 |  |  |  |  |  | $fmt .= "${1}TZD"; | 
| 2801 |  |  |  |  |  |  | } | 
| 2802 | 0 | 0 |  |  |  |  | if ( s/^\s[+-]\d\d(\D)\d\d// ) { | 
| 2803 | 0 |  |  |  |  |  | $fmt .= " TZH${1}TZM"; | 
| 2804 |  |  |  |  |  |  | } | 
| 2805 |  |  |  |  |  |  | } | 
| 2806 | 0 |  |  |  |  |  | return $fmt; | 
| 2807 |  |  |  |  |  |  | } | 
| 2808 |  |  |  |  |  |  |  | 
| 2809 |  |  |  |  |  |  | { | 
| 2810 |  |  |  |  |  |  | my %type_map = ( TABLE => 'T', VIEW => 'V', PROCEDURE => 'P' ); | 
| 2811 |  |  |  |  |  |  |  | 
| 2812 |  |  |  |  |  |  | sub obj_type { | 
| 2813 | 0 |  |  | 0 |  |  | my ( $self, $name ) = @_; | 
| 2814 | 0 |  |  |  |  |  | $name = uc($name); | 
| 2815 | 0 |  |  |  |  |  | my $type; | 
| 2816 | 0 | 0 |  |  |  |  | if ( $name =~ /^([^.]+)\.(.+)/ ) { | 
| 2817 | 0 |  |  |  |  |  | my ($schema, $table) = ($1, $2); | 
| 2818 |  |  |  |  |  |  | $type = $self->{DBH}->selectrow_array( | 
| 2819 | 0 |  |  |  |  |  | "select object_type from all_objects where owner = ? and object_name = ?", | 
| 2820 |  |  |  |  |  |  | undef, | 
| 2821 |  |  |  |  |  |  | $schema, | 
| 2822 |  |  |  |  |  |  | $table, | 
| 2823 |  |  |  |  |  |  | ); | 
| 2824 |  |  |  |  |  |  | } else { | 
| 2825 |  |  |  |  |  |  | $type = $self->{DBH}->selectrow_array( | 
| 2826 | 0 |  |  |  |  |  | "select object_type from user_objects where object_name = ?", | 
| 2827 |  |  |  |  |  |  | undef, | 
| 2828 |  |  |  |  |  |  | $name | 
| 2829 |  |  |  |  |  |  | ); | 
| 2830 |  |  |  |  |  |  | } | 
| 2831 | 0 | 0 |  |  |  |  | return unless $type; | 
| 2832 | 0 |  | 0 |  |  |  | return $type_map{$type} || confess "Don't know about type $type for object $name"; | 
| 2833 |  |  |  |  |  |  | } | 
| 2834 |  |  |  |  |  |  | } | 
| 2835 |  |  |  |  |  |  |  | 
| 2836 |  |  |  |  |  |  | sub curr_schema { | 
| 2837 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 2838 | 0 |  |  |  |  |  | return $self->get("sys_context('USERENV', 'SESSION_SCHEMA')"); | 
| 2839 |  |  |  |  |  |  | } | 
| 2840 |  |  |  |  |  |  |  | 
| 2841 |  |  |  |  |  |  | { | 
| 2842 |  |  |  |  |  |  | my $sql_t = < | 
| 2843 |  |  |  |  |  |  | SELECT | 
| 2844 |  |  |  |  |  |  | b.index_name, | 
| 2845 |  |  |  |  |  |  | b.column_name | 
| 2846 |  |  |  |  |  |  | FROM all_indexes a, all_ind_columns b | 
| 2847 |  |  |  |  |  |  | WHERE a.owner = b.index_owner | 
| 2848 |  |  |  |  |  |  | AND a.index_name = b.index_name | 
| 2849 |  |  |  |  |  |  | AND a.table_owner = %s | 
| 2850 |  |  |  |  |  |  | AND a.table_name = %s | 
| 2851 |  |  |  |  |  |  | SQL | 
| 2852 |  |  |  |  |  |  |  | 
| 2853 |  |  |  |  |  |  |  | 
| 2854 |  |  |  |  |  |  | sub index_info { | 
| 2855 | 0 |  |  | 0 |  |  | my ( $self, $table, $all_indexes ) = @_; | 
| 2856 |  |  |  |  |  |  |  | 
| 2857 | 0 |  |  |  |  |  | my $dbh = $self->{DBH}; | 
| 2858 | 0 |  |  |  |  |  | my ( $schema, $tbl ) = split /\./, uc($table); | 
| 2859 | 0 | 0 |  |  |  |  | if ( !$tbl ) { | 
| 2860 | 0 |  |  |  |  |  | $tbl = $schema; | 
| 2861 | 0 |  |  |  |  |  | $schema = $self->curr_schema(); | 
| 2862 |  |  |  |  |  |  | } | 
| 2863 | 0 |  |  |  |  |  | my $sql = sprintf $sql_t, $dbh->quote($schema), $dbh->quote($tbl); | 
| 2864 | 0 | 0 |  |  |  |  | $sql .= "and a.uniqueness = 'UNIQUE'\n" unless $all_indexes; | 
| 2865 | 0 |  |  |  |  |  | $sql .= "ORDER BY b.column_position\n"; | 
| 2866 | 0 |  |  |  |  |  | my $sth = $dbh->prepare($sql); | 
| 2867 | 0 |  |  |  |  |  | $sth->execute(); | 
| 2868 | 0 |  |  |  |  |  | my @col_names = @{$sth->{NAME_lc}}; | 
|  | 0 |  |  |  |  |  |  | 
| 2869 | 0 |  |  |  |  |  | my %row; $sth->bind_columns(\@row{@col_names}); | 
|  | 0 |  |  |  |  |  |  | 
| 2870 | 0 |  |  |  |  |  | my %ind; | 
| 2871 | 0 |  |  |  |  |  | while ($sth->fetch()) { | 
| 2872 | 0 |  |  |  |  |  | push @{$ind{$row{index_name}}}, lc($row{column_name}); | 
|  | 0 |  |  |  |  |  |  | 
| 2873 |  |  |  |  |  |  | } | 
| 2874 | 0 | 0 |  |  |  |  | return unless %ind; | 
| 2875 | 0 |  |  |  |  |  | return \%ind; | 
| 2876 |  |  |  |  |  |  | } | 
| 2877 |  |  |  |  |  |  | } | 
| 2878 |  |  |  |  |  |  |  | 
| 2879 |  |  |  |  |  |  | sub primary_key { | 
| 2880 | 0 |  |  | 0 |  |  | my ($self, $table) = @_; | 
| 2881 | 0 |  |  |  |  |  | $table = uc($table); | 
| 2882 | 0 |  |  |  |  |  | my ($schema, $tbl) = split /\./, $table; | 
| 2883 | 0 | 0 |  |  |  |  | if ( !$tbl ) { | 
| 2884 | 0 |  |  |  |  |  | $tbl = $schema; | 
| 2885 | 0 |  |  |  |  |  | $schema = $self->curr_schema(); | 
| 2886 |  |  |  |  |  |  | } | 
| 2887 | 0 |  |  |  |  |  | my @pk = map lc, $self->{DBH}->primary_key(undef, $schema, $tbl); | 
| 2888 | 0 | 0 |  |  |  |  | return unless @pk; | 
| 2889 | 0 |  |  |  |  |  | return \@pk; | 
| 2890 |  |  |  |  |  |  | } | 
| 2891 |  |  |  |  |  |  |  | 
| 2892 |  |  |  |  |  |  | { | 
| 2893 |  |  |  |  |  |  |  | 
| 2894 |  |  |  |  |  |  | my $sql = < | 
| 2895 |  |  |  |  |  |  | MERGE %s INTO %s d | 
| 2896 |  |  |  |  |  |  | USING %s s | 
| 2897 |  |  |  |  |  |  | ON (%s) | 
| 2898 |  |  |  |  |  |  | WHEN MATCHED THEN UPDATE SET %s | 
| 2899 |  |  |  |  |  |  | WHEN NOT MATCHED THEN INSERT (%s) | 
| 2900 |  |  |  |  |  |  | VALUES (%s) | 
| 2901 |  |  |  |  |  |  | SQL | 
| 2902 |  |  |  |  |  |  |  | 
| 2903 |  |  |  |  |  |  | sub merge { | 
| 2904 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 2905 | 0 |  |  |  |  |  | my %args = @_; | 
| 2906 |  |  |  |  |  |  |  | 
| 2907 | 0 |  |  |  |  |  | my $dbh       = $self->{DBH}; | 
| 2908 | 0 |  |  |  |  |  | my $table     = $args{Table}; | 
| 2909 | 0 |  |  |  |  |  | my $stg_table = $args{StgTable}; | 
| 2910 |  |  |  |  |  |  |  | 
| 2911 | 0 |  |  |  |  |  | my $stg_info = $self->column_info($stg_table); | 
| 2912 | 0 |  |  |  |  |  | my $stg_map  = $stg_info->{MAP}; | 
| 2913 | 0 |  |  |  |  |  | my %stg_has; $stg_has{$_}++ for @{$stg_info->{LIST}}; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 2914 |  |  |  |  |  |  |  | 
| 2915 | 0 | 0 | 0 |  |  |  | my $key_col_ref = ($args{KeyCols} && @{$args{KeyCols}}) ? $args{KeyCols} : $self->key_columns($table); | 
| 2916 | 0 | 0 | 0 |  |  |  | my $upd_col_ref = ($args{UpdCols} && @{$args{UpdCols}}) ? $args{UpdCols} : $self->upd_columns($table); | 
| 2917 |  |  |  |  |  |  |  | 
| 2918 |  |  |  |  |  |  | # Normalize all columns and maps to lowercase | 
| 2919 | 0 |  |  |  |  |  | my @key_cols = map lc, @$key_col_ref; | 
| 2920 | 0 |  |  |  |  |  | my @upd_cols = map lc, @$upd_col_ref; | 
| 2921 | 0 |  |  |  |  |  | my @fields = (@key_cols, @upd_cols); | 
| 2922 |  |  |  |  |  |  | my %col_map = $args{ColMap} | 
| 2923 | 0 | 0 |  |  |  |  | ? map lc, %{$args{ColMap}} | 
|  | 0 |  |  |  |  |  |  | 
| 2924 |  |  |  |  |  |  | : (); | 
| 2925 |  |  |  |  |  |  |  | 
| 2926 |  |  |  |  |  |  | my $upd_col_str = join(",", map { | 
| 2927 | 0 |  |  |  |  |  | $col_map{$_} ? $stg_has{$col_map{$_}} ? "d.$_=s.$col_map{$_}" : "d.$_=$col_map{$_}" | 
| 2928 | 0 | 0 |  |  |  |  | : $stg_has{$_} ? "d.$_=s.$_" : () | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 2929 |  |  |  |  |  |  | } @upd_cols), | 
| 2930 |  |  |  |  |  |  |  | 
| 2931 |  |  |  |  |  |  | # Determine if last_chg_user, last_chg_date need to be updated | 
| 2932 |  |  |  |  |  |  | # If staging table does not have the columns, and the target table does | 
| 2933 |  |  |  |  |  |  | # Then default the values | 
| 2934 |  |  |  |  |  |  | my %chg_col = $self->last_chg_list($table, \@fields); | 
| 2935 | 0 |  |  |  |  |  | delete $chg_col{$_} for grep $stg_has{$_}, qw(last_chg_user last_chg_date); | 
| 2936 | 0 |  |  |  |  |  | for my $col ( sort { $b cmp $a } keys %chg_col ) { | 
|  | 0 |  |  |  |  |  |  | 
| 2937 |  |  |  |  |  |  | $upd_col_str .= ",$col=".( ($col eq 'last_chg_user') | 
| 2938 | 0 | 0 |  |  |  |  | ? "'".uc(substr($dbh->{Username}, 0, $chg_col{$col}))."'" | 
| 2939 |  |  |  |  |  |  | : 'SYSTIMESTAMP' | 
| 2940 |  |  |  |  |  |  | ); | 
| 2941 |  |  |  |  |  |  | } | 
| 2942 |  |  |  |  |  |  |  | 
| 2943 | 0 | 0 |  |  |  |  | my $parallel = $args{Parallel} ? '/* parallel(8) append */' : ''; | 
| 2944 |  |  |  |  |  |  |  | 
| 2945 |  |  |  |  |  |  | my $merge_sql = sprintf($sql, | 
| 2946 |  |  |  |  |  |  | $parallel, | 
| 2947 |  |  |  |  |  |  | $table, | 
| 2948 |  |  |  |  |  |  | $args{MergeFilter} ? "$args{StgTable} WHERE $args{MergeFilter}": $args{StgTable}, | 
| 2949 |  |  |  |  |  |  | join(" AND ", map "d.$_=s.".($col_map{$_}||$_), @key_cols), | 
| 2950 |  |  |  |  |  |  | $upd_col_str, | 
| 2951 |  |  |  |  |  |  | join(",", @fields), | 
| 2952 |  |  |  |  |  |  | join(",", map { | 
| 2953 | 0 | 0 | 0 |  |  |  | $col_map{$_} ? $stg_has{$col_map{$_}} ? "s.$col_map{$_}" : $col_map{$_} | 
| 2954 | 0 | 0 |  |  |  |  | : $stg_has{$_} ? "s.$_" : "NULL" | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 2955 |  |  |  |  |  |  | } @fields), | 
| 2956 |  |  |  |  |  |  | ); | 
| 2957 |  |  |  |  |  |  |  | 
| 2958 |  |  |  |  |  |  | # No update if no update columns | 
| 2959 | 0 | 0 |  |  |  |  | $merge_sql =~ s/^WHEN MATCHED.*\n//m unless @upd_cols; | 
| 2960 | 0 |  |  |  |  |  | print("Executing: $merge_sql\n"); | 
| 2961 | 0 | 0 |  |  |  |  | return 1 if $args{NoExec}; | 
| 2962 |  |  |  |  |  |  |  | 
| 2963 | 0 | 0 |  |  |  |  | $dbh->do("ALTER SESSION ENABLE PARALLEL DML") if $args{Parallel}; | 
| 2964 |  |  |  |  |  |  |  | 
| 2965 | 0 |  |  |  |  |  | my $rows = $dbh->do($merge_sql) + 0; | 
| 2966 | 0 |  |  |  |  |  | print("$rows rows updated/inserted\n\n"); | 
| 2967 | 0 |  |  |  |  |  | return $rows; | 
| 2968 |  |  |  |  |  |  |  | 
| 2969 |  |  |  |  |  |  | } | 
| 2970 |  |  |  |  |  |  | } | 
| 2971 |  |  |  |  |  |  |  | 
| 2972 |  |  |  |  |  |  | # #!!!UNFINISHED!!! | 
| 2973 |  |  |  |  |  |  | # Static block for mk_ext_table | 
| 2974 |  |  |  |  |  |  | { | 
| 2975 |  |  |  |  |  |  |  | 
| 2976 |  |  |  |  |  |  | my $sql = < | 
| 2977 |  |  |  |  |  |  | CREATE TABLE %s ( | 
| 2978 |  |  |  |  |  |  | %s | 
| 2979 |  |  |  |  |  |  | ) | 
| 2980 |  |  |  |  |  |  | ORGANIZATION EXTERNAL ( | 
| 2981 |  |  |  |  |  |  | TYPE oracle_loader | 
| 2982 |  |  |  |  |  |  | DEFAULT DIRECTORY %s | 
| 2983 |  |  |  |  |  |  | ACCESS PARAMETERS | 
| 2984 |  |  |  |  |  |  | ( | 
| 2985 |  |  |  |  |  |  | RECORDS DELIMITED BY NEWLINE | 
| 2986 |  |  |  |  |  |  | LOGFILE 'TEST.log' | 
| 2987 |  |  |  |  |  |  | FIELDS TERMINATED BY '%s' | 
| 2988 |  |  |  |  |  |  | ) | 
| 2989 |  |  |  |  |  |  | LOCATION ('%s') | 
| 2990 |  |  |  |  |  |  | ) | 
| 2991 |  |  |  |  |  |  | SQL | 
| 2992 |  |  |  |  |  |  |  | 
| 2993 |  |  |  |  |  |  | sub mk_ext_table { | 
| 2994 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 2995 |  |  |  |  |  |  |  | 
| 2996 | 0 |  |  |  |  |  | my %args = @_; | 
| 2997 |  |  |  |  |  |  |  | 
| 2998 | 0 | 0 |  |  |  |  | my $table = $args{Table} or confess "Need table prototype for external table"; | 
| 2999 |  |  |  |  |  |  |  | 
| 3000 | 0 |  | 0 |  |  |  | my $ext_table = $args{Name} || "ext_${table}$$"; | 
| 3001 | 0 | 0 |  |  |  |  | my $dir  = $args{Dir}  or confess "Need directory for external table $table"; | 
| 3002 | 0 | 0 |  |  |  |  | my $file = $args{File} or confess "Need file for external table $table"; | 
| 3003 |  |  |  |  |  |  |  | 
| 3004 | 0 |  |  |  |  |  | my $cols = $self->column_info($table); | 
| 3005 | 0 |  |  |  |  |  | my $cmap = $cols->{MAP}; | 
| 3006 |  |  |  |  |  |  |  | 
| 3007 | 0 |  |  |  |  |  | my @col_list; | 
| 3008 | 0 |  |  |  |  |  | for my $col (@{$cols->{LIST}}) { | 
|  | 0 |  |  |  |  |  |  | 
| 3009 | 0 |  |  |  |  |  | my $col_str = $col; | 
| 3010 |  |  |  |  |  |  |  | 
| 3011 | 0 |  |  |  |  |  | my $cdata = $cmap->{$col}; | 
| 3012 | 0 |  |  |  |  |  | my $type = $cdata->{TYPE_NAME}; | 
| 3013 | 0 |  |  |  |  |  | my $dec  = $cdata->{DECIMAL_DIGITS}; | 
| 3014 |  |  |  |  |  |  |  | 
| 3015 | 0 |  |  |  |  |  | $col_str .= " $type"; | 
| 3016 | 0 |  |  |  |  |  | my $size = $cdata->{COLUMN_SIZE}; | 
| 3017 |  |  |  |  |  |  |  | 
| 3018 | 0 |  |  |  |  |  | for ($type) { | 
| 3019 | 0 | 0 |  |  |  |  | $col_str .= | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 3020 |  |  |  |  |  |  | /CHAR/   ? "($size)" | 
| 3021 |  |  |  |  |  |  | : /NUMBER/ ? (defined $dec) ? "($size,$dec)" : '' | 
| 3022 |  |  |  |  |  |  | : ''; | 
| 3023 |  |  |  |  |  |  | } | 
| 3024 |  |  |  |  |  |  |  | 
| 3025 |  |  |  |  |  |  | #$col_str .= " DEFAULT $cdata->{COLUMN_DEF}" if defined $cdata->{COLUMN_DEF}; | 
| 3026 |  |  |  |  |  |  | #$col_str =~ s/\s+$//; | 
| 3027 |  |  |  |  |  |  | #$col_str .= " NOT NULL" unless $cdata->{NULLABLE}; | 
| 3028 |  |  |  |  |  |  |  | 
| 3029 | 0 |  |  |  |  |  | push @col_list, $col_str; | 
| 3030 |  |  |  |  |  |  | } | 
| 3031 |  |  |  |  |  |  |  | 
| 3032 |  |  |  |  |  |  | my $create_sql = sprintf($sql, | 
| 3033 |  |  |  |  |  |  | $ext_table, | 
| 3034 |  |  |  |  |  |  | join(",\n", @col_list ), | 
| 3035 |  |  |  |  |  |  | $dir, | 
| 3036 |  |  |  |  |  |  | #$args{RowDelimiter} || "\\n", | 
| 3037 | 0 |  | 0 |  |  |  | $args{Delimiter} || "|", | 
| 3038 |  |  |  |  |  |  | $file, | 
| 3039 |  |  |  |  |  |  | ); | 
| 3040 |  |  |  |  |  |  |  | 
| 3041 | 0 |  |  |  |  |  | $self->{DBH}->do($create_sql); | 
| 3042 |  |  |  |  |  |  |  | 
| 3043 | 0 |  |  |  |  |  | return $ext_table; | 
| 3044 |  |  |  |  |  |  | } | 
| 3045 |  |  |  |  |  |  | } | 
| 3046 |  |  |  |  |  |  |  | 
| 3047 |  |  |  |  |  |  | package DBIx::BulkUtil::Release; | 
| 3048 |  |  |  |  |  |  |  | 
| 3049 |  |  |  |  |  |  | sub new { | 
| 3050 | 0 |  |  | 0 |  |  | my ($class, $f) = @_; | 
| 3051 | 0 |  |  |  |  |  | bless $f, $class; | 
| 3052 |  |  |  |  |  |  | } | 
| 3053 |  |  |  |  |  |  |  | 
| 3054 | 0 |  |  | 0 |  |  | sub DESTROY { $_[0]->() } | 
| 3055 |  |  |  |  |  |  |  | 
| 3056 |  |  |  |  |  |  | 1; | 
| 3057 |  |  |  |  |  |  |  | 
| 3058 |  |  |  |  |  |  | __END__ |