File Coverage

blib/lib/App/Sqitch/Engine/oracle.pm
Criterion Covered Total %
statement 159 273 58.2
branch 38 80 47.5
condition 22 63 34.9
subroutine 51 68 75.0
pod 21 21 100.0
total 291 505 57.6


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