File Coverage

blib/lib/App/Sqitch/Engine/oracle.pm
Criterion Covered Total %
statement 156 269 57.9
branch 30 70 42.8
condition 21 60 35.0
subroutine 52 68 76.4
pod 23 23 100.0
total 282 490 57.5


line stmt bran cond sub pod time code
1              
2             use 5.010;
3 2     2   16157 use Moo;
  2         6  
4 2     2   9 use utf8;
  2         3  
  2         13  
5 2     2   531 use Path::Class;
  2         5  
  2         21  
6 2     2   56 use DBI;
  2         4  
  2         97  
7 2     2   1111 use Try::Tiny;
  2         13058  
  2         68  
8 2     2   10 use App::Sqitch::X qw(hurl);
  2         4  
  2         93  
9 2     2   11 use Locale::TextDomain qw(App-Sqitch);
  2         4  
  2         14  
10 2     2   533 use App::Sqitch::Plan::Change;
  2         4  
  2         12  
11 2     2   283 use List::Util qw(first);
  2         4  
  2         39  
12 2     2   10 use App::Sqitch::Types qw(DBH Dir ArrayRef);
  2         4  
  2         108  
13 2     2   12 use namespace::autoclean;
  2         4  
  2         14  
14 2     2   1571  
  2         3  
  2         34  
15             extends 'App::Sqitch::Engine';
16              
17             our $VERSION = 'v1.3.1'; # VERSION
18              
19             BEGIN {
20             # We tell the Oracle connector which encoding to use. The last part of the
21             # environment variable NLS_LANG is relevant concerning data encoding.
22             $ENV{NLS_LANG} = 'AMERICAN_AMERICA.AL32UTF8';
23 2     2   279  
24             # Disable SQLPATH so that no start scripts run.
25             $ENV{SQLPATH} = '';
26 2         7642 }
27              
28             my $self = shift;
29              
30 15     15 1 5366 # Just use the target name if it doesn't look like a URI or if the URI
31             # includes the database name.
32             return $self->target->name if $self->target->name !~ /:/
33             || $self->target->uri->dbname;
34 15 100 66     119  
35             # Use the URI sans password, and with the database name added.
36             my $uri = $self->target->uri->clone;
37             $uri->password(undef) if $uri->password;
38 12         407 $uri->dbname(
39 12 50       118 $ENV{TWO_TASK}
40             || ( App::Sqitch::ISWIN ? $ENV{LOCAL} : undef )
41             || $ENV{ORACLE_SID}
42             || $self->username
43             );
44 12   66     345 return $uri->as_string;
45             }
46 12         439  
47             # No username or password defaults.
48              
49             has _sqlplus => (
50       3     is => 'ro',
51       3     isa => ArrayRef,
52             lazy => 1,
53             default => sub {
54             my $self = shift;
55             [ $self->client, qw(-S -L /nolog) ];
56             },
57             );
58              
59              
60             has tmpdir => (
61             is => 'ro',
62             isa => Dir,
63 7     7 1 2005 lazy => 1,
  7         120  
64             default => sub {
65             require File::Temp;
66             dir File::Temp::tempdir( CLEANUP => 1 );
67             },
68             );
69              
70              
71             file( ($ENV{ORACLE_HOME} || ()), 'sqlplus' )->stringify
72             }
73              
74             has dbh => (
75 4     4 1 4857 is => 'rw',
76 3     3 1 26 isa => DBH,
77 1     1 1 3 lazy => 1,
78 6     6 1 886 default => sub {
79             my $self = shift;
80             $self->use_driver;
81 2   66 2 1 275  
82             my $uri = $self->uri;
83             DBI->connect($uri->dbi_dsn, $self->username, $self->password, {
84             PrintError => 0,
85             RaiseError => 0,
86             AutoCommit => 1,
87             FetchHashKeyName => 'NAME_lc',
88             HandleError => sub {
89             my ($err, $dbh) = @_;
90             $@ = $err;
91             @_ = ($dbh->state || 'DEV' => $dbh->errstr);
92             goto &hurl;
93             },
94             Callbacks => {
95             connected => sub {
96             my $dbh = shift;
97             $dbh->do("ALTER SESSION SET $_='YYYY-MM-DD HH24:MI:SS TZR'") for qw(
98             nls_date_format
99             nls_timestamp_format
100             nls_timestamp_tz_format
101             );
102             if (my $schema = $self->registry) {
103             try {
104             $dbh->do("ALTER SESSION SET CURRENT_SCHEMA = $schema");
105             # https://www.nntp.perl.org/group/perl.dbi.dev/2013/11/msg7622.html
106             $dbh->set_err(undef, undef) if $dbh->err;
107             };
108             }
109             return;
110             },
111             },
112             });
113             }
114             );
115              
116             # Need to wait until dbh is defined.
117             with 'App::Sqitch::Role::DBIEngine';
118              
119             [ map { $_->format_name } $_[1]->tags ];
120             }
121              
122             [ map { $_->as_string } $_[1]->requires ];
123             }
124              
125             [ map { $_->as_string } $_[1]->conflicts ];
126             }
127              
128             # q{CAST(to_char(%1$s AT TIME ZONE 'UTC', '"year":YYYY:"month":MM:"day":DD') AS VARCHAR2(100 byte)) || CAST(to_char(%1$s AT TIME ZONE 'UTC', ':"hour":HH24:"minute":MI:"second":SS:"time_zone":"UTC"') AS VARCHAR2(168 byte))}
129             # Good grief, Oracle, WTF? https://github.com/sqitchers/sqitch/issues/316
130 1     1   12 join ' || ', (
  3         12  
131             q{to_char(%1$s AT TIME ZONE 'UTC', '"year":YYYY')},
132             q{to_char(%1$s AT TIME ZONE 'UTC', ':"month":MM')},
133             q{to_char(%1$s AT TIME ZONE 'UTC', ':"day":DD')},
134 1     1   212 q{to_char(%1$s AT TIME ZONE 'UTC', ':"hour":HH24')},
  3         11  
135             q{to_char(%1$s AT TIME ZONE 'UTC', ':"minute":MI')},
136             q{to_char(%1$s AT TIME ZONE 'UTC', ':"second":SS')},
137             q{':time_zone:UTC'},
138 1     1   8 );
  3         10  
