File Coverage

blib/lib/App/Sqitch/Engine/vertica.pm
Criterion Covered Total %
statement 88 173 50.8
branch 18 40 45.0
condition 11 27 40.7
subroutine 37 59 62.7
pod 21 21 100.0
total 175 320 54.6


line stmt bran cond sub pod time code
1              
2             use 5.010;
3 3     3   18480 use Moo;
  3         11  
4 3     3   19 use utf8;
  3         9  
  3         17  
5 3     3   1302 use Path::Class;
  3         8  
  3         18  
6 3     3   101 use DBI;
  3         5  
  3         152  
7 3     3   1331 use Try::Tiny;
  3         15427  
  3         120  
8 3     3   43 use App::Sqitch::X qw(hurl);
  3         8  
  3         153  
9 3     3   17 use Locale::TextDomain qw(App-Sqitch);
  3         10  
  3         26  
10 3     3   862 use App::Sqitch::Types qw(DBH ArrayRef);
  3         6  
  3         19  
11 3     3   563  
  3         7  
  3         25  
12             extends 'App::Sqitch::Engine';
13              
14             our $VERSION = 'v1.3.1'; # VERSION
15              
16              
17 4     4 1 9498 my $self = shift;
18 3     3 1 23  
19 1     1 1 3 # Just use the target name if it doesn't look like a URI or if the URI
20 1     1 1 155 # includes the database name.
21             return $self->target->name if $self->target->name !~ /:/
22             || $self->target->uri->dbname;
23 13     13 1 447  
24             # Use the URI sans password, and with the database name added.
25             my $uri = $self->target->uri->clone;
26             $uri->password(undef) if $uri->password;
27 13 50 33     117 $uri->dbname( $ENV{VSQL_DATABASE} || $self->username );
28             return $uri->as_string;
29             }
30              
31 13         490  
32 13 50       141  
33 13   66     485 has _vsql => (
34 13         734 is => 'ro',
35             isa => ArrayRef,
36             lazy => 1,
37             default => sub {
38 8 100   8   394 my $self = shift;
39 4     4   200 my $uri = $self->uri;
40             my @ret = ( $self->client );
41             # Use _port instead of port so it's empty if no port is in the URI.
42             # https://github.com/sqitchers/sqitch/issues/675
43             for my $spec (
44             [ username => $self->username ],
45             [ dbname => $uri->dbname ],
46             [ host => $uri->host ],
47             [ port => $uri->_port ],
48             ) {
49             push @ret, "--$spec->[0]" => $spec->[1] if $spec->[1];
50             }
51              
52             if (my %vars = $self->variables) {
53             push @ret => map {; '--set', "$_=$vars{$_}" } sort keys %vars;
54             }
55              
56             push @ret => $self->_client_opts;
57             return \@ret;
58             },
59             );
60              
61              
62             has dbh => (
63             is => 'rw',
64             isa => DBH,
65             lazy => 1,
66             default => sub {
67             my $self = shift;
68             $self->use_driver;
69 27     27 1 9381  
  27         465  
70             # Set defaults in the URI.
71             my $target = $self->target;
72             my $uri = $self->uri;
73             # https://my.vertica.com/docs/5.1.6/HTML/index.htm#2736.htm
74             $uri->dbname($ENV{VSQL_DATABASE}) if !$uri->dbname && $ENV{VSQL_DATABASE};
75             $uri->host($ENV{VSQL_HOST}) if !$uri->host && $ENV{VSQL_HOST};
76             $uri->port($ENV{VSQL_PORT}) if !$uri->_port && $ENV{VSQL_PORT};
77              
78             DBI->connect($uri->dbi_dsn, $self->username, $self->password, {
79             PrintError => 0,
80             RaiseError => 0,
81             AutoCommit => 1,
82             odbc_utf8_on => 1,
83             HandleError => sub {
84             my ($err, $dbh) = @_;
85             $@ = $err;
86             @_ = ($dbh->state || 'DEV' => $dbh->errstr);
87             goto &hurl;
88             },
89             Callbacks => {
90             connected => sub {
91             my $dbh = shift;
92             try {
93             $dbh->do(
94             'SET search_path = ' . $dbh->quote($self->registry)
95             );
96             # https://www.nntp.perl.org/group/perl.dbi.dev/2013/11/msg7622.html
97             $dbh->set_err(undef, undef) if $dbh->err;
98             };
99             return;
100             },
101             },
102             });
103             }
104             );
105              
106              
107             # Need to wait until dbh is defined.
108             with 'App::Sqitch::Role::DBIEngine';
109              
110             return (
111             '--quiet',
112             '--no-vsqlrc',
113             '--no-align',
114             '--tuples-only',
115 1     1   4861 '--set' => 'ON_ERROR_STOP=1',
116             '--set' => 'registry=' . shift->registry,
117             );
118             }
119              
120             my $self = shift;
121             return $self->dbh->selectcol_arrayref(q{
122 4     4   55 SELECT EXISTS(
123             SELECT TRUE FROM v_catalog.schemata WHERE schema_name = ?
124             )
125             }, undef, $self->registry)->[0];
126             }
127              
128             my $self = shift;
129             my $schema = $self->registry;
130             hurl engine => __x(
131             'Sqitch schema "{schema}" already exists',
132 0     0 1 0 schema => $schema
133 0         0 ) if $self->initialized;
134              
135             $self->_run_registry_file( file(__FILE__)->dir->file('vertica.sql') );
136             $self->dbh->do('SET search_path = ' . $self->dbh->quote($schema));
137             $self->_register_release;
138             }
139              
140             shift->_run_registry_file(@_);
141 0     0 1 0 }
142 0         0  
143 0 0       0 my ($self, $file) = @_;
144              
145             # Check the database version.
146             my $vline = $self->dbh->selectcol_arrayref('SELECT version()')->[0];
147             my ($maj) = $vline =~ /\bv?(\d+)/;
148 0         0  
149 0         0 # Need to write a temp file; no :"registry" variable syntax.
150 0         0 my ($schema) = $self->dbh->selectrow_array(
151             'SELECT quote_ident(?)', undef, $self->registry
152             );
153             (my $sql = scalar $file->slurp) =~ s{:"registry"}{$schema}g;
154 0     0 1 0  
155             # No LONG VARCHAR before Vertica 7.
156             $sql =~ s/LONG //g if $maj < 7;
157              
158 0     0   0 # Write out the temporary file.
159             require File::Temp;
160             my $fh = File::Temp->new;
161 0         0 print $fh $sql;
162 0         0 close $fh;
163              
164             # Now we can execute the file.
165 0         0 $self->_run_with_verbosity( $fh->filename );
166             }
167              
168 0         0 return $DBI::state && $DBI::state eq '42V01'; # ERRCODE_UNDEFINED_TABLE
169             }
170              
171 0 0       0 return $DBI::state && $DBI::state eq '42703'; # ERRCODE_UNDEFINED_COLUMN
172             }
173              
174 0         0 require App::Sqitch::DateTime;
175 0         0 return App::Sqitch::DateTime->new(split /:/ => shift);
176 0         0 }
177 0         0  
178             my ($self, $count, $expr) = @_;
179             return join "\nUNION ALL ", ("SELECT $expr") x $count;
180 0         0 }
181              
182             return 'CAST(? AS CHAR(40)), CAST(? AS VARCHAR), CAST(? AS VARCHAR), CAST(? AS CHAR(40))';
183             }
184 5   100 5   40  
185             my $self = shift;
186             return join(', ',
187             'CAST(? AS CHAR(40))',
188 4   100 4   24 'CAST(? AS VARCHAR)',
189             'CAST(? AS VARCHAR)',
190             'CAST(? AS CHAR(40))',
191             'CAST(? AS VARCHAR)',
192 1     1   591 'CAST(? AS VARCHAR)',
193 1         13 'CAST(? AS VARCHAR)',
194             'CAST(? AS TIMESTAMPTZ)',
195             'CAST(? AS VARCHAR)',
196             'CAST(? AS VARCHAR)',
197 0     0   0 $self->_ts_default,
198 0         0 );
199             }
200              
201             my $self = shift;
202 0     0   0 return join(', ',
203             'CAST(? AS CHAR(40)) AS tid',
204             'CAST(? AS VARCHAR) AS tname',
205             'CAST(? AS VARCHAR) AS proj',
206 0     0   0 'CAST(? AS CHAR(40)) AS cid',
207 0         0 'CAST(? AS VARCHAR) AS note',
208             'CAST(? AS VARCHAR) AS cuser',
209             'CAST(? AS VARCHAR) AS cemail',
210             'CAST(? AS TIMESTAMPTZ) AS tts',
211             'CAST(? AS VARCHAR) AS puser',
212             'CAST(? AS VARCHAR) AS pemail',
213             $self->_ts_default,
214             );
215             }
216              
217             my ( $self, $project, $with_hash ) = @_;
218             my $cdtcol = sprintf $self->_ts2char_format, 'c.committed_at';
219             my $pdtcol = sprintf $self->_ts2char_format, 'c.planned_at';
220             my $hshcol = $with_hash ? "c.script_hash\n , " : '';
221             return $self->dbh->selectrow_hashref(qq{
222             SELECT c.change_id
223 0     0   0 , ${hshcol}c.change
224 0         0 , c.project
225             , c.note
226             , c.committer_name
227             , c.committer_email
228             , $cdtcol AS committed_at
229             , c.planner_name
230             , c.planner_email
231             , $pdtcol AS planned_at
232             FROM changes c
233             WHERE c.project = ?
234             ORDER BY c.committed_at DESC
235             LIMIT 1
236             }, undef, $project // $self->plan->project );
237             }
238              
239             my ( $self, $project ) = @_;
240 0     0   0 my $state = try {
241 0         0 $self->_select_state($project, 1)
242 0         0 } catch {
243 0 0       0 return if $self->_no_table_error && !$self->initialized;
244 0   0     0 return $self->_select_state($project, 0) if $self->_no_column_error;
245             die $_;
246             } or return undef;
247              
248             $state->{tags} = $self->dbh->selectcol_arrayref(
249             'SELECT tag FROM tags WHERE change_id = ? ORDER BY committed_at',
250             undef, $state->{change_id}
251             );
252             $state->{committed_at} = _dt $state->{committed_at};
253             $state->{planned_at} = _dt $state->{planned_at};
254             return $state;
255             }
256              
257             my ($self, $sql, @params) = @_;
258             my $sth = $self->dbh->prepare($sql);
259             $sth->execute(@params);
260              
261             my ($last_id, @changes) = ('');
262             while (my $res = $sth->fetchrow_hashref) {
263 1     1 1 466 if ($res->{id} eq $last_id) {
264             push @{ $changes[-1]->{tags} } => $res->{tag};
265 1     1   98 } else {
266             $last_id = $res->{id};
267 1 50 33 1   21 $res->{tags} = [ delete $res->{tag} || () ];
268 1 50       3 $res->{timestamp} = _dt $res->{timestamp};
269 1         7 push @changes => $res;
270 1 0       9 }
271             }
272             return @changes;
273             }
274              
275 0         0 my $self = shift;
276 0         0 my $tscol = sprintf $self->_ts2char_format, 'c.planned_at';
277 0         0 return $self->_deployed_changes(qq{
278 0         0 SELECT c.change_id AS id, c.change AS name, c.project, c.note,
279             $tscol AS "timestamp", c.planner_name, c.planner_email,
280             t.tag AS tag, c.script_hash
281             FROM changes c
282 0     0   0 LEFT JOIN tags t ON c.change_id = t.change_id
283 0         0 WHERE c.project = ?
284 0         0 ORDER BY c.committed_at ASC
285             }, $self->plan->project);
286 0         0 }
287 0         0  
288 0 0       0 my ( $self, $change ) = @_;
289 0         0 my $tscol = sprintf $self->_ts2char_format, 'c.planned_at';
  0         0  
290             $self->_deployed_changes(qq{
291 0         0 SELECT c.change_id AS id, c.change AS name, c.project, c.note,
292 0   0     0 $tscol AS "timestamp", c.planner_name, c.planner_email,
293 0         0 t.tag AS tag, c.script_hash
294 0         0 FROM changes c
295             LEFT JOIN tags t ON c.change_id = t.change_id
296             WHERE c.project = ?
297 0         0 AND c.committed_at > (SELECT committed_at FROM changes WHERE change_id = ?)
298             ORDER BY c.committed_at ASC
299             }, $self->plan->project, $change->id);
300             }
301 0     0 1 0  
302 0         0 my ( $self, $change_id ) = @_;
303 0         0 my $tscol = sprintf $self->_ts2char_format, 'c.planned_at';
304             my @res = $self->_deployed_changes(qq{
305             SELECT c.change_id AS id, c.change AS name, c.project, c.note,
306             $tscol AS "timestamp", c.planner_name, c.planner_email,
307             t.tag AS tag, c.script_hash
308             FROM changes c
309             LEFT JOIN tags t ON c.change_id = t.change_id
310             WHERE c.change_id = ?
311             }, $change_id);
312             return $res[0];
313             }
314              
315 0     0 1 0 my ( $self, $offset ) = @_;
316 0         0 my ( $dir, $op ) = $offset > 0 ? ( 'ASC', '>' ) : ( 'DESC' , '<' );
317 0         0 return $dir, $op, 'OFFSET ' . (abs($offset) - 1);
318             }
319              
320             my ( $self, $change_id, $offset ) = @_;
321              
322             # Just return the ID if there is no offset.
323             return $change_id unless $offset;
324              
325             # Are we offset forwards or backwards?
326             my ($dir, $op, $offset_expr) = $self->_offset_op($offset);
327             return $self->dbh->selectcol_arrayref(qq{
328             SELECT change_id
329             FROM changes
330 0     0 1 0 WHERE project = ?
331 0         0 AND committed_at $op (
332 0         0 SELECT committed_at FROM changes WHERE change_id = ?
333             )
334             ORDER BY committed_at $dir
335             LIMIT 1 $offset_expr
336             }, undef, $self->plan->project, $change_id)->[0];
337             }
338              
339             my ( $self, $change_id, $offset ) = @_;
340 0         0  
341             # Just return the object if there is no offset.
342             return $self->load_change($change_id) unless $offset;
343              
344 0     0   0 # Are we offset forwards or backwards?
345 0 0       0 my ($dir, $op, $offset_expr) = $self->_offset_op($offset);
346 0         0 my $tscol = sprintf $self->_ts2char_format, 'c.planned_at';
347              
348             my @res = $self->_deployed_changes(qq{
349             SELECT c.change_id AS id, c.change AS name, c.project, c.note,
350 0     0 1 0 $tscol AS "timestamp", c.planner_name, c.planner_email,
351             t.tag AS tag, c.script_hash
352             FROM changes c
353 0 0       0 LEFT JOIN tags t ON c.change_id = t.change_id
354             WHERE c.project = ?
355             AND c.committed_at $op (
356 0         0 SELECT committed_at FROM changes WHERE change_id = ?
357 0         0 )
358             ORDER BY c.committed_at $dir
359             $offset_expr
360             }, $self->plan->project, $change_id);
361             return $res[0];
362             }
363              
364             q{to_char(%s AT TIME ZONE 'UTC', '"year":YYYY:"month":MM:"day":DD:"hour":HH24:"minute":MI:"second":SS:"time_zone":"UTC"')};
365             }
366              
367              
368              
369              
370 0     0 1 0 # Override to lock the changes table. This ensures that only one instance of
371             # Sqitch runs at one time.
372             my $self = shift;
373 0 0       0 my $dbh = $self->dbh;
374              
375             # Start transaction and lock changes to allow only one change at a time.
376 0         0 $dbh->begin_work;
377 0         0 $dbh->do('LOCK TABLE changes IN EXCLUSIVE MODE');
378             return $self;
379 0         0 }
380              
381             my ($self, $file) = @_;
382             $self->_run('--file' => $file);
383             }
384              
385              
386             my $self = shift;
387             my $meth = $self->can($self->sqitch->verbosity > 1 ? '_run' : '_capture');
388             return $self->$meth('--file' => @_);
389             }
390              
391             my ($self, $fh) = @_;
392 0         0 $self->_spool($fh);
393             }
394              
395             my ( $self, $ord, $offset, $project ) = @_;
396 1     1   997  
397             my $offexpr = $offset ? " OFFSET $offset" : '';
398             return try {
399 0     0   0 return $self->dbh->selectcol_arrayref(qq{
400             SELECT change_id
401 0     0   0 FROM changes
402             WHERE project = ?
403 0     0   0 ORDER BY committed_at $ord
404             LIMIT 1$offexpr
405             }, undef, $project || $self->plan->project)->[0];
406             } catch {
407             return if $self->_no_table_error && !$self->initialized;
408 0     0 1 0 die $_;
409 0         0 };
410             }
411              
412 0         0 my ( $self, $change ) = @_;
413 0         0 # Why CTE: https://forums.oracle.com/forums/thread.jspa?threadID=1005221
414 0         0 return @{ $self->dbh->selectall_arrayref(q{
415             WITH tag AS (
416             SELECT tag, committed_at, project,
417             ROW_NUMBER() OVER (partition by project ORDER BY committed_at) AS rnk
418 1     1 1 567 FROM tags
419 1         3 )
420             SELECT c.change_id, c.project, c.change, t.tag AS asof_tag
421             FROM dependencies d
422 2     2 1 1256 JOIN changes c ON c.change_id = d.change_id
423             LEFT JOIN tag t ON t.project = c.project AND t.committed_at >= c.committed_at
424             WHERE d.dependency_id = ?
425 2     2   3 AND (t.rnk IS NULL OR t.rnk = 1)
426 2 100       28 }, { Slice => {} }, $change->id) };
427 2         134 }
428              
429             my ( $self, $change_id ) = @_;
430             # Why CTE: https://forums.oracle.com/forums/thread.jspa?threadID=1005221
431 1     1 1 570 return $self->dbh->selectcol_arrayref(q{
432 1         4 WITH tag AS (
433             SELECT tag, committed_at, project,
434             ROW_NUMBER() OVER (partition by project ORDER BY committed_at) AS rnk
435             FROM tags
436 1     1   505 )
437             SELECT change || COALESCE(t.tag, '@HEAD')
438 1 50       4 FROM changes c
439             LEFT JOIN tag t ON c.project = t.project AND t.committed_at >= c.committed_at
440 1   0 1   84 WHERE change_id = ?
441             AND (t.rnk IS NULL OR t.rnk = 1)
442             }, undef, $change_id)->[0];
443             }
444              
445             my $self = shift;
446             my $sqitch = $self->sqitch;
447             my $pass = $self->password or return $sqitch->run( $self->vsql, @_ );
448 1 50 33 1   17 local $ENV{VSQL_PASSWORD} = $pass;
449 1         7 return $sqitch->run( $self->vsql, @_ );
450 1         7 }
451              
452             my $self = shift;
453             my $sqitch = $self->sqitch;
454 0     0 1 0 my $pass = $self->password or return $sqitch->capture( $self->vsql, @_ );
455             local $ENV{VSQL_PASSWORD} = $pass;
456 0         0 return $sqitch->capture( $self->vsql, @_ );
  0         0  
457             }
458              
459             my $self = shift;
460             my $sqitch = $self->sqitch;
461             my $pass = $self->password or return $sqitch->probe( $self->vsql, @_ );
462             local $ENV{VSQL_PASSWORD} = $pass;
463             return $sqitch->probe( $self->vsql, @_ );
464             }
465              
466             my $self = shift;
467             my $fh = shift;
468             my $sqitch = $self->sqitch;
469             my $pass = $self->password or return $sqitch->spool( $fh, $self->vsql, @_ );
470             local $ENV{VSQL_PASSWORD} = $pass;
471             return $sqitch->spool( $fh, $self->vsql, @_ );
472 0     0 1 0 }
473              
474 0         0 1;
475              
476              
477             =head1 Name
478              
479             App::Sqitch::Engine::vertica - Sqitch Vertica Engine
480              
481             =head1 Synopsis
482              
483             my $vertica = App::Sqitch::Engine->load( engine => 'vertica' );
484              
485             =head1 Description
486              
487             App::Sqitch::Engine::vertica provides the Vertica storage engine for Sqitch.
488             It supports Vertica 6.
489 4     4   1776  
490 4         10 =head1 Interface
491 4 100       79  
492 1         107 =head2 Instance Methods
493 1         4  
494             =head3 C<initialized>
495              
496             $vertica->initialize unless $vertica->initialized;
497 3     3   1155  
498 3         10 Returns true if the database has been initialized for Sqitch, and false if it
499 3 100       61 has not.
500 1         13  
501 1         4 =head3 C<initialize>
502              
503             $vertica->initialize;
504              
505 2     2   1206 Initializes a database for Sqitch by installing the Sqitch registry schema.
506 2         7  
507 2 100       41 =head3 C<vsql>
508 1         13  
509 1         4 Returns a list containing the C<vsql> client and options to be passed to it.
510             Used internally when executing scripts.
511              
512             =head1 Author
513 3     3   1179  
514 3         5 David E. Wheeler <david@justatheory.com>
515 3         9  
516 3 100       59 =head1 License
517 1         14  
518 1         4 Copyright (c) 2012-2022 iovation Inc., David E. Wheeler
519              
520             Permission is hereby granted, free of charge, to any person obtaining a copy
521             of this software and associated documentation files (the "Software"), to deal
522             in the Software without restriction, including without limitation the rights
523             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
524             copies of the Software, and to permit persons to whom the Software is
525             furnished to do so, subject to the following conditions:
526              
527             The above copyright notice and this permission notice shall be included in all
528             copies or substantial portions of the Software.
529              
530             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
531             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
532             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
533             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
534             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
535             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
536             SOFTWARE.
537              
538             =cut