File Coverage

blib/lib/App/Sqitch/Engine/firebird.pm
Criterion Covered Total %
statement 153 311 49.2
branch 33 112 29.4
condition 11 34 32.3
subroutine 48 70 68.5
pod 25 25 100.0
total 270 552 48.9


line stmt bran cond sub pod time code
1              
2             use 5.010;
3 2     2   27190 use strict;
  2         8  
4 2     2   11 use warnings;
  2         3  
  2         42  
5 2     2   8 use utf8;
  2         3  
  2         61  
6 2     2   20 use Try::Tiny;
  2         4  
  2         12  
7 2     2   62 use App::Sqitch::X qw(hurl);
  2         6  
  2         143  
8 2     2   13 use Locale::TextDomain qw(App-Sqitch);
  2         4  
  2         48  
9 2     2   585 use App::Sqitch::Plan::Change;
  2         12  
  2         15  
10 2     2   336 use Path::Class;
  2         4  
  2         61  
11 2     2   10 use File::Basename;
  2         4  
  2         100  
12 2     2   12 use Time::Local;
  2         10  
  2         127  
13 2     2   944 use Time::HiRes qw(sleep);
  2         2721  
  2         110  
14 2     2   14 use Moo;
  2         4  
  2         15  
15 2     2   172 use App::Sqitch::Types qw(DBH URIDB ArrayRef Maybe Int);
  2         4  
  2         13  
16 2     2   786 use namespace::autoclean;
  2         5  
  2         27  
17 2     2   2218  
  2         4  
  2         17  