139             }
140              
141              
142             my $dt = $_[1];
143             join ' ', $dt->ymd('-'), $dt->hms(':'), $dt->time_zone->name;
144 1     1   1073 }
145              
146             # https://stackoverflow.com/q/16313631/79202
147             return q{CAST(COLLECT(CAST(%s AS VARCHAR2(512))) AS sqitch_array)};
148             }
149              
150              
151              
152             my ($self, $count, $expr) = @_;
153             return join "\nUNION ALL ", ("SELECT $expr FROM dual") x $count;
154 1     1   4 }
155              
156 1     1   5 require App::Sqitch::DateTime;
157             return App::Sqitch::DateTime->new(split /:/ => shift);
158             }
159 1     1   4220  
160 1         7 my ( $self, $ord, $offset, $project ) = @_;
161              
162             return try {
163             return $self->dbh->selectcol_arrayref(qq{
164             SELECT change_id FROM (
165 1     1   503 SELECT change_id, rownum as rnum FROM (
166             SELECT change_id
167             FROM changes
168 1     1   5 WHERE project = ?
169             ORDER BY committed_at $ord
170 1     1   5 )
171             ) WHERE rnum = ?
172             }, undef, $project || $self->plan->project, ($offset // 0) + 1)->[0];
173 3     3   7 } catch {
174 3         55 return if $self->_no_table_error;
175             die $_;
176             };
177             }
178 1     1   663  
179 1         12 my ($self, $project, $change) = @_;
180             return $self->dbh->selectcol_arrayref(qq{
181             SELECT change_id FROM (
182             SELECT change_id
183 1     1   334 FROM changes
184             WHERE project = ?
185             AND change = ?
186 1   0 1   152 ORDER BY committed_at DESC
      0        
187             ) WHERE rownum = 1
188             }, undef, $project, $change)->[0];
189             }
190              
191             my ( $self, $project, $with_hash ) = @_;
192             my $cdtcol = sprintf $self->_ts2char_format, 'c.committed_at';
193             my $pdtcol = sprintf $self->_ts2char_format, 'c.planned_at';
194             my $tagcol = sprintf $self->_listagg_format, 't.tag';
195             my $hshcol = $with_hash ? "c.script_hash\n , " : '';
196             my $dbh = $self->dbh;
197 1 50   1   36 return $dbh->selectrow_hashref(qq{
198 1         8 SELECT * FROM (
199 1         28 SELECT c.change_id
200             , ${hshcol}c.change
201             , c.project
202             , c.note
203 0     0   0 , c.committer_name
204 0         0 , c.committer_email
205             , $cdtcol AS committed_at
206             , c.planner_name
207             , c.planner_email
208             , $pdtcol AS planned_at
209             , $tagcol AS tags
210             FROM changes c
211             LEFT JOIN tags t ON c.change_id = t.change_id
212             WHERE c.project = ?
213             GROUP BY c.change_id
214             , ${hshcol}c.change
215             , c.project
216 0     0   0 , c.note
217 0         0 , c.committer_name
218 0         0 , c.committer_email
219 0         0 , c.committed_at
220 0 0       0 , c.planner_name
221 0         0 , c.planner_email
222 0   0     0 , c.planned_at
223             ORDER BY c.committed_at DESC
224             ) WHERE rownum = 1
225             }, undef, $project // $self->plan->project);
226             }
227              
228             my ( $self, $change ) = @_;
229             $self->dbh->selectcol_arrayref(
230             'SELECT 1 FROM changes WHERE change_id = ?',
231             undef, $change->id
232             )->[0];
233             }
234              
235             my $self = shift;
236             return $self->dbh->selectcol_arrayref(q{
237             SELECT 1
238             FROM all_tables
239             WHERE owner = UPPER(?)
240             AND table_name = 'CHANGES'
241             }, undef, $self->registry || $self->username)->[0];
242             }
243              
244             my ( $self, $event, $change, $tags, $requires, $conflicts) = @_;
245             my $dbh = $self->dbh;
246             my $sqitch = $self->sqitch;
247              
248             $tags ||= $self->_log_tags_param($change);
249             $requires ||= $self->_log_requires_param($change);
250             $conflicts ||= $self->_log_conflicts_param($change);
251              
252             # Use the sqitch_array() constructor to insert arrays of values.
253             my $tag_ph = 'sqitch_array('. join(', ', ('?') x @{ $tags }) . ')';
254 0     0 1 0 my $req_ph = 'sqitch_array('. join(', ', ('?') x @{ $requires }) . ')';
255 0         0 my $con_ph = 'sqitch_array('. join(', ', ('?') x @{ $conflicts }) . ')';
256             my $ts = $self->_ts_default;
257              
258             $dbh->do(qq{
259             INSERT INTO events (
260             event
261             , change_id
262 1     1 1 750 , change
263 1   0     12 , project
264             , note
265             , tags
266             , requires
267             , conflicts
268             , committer_name
269             , committer_email
270             , planned_at
271             , planner_name
272 0     0   0 , planner_email
273 0         0 , committed_at
274 0         0 )
275             VALUES (?, ?, ?, ?, ?, $tag_ph, $req_ph, $con_ph, ?, ?, ?, ?, ?, $ts)
276 0   0     0 }, undef,
277 0   0     0 $event,
278 0   0     0 $change->id,
279             $change->name,
280             $change->project,
281 0         0 $change->note,
  0         0  
282 0         0 @{ $tags },
  0         0  
283 0         0 @{ $requires },
  0         0  
284 0         0 @{ $conflicts },
285             $sqitch->user_name,
286             $sqitch->user_email,
287             $self->_char2ts( $change->timestamp ),
288             $change->planner_name,
289             $change->planner_email,
290             );
291              
292             return $self;
293             }
294              
295             my ( $self, $change ) = @_;
296             # Why CTE: https://forums.oracle.com/forums/thread.jspa?threadID=1005221
297             return @{ $self->dbh->selectall_arrayref(q{
298             WITH tag AS (
299             SELECT tag, committed_at, project,
300             ROW_NUMBER() OVER (partition by project ORDER BY committed_at) AS rnk
301             FROM tags
302             )
303             SELECT c.change_id, c.project, c.change, t.tag AS asof_tag
304             FROM dependencies d
305             JOIN changes c ON c.change_id = d.change_id
306             LEFT JOIN tag t ON t.project = c.project AND t.committed_at >= c.committed_at
307             WHERE d.dependency_id = ?
308             AND (t.rnk IS NULL OR t.rnk = 1)
309             }, { Slice => {} }, $change->id) };
310 0         0 }
311 0         0  
312 0         0 my ( $self, $change_id ) = @_;
  0         0  
