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