18             extends 'App::Sqitch::Engine';
19              
20             our $VERSION = 'v1.3.0'; # VERSION
21              
22             has registry_uri => (
23             is => 'ro',
24             isa => URIDB,
25             lazy => 1,
26             default => sub {
27             my $self = shift;
28             my $uri = $self->uri->clone;
29             my $reg = $self->registry;
30              
31             if ( file($reg)->is_absolute ) {
32             # Just use an absolute path.
33             $uri->dbname($reg);
34             } elsif (my @segs = $uri->path_segments) {
35             # Use the same name, but replace $name.$ext with $reg.$ext.
36             my $reg = $self->registry;
37             if ($reg =~ /[.]/) {
38             $segs[-1] =~ s/^[^.]+(?:[.].+)?$/$reg/;
39             } else {
40             $segs[-1] =~ s{^[^.]+([.].+)?$}{$reg . ($1 // '')}e;
41             }
42             $uri->path_segments(@segs);
43             } else {
44             # No known path, so no name.
45             $uri->dbname(undef);
46             }
47              
48             return $uri;
49             },
50             );
51              
52             my $uri = shift->registry_uri;
53             if ($uri->password) {
54 2     2 1 2816 $uri = $uri->clone;
55 2 100       33 $uri->password(undef);
56 1         41 }
57 1         14 return $uri->as_string;
58             }
59 2         121  
60              
61             has dbh => (
62 2     2   113 is => 'rw',
63 3     3   183 isa => DBH,
64             lazy => 1,
65             clearer => '_clear_dbh',
66             default => sub {
67             my $self = shift;
68             my $uri = $self->registry_uri;
69             $self->use_driver;
70              
71             my $dsn = $uri->dbi_dsn . ';ib_dialect=3;ib_charset=UTF8';
72             return DBI->connect($dsn, scalar $self->username, scalar $self->password, {
73             PrintError => 0,
74             RaiseError => 0,
75             AutoCommit => 1,
76             ib_enable_utf8 => 1,
77             FetchHashKeyName => 'NAME_lc',
78             HandleError => sub {
79             my ($err, $dbh) = @_;
80             $@ = $err;
81             @_ = ($dbh->state || 'DEV' => $dbh->errstr);
82             goto &hurl;
83             },
84             });
85             }
86             );
87              
88             # Need to wait until dbh is defined.
89             with 'App::Sqitch::Role::DBIEngine';
90              
91             has _isql => (
92             is => 'ro',
93             isa => ArrayRef,
94             lazy => 1,
95             default => sub {
96             my $self = shift;
97             my $uri = $self->uri;
98             my @ret = ( $self->client );
99             for my $spec (
100             [ user => $self->username ],
101             [ password => $self->password ],
102             ) {
103             push @ret, "-$spec->[0]" => $spec->[1] if $spec->[1];
104             }
105              
106             push @ret => (
107             '-quiet',
108             '-bail',
109             '-sqldialect' => '3',
110             '-pagelength' => '16384',
111             '-charset' => 'UTF8',
112             $self->connection_string($uri),
113             );
114              
115             return \@ret;
116             },
117             );
118              
119              
120             has tz_offset => (
121             is => 'ro',
122             isa => Maybe[Int],
123 22     22 1 6962 lazy => 1,
  22         403  
124             default => sub {
125             # From: https://stackoverflow.com/questions/2143528/whats-the-best-way-to-get-the-utc-offset-in-perl
126             my @t = localtime(time);
127             my $gmt_offset_in_seconds = timegm(@t) - timelocal(@t);
128             my $offset = -($gmt_offset_in_seconds / 3600);
129             return $offset;
130             },
131             );
132              
133              
134             my $dt = $_[1];
135             $dt->set_time_zone('UTC');
136             return join ' ', $dt->ymd('-'), $dt->hms(':');
137             }
138 3     3 1 11781  
139 2     2 1 33 return qq{'year:' || CAST(EXTRACT(YEAR FROM %s) AS SMALLINT)
140 0     0 1 0 || ':month:' || CAST(EXTRACT(MONTH FROM %1\$s) AS SMALLINT)
141             || ':day:' || CAST(EXTRACT(DAY FROM %1\$s) AS SMALLINT)
142             || ':hour:' || CAST(EXTRACT(HOUR FROM %1\$s) AS SMALLINT)
143 0     0   0 || ':minute:' || CAST(EXTRACT(MINUTE FROM %1\$s) AS SMALLINT)
144 0         0 || ':second:' || FLOOR(CAST(EXTRACT(SECOND FROM %1\$s) AS NUMERIC(9,4)))
145 0         0 || ':time_zone:UTC'};
146             }
147              
148             my $offset = shift->tz_offset;
149 5     5   1015 sleep 0.01; # give Firebird a little time to tick microseconds.
150             return qq(DATEADD($offset HOUR TO CURRENT_TIMESTAMP(3)));
151             }
152              
153             # Turns out, if you cast to varchar, the trailing 0s get removed. So value
154             # 1.1, represented as 1.10000002384186, returns as preferred value 1.1.
155             'SELECT CAST(ROUND(MAX(version), 1) AS VARCHAR(24)) AS v FROM releases',
156             }
157              
158             my ( $self, $change ) = @_;
159 0     0   0 return $self->dbh->selectcol_arrayref(
160 0         0 'SELECT 1 FROM changes WHERE change_id = ?',
161 0         0 undef, $change->id
162             )->[0];
163             }
164              
165             my ( $self, $tag ) = @_;
166             return $self->dbh->selectcol_arrayref(q{
167 0     0   0 SELECT 1
168             FROM tags
169             WHERE tag_id = ?
170             }, undef, $tag->id)->[0];
171 0     0 1 0 }
172 0         0  
173             my $self = shift;
174              
175             # Try to connect.
176             my $err = 0;
177             my $dbh = try { $self->dbh } catch { $err = $DBI::err; $self->sqitch->debug($_); };
178             return 0 if $err;
179 0     0 1 0  
180 0         0 return $self->dbh->selectcol_arrayref(qq{
181             SELECT COUNT(RDB\$RELATION_NAME)
182             FROM RDB\$RELATIONS
183             WHERE RDB\$SYSTEM_FLAG=0
184             AND RDB\$VIEW_BLR IS NULL
185             AND RDB\$RELATION_NAME = ?
186             }, undef, 'CHANGES')->[0];
187             }
188 0     0   0  
189             my $self = shift;
190             my $uri = $self->registry_uri;
191 0         0 hurl engine => __x(
192 0     0   0 'Sqitch database {database} already initialized',
  0         0  
  0         0  
  0         0  
193 0 0       0 database => $uri->dbname,
194             ) if $self->initialized;
195 0         0  
196             my $sqitch_db = $self->connection_string($uri);
197              
198             # Create the registry database if it does not exist.
199             $self->use_driver;
200             try {
201             DBD::Firebird->create_database({
202             db_path => $sqitch_db,
203             user => scalar $self->username,
204             password => scalar $self->password,
205 1     1 1 744 character_set => 'UTF8',
206 1         24 page_size => 16384,
207 1 50       28 });
208             }
209             catch {
210             hurl firebird => __x(
211             'Cannot create database {database}: {error}',
212 1         8 database => $sqitch_db,
213             error => $_,
214             );
215 1         5 };
216              
217 1     1   63 # Load up our database. The database must exist!
218             $self->run_upgrade( file(__FILE__)->dir->file('firebird.sql') );
219             $self->_register_release;
220             }
221              
222             my ($self, $uri) = @_;
223             my $file = $uri->dbname or hurl firebird => __x(
224             'Database name missing in URI {uri}',
225             uri => $uri,
226 1     1   73 );
227             my $host = $uri->host or return $file;
228             my $port = $uri->_port or return "$host:$file";
229             return "$host/$port:$file";
230             }
231 1         9  
232             # Override to lock the Sqitch tables. This ensures that only one instance of
233             # Sqitch runs at one time.
234 0         0 my $self = shift;
235 0         0 my $dbh = $self->dbh;
236              
237             # Start transaction and lock all tables to disallow concurrent changes.
238             # This should be equivalent to 'LOCK TABLE changes' ???
239 16     16 1 7084 # http://conferences.embarcadero.com/article/32280#TableReservation
240 16 100       69 $dbh->func(
241             -lock_resolution => 'no_wait',
242             -reserving => {
243             changes => {
244 15 100       880 lock => 'read',
245 4 50       138 access => 'protected',
246 4         98 },
247             },
248             'ib_set_tx_param'
249             );
250             $dbh->begin_work;
251             return $self;
252 0     0 1 0 }
253 0         0  
254             # Override to unlock the tables, otherwise future transactions on this
255             # connection can fail.
256             my $self = shift;
257             my $dbh = $self->dbh;
258 0         0 $dbh->commit;
259             $dbh->func( 'ib_set_tx_param' ); # reset parameters
260             return $self;
261             }
262              
263             require App::Sqitch::DateTime;
264             return App::Sqitch::DateTime->new(split /:/ => shift);
265             }
266              
267             return $DBI::errstr && $DBI::errstr =~ /^-Table unknown|No such file or directory/m;
268 0         0 }
269 0         0  
270             return $DBI::errstr && $DBI::errstr =~ /^-Column unknown/m;
271             }
272              
273             # REGEXP :(
274              
275 0     0 1 0  
276 0         0 return q{LIST(ALL %s, ' ')}; # Firebird v2.1.4 minimum
277 0         0 }
278 0         0  
279 0         0 my $self = shift;
280             my $sqitch = $self->sqitch;
281             my $pass = $self->password or return $sqitch->run( $self->isql, @_ );
282             local $ENV{ISC_PASSWORD} = $pass;
283 1     1   557 return $sqitch->run( $self->isql, @_ );
284 1         14 }
285              
286             my $self = shift;
287             my $sqitch = $self->sqitch;
288 6   100 6   6015 my $pass = $self->password or return $sqitch->capture( $self->isql, @_ );
289             local $ENV{ISC_PASSWORD} = $pass;
290             return $sqitch->capture( $self->isql, @_ );
291             }
292 4   100 4   30  
293             my $self = shift;
294             my $fh = shift;
295 0     0   0 my $sqitch = $self->sqitch;
296             my $pass = $self->password or return $sqitch->spool( $fh, $self->isql, @_ );
297             local $ENV{ISC_PASSWORD} = $pass;
298 1     1   380 return $sqitch->spool( $fh, $self->isql, @_ );
299             }
300              
301 2     2   5 my ($self, $file) = @_;
302             $self->_run( '-input' => $file );
303             }
304              
305 4     4   11422 my ($self, $file) = @_;
306 4         12 # Suppress STDOUT unless we want extra verbosity.
307 4 100       76 my $meth = $self->can($self->sqitch->verbosity > 1 ? '_run' : '_capture');
308 1         14 $self->$meth( '-input' => $file );
309 1         4 }
310              
311             my ($self, $file) = @_;
312             my $uri = $self->registry_uri;
313 3     3   1108 my @cmd = $self->isql;
314 3         8 $cmd[-1] = $self->connection_string($uri);
315 3 100       55 my $sqitch = $self->sqitch;
316 1         14 unless ($uri->host) {
317 1         3 # Only one connection allowed when using an embedded database (Engine 12
318             # provider). So disconnect so that the upgrade can connect and succeed,
319             # and clear the disconnected handle so that the next call to ->dbh will
320             # reconnect.
321 3     3   1173 $self->dbh->disconnect; $self->_clear_dbh;
322 3         5 }
323 3         9 $sqitch->run( @cmd, '-input' => $sqitch->quote_shell($file) );
324 3 100       71 }
325 1         13  
326 1         4 my ($self, $fh) = @_;
327             $self->_spool($fh);
328             }
329              
330 1     1 1 565 my ( $self, $ord, $offset, $project ) = @_;
331 1         3  
332             my $offexpr = $offset ? " SKIP $offset" : '';
333             return try {
334             return $self->dbh->selectcol_arrayref(qq{
335 2     2 1 1220 SELECT FIRST 1$offexpr change_id
336             FROM changes
337 2 100       27 WHERE project = ?
338 2         122 ORDER BY committed_at $ord;
339             }, undef, $project || $self->plan->project)->[0];
340             } catch {
341             # Firebird generic error code -902, one possible message:
342 1     1 1 21838 # -I/O error during "open" operation for file...
343 1         4 # -Error while trying to open file
344 1         6 # -No such file or directory
345 1         21 # print "===DBI ERROR: $DBI::err\n";
346 1         136 return if $DBI::err == -902; # can't connect to database
347 1 50       6 die $_;
348             };
349             }
350              
351             my ( $self, $project ) = @_;
352 1         23 my $cdtcol = sprintf $self->_ts2char_format, 'c.committed_at';
  1         7  