313             # Why CTE: https://forums.oracle.com/forums/thread.jspa?threadID=1005221
314             return $self->dbh->selectcol_arrayref(q{
315             WITH tag AS (
316             SELECT tag, committed_at, project,
317             ROW_NUMBER() OVER (partition by project ORDER BY committed_at) AS rnk
318             FROM tags
319             )
320 0         0 SELECT change || COALESCE(t.tag, '@HEAD')
321             FROM changes c
322             LEFT JOIN tag t ON c.project = t.project AND t.committed_at >= c.committed_at
323             WHERE change_id = ?
324 0     0 1 0 AND (t.rnk IS NULL OR t.rnk = 1)
325             }, undef, $change_id)->[0];
326 0         0 }
  0         0  
327              
328             my ( $self, $change_id, $offset ) = @_;
329              
330             # Just return the ID if there is no offset.
331             return $change_id unless $offset;
332              
333             # Are we offset forwards or backwards?
334             my ( $dir, $op ) = $offset > 0 ? ( 'ASC', '>' ) : ( 'DESC' , '<' );
335             return $self->dbh->selectcol_arrayref(qq{
336             SELECT id FROM (
337             SELECT id, rownum AS rnum FROM (
338             SELECT change_id AS id
339             FROM changes
340             WHERE project = ?
341             AND committed_at $op (
342 0     0 1 0 SELECT committed_at FROM changes WHERE change_id = ?
343             )
344 0         0 ORDER BY committed_at $dir
345             )
346             ) WHERE rnum = ?
347             }, undef, $self->plan->project, $change_id, abs $offset)->[0];
348             }
349              
350             my ( $self, $change_id, $offset ) = @_;
351              
352             # Just return the object if there is no offset.
353             return $self->load_change($change_id) unless $offset;
354              
355             # Are we offset forwards or backwards?
356             my ( $dir, $op ) = $offset > 0 ? ( 'ASC', '>' ) : ( 'DESC' , '<' );
357             my $tscol = sprintf $self->_ts2char_format, 'c.planned_at';
358             my $tagcol = sprintf $self->_listagg_format, 't.tag';
359 0     0 1 0  
360             my $change = $self->dbh->selectrow_hashref(qq{
361             SELECT id, name, project, note, timestamp, planner_name, planner_email, tags, script_hash
362 0 0       0 FROM (
363             SELECT id, name, project, note, timestamp, planner_name, planner_email, tags, script_hash, rownum AS rnum
364             FROM (
365 0 0       0 SELECT c.change_id AS id, c.change AS name, c.project, c.note,
366 0         0 $tscol AS timestamp, c.planner_name, c.planner_email,
367             $tagcol AS tags, c.script_hash
368             FROM changes c
369             LEFT JOIN tags t ON c.change_id = t.change_id
370             WHERE c.project = ?
371             AND c.committed_at $op (
372             SELECT committed_at FROM changes WHERE change_id = ?
373             )
374             GROUP BY c.change_id, c.change, c.project, c.note, c.planned_at,
375             c.planner_name, c.planner_email, c.committed_at, c.script_hash
376             ORDER BY c.committed_at $dir
377             )
378             ) WHERE rnum = ?
379             }, undef, $self->plan->project, $change_id, abs $offset) || return undef;
380             $change->{timestamp} = _dt $change->{timestamp};
381             return $change;
382 0     0 1 0 }
383              
384             my ( $self, $tag ) = @_;
385 0 0       0 return $self->dbh->selectcol_arrayref(
386             'SELECT 1 FROM tags WHERE tag_id = ?',
387             undef, $tag->id
388 0 0       0 )->[0];
389 0         0 }
390 0         0  
391             my $self = shift;
392 0   0     0 @{ $self->dbh->selectcol_arrayref(
393             'SELECT change_id FROM changes WHERE ' . _change_id_in(scalar @_),
394             undef,
395             map { $_->id } @_,
396             ) };
397             }
398              
399             my $i = shift;
400             my @qs;
401             while ($i > 250) {
402             push @qs => 'change_id IN (' . join(', ' => ('?') x 250) . ')';
403             $i -= 250;
404             }
405             push @qs => 'change_id IN (' . join(', ' => ('?') x $i) . ')' if $i > 0;
406             return join ' OR ', @qs;
407             }
408              
409             my $self = shift;
410             my $schema = $self->registry;
411             return $schema ? ("DEFINE registry=$schema") : (
412 0         0 # Select the current schema into &registry.
413 0         0 # https://www.orafaq.com/node/515
414             'COLUMN sname for a30 new_value registry',
415             q{SELECT SYS_CONTEXT('USERENV', 'SESSION_SCHEMA') AS sname FROM DUAL;},
416             );
417 0     0 1 0 }
418 0         0  
419             my $self = shift;
420             my $schema = $self->registry;
421             hurl engine => __ 'Sqitch already initialized' if $self->initialized;
422              
423             # Load up our database.
424             (my $file = file(__FILE__)->dir->file('oracle.sql')) =~ s/"/""/g;
425 0     0 1 0 $self->_run_with_verbosity($file);
426 0         0 $self->dbh->do("ALTER SESSION SET CURRENT_SCHEMA = $schema") if $schema;
427             $self->_register_release;
428             }
429 0         0  
  0         0  
