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