File Coverage

blib/lib/App/Sqitch/Engine/oracle.pm
Criterion Covered Total %
statement 162 276 58.7
branch 38 80 47.5
condition 22 63 34.9
subroutine 52 69 75.3
pod 21 21 100.0
total 295 509 57.9


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