353             my $pdtcol = sprintf $self->_ts2char_format, 'c.planned_at';
354 1         9 my $tagcol = sprintf $self->_listagg_format, 't.tag';
355             my $state = try {
356             $self->dbh->selectrow_hashref(qq{
357             SELECT FIRST 1 c.change_id
358 1     1 1 554 , c.script_hash
359 1         4 , c.change
360             , c.project
361             , c.note
362             , c.committer_name
363 2     2   2210 , c.committer_email
364             , $cdtcol AS committed_at
365 2 50       6 , c.planner_name
366             , c.planner_email
367 2   0 2   79 , $pdtcol AS planned_at
368             , $tagcol AS tags
369             FROM changes c
370             LEFT JOIN tags t ON c.change_id = t.change_id
371             WHERE c.project = ?
372             GROUP BY c.change_id
373             , c.script_hash
374             , c.change
375             , c.project
376             , c.note
377             , c.committer_name
378             , c.committer_email
379 2 100   2   89 , c.committed_at
380 1         10 , c.planner_name
381 2         15 , c.planner_email
382             , c.planned_at
383             ORDER BY c.committed_at DESC
384             }, undef, $project // $self->plan->project );
385 2     2 1 899 } catch {
386 2         6 return if $self->_no_table_error && !$self->initialized;
387 2         5 die $_;
388 2         7 } or return undef;
389              
390 2   0 2   73 unless (ref $state->{tags}) {
391             $state->{tags} = $state->{tags} ? [ split / / => $state->{tags} ] : [];
392             }
393             $state->{committed_at} = _dt $state->{committed_at};
394             $state->{planned_at} = _dt $state->{planned_at};
395             return $state;
396             }
397              
398             my ( $self, %p ) = @_;
399              
400             # Determine order direction.
401             my $dir = 'DESC';
402             if (my $d = delete $p{direction}) {
403             $dir = $d =~ /^ASC/i ? 'ASC'
404             : $d =~ /^DESC/i ? 'DESC'
405             : hurl 'Search direction must be either "ASC" or "DESC"';
406             }
407              
408             # Limit with regular expressions?
409             my (@wheres, @params);
410             my $op = $self->_regex_op;
411             for my $spec (
412             [ committer => 'e.committer_name' ],
413             [ planner => 'e.planner_name' ],
414             [ change => 'e.change' ],
415             [ project => 'e.project' ],
416             ) {
417             my $regex = delete $p{ $spec->[0] } // next;
418             # Trying to adapt REGEXP for SIMILAR TO from Firebird 2.5 :)
419             # Yes, I know is ugly...
420 2 100 66 2   35 # There is no support for ^ and $ as in normal REGEXP.
421 1         7 #
422 2 50       12 # From the docs:
423             # Description: SIMILAR TO matches a string against an SQL
424 0 0       0 # regular expression pattern. UNLIKE in some other languages,
425 0 0       0 # the pattern MUST MATCH THE ENTIRE STRING in order to succeed
426             # – matching a substring is not enough. If any operand is
427 0         0 # NULL, the result is NULL. Otherwise, the result is TRUE or
428 0         0 # FALSE.
429 0         0 #
430             # Maybe use the CONTAINING operator instead?
431             # print "===REGEX: $regex\n";
432             if ( $regex =~ m{^\^} and $regex =~ m{\$$} ) {
433 0     0 1 0 $regex =~ s{\^}{};
434             $regex =~ s{\$}{};
435             $regex = "%$regex%";
436 0         0 }
437 0 0       0 else {
438 0 0       0 if ( $regex !~ m{^\^} and $regex !~ m{\$$} ) {
    0          
439             $regex = "%$regex%";
440             }
441             }
442             if ( $regex =~ m{\$$} ) {
443             $regex =~ s{\$}{};
444 0         0 $regex = "%$regex";
445 0         0 }
446 0         0 if ( $regex =~ m{^\^} ) {
447             $regex =~ s{\^}{};
448             $regex = "$regex%";
449             }
450             # print "== SIMILAR TO: $regex\n";
451             push @wheres => "$spec->[1] $op ?";
452 0   0     0 push @params => "$regex";
453             }
454              
455             # Match events?
456             if (my $e = delete $p{event} ) {
457             my ($in, @vals) = $self->_in_expr( $e );
458             push @wheres => "e.event $in";
459             push @params => @vals;
460             }
461              
462             # Assemble the where clause.
463             my $where = @wheres
464             ? "\n WHERE " . join( "\n ", @wheres )
465             : '';
466              
467 0 0 0     0 # Handle remaining parameters.
468 0         0 my $limits = '';
469 0         0 if (exists $p{limit} || exists $p{offset}) {
470 0         0 my $lim = delete $p{limit};
471             if ($lim) {
472             $limits = " FIRST ? ";
473 0 0 0     0 push @params => $lim;
474 0         0 }
475             if (my $off = delete $p{offset}) {
476             $limits .= " SKIP ? ";
477 0 0       0 push @params => $off;
478 0         0 }
479 0         0 }
480              
481 0 0       0 hurl 'Invalid parameters passed to search_events(): '
482 0         0 . join ', ', sort keys %p if %p;
483 0         0  
484             $self->dbh->{ib_softcommit} = 1;
485              
486 0         0 # Prepare, execute, and return.
487 0         0 my $cdtcol = sprintf $self->_ts2char_format, 'e.committed_at';
488             my $pdtcol = sprintf $self->_ts2char_format, 'e.planned_at';
489             my $sth = $self->dbh->prepare(qq{
490             SELECT $limits e.event
491 0 0       0 , e.project
492 0         0 , e.change_id
493 0         0 , e.change
494 0         0 , e.note
495             , e.requires
496             , e.conflicts
497             , e.tags
498 0 0       0 , e.committer_name
499             , e.committer_email
500             , $cdtcol AS committed_at
501             , e.planner_name
502             , e.planner_email
503 0         0 , $pdtcol AS planned_at
504 0 0 0     0 FROM events e$where
505 0         0 ORDER BY e.committed_at $dir
506 0 0       0 });
507 0         0 $sth->execute(@params);
508 0         0 return sub {
509             my $row = $sth->fetchrow_hashref or return;
510 0 0       0 $row->{committed_at} = _dt $row->{committed_at};
511 0         0 $row->{planned_at} = _dt $row->{planned_at};
512 0         0 return $row;
513             };
514             }
515              
516 0 0       0 my ( $self, $change ) = @_;
517             return @{ $self->dbh->selectall_arrayref(q{
518             SELECT c.change_id, c.project, c.change, (
519 0         0 SELECT FIRST 1 tag
520             FROM changes c2
521             JOIN tags ON c2.change_id = tags.change_id
522 0         0 WHERE c2.project = c.project
523 0         0 AND c2.committed_at >= c.committed_at
524 0         0 ORDER BY c2.committed_at
525             ) AS asof_tag
526             FROM dependencies d
527             JOIN changes c ON c.change_id = d.change_id
528             WHERE d.dependency_id = ?
529             }, { Slice => {} }, $change->id) };
530             }
531              
532             my ( $self, $change_id ) = @_;
533             return $self->dbh->selectcol_arrayref(q{
534             SELECT c.change || COALESCE((
535             SELECT FIRST 1 tag
536             FROM changes c2
537             JOIN tags ON c2.change_id = tags.change_id
538             WHERE c2.committed_at >= c.committed_at
539             AND c2.project = c.project
540             ), '@HEAD')
541             FROM changes c
542 0         0 WHERE change_id = ?
543             }, undef, $change_id)->[0];
544 0 0   0   0 }
545 0         0  
546 0         0 my ( $self, $offset ) = @_;
547 0         0 my ( $dir, $op ) = $offset > 0 ? ( 'ASC', '>' ) : ( 'DESC' , '<' );
548 0         0 return $dir, $op, 'SKIP ' . (abs($offset) - 1);
549             }
550              
551             my ( $self, $change_id, $offset ) = @_;
552 0     0 1 0  
553 0         0 # Just return the ID if there is no offset.
  0         0  
554             return $change_id unless $offset;
555              
556             my ($dir, $op, $offset_expr) = $self->_offset_op($offset);
557             return $self->dbh->selectcol_arrayref(qq{
558             SELECT FIRST 1 $offset_expr change_id AS "id"
559             FROM changes
560             WHERE project = ?
561             AND committed_at $op (
562             SELECT committed_at FROM changes WHERE change_id = ?
563             )
564             ORDER BY committed_at $dir
565             }, undef, $self->plan->project, $change_id )->[0];
566             }
567              
568             my ( $self, $change_id, $offset ) = @_;
569 0     0 1 0  
570 0         0 # Just return the object if there is no offset.
571             return $self->load_change($change_id) unless $offset;
572              
573             # Are we offset forwards or backwards?
574             my ($dir, $op, $offset_expr) = $self->_offset_op($offset);
575             my $tscol = sprintf $self->_ts2char_format, 'c.planned_at';
576             my $tagcol = sprintf $self->_listagg_format, 't.tag';
577              
578             my $change = $self->dbh->selectrow_hashref(qq{
579             SELECT FIRST 1 $offset_expr
580             c.change_id AS "id", c.change AS name, c.project, c.note,
581             $tscol AS "timestamp", c.planner_name, c.planner_email,
582             $tagcol AS tags, c.script_hash
583             FROM changes c
584 0     0   0 LEFT JOIN tags t ON c.change_id = t.change_id
585 0 0       0 WHERE c.project = ?
586 0         0 AND c.committed_at $op (
587             SELECT committed_at FROM changes WHERE change_id = ?
588             )
589             GROUP BY c.change_id, c.change, c.project, c.note, c.planned_at,
590 0     0 1 0 c.planner_name, c.planner_email, c.committed_at, c.script_hash
591             ORDER BY c.committed_at $dir
592             }, undef, $self->plan->project, $change_id ) || return undef;
593 0 0       0 $change->{timestamp} = _dt $change->{timestamp};
594             unless ( ref $change->{tags} ) {
595 0         0 $change->{tags} = $change->{tags} ? [ split / / => $change->{tags} ] : [];
596 0         0 }
597             return $change;
598             }
599              
600             my ($self, $project, $change) = @_;
601             return $self->dbh->selectcol_arrayref(q{
602             SELECT FIRST 1 change_id
603             FROM changes
604             WHERE project = ?
605             AND changes.change = ?
606             ORDER BY committed_at DESC
607             }, undef, $project, $change)->[0];
608 0     0 1 0 }
609              
610             my ( $self, %p) = @_;
611 0 0       0 my $dbh = $self->dbh;
612              
613             if ( my $cid = $p{change_id} ) {
614 0         0 # Find by ID.
615 0         0 return $dbh->selectcol_arrayref(q{
616 0         0 SELECT change_id
617             FROM changes
618 0   0     0 WHERE change_id = ?
619             }, undef, $cid)->[0];
620             }
621              
622             my $project = $p{project} || $self->plan->project;
623             if ( my $change = $p{change} ) {
624             if ( my $tag = $p{tag} ) {
625             # There is nothing before the first tag.
626             return undef if $tag eq 'ROOT';
627              
628             # Find closest to the end for @HEAD.
629             return $self->_cid_head($project, $change) if $tag eq 'HEAD';
630              
631             # Find by change name and following tag.
632             return $dbh->selectcol_arrayref(q{
633 0         0 SELECT FIRST 1 changes.change_id
634 0 0       0 FROM changes
635 0 0       0 JOIN tags
636             ON changes.committed_at <= tags.committed_at
637 0         0 AND changes.project = tags.project
638             WHERE changes.project = ?
639             AND changes.change = ?
640             AND tags.tag = ?
641 0     0   0 ORDER BY changes.committed_at DESC
642 0         0 }, undef, $project, $change, '@' . $tag)->[0];
643             }
644              
645             # Find earliest by change name.
646             my $ids = $dbh->selectcol_arrayref(qq{
647             SELECT change_id
648             FROM changes
649             WHERE project = ?
650             AND changes.change = ?
651             ORDER BY changes.committed_at ASC
652 1     1 1 467 }, undef, $project, $change);
653 1         4  
654             # Return the ID.
655 1 50       6 return $ids->[0] if $p{first};
656             return $self->_handle_lookup_index($change, $ids);
657 0         0 }
658              
659             if ( my $tag = $p{tag} ) {
660             # Just return the latest for @HEAD.
661             return $self->_cid('DESC', 0, $project) if $tag eq 'HEAD';
662              
663             # Just return the earliest for @ROOT.
664 1   33     3 return $self->_cid('ASC', 0, $project) if $tag eq 'ROOT';
665 1 50       4  
666 0 0       0 # Find by tag name.
667             return $dbh->selectcol_arrayref(q{
668 0 0       0 SELECT change_id
669             FROM tags
670             WHERE project = ?
671 0 0       0 AND tag = ?
672             }, undef, $project, '@' . $tag)->[0];
673             }
674 0         0  
675             # We got nothin.
676             return undef;
677             }
678              
679             my ( $self, $change ) = @_;
680             my @tags = $change->tags or return $self;
681             my $sqitch = $self->sqitch;
682              
683             my ($id, $name, $proj, $user, $email) = (
684             $change->id,
685             $change->format_name,
686             $change->project,
687             $sqitch->user_name,
688 0         0 $sqitch->user_email
689             );
690              
691             my $ts = $self->_ts_default;
692             my $sf = $self->_simple_from;
693              
694             my $sql = q{
695             INSERT INTO tags (
696             tag_id
697 0 0       0 , tag
698 0         0 , project
699             , change_id
700             , note
701 1 50       3 , committer_name
702             , committer_email
703 0 0       0 , planned_at
704             , planner_name
705             , planner_email
706 0 0       0 , committed_at
707             )
708             SELECT i.* FROM (
709 0         0 } . join(
710             "\n UNION ALL ",
711             ("SELECT CAST(? AS CHAR(40)) AS tid
712             , CAST(? AS VARCHAR(250)) AS tname
713             , CAST(? AS VARCHAR(255)) AS proj
714             , CAST(? AS CHAR(40)) AS cid
715             , CAST(? AS VARCHAR(4000)) AS note
716             , CAST(? AS VARCHAR(512)) AS cuser
717             , CAST(? AS VARCHAR(512)) AS cemail
718 1         4 , CAST(? AS TIMESTAMP) AS tts
719             , CAST(? AS VARCHAR(512)) AS puser
720             , CAST(? AS VARCHAR(512)) AS pemail
721             , CAST($ts$sf AS TIMESTAMP) AS cts"
722 0     0 1 0 ) x @tags ) . q{
723 0 0       0 FROM RDB$DATABASE ) i
724 0         0 LEFT JOIN tags ON i.tid = tags.tag_id
725             WHERE tags.tag_id IS NULL
726 0         0 };
727             my @params = map { (
728             $_->id,
729             $_->format_name,
730             $proj,
731             $id,
732             $_->note,
733             $user,
734 0         0 $email,
735 0         0 $self->_char2ts( $_->timestamp ),
736             $_->planner_name,
737 0         0 $_->planner_email,
738             ) } @tags;
739             $self->dbh->do($sql, undef, @params );
740             return $self;
741             }
742              
743             my ($self, $change) = @_;
744             my $dbh = $self->dbh;
745             my $sqitch = $self->sqitch;
746              
747             my ($id, $name, $proj, $user, $email) = (
748             $change->id,
749             $change->format_name,
750             $change->project,
751             $sqitch->user_name,
752             $sqitch->user_email
753             );
754              
755             my $ts = $self->_ts_default;
756             my $cols = join "\n , ", $self->_quote_idents(qw(
757             change_id
758             script_hash
759             change
760             project
761             note
762             committer_name
763             committer_email
764             planned_at
765             planner_name
766             planner_email
767             committed_at
768             ));
769             $dbh->do(qq{
770 0         0 INSERT INTO changes (
771 0         0 $cols
772             )
773             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, $ts)
774             }, undef,
775             $id,
776             $change->script_hash,
777             $name,
778             $proj,
779             $change->note,
780             $user,
781             $email,
782 0         0 $self->_char2ts( $change->timestamp ),
783 0         0 $change->planner_name,
784             $change->planner_email,
785             );
786              
787 0     0 1 0 if ( my @deps = $change->dependencies ) {
788 0         0 foreach my $dep (@deps) {
789 0         0 my $sql = q{
790             INSERT INTO dependencies (
791 0         0 change_id
792             , type
793             , dependency
794             , dependency_id
795             ) VALUES ( ?, ?, ?, ? ) };
796             $dbh->do( $sql, undef,
797             ( $id, $dep->type, $dep->as_string, $dep->resolved_id ) );
798             }
799 0         0 }
800 0         0  
801             if ( my @tags = $change->tags ) {
802             foreach my $tag (@tags) {
803             my $sql = qq{
804             INSERT INTO tags (
805             tag_id
806             , tag
807             , project
808             , change_id
809             , note
810             , committer_name
811             , committer_email
812             , planned_at
813 0         0 , planner_name
814             , planner_email
815             , committed_at
816             ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, $ts) };
817             $dbh->do(
818             $sql, undef,
819             ( $tag->id, $tag->format_name,
820             $proj, $id,
821             $tag->note, $user,
822             $email, $self->_char2ts( $tag->timestamp ),
823             $tag->planner_name, $tag->planner_email,
824             )
825             );
826             }
827             }
828              
829             return $self->_log_event( deploy => $change );
830             }
831 0 0       0  
832 0         0 my $self = shift;
833 0         0 my $ext = App::Sqitch::ISWIN || $^O eq 'cygwin' ? '.exe' : '';
834              
835             # Create a script to run.
836             require File::Temp;
837             my $fh = File::Temp->new( CLEANUP => 1 );
838             my @opts = (qw(-z -q -i), $fh->filename);
839             $fh->print("quit;\n");
840 0         0 $fh->close;
841              
842             # Suppress STDERR, including in subprocess.
843             open my $olderr, '>&', \*STDERR or hurl firebird => __x(
844             'Cannot dup STDERR: {error}', $!
845 0 0       0 );
846 0         0 close STDERR;
847 0         0 open STDERR, '>', \my $stderr or hurl firebird => __x(
848             'Cannot reirect STDERR: {error}', $!
849             );
850              
851             # Try to find a client in the path.
852             for my $try ( map { $_ . $ext } qw(fbsql isql-fb isql) ) {
853             my $loops = 0;
854             for my $dir (File::Spec->path) {
855             my $path = file $dir, $try;
856             # GetShortPathName returns undef for nonexistent files.
857             $path = Win32::GetShortPathName($path) // next if App::Sqitch::ISWIN;
858             if (-f $path && -x $path) {
859             if (try { App::Sqitch->probe($path, @opts) =~ /Firebird/ } ) {
860             # Restore STDERR and return.
861 0         0 open STDERR, '>&', $olderr or hurl firebird => __x(
862             'Cannot dup STDERR: {error}', $!
863             );
864             return $loops ? $path->stringify : $try;
865             }
866             $loops++;
867             }
868             }
869             }
870              
871             # Restore STDERR and die.
872             open STDERR, '>&', $olderr or hurl firebird => __x(
873 0         0 'Cannot dup STDERR: {error}', $!
874             );
875             hurl firebird => __(
876             'Unable to locate Firebird ISQL; set "engine.firebird.client" via sqitch config'
877 2     2 1 1993 );
878 2 50       12 }
879              
880             my $self = shift;
881 2         11 my $plan = $self->plan;
882 2         17 my $proj = $plan->project;
883 2         827 my $dbh = $self->dbh;
884 2         28  
885 2         59 $self->begin_work;
886             # Firebird refuses to update via a prepared statement, so use do(). :-(
887             $dbh->do(
888 2 50       134 'UPDATE changes SET script_hash = ? WHERE change_id = ?',
889             undef, $_->script_hash, $_->id
890             ) for $plan->changes;
891 2         13 $dbh->do(q{
892 1 50   1   5 UPDATE changes SET script_hash = NULL
  1         2  
  1         6  
  2         33  
893             WHERE project = ? AND script_hash = change_id
894             }, undef, $proj);
895              
896             $self->finish_work;
897 2         699 return $self;
  6         15  
898 6         149 }
899 6         66  
900 30         1164 1;
901              
902 30         1987  
903 30 100 66     86 =encoding utf8
904 1 50   1   97  
  1         36  
905             =head1 Name
906 0 0       0  
907             App::Sqitch::Engine::firebird - Sqitch Firebird Engine
908              
909 0 0       0 =head1 Synopsis
910              
911 1         986 my $firebird = App::Sqitch::Engine->load( engine => 'firebird' );
912              
913             =head1 Description
914              
915             App::Sqitch::Engine::firebird provides the Firebird storage engine for Sqitch.
916              
917 2 50       128 =head1 Interface
918              
919             =head2 Instance Methods
920 2         29  
921             =head3 C<connection_string>
922              
923             Constructs a connection string from a database URI for passing to C<isql>.
924              
925             =head3 C<isql>
926 0     0      
927 0           Returns a list containing the C<isql> client and options to be passed to it.
928 0           Used internally when executing scripts.
929 0            
930             =head1 Author
931 0            
932             David E. Wheeler <david@justatheory.com>
933              
934             Ștefan Suciu <stefan@s2i2.ro>
935              
936 0           =head1 License
937 0            
938             Copyright (c) 2012-2022 iovation Inc., David E. Wheeler
939              
940             Copyright (c) 2013 Ștefan Suciu
941              
942 0           Permission is hereby granted, free of charge, to any person obtaining a copy
943 0           of this software and associated documentation files (the "Software"), to deal
944             in the Software without restriction, including without limitation the rights
945             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
946             copies of the Software, and to permit persons to whom the Software is
947             furnished to do so, subject to the following conditions:
948              
949             The above copyright notice and this permission notice shall be included in all
950             copies or substantial portions of the Software.
951              
952             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
953             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
954             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
955             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
956             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
957             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
958             SOFTWARE.
959              
960             =cut