430             # Override for special handling of regular the expression operator and
431             # LIMIT/OFFSET.
432             my ( $self, %p ) = @_;
433              
434 12     12   2360 # Determine order direction.
435 12         16 my $dir = 'DESC';
436 12         22 if (my $d = delete $p{direction}) {
437 8         70 $dir = $d =~ /^ASC/i ? 'ASC'
438 8         11 : $d =~ /^DESC/i ? 'DESC'
439             : hurl 'Search direction must be either "ASC" or "DESC"';
440 12 100       60 }
441 12         132  
442             # Limit with regular expressions?
443             my (@wheres, @params);
444             for my $spec (
445 20     20   26 [ committer => 'committer_name' ],
446 20         251 [ planner => 'planner_name' ],
447 20 100       669 [ change => 'change' ],
448             [ project => 'project' ],
449             ) {
450             my $regex = delete $p{ $spec->[0] } // next;
451             push @wheres => "REGEXP_LIKE($spec->[1], ?)";
452             push @params => $regex;
453             }
454              
455             # Match events?
456 0     0 1 0 if (my $e = delete $p{event} ) {
457 0         0 my ($in, @vals) = $self->_in_expr( $e );
458 0 0       0 push @wheres => "event $in";
459             push @params => @vals;
460             }
461 0         0  
462 0         0 # Assemble the where clause.
463 0 0       0 my $where = @wheres
464 0         0 ? "\n WHERE " . join( "\n ", @wheres )
465             : '';
466              
467             # Handle remaining parameters.
468             my ($lim, $off) = (delete $p{limit}, delete $p{offset});
469              
470 0     0 1 0 hurl 'Invalid parameters passed to search_events(): '
471             . join ', ', sort keys %p if %p;
472              
473 0         0 # Prepare, execute, and return.
474 0 0       0 my $cdtcol = sprintf $self->_ts2char_format, 'committed_at';
475 0 0       0 my $pdtcol = sprintf $self->_ts2char_format, 'planned_at';
    0          
