File Coverage

blib/lib/App/Sqitch/Role/DBIEngine.pm
Criterion Covered Total %
statement 72 330 21.8
branch 18 110 16.3
condition 8 54 14.8
subroutine 26 71 36.6
pod 27 27 100.0
total 151 592 25.5


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