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