476             my $sql = qq{
477             SELECT event
478             , project
479             , change_id
480             , change
481 0         0 , note
482 0         0 , requires
483             , conflicts
484             , tags
485             , committer_name
486             , committer_email
487             , $cdtcol AS committed_at
488 0   0     0 , planner_name
489 0         0 , planner_email
490 0         0 , $pdtcol AS planned_at
491             FROM events$where
492             ORDER BY events.committed_at $dir
493             };
494 0 0       0  
495 0         0 if ($lim || $off) {
496 0         0 my @limits;
497 0         0 if ($lim) {
498             $off //= 0;
499             push @params => $lim + $off;
500             push @limits => 'rnum <= ?';
501 0 0       0 }
502             if ($off) {
503             push @params => $off;
504             push @limits => 'rnum > ?';
505             }
506 0         0  
507             $sql = "SELECT * FROM ( SELECT ROWNUM AS rnum, i.* FROM ($sql) i ) WHERE "
508 0 0       0 . join ' AND ', @limits;
509             }
510              
511             my $sth = $self->dbh->prepare($sql);
512 0         0 $sth->execute(@params);
513 0         0 return sub {
514 0         0 my $row = $sth->fetchrow_hashref or return;
515             delete $row->{rnum};
516             $row->{committed_at} = _dt $row->{committed_at};
517             $row->{planned_at} = _dt $row->{planned_at};
518             return $row;
519             };
520             }
521              
522             # Override to lock the changes table. This ensures that only one instance of
523             # Sqitch runs at one time.
524             my $self = shift;
525             my $dbh = $self->dbh;
526              
527             # Start transaction and lock changes to allow only one change at a time.
528             $dbh->begin_work;
529             $dbh->do('LOCK TABLE changes IN EXCLUSIVE MODE');
530             return $self;
531             }
532              
533 0 0 0     0 my ($self, $file) = @_;
534 0         0  
535 0 0       0 # Just use the file if no special character.
536 0   0     0 if ($file !~ /[@?%\$]/) {
537 0         0 $file =~ s/"/""/g;
538 0         0 return $file;
539             }
540 0 0       0  
541 0         0 # Alias or copy the file to a temporary directory that's removed on exit.
542 0         0 (my $alias = $file->basename) =~ s/[@?%\$]/_/g;
543             $alias = $self->tmpdir->file($alias);
544              
545 0         0 # Remove existing file.
546             if (-e $alias) {
547             $alias->remove or hurl oracle => __x(
548             'Cannot remove {file}: {error}',
549 0         0 file => $alias,
550 0         0 error => $!
551             );
552 0 0   0   0 }
553 0         0  
554 0         0 if (App::Sqitch::ISWIN) {
555 0         0 # Copy it.
556 0         0 $file->copy_to($alias) or hurl oracle => __x(
557 0         0 'Cannot copy {file} to {alias}: {error}',
558             file => $file,
559             alias => $alias,
560             error => $!
561             );
562             } else {
563 0     0 1 0 # Symlink it.
564 0         0 $alias->remove;
565             symlink $file->absolute, $alias or hurl oracle => __x(
566             'Cannot symlink {file} to {alias}: {error}',
567 0         0 file => $file,
568 0         0 alias => $alias,
569 0         0 error => $!
570             );
571             }
572              
573 9     9   6624 # Return the alias.
574             $alias =~ s/"/""/g;
575             return $alias;
576 9 100       61 }
577 6         37  
578 6         47 my $self = shift;
579             my $file = $self->_file_for_script(shift);
580             $self->_run(qq{\@"$file"});
581             }
582 3         130  
583 3         70 my $self = shift;
584             my $file = $self->_file_for_script(shift);
585             # Suppress STDOUT unless we want extra verbosity.
586 3 100       178 my $meth = $self->can($self->sqitch->verbosity > 1 ? '_run' : '_capture');
587 1 50       41 $self->$meth(qq{\@"$file"});
588             }
589              
590              
591             my ($self, $fh) = @_;
592             my $conn = $self->_script;
593             open my $tfh, '<:utf8_strict', \$conn;
594 2         79 $self->sqitch->spool( [$tfh, $fh], $self->sqlplus );
595             }
596              
597             # Override to take advantage of the RETURNING expression, and to save tags as
598             # an array rather than a space-delimited string.
599             my ($self, $change) = @_;
600             my $dbh = $self->dbh;
601             my $cid = $change->id;
602              
603             # Delete tags.
604 2         13 my $sth = $dbh->prepare(
605 2 50       95 'DELETE FROM tags WHERE change_id = ? RETURNING tag INTO ?',
606             );
607             $sth->bind_param(1, $cid);
608             $sth->bind_param_inout_array(2, my $del_tags = [], 0, {
609             ora_type => DBD::Oracle::ORA_VARCHAR2()
610             });
611             $sth->execute;
612              
613             # Retrieve dependencies.
614 2         210 my $depcol = sprintf $self->_listagg_format, 'dependency';
615 2         79 my ($req, $conf) = $dbh->selectrow_array(qq{
616             SELECT (
617             SELECT $depcol
618             FROM dependencies
619 2     2 1 1162 WHERE change_id = ?
620 2         6 AND type = 'require'
621 2         11 ),
622             (
623             SELECT $depcol
624             FROM dependencies
625 2     2   230 WHERE change_id = ?
626 2         6 AND type = 'conflict'
627             ) FROM dual
628 2 100       35 }, undef, $cid, $cid);
629 2         254  
630             # Delete the change record.
631             $dbh->do(
632 0     0 1 0 'DELETE FROM changes where change_id = ?',
633 2     2 1 742 undef, $change->id,
634             );
635              
636 1     1 1 645 # Log it.
637 1         7 return $self->_log_event( revert => $change, $del_tags, $req, $conf );
638 1         17 }
639 1         17  
640             return $DBI::err && $DBI::err == 942; # ORA-00942: table or view does not exist
641             }
642              
643             return $DBI::err && $DBI::err == 904; # ORA-00904: invalid identifier
644             }
645 0     0 1 0  
646 0         0 my $self = shift;
647 0         0 my $uri = $self->uri;
648             my $conn = '';
649             # Use _port instead of port so it's empty if no port is in the URI.
650 0         0 # https://github.com/sqitchers/sqitch/issues/675
651             my ($user, $pass, $host, $port) = (
652             $self->username, $self->password, $uri->host, $uri->_port
653 0         0 );
654 0         0 if ($user || $pass || $host || $port) {
655             $conn = $user // '';
656             if ($pass) {
657 0         0 $pass =~ s/"/""/g;
658             $conn .= qq{/"$pass"};
659             }
660 0         0 if (my $db = $uri->dbname) {
661 0         0 $conn .= '@';
662             $db =~ s/"/""/g;
663             if ($host || $port) {
664             $conn .= '//' . ($host || '');
665             if ($port) {
666             $conn .= ":$port";
667             }
668             $conn .= qq{/"$db"};
669             } else {
670             $conn .= qq{"$db"};
671             }
672             }
673             } else {
674             # OS authentication or Oracle wallet (no username or password).
675             if (my $db = $uri->dbname) {
676             $db =~ s/"/""/g;
677 0         0 $conn = qq{/@"$db"};
678             }
679             }
680             my %vars = $self->variables;
681              
682             return join "\n" => (
683 0         0 'SET ECHO OFF NEWP 0 SPA 0 PAGES 0 FEED OFF HEAD OFF TRIMS ON TAB OFF VERIFY OFF',
684             'WHENEVER OSERROR EXIT 9;',
685             'WHENEVER SQLERROR EXIT SQL.SQLCODE;',
686             (map {; (my $v = $vars{$_}) =~ s/"/""/g; qq{DEFINE $_="$v"} } sort keys %vars),
687 4   100 4   46 "connect $conn",
688             $self->_registry_variable,
689             @_
690             );
691 3   100 3   23 }
692              
693             my $self = shift;
694             my $script = $self->_script(@_);
695 14     14   4020 open my $fh, '<:utf8_strict', \$script;
696 14         267 return $self->sqitch->spool( $fh, $self->sqlplus );
697 14         341 }
698              
699             my $self = shift;
700 14         187 my $conn = $self->_script(@_);
701             my @out;
702              
703 14 100 66     1293 require IPC::Run3;
      66        
      33        
704 11   50     35 IPC::Run3::run3(
705 11 50       19 [$self->sqlplus], \$conn, \@out, \@out,
706 11         23 { return_if_system_error => 1 },
707 11         19 );
708             if (my $err = $?) {
709 11 50       58 # Ugh, send everything to STDERR.
710 11         603 $self->sqitch->vent(@out);
711 11         19 hurl io => __x(
712 11 100 66     30 '{command} unexpectedly returned exit value {exitval}',
713 10   50     18 command => $self->client,
714 10 100       17 exitval => ($err >> 8),
715 9         11 );
716             }
717 10         16  
718             return wantarray ? @out : \@out;
719 1         3 }
720              
721             1;
722              
723              
724 3 100       9 =head1 Name
725 2         88  
726 2         5 App::Sqitch::Engine::oracle - Sqitch Oracle Engine
727              
728             =head1 Synopsis
729 14         81  
730             my $oracle = App::Sqitch::Engine->load( engine => 'oracle' );
731              
732             =head1 Description
733              
734             App::Sqitch::Engine::oracle provides the Oracle storage engine for Sqitch. It
735 14         121 supports Oracle 10g and higher.
  3         6  
  3         8  
736              
737             =head1 Interface
738              
739             =head2 Instance Methods
740              
741             =head3 C<initialized>
742              
743 1     1   3926 $oracle->initialize unless $oracle->initialized;
744 1         3  
745 1     1   25 Returns true if the database has been initialized for Sqitch, and false if it
  1     1   5  
  1         2  
  1         6  
  1         567  
  1         2  
  1         4  
746 1         686 has not.
747              
748             =head3 C<initialize>
749              
750 3     3   1492 $oracle->initialize;
751 3         11  
752 3         4 Initializes a database for Sqitch by installing the Sqitch registry schema.
753              
754 3         15 =head3 C<sqlplus>
755 3         10  
756             Returns a list containing the C<sqlplus> client and options to be passed to it.
757             Used internally when executing scripts.
758              
759 3 100       19039 =head1 Author
760              
761 1         42 David E. Wheeler <david@justatheory.com>
762 1         72  
763             =head1 License
764              
765             Copyright (c) 2012-2022 iovation Inc., David E. Wheeler
766              
767             Permission is hereby granted, free of charge, to any person obtaining a copy
768             of this software and associated documentation files (the "Software"), to deal
769 2 100       41 in the Software without restriction, including without limitation the rights
770             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
771             copies of the Software, and to permit persons to whom the Software is
772             furnished to do so, subject to the following conditions:
773              
774             The above copyright notice and this permission notice shall be included in all
775             copies or substantial portions of the Software.
776              
777             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
778             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
779             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
780             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
781             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
782             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
783             SOFTWARE.
784              
785             =cut