File Coverage

blib/lib/App/Sqitch/Role/DBIEngine.pm
Criterion Covered Total %
statement 72 346 20.8
branch 18 106 16.9
condition 8 56 14.2
subroutine 26 78 33.3
pod 28 28 100.0
total 152 614 24.7


line stmt bran cond sub pod time code
1             package App::Sqitch::Role::DBIEngine;
2              
3 30     30   119287 use 5.010;
  30         857  
4 30     30   193 use strict;
  30         535  
  30         1393  
5 30     30   161 use warnings;
  30         313  
  30         1744  
6 30     30   171 use utf8;
  30         152  
  30         311  
7 30     30   24079 use DBI 1.631;
  30         310585  
  30         2236  
8 30     30   722 use Moo::Role;
  30         14978  
  30         405  
9 30     30   19223 use Try::Tiny;
  30         1723  
  30         2633  
10 30     30   757 use App::Sqitch::X qw(hurl);
  30         67  
  30         359  
11 30     30   12972 use Locale::TextDomain qw(App-Sqitch);
  30         31876  
  30         400  
12 30     30   29292 use namespace::autoclean;
  30         17733  
  30         479  
13              
14             our $VERSION = 'v1.6.1'; # VERSION
15              
16             requires 'dbh';
17             requires 'sqitch';
18             requires 'plan';
19             requires '_regex_op';
20             requires '_ts2char_format';
21             requires '_char2ts';
22             requires '_listagg_format';
23             requires '_no_table_error';
24             requires '_unique_error';
25             requires '_handle_lookup_index';
26             requires '_no_registry';
27             requires 'initialized';
28              
29             # Called on connect if the registry schema does not exist.
30             sub _handle_no_registry {
31 0     0   0 my ($self, $dbh) = @_;
32             # https://www.nntp.perl.org/group/perl.dbi.dev/2013/11/msg7622.html
33 0         0 $dbh->set_err(undef, undef);
34 0         0 $self->_no_registry(1);
35 0         0 return;
36             }
37              
38             after use_driver => sub {
39             DBI->trace(1) if $_[0]->sqitch->verbosity > 2;
40             };
41              
42 0     0   0 sub _dsn { shift->target->uri->dbi_dsn }
43              
44             sub _dt($) {
45 3     3   3486 require App::Sqitch::DateTime;
46 3         55 return App::Sqitch::DateTime->new(split /:/ => shift);
47             }
48              
49             sub _log_tags_param {
50 0     0   0 join ' ' => map { $_->format_name } $_[1]->tags;
  0         0  
51             }
52              
53             sub _log_requires_param {
54 0     0   0 join ',' => map { $_->as_string } $_[1]->requires;
  0         0  
55             }
56              
57             sub _log_conflicts_param {
58 0     0   0 join ',' => map { $_->as_string } $_[1]->conflicts;
  0         0  
59             }
60              
61 0 0   0   0 sub _parse_array { return $_[1] ? [ split / / => $_[1] ] : [] }
62              
63 1     1   313783 sub _ts_default { 'DEFAULT' }
64              
65 0     0   0 sub _can_limit { 1 }
66 2     2   8 sub _limit_default { undef }
67              
68 2     2   11 sub _simple_from { '' }
69              
70 0     0   0 sub _quote_idents { shift; @_ }
  0         0  
71              
72             sub _in_expr {
73 0     0   0 my ($self, $vals) = @_;
74 0         0 my $in = sprintf 'IN (%s)', join ', ', ('?') x @{ $vals };
  0         0  
75 0         0 return $in, @{ $vals };
  0         0  
76             }
77              
78             sub _register_release {
79 0     0   0 my $self = shift;
80 0   0     0 my $version = shift || $self->registry_release;
81 0         0 my $sqitch = $self->sqitch;
82 0         0 my $ts = $self->_ts_default;
83              
84 0         0 $self->begin_work;
85 0         0 $self->dbh->do(qq{
86             INSERT INTO releases (version, installed_at, installer_name, installer_email)
87             VALUES (?, $ts, ?, ?)
88             }, undef, $version, $sqitch->user_name, $sqitch->user_email);
89 0         0 $self->finish_work;
90 0         0 return $self;
91             }
92              
93 0     0   0 sub _version_query { 'SELECT MAX(version) FROM releases' }
94              
95             sub registry_version {
96 2     2 1 1179 my $self = shift;
97             try {
98 2     2   239 $self->dbh->selectcol_arrayref($self->_version_query)->[0];
99             } catch {
100 2 100   2   54 return 0 if $self->_no_table_error;
101 1         24 die $_;
102 2         25 };
103             }
104              
105             sub _cid {
106 2     2   68 my ( $self, $ord, $offset, $project ) = @_;
107             return try {
108 2   0 2   207 $self->dbh->selectcol_arrayref(qq{
109             SELECT change_id
110             FROM changes
111             WHERE project = ?
112             ORDER BY committed_at $ord
113             LIMIT 1
114             OFFSET COALESCE(?, 0)
115             }, undef, $project || $self->plan->project, $offset)->[0];
116             } catch {
117 2 100 66 2   49 return if $self->_no_table_error && !$self->initialized;
118 1         42 die $_;
119 2         51 };
120             }
121              
122             sub earliest_change_id {
123 0     0 1 0 shift->_cid('ASC', @_);
124             }
125              
126             sub latest_change_id {
127 0     0 1 0 shift->_cid('DESC', @_);
128             }
129              
130             sub _select_state {
131 0     0   0 my ( $self, $project, $with_hash ) = @_;
132 0         0 my $cdtcol = sprintf $self->_ts2char_format, 'c.committed_at';
133 0         0 my $pdtcol = sprintf $self->_ts2char_format, 'c.planned_at';
134 0         0 my $tagcol = sprintf $self->_listagg_format, 't.tag';
135 0 0       0 my $hshcol = $with_hash ? "c.script_hash\n , " : '';
136 0         0 my $dbh = $self->dbh;
137 0   0     0 $dbh->selectrow_hashref(qq{
138             SELECT c.change_id
139             , ${hshcol}c.change
140             , c.project
141             , c.note
142             , c.committer_name
143             , c.committer_email
144             , $cdtcol AS committed_at
145             , c.planner_name
146             , c.planner_email
147             , $pdtcol AS planned_at
148             , $tagcol AS tags
149             FROM changes c
150             LEFT JOIN tags t ON c.change_id = t.change_id
151             WHERE c.project = ?
152             GROUP BY c.change_id
153             , ${hshcol}c.change
154             , c.project
155             , c.note
156             , c.committer_name
157             , c.committer_email
158             , c.committed_at
159             , c.planner_name
160             , c.planner_email
161             , c.planned_at
162             ORDER BY c.committed_at DESC
163             LIMIT 1
164             }, undef, $project // $self->plan->project );
165             }
166              
167             sub current_state {
168 3     3 1 93 my ( $self, $project ) = @_;
169             my $state = try {
170 3     3   325 $self->_select_state($project, 1)
171             } catch {
172 3 100 100 3   79 return if $self->_no_table_error && !$self->initialized;
173 2 100       22 return $self->_select_state($project, 0) if $self->_no_column_error;
174 1         21 die $_;
175 3 50       30 } or return undef;
176              
177 0 0       0 unless (ref $state->{tags}) {
178 0         0 $state->{tags} = $self->_parse_array($state->{tags});
179             }
180 0         0 $state->{committed_at} = _dt $state->{committed_at};
181 0         0 $state->{planned_at} = _dt $state->{planned_at};
182 0         0 return $state;
183             }
184              
185             sub current_changes {
186 0     0 1 0 my ( $self, $project ) = @_;
187 0         0 my $cdtcol = sprintf $self->_ts2char_format, 'c.committed_at';
188 0         0 my $pdtcol = sprintf $self->_ts2char_format, 'c.planned_at';
189 0         0 my $sth = $self->dbh->prepare(qq{
190             SELECT c.change_id
191             , c.script_hash
192             , c.change
193             , c.committer_name
194             , c.committer_email
195             , $cdtcol AS committed_at
196             , c.planner_name
197             , c.planner_email
198             , $pdtcol AS planned_at
199             FROM changes c
200             WHERE project = ?
201             ORDER BY c.committed_at DESC
202             });
203 0   0     0 $sth->execute($project // $self->plan->project);
204             return sub {
205 0 0   0   0 my $row = $sth->fetchrow_hashref or return;
206 0         0 $row->{committed_at} = _dt $row->{committed_at};
207 0         0 $row->{planned_at} = _dt $row->{planned_at};
208 0         0 return $row;
209 0         0 };
210             }
211              
212             sub current_tags {
213 0     0 1 0 my ( $self, $project ) = @_;
214 0         0 my $cdtcol = sprintf $self->_ts2char_format, 'committed_at';
215 0         0 my $pdtcol = sprintf $self->_ts2char_format, 'planned_at';
216 0         0 my $sth = $self->dbh->prepare(qq{
217             SELECT tag_id
218             , tag
219             , committer_name
220             , committer_email
221             , $cdtcol AS committed_at
222             , planner_name
223             , planner_email
224             , $pdtcol AS planned_at
225             FROM tags
226             WHERE project = ?
227             ORDER BY tags.committed_at DESC
228             });
229 0   0     0 $sth->execute($project // $self->plan->project);
230             return sub {
231 0 0   0   0 my $row = $sth->fetchrow_hashref or return;
232 0         0 $row->{committed_at} = _dt $row->{committed_at};
233 0         0 $row->{planned_at} = _dt $row->{planned_at};
234 0         0 return $row;
235 0         0 };
236             }
237              
238             sub search_events {
239 0     0 1 0 my ( $self, %p ) = @_;
240              
241             # Determine order direction.
242 0         0 my $dir = 'DESC';
243 0 0       0 if (my $d = delete $p{direction}) {
244 0 0       0 $dir = $d =~ /^ASC/i ? 'ASC'
    0          
245             : $d =~ /^DESC/i ? 'DESC'
246             : hurl 'Search direction must be either "ASC" or "DESC"';
247             }
248              
249             # Limit with regular expressions?
250 0         0 my (@wheres, @params);
251 0         0 for my $spec (
252             [ committer => 'e.committer_name' ],
253             [ planner => 'e.planner_name' ],
254             [ change => 'e.change' ],
255             [ project => 'e.project' ],
256             ) {
257 0   0     0 my $regex = delete $p{ $spec->[0] } // next;
258 0         0 my ($op, $expr) = $self->_regex_expr($spec->[1], $regex);
259 0         0 push @wheres => $op;
260 0         0 push @params => $expr;
261             }
262              
263             # Match events?
264 0 0       0 if (my $e = delete $p{event} ) {
265 0         0 my ($in, @vals) = $self->_in_expr( $e );
266 0         0 push @wheres => "e.event $in";
267 0         0 push @params => @vals;
268             }
269              
270             # Assemble the where clause.
271 0 0       0 my $where = @wheres
272             ? "\n WHERE " . join( "\n ", @wheres )
273             : '';
274              
275             # Handle remaining parameters.
276 0         0 my $limits = '';
277 0 0 0     0 if (exists $p{limit} || exists $p{offset}) {
278 0         0 my ($exprs, $values) = $self->_limit_offset(delete $p{limit}, delete $p{offset});
279 0 0       0 if (@{ $exprs}) {
  0         0  
280 0         0 $limits = join "\n ", '', @{ $exprs };
  0         0  
281 0 0       0 push @params => @{ $values || [] };
  0         0  
282             }
283             }
284              
285 0 0       0 hurl 'Invalid parameters passed to search_events(): '
286             . join ', ', sort keys %p if %p;
287              
288             # Prepare, execute, and return.
289 0         0 my $cdtcol = sprintf $self->_ts2char_format, 'e.committed_at';
290 0         0 my $pdtcol = sprintf $self->_ts2char_format, 'e.planned_at';
291 0         0 my $sth = $self->dbh->prepare(qq{
292             SELECT e.event
293             , e.project
294             , e.change_id
295             , e.change
296             , e.note
297             , e.requires
298             , e.conflicts
299             , e.tags
300             , e.committer_name
301             , e.committer_email
302             , $cdtcol AS committed_at
303             , e.planner_name
304             , e.planner_email
305             , $pdtcol AS planned_at
306             FROM events e$where
307             ORDER BY e.committed_at $dir$limits
308             });
309              
310 0         0 $sth->execute(@params);
311             return sub {
312 0 0   0   0 my $row = $sth->fetchrow_hashref or return;
313 0         0 $row->{committed_at} = _dt $row->{committed_at};
314 0         0 $row->{planned_at} = _dt $row->{planned_at};
315             # Some database engines don't support arrays, so we store
316             # space-delimited lists that must be parsed.
317 0         0 for my $col (qw(tags requires conflicts)) {
318 0 0       0 $row->{$col} = $self->_parse_array($row->{$col}) unless ref $row->{$col};
319             }
320 0         0 return $row;
321 0         0 };
322             }
323              
324             sub _regex_expr {
325 2     2   46 my ( $self, $col, $regex ) = @_;
326 2         12 my $op = $self->_regex_op;
327 2         28 return "$col $op ?", $regex;
328             }
329              
330             sub _limit_offset {
331 6     6   476 my ($self, $lim, $off) = @_;
332 6         13 my (@limits, @params);
333              
334 6 100       19 if ($lim) {
335 3         10 push @limits => 'LIMIT ?';
336 3         8 push @params => $lim;
337             }
338 6 100       16 if ($off) {
339 2 100 66     13 if (!$lim && ($lim = $self->_limit_default)) {
340             # Some drivers require LIMIT when OFFSET is set.
341 1         4 push @limits => 'LIMIT ?';
342 1         2 push @params => $lim;
343             }
344 2         12 push @limits => 'OFFSET ?';
345 2         4 push @params => $off;
346             }
347 6         72 return \@limits, \@params;
348             }
349              
350             sub registered_projects {
351 0     0 1 0 return @{ shift->dbh->selectcol_arrayref(
352 0         0 'SELECT project FROM projects ORDER BY project'
353             ) };
354             }
355              
356             sub register_project {
357 0     0 1 0 my $self = shift;
358 0         0 my $sqitch = $self->sqitch;
359 0         0 my $dbh = $self->dbh;
360 0         0 my $plan = $self->plan;
361 0         0 my $proj = $plan->project;
362 0         0 my $uri = $plan->uri;
363              
364 0         0 my $res = $dbh->selectcol_arrayref(
365             'SELECT uri FROM projects WHERE project = ?',
366             undef, $proj
367             );
368              
369 0 0       0 if (@{ $res }) {
  0         0  
370             # A project with that name is already registered. Compare URIs.
371 0         0 my $reg_uri = $res->[0];
372 0 0 0     0 if ( defined $uri && !defined $reg_uri ) {
    0 0        
    0 0        
373 0         0 hurl engine => __x(
374             'Cannot register "{project}" with URI {uri}: already exists with NULL URI',
375             project => $proj,
376             uri => $uri
377             );
378             } elsif ( !defined $uri && defined $reg_uri ) {
379 0         0 hurl engine => __x(
380             'Cannot register "{project}" without URI: already exists with URI {uri}',
381             project => $proj,
382             uri => $reg_uri
383             );
384             } elsif ( defined $uri && defined $reg_uri ) {
385 0 0       0 hurl engine => __x(
386             'Cannot register "{project}" with URI {uri}: already exists with URI {reg_uri}',
387             project => $proj,
388             uri => $uri,
389             reg_uri => $reg_uri,
390             ) if $uri ne $reg_uri;
391             } else {
392             # Both are undef, so cool.
393             }
394             } else {
395             # No project with that name exists. Check to see if the URI does.
396 0 0       0 if (defined $uri) {
397             # Does the URI already exist?
398 0         0 my $res = $dbh->selectcol_arrayref(
399             'SELECT project FROM projects WHERE uri = ?',
400             undef, $uri
401             );
402              
403             hurl engine => __x(
404             'Cannot register "{project}" with URI {uri}: project "{reg_proj}" already using that URI',
405             project => $proj,
406             uri => $uri,
407             reg_proj => $res->[0],
408 0 0       0 ) if @{ $res };
  0         0  
409             }
410              
411             # Insert the project.
412 0         0 my $ts = $self->_ts_default;
413 0         0 $dbh->do(qq{
414             INSERT INTO projects (project, uri, creator_name, creator_email, created_at)
415             VALUES (?, ?, ?, ?, $ts)
416             }, undef, $proj, $uri, $sqitch->user_name, $sqitch->user_email);
417             }
418              
419 0         0 return $self;
420             }
421              
422             sub is_deployed_change {
423 0     0 1 0 my ( $self, $change ) = @_;
424 0         0 $self->dbh->selectcol_arrayref(q{
425             SELECT EXISTS(
426             SELECT 1
427             FROM changes
428             WHERE change_id = ?
429             )
430             }, undef, $change->id)->[0];
431             }
432              
433             sub are_deployed_changes {
434 0     0 1 0 my $self = shift;
435 0         0 my $qs = join ', ' => ('?') x @_;
436 0         0 @{ $self->dbh->selectcol_arrayref(
437             "SELECT change_id FROM changes WHERE change_id IN ($qs)",
438             undef,
439 0         0 map { $_->id } @_,
  0         0  
440             ) };
441             }
442              
443             sub is_deployed_tag {
444 0     0 1 0 my ( $self, $tag ) = @_;
445 0         0 return $self->dbh->selectcol_arrayref(q{
446             SELECT EXISTS(
447             SELECT 1
448             FROM tags
449             WHERE tag_id = ?
450             );
451             }, undef, $tag->id)->[0];
452             }
453              
454             sub _multi_values {
455 0     0   0 my ($self, $count, $expr) = @_;
456 0         0 return 'VALUES ' . join(', ', ("($expr)") x $count)
457             }
458              
459             sub _dependency_placeholders {
460 0     0   0 return '?, ?, ?, ?';
461             }
462              
463             sub _tag_placeholders {
464 0     0   0 my $self = shift;
465 0         0 return '?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ' . $self->_ts_default;
466             }
467              
468             sub _tag_subselect_columns {
469 0     0   0 my $self = shift;
470 0         0 return join(', ',
471             '? AS tid',
472             '? AS tname',
473             '? AS proj',
474             '? AS cid',
475             '? AS note',
476             '? AS cuser',
477             '? AS cemail',
478             '? AS tts',
479             '? AS puser',
480             '? AS pemail',
481             $self->_ts_default,
482             );
483             }
484              
485 0     0   0 sub _prepare_to_log { $_[0] }
486              
487             sub log_deploy_change {
488 0     0 1 0 my ($self, $change) = @_;
489 0         0 my $dbh = $self->dbh;
490 0         0 my $sqitch = $self->sqitch;
491              
492 0         0 my ($id, $name, $proj, $user, $email) = (
493             $change->id,
494             $change->format_name,
495             $change->project,
496             $sqitch->user_name,
497             $sqitch->user_email
498             );
499              
500 0         0 my $ts = $self->_ts_default;
501 0         0 my $cols = join "\n , ", $self->_quote_idents(qw(
502             change_id
503             script_hash
504             change
505             project
506             note
507             committer_name
508             committer_email
509             planned_at
510             planner_name
511             planner_email
512             committed_at
513             ));
514              
515 0         0 $self->_prepare_to_log(changes => $change);
516             try {
517 0     0   0 $dbh->do(qq{
518             INSERT INTO changes (
519             $cols
520             )
521             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, $ts)
522             }, undef,
523             $id,
524             $change->script_hash,
525             $name,
526             $proj,
527             $change->note,
528             $user,
529             $email,
530             $self->_char2ts( $change->timestamp ),
531             $change->planner_name,
532             $change->planner_email,
533             )
534             } catch {
535 0 0   0   0 hurl engine => __x(
536             'Cannot log change "{change}": The deploy script is not unique',
537             change => $name,
538             ) if $self->_unique_error;
539 0         0 die $_;
540 0         0 };
541              
542 0 0       0 if ( my @deps = $change->dependencies ) {
543             $dbh->do(q{
544             INSERT INTO dependencies(
545             change_id
546             , type
547             , dependency
548             , dependency_id
549             ) } . $self->_multi_values(scalar @deps, $self->_dependency_placeholders),
550             undef,
551 0         0 map { (
552 0         0 $id,
553             $_->type,
554             $_->as_string,
555             $_->resolved_id,
556             ) } @deps
557             );
558             }
559              
560 0 0       0 if ( my @tags = $change->tags ) {
561             $dbh->do(q{
562             INSERT INTO tags (
563             tag_id
564             , tag
565             , project
566             , change_id
567             , note
568             , committer_name
569             , committer_email
570             , planned_at
571             , planner_name
572             , planner_email
573             , committed_at
574             ) } . $self->_multi_values(scalar @tags, $self->_tag_placeholders),
575             undef,
576 0         0 map { (
577 0         0 $_->id,
578             $_->format_name,
579             $proj,
580             $id,
581             $_->note,
582             $user,
583             $email,
584             $self->_char2ts( $_->timestamp ),
585             $_->planner_name,
586             $_->planner_email,
587             ) } @tags
588             );
589             }
590              
591 0         0 return $self->_log_event( deploy => $change );
592             }
593              
594             sub log_fail_change {
595 0     0 1 0 shift->_log_event( fail => shift );
596             }
597              
598             sub _log_event {
599 0     0   0 my ( $self, $event, $change, $tags, $requires, $conflicts) = @_;
600 0         0 my $dbh = $self->dbh;
601 0         0 my $sqitch = $self->sqitch;
602              
603 0         0 my $ts = $self->_ts_default;
604 0         0 my $cols = join "\n , ", $self->_quote_idents(qw(
605             event
606             change_id
607             change
608             project
609             note
610             tags
611             requires
612             conflicts
613             committer_name
614             committer_email
615             planned_at
616             planner_name
617             planner_email
618             committed_at
619             ));
620              
621 0         0 $self->_prepare_to_log(events => $change);
622 0   0     0 $dbh->do(qq{
      0        
      0        
623             INSERT INTO events (
624             $cols
625             )
626             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, $ts)
627             }, undef,
628             $event,
629             $change->id,
630             $change->name,
631             $change->project,
632             $change->note,
633             $tags || $self->_log_tags_param($change),
634             $requires || $self->_log_requires_param($change),
635             $conflicts || $self->_log_conflicts_param($change),
636             $sqitch->user_name,
637             $sqitch->user_email,
638             $self->_char2ts( $change->timestamp ),
639             $change->planner_name,
640             $change->planner_email,
641             );
642              
643 0         0 return $self;
644             }
645              
646             sub changes_requiring_change {
647 0     0 1 0 my ( $self, $change ) = @_;
648 0         0 return @{ $self->dbh->selectall_arrayref(q{
  0         0  
649             SELECT c.change_id, c.project, c.change, (
650             SELECT tag
651             FROM changes c2
652             JOIN tags ON c2.change_id = tags.change_id
653             WHERE c2.project = c.project
654             AND c2.committed_at >= c.committed_at
655             ORDER BY c2.committed_at
656             LIMIT 1
657             ) AS asof_tag
658             FROM dependencies d
659             JOIN changes c ON c.change_id = d.change_id
660             WHERE d.dependency_id = ?
661             }, { Slice => {} }, $change->id) };
662             }
663              
664             sub name_for_change_id {
665 0     0 1 0 my ( $self, $change_id ) = @_;
666 0         0 return $self->dbh->selectcol_arrayref(q{
667             SELECT c.change || COALESCE((
668             SELECT tag
669             FROM changes c2
670             JOIN tags ON c2.change_id = tags.change_id
671             WHERE c2.committed_at >= c.committed_at
672             AND c2.project = c.project
673             LIMIT 1
674             ), '@HEAD')
675             FROM changes c
676             WHERE change_id = ?
677             }, undef, $change_id)->[0];
678             }
679              
680             sub log_new_tags {
681 0     0 1 0 my ( $self, $change ) = @_;
682 0 0       0 my @tags = $change->tags or return $self;
683 0         0 my $sqitch = $self->sqitch;
684              
685 0         0 my ($id, $name, $proj, $user, $email) = (
686             $change->id,
687             $change->format_name,
688             $change->project,
689             $sqitch->user_name,
690             $sqitch->user_email
691             );
692              
693 0         0 my $subselect = 'SELECT ' . $self->_tag_subselect_columns . $self->_simple_from;
694             $self->dbh->do(
695             q{
696             INSERT INTO tags (
697             tag_id
698             , tag
699             , project
700             , change_id
701             , note
702             , committer_name
703             , committer_email
704             , planned_at
705             , planner_name
706             , planner_email
707             , committed_at
708             )
709             SELECT i.* FROM (
710             } . join(
711             "\n UNION ALL ",
712             ($subselect) x @tags
713             ) . q{
714             ) i
715             LEFT JOIN tags ON i.tid = tags.tag_id
716             WHERE tags.tag_id IS NULL
717             },
718             undef,
719 0         0 map { (
720 0         0 $_->id,
721             $_->format_name,
722             $proj,
723             $id,
724             $_->note,
725             $user,
726             $email,
727             $self->_char2ts( $_->timestamp ),
728             $_->planner_name,
729             $_->planner_email,
730             ) } @tags
731             );
732              
733 0         0 return $self;
734             }
735              
736             sub log_revert_change {
737 0     0 1 0 my ($self, $change) = @_;
738 0         0 my $dbh = $self->dbh;
739 0         0 my $cid = $change->id;
740              
741             # Retrieve and delete tags.
742 0 0       0 my $del_tags = join ',' => @{ $dbh->selectcol_arrayref(
  0         0  
743             'SELECT tag FROM tags WHERE change_id = ?',
744             undef, $cid
745             ) || [] };
746              
747 0         0 $dbh->do(
748             'DELETE FROM tags WHERE change_id = ?',
749             undef, $cid
750             );
751              
752             # Retrieve dependencies and delete.
753 0         0 my $sth = $dbh->prepare(q{
754             SELECT dependency
755             FROM dependencies
756             WHERE change_id = ?
757             AND type = ?
758             });
759 0         0 my $req = join ',' => @{ $dbh->selectcol_arrayref(
  0         0  
760             $sth, undef, $cid, 'require'
761             ) };
762              
763 0         0 my $conf = join ',' => @{ $dbh->selectcol_arrayref(
  0         0  
764             $sth, undef, $cid, 'conflict'
765             ) };
766              
767 0         0 $dbh->do('DELETE FROM dependencies WHERE change_id = ?', undef, $cid);
768              
769             # Delete the change record.
770 0         0 $dbh->do(
771             'DELETE FROM changes where change_id = ?',
772             undef, $cid,
773             );
774              
775             # Log it.
776 0         0 return $self->_log_event( revert => $change, $del_tags, $req, $conf );
777             }
778              
779             sub deployed_changes {
780 0     0 1 0 my $self = shift;
781 0         0 my $tscol = sprintf $self->_ts2char_format, 'c.planned_at';
782 0         0 my $tagcol = sprintf $self->_listagg_format, 't.tag';
783             return map {
784 0         0 $_->{timestamp} = _dt $_->{timestamp};
785 0 0       0 unless (ref $_->{tags}) {
786 0         0 $_->{tags} = $self->_parse_array($_->{tags});
787             }
788 0         0 $_;
789 0         0 } @{ $self->dbh->selectall_arrayref(qq{
  0         0  
790             SELECT c.change_id AS id, c.change AS name, c.project, c.note,
791             $tscol AS "timestamp", c.planner_name, c.planner_email,
792             $tagcol AS tags, c.script_hash
793             FROM changes c
794             LEFT JOIN tags t ON c.change_id = t.change_id
795             WHERE c.project = ?
796             GROUP BY c.change_id, c.change, c.project, c.note, c.planned_at,
797             c.planner_name, c.planner_email, c.committed_at, c.script_hash
798             ORDER BY c.committed_at ASC
799             }, { Slice => {} }, $self->plan->project) };
800             }
801              
802             sub deployed_changes_since {
803 0     0 1 0 my ( $self, $change ) = @_;
804 0         0 my $tscol = sprintf $self->_ts2char_format, 'c.planned_at';
805 0         0 my $tagcol = sprintf $self->_listagg_format, 't.tag';
806             return map {
807 0         0 $_->{timestamp} = _dt $_->{timestamp};
808 0 0       0 unless (ref $_->{tags}) {
809 0         0 $_->{tags} = $self->_parse_array($_->{tags});
810             }
811 0         0 $_;
812 0         0 } @{ $self->dbh->selectall_arrayref(qq{
  0         0  
813             SELECT c.change_id AS id, c.change AS name, c.project, c.note,
814             $tscol AS "timestamp", c.planner_name, c.planner_email,
815             $tagcol AS tags, c.script_hash
816             FROM changes c
817             LEFT JOIN tags t ON c.change_id = t.change_id
818             WHERE c.project = ?
819             AND c.committed_at > (SELECT committed_at FROM changes WHERE change_id = ?)
820             GROUP BY c.change_id, c.change, c.project, c.note, c.planned_at,
821             c.planner_name, c.planner_email, c.committed_at, c.script_hash
822             ORDER BY c.committed_at ASC
823             }, { Slice => {} }, $self->plan->project, $change->id) };
824             }
825              
826             sub load_change {
827 0     0 1 0 my ( $self, $change_id ) = @_;
828 0         0 my $tscol = sprintf $self->_ts2char_format, 'c.planned_at';
829 0         0 my $tagcol = sprintf $self->_listagg_format, 't.tag';
830 0   0     0 my $change = $self->dbh->selectrow_hashref(qq{
831             SELECT c.change_id AS id, c.change AS name, c.project, c.note,
832             $tscol AS "timestamp", c.planner_name, c.planner_email,
833             $tagcol AS tags, c.script_hash
834             FROM changes c
835             LEFT JOIN tags t ON c.change_id = t.change_id
836             WHERE c.change_id = ?
837             GROUP BY c.change_id, c.change, c.project, c.note, c.planned_at,
838             c.planner_name, c.planner_email, c.script_hash
839             }, undef, $change_id) || return undef;
840 0         0 $change->{timestamp} = _dt $change->{timestamp};
841 0 0       0 unless (ref $change->{tags}) {
842 0         0 $change->{tags} = $self->_parse_array($change->{tags});
843             }
844 0         0 return $change;
845             }
846              
847             sub _offset_op {
848 0     0   0 my ( $self, $offset ) = @_;
849 0 0       0 my ( $dir, $op ) = $offset > 0 ? ( 'ASC', '>' ) : ( 'DESC' , '<' );
850 0         0 return $dir, $op, 'OFFSET ' . (abs($offset) - 1);
851             }
852              
853             sub change_id_offset_from_id {
854 0     0 1 0 my ( $self, $change_id, $offset ) = @_;
855              
856             # Just return the ID if there is no offset.
857 0 0       0 return $change_id unless $offset;
858              
859 0         0 my ($dir, $op, $offset_expr) = $self->_offset_op($offset);
860 0         0 return $self->dbh->selectcol_arrayref(qq{
861             SELECT change_id
862             FROM changes
863             WHERE project = ?
864             AND committed_at $op (
865             SELECT committed_at FROM changes WHERE change_id = ?
866             )
867             ORDER BY committed_at $dir
868             LIMIT 1 $offset_expr
869             }, undef, $self->plan->project, $change_id)->[0];
870             }
871              
872             sub change_offset_from_id {
873 0     0 1 0 my ( $self, $change_id, $offset ) = @_;
874              
875             # Just return the object if there is no offset.
876 0 0       0 return $self->load_change($change_id) unless $offset;
877              
878             # Are we offset forwards or backwards?
879 0         0 my ($dir, $op, $offset_expr) = $self->_offset_op($offset);
880 0         0 my $tscol = sprintf $self->_ts2char_format, 'c.planned_at';
881 0         0 my $tagcol = sprintf $self->_listagg_format, 't.tag';
882              
883 0   0     0 my $change = $self->dbh->selectrow_hashref(qq{
884             SELECT c.change_id AS id, c.change AS name, c.project, c.note,
885             $tscol AS "timestamp", c.planner_name, c.planner_email,
886             $tagcol AS tags, c.script_hash
887             FROM changes c
888             LEFT JOIN tags t ON c.change_id = t.change_id
889             WHERE c.project = ?
890             AND c.committed_at $op (
891             SELECT committed_at FROM changes WHERE change_id = ?
892             )
893             GROUP BY c.change_id, c.change, c.project, c.note, c.planned_at,
894             c.planner_name, c.planner_email, c.committed_at, c.script_hash
895             ORDER BY c.committed_at $dir
896             LIMIT 1 $offset_expr
897             }, undef, $self->plan->project, $change_id) || return undef;
898 0         0 $change->{timestamp} = _dt $change->{timestamp};
899 0 0       0 unless (ref $change->{tags}) {
900 0         0 $change->{tags} = $self->_parse_array($change->{tags});
901             }
902 0         0 return $change;
903             }
904              
905             sub _cid_head {
906 0     0   0 my ($self, $project, $change) = @_;
907 0         0 return $self->dbh->selectcol_arrayref(q{
908             SELECT change_id
909             FROM changes
910             WHERE project = ?
911             AND changes.change = ?
912             ORDER BY committed_at DESC
913             LIMIT 1
914             }, undef, $project, $change)->[0];
915             }
916              
917             sub change_id_for {
918 1     1 1 6 my ( $self, %p) = @_;
919 1         6 my $dbh = $self->dbh;
920              
921 1 50       7 if ( my $cid = $p{change_id} ) {
922             # Find by ID.
923 0         0 return $dbh->selectcol_arrayref(q{
924             SELECT change_id
925             FROM changes
926             WHERE change_id = ?
927             }, undef, $cid)->[0];
928             }
929              
930 1   33     6 my $project = $p{project} || $self->plan->project;
931 1 50       4 if ( my $change = $p{change} ) {
932 0 0       0 if ( my $tag = $p{tag} ) {
933             # There is nothing before the first tag.
934 0 0       0 return undef if $tag eq 'ROOT';
935              
936             # Find closest to the end for @HEAD.
937 0 0       0 return $self->_cid_head($project, $change) if $tag eq 'HEAD';
938              
939             # Find by change name and following tag.
940 0 0       0 my $limit = $self->_can_limit ? "\n LIMIT 1" : '';
941 0         0 return $dbh->selectcol_arrayref(qq{
942             SELECT changes.change_id
943             FROM changes
944             JOIN tags
945             ON changes.committed_at <= tags.committed_at
946             AND changes.project = tags.project
947             WHERE changes.project = ?
948             AND changes.change = ?
949             AND tags.tag = ?
950             ORDER BY changes.committed_at DESC$limit
951             }, undef, $project, $change, '@' . $tag)->[0];
952             }
953              
954             # Find earliest by change name.
955 0         0 my $ids = $dbh->selectcol_arrayref(qq{
956             SELECT change_id
957             FROM changes
958             WHERE project = ?
959             AND changes.change = ?
960             ORDER BY changes.committed_at ASC
961             }, undef, $project, $change);
962              
963             # Return the ID.
964 0 0       0 return $ids->[0] if $p{first};
965 0         0 return $self->_handle_lookup_index($change, $ids);
966             }
967              
968 1 50       6 if ( my $tag = $p{tag} ) {
969             # Just return the latest for @HEAD.
970 0 0       0 return $self->_cid('DESC', 0, $project) if $tag eq 'HEAD';
971              
972             # Just return the earliest for @ROOT.
973 0 0       0 return $self->_cid('ASC', 0, $project) if $tag eq 'ROOT';
974              
975             # Find by tag name.
976 0         0 return $dbh->selectcol_arrayref(q{
977             SELECT change_id
978             FROM tags
979             WHERE project = ?
980             AND tag = ?
981             }, undef, $project, '@' . $tag)->[0];
982             }
983              
984             # We got nothin.
985 1         6 return undef;
986             }
987              
988             sub _update_script_hashes {
989 0     0     my $self = shift;
990 0           my $plan = $self->plan;
991 0           my $proj = $plan->project;
992 0           my $dbh = $self->dbh;
993 0           my $sth = $dbh->prepare(
994             'UPDATE changes SET script_hash = ? WHERE change_id = ? AND script_hash = ?'
995             );
996              
997 0           $self->begin_work;
998 0           $sth->execute($_->script_hash, $_->id, $_->id) for $plan->changes;
999 0           $dbh->do(q{
1000             UPDATE changes SET script_hash = NULL
1001             WHERE project = ? AND script_hash = change_id
1002             }, undef, $proj);
1003              
1004 0           $self->finish_work;
1005 0           return $self;
1006             }
1007              
1008              
1009             sub begin_work {
1010 0     0 1   my $self = shift;
1011             # Note: Engines should acquire locks to prevent concurrent Sqitch activity.
1012 0           $self->dbh->begin_work;
1013 0           return $self;
1014             }
1015              
1016             sub finish_work {
1017 0     0 1   my $self = shift;
1018 0           $self->dbh->commit;
1019 0           return $self;
1020             }
1021              
1022             sub rollback_work {
1023 0     0 1   my $self = shift;
1024 0           $self->dbh->rollback;
1025 0           return $self;
1026             }
1027              
1028             sub _eh {
1029 0     0     my ($err, $dbh) = @_;
1030 0           $@ = $err;
1031 0   0       @_ = ($dbh->state || 'DEV' => $dbh->errstr);
1032 0           goto &hurl;
1033             }
1034              
1035              
1036 0     0 1   sub error_handler { \&_eh }
1037              
1038             1;
1039              
1040             __END__
1041              
1042             =head1 Name
1043              
1044             App::Sqitch::Command::checkout - An engine based on the DBI
1045              
1046             =head1 Synopsis
1047              
1048             package App::Sqitch::Engine::sqlite;
1049             extends 'App::Sqitch::Engine';
1050             with 'App::Sqitch::Role::DBIEngine';
1051              
1052             =head1 Description
1053              
1054             This role encapsulates the common attributes and methods required by
1055             DBI-powered engines.
1056              
1057             =head1 Interface
1058              
1059             =head2 Instance Methods
1060              
1061             =head3 C<earliest_change_id>
1062              
1063             =head3 C<latest_change_id>
1064              
1065             =head3 C<current_state>
1066              
1067             =head3 C<current_changes>
1068              
1069             =head3 C<current_tags>
1070              
1071             =head3 C<search_events>
1072              
1073             =head3 C<registered_projects>
1074              
1075             =head3 C<register_project>
1076              
1077             =head3 C<is_deployed_change>
1078              
1079             =head3 C<are_deployed_changes>
1080              
1081             =head3 C<log_deploy_change>
1082              
1083             =head3 C<log_fail_change>
1084              
1085             =head3 C<changes_requiring_change>
1086              
1087             =head3 C<name_for_change_id>
1088              
1089             =head3 C<log_new_tags>
1090              
1091             =head3 C<log_revert_change>
1092              
1093             =head3 C<begin_work>
1094              
1095             =head3 C<finish_work>
1096              
1097             =head3 C<rollback_work>
1098              
1099             =head3 C<is_deployed_tag>
1100              
1101             =head3 C<deployed_changes>
1102              
1103             =head3 C<deployed_changes_since>
1104              
1105             =head3 C<load_change>
1106              
1107             =head3 C<change_offset_from_id>
1108              
1109             =head3 C<change_id_offset_from_id>
1110              
1111             =head3 C<change_id_for>
1112              
1113             =head3 C<registry_version>
1114              
1115             =head3 C<error_handler>
1116              
1117             =head1 See Also
1118              
1119             =over
1120              
1121             =item L<App::Sqitch::Engine::pg>
1122              
1123             The PostgreSQL engine.
1124              
1125             =item L<App::Sqitch::Engine::sqlite>
1126              
1127             The SQLite engine.
1128              
1129             =item L<App::Sqitch::Engine::oracle>
1130              
1131             The Oracle engine.
1132              
1133             =item L<App::Sqitch::Engine::mysql>
1134              
1135             The MySQL engine.
1136              
1137             =item L<App::Sqitch::Engine::vertica>
1138              
1139             The Vertica engine.
1140              
1141             =item L<App::Sqitch::Engine::exasol>
1142              
1143             The Exasol engine.
1144              
1145             =item L<App::Sqitch::Engine::snowflake>
1146              
1147             The Snowflake engine.
1148              
1149             =back
1150              
1151             =head1 Author
1152              
1153             David E. Wheeler <david@justatheory.com>
1154              
1155             =head1 License
1156              
1157             Copyright (c) 2012-2026 David E. Wheeler, 2012-2021 iovation Inc.
1158              
1159             Permission is hereby granted, free of charge, to any person obtaining a copy
1160             of this software and associated documentation files (the "Software"), to deal
1161             in the Software without restriction, including without limitation the rights
1162             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1163             copies of the Software, and to permit persons to whom the Software is
1164             furnished to do so, subject to the following conditions:
1165              
1166             The above copyright notice and this permission notice shall be included in all
1167             copies or substantial portions of the Software.
1168              
1169             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1170             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1171             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1172             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1173             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1174             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1175             SOFTWARE.
1176              
1177             =cut