File Coverage

blib/lib/App/Sqitch/Engine/sqlite.pm
Criterion Covered Total %
statement 58 87 66.6
branch 2 6 33.3
condition 6 9 66.6
subroutine 25 38 65.7
pod 10 10 100.0
total 101 150 67.3


line stmt bran cond sub pod time code
1             package App::Sqitch::Engine::sqlite;
2              
3 15     15   45547 use 5.010;
  15         69  
4 15     15   110 use strict;
  15         46  
  15         440  
5 15     15   108 use warnings;
  15         42  
  15         545  
6 15     15   102 use utf8;
  15         37  
  15         176  
7 15     15   512 use Try::Tiny;
  15         57  
  15         1367  
8 15     15   123 use App::Sqitch::X qw(hurl);
  15         59  
  15         200  
9 15     15   5854 use Locale::TextDomain qw(App-Sqitch);
  15         52  
  15         410  
10 15     15   4072 use App::Sqitch::Plan::Change;
  15         46  
  15         669  
11 15     15   127 use Path::Class;
  15         48  
  15         906  
12 15     15   134 use Moo;
  15         42  
  15         179  
13 15     15   7633 use App::Sqitch::Types qw(URIDB DBH ArrayRef);
  15         40  
  15         284  
14 15     15   17853 use namespace::autoclean;
  15         56  
  15         164  
15              
16             extends 'App::Sqitch::Engine';
17              
18             our $VERSION = 'v1.4.0'; # VERSION
19              
20             has registry_uri => (
21             is => 'ro',
22             isa => URIDB,
23             lazy => 1,
24             default => sub {
25             my $self = shift;
26             my $uri = $self->uri->clone;
27             my $reg = $self->registry;
28              
29             if ( file($reg)->is_absolute ) {
30             # Just use an absolute path.
31             $uri->dbname($reg);
32             } elsif (my @segs = $uri->path_segments) {
33             # Use the same name, but replace $name.$ext with $reg.$ext.
34             my $bn = file( $segs[-1] )->basename;
35             if ($reg =~ /[.]/ || $bn !~ /[.]/) {
36             $segs[-1] =~ s/\Q$bn\E$/$reg/;
37             } else {
38             my ($b, $e) = split /[.]/, $bn, 2;
39             $segs[-1] =~ s/\Q$b\E[.]$e$/$reg.$e/;
40             }
41             $uri->path_segments(@segs);
42             } else {
43             # No known path, so no name.
44             $uri->dbname(undef);
45             }
46              
47             return $uri;
48             },
49             );
50              
51             sub registry_destination {
52 12     12 1 6529 my $uri = shift->registry_uri;
53 12 100       341 if ($uri->password) {
54 1         55 $uri = $uri->clone;
55 1         17 $uri->password(undef);
56             }
57 12         539 return $uri->as_string;
58             }
59              
60 6     6 1 7483 sub key { 'sqlite' }
61 6     6 1 47 sub name { 'SQLite' }
62 4     4 1 2985 sub driver { 'DBD::SQLite 1.37' }
63 5     5 1 366 sub default_client { 'sqlite3' }
64              
65             has dbh => (
66             is => 'rw',
67             isa => DBH,
68             lazy => 1,
69             default => sub {
70             my $self = shift;
71             $self->use_driver;
72              
73             my $uri = $self->registry_uri;
74             my $dbh = DBI->connect($uri->dbi_dsn, '', '', {
75             PrintError => 0,
76             RaiseError => 0,
77             AutoCommit => 1,
78             sqlite_unicode => 1,
79             sqlite_use_immediate_transaction => 1,
80             HandleError => sub {
81             my ($err, $dbh) = @_;
82             $@ = $err;
83             @_ = ($dbh->state || 'DEV' => $dbh->errstr);
84             goto &hurl;
85             },
86             Callbacks => {
87             connected => sub {
88             my $dbh = shift;
89             $dbh->do('PRAGMA foreign_keys = ON');
90             return;
91             },
92             },
93             });
94              
95             # Make sure we support this version.
96             my @v = split /[.]/ => $dbh->{sqlite_version};
97             hurl sqlite => __x(
98             'Sqitch requires SQLite 3.8.6 or later; DBD::SQLite was built with {version}',
99             version => $dbh->{sqlite_version}
100             ) unless $v[0] > 3 || ($v[0] == 3 && ($v[1] > 8 || ($v[1] == 8 && $v[2] >= 6)));
101              
102             return $dbh;
103             }
104             );
105              
106             # Need to wait until dbh is defined.
107             with 'App::Sqitch::Role::DBIEngine';
108              
109             has _sqlite3 => (
110             is => 'ro',
111             isa => ArrayRef,
112             lazy => 1,
113             default => sub {
114             my $self = shift;
115              
116             # Make sure we can use this version of SQLite.
117             my @v = split /[.]/ => (
118             split / / => scalar $self->sqitch->capture( $self->client, '-version' )
119             )[0];
120             hurl sqlite => __x(
121             'Sqitch requires SQLite 3.3.9 or later; {client} is {version}',
122             client => $self->client,
123             version => join( '.', @v)
124             ) unless $v[0] > 3 || ($v[0] == 3 && ($v[1] > 3 || ($v[1] == 3 && $v[2] >= 9)));
125              
126             my $dbname = $self->uri->dbname or hurl sqlite => __x(
127             'Database name missing in URI {uri}',
128             uri => $self->uri,
129             );
130              
131             return [
132             $self->client,
133             '-noheader',
134             '-bail',
135             '-batch',
136             '-csv', # or -column or -line?
137             $dbname,
138             ];
139             },
140             );
141              
142 27     27 1 10827 sub sqlite3 { @{ shift->_sqlite3 } }
  27         674  
143              
144 0     0   0 sub _version_query { 'SELECT CAST(ROUND(MAX(version), 1) AS TEXT) FROM releases' }
145              
146             sub _initialized {
147 0     0   0 my $self = shift;
148 0         0 return $self->dbh->selectcol_arrayref(q{
149             SELECT EXISTS(
150             SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?
151             )
152             }, undef, 'changes')->[0];
153             }
154              
155             sub _initialize {
156 0     0   0 my $self = shift;
157 0 0       0 hurl engine => __x(
158             'Sqitch database {database} already initialized',
159             database => $self->registry_uri->dbname,
160             ) if $self->initialized;
161              
162             # Load up our database.
163 0         0 my @cmd = $self->sqlite3;
164 0         0 $cmd[-1] = $self->registry_uri->dbname;
165 0         0 my $file = file(__FILE__)->dir->file('sqlite.sql');
166 0         0 $self->sqitch->run( @cmd, $self->_read($file) );
167 0         0 $self->_register_release;
168             }
169              
170             sub _no_table_error {
171 3   100 3   489 return $DBI::errstr && $DBI::errstr =~ /^\Qno such table:/;
172             }
173              
174             sub _no_column_error {
175 3   100 3   28 return $DBI::errstr && $DBI::errstr =~ /^\Qno such column:/;
176             }
177              
178             sub _unique_error {
179 0   0 0   0 return $DBI::errstr && $DBI::errstr =~ /^\QUNIQUE constraint failed:/;
180             }
181              
182 0     0   0 sub _regex_op { 'REGEXP' }
183              
184 0     0   0 sub _limit_default { -1 }
185              
186             sub _ts_default {
187 0     0   0 q{strftime('%Y-%m-%d %H:%M:%f')};
188             }
189              
190             sub _ts2char_format {
191 1     1   2160 return q{strftime('year:%%Y:month:%%m:day:%%d:hour:%%H:minute:%%M:second:%%S:time_zone:UTC', %s)};
192             }
193              
194             sub _listagg_format {
195             # The order of the concatenated elements is arbitrary.
196             # https://www.sqlite.org/lang_aggfunc.html
197 0     0   0 return q{group_concat(%s, ' ')};
198             }
199              
200             sub _char2ts {
201 0     0   0 my $dt = $_[1];
202 0         0 $dt->set_time_zone('UTC');
203 0         0 return join ' ', $dt->ymd('-'), $dt->hms(':');
204             }
205              
206             sub _run {
207 1     1   3610 my $self = shift;
208 1         10 return $self->sqitch->run( $self->sqlite3, @_ );
209             }
210              
211             sub _capture {
212 1     1   678 my $self = shift;
213 1         6 return $self->sqitch->capture( $self->sqlite3, @_ );
214             }
215              
216             sub _spool {
217 2     2   744 my $self = shift;
218 2         5 my $fh = shift;
219 2         12 return $self->sqitch->spool( $fh, $self->sqlite3, @_ );
220             }
221              
222             sub run_file {
223 0     0 1 0 my ($self, $file) = @_;
224 0         0 $self->_run( $self->_read($file) );
225             }
226              
227             sub run_verify {
228 0     0 1 0 my ($self, $file) = @_;
229             # Suppress STDOUT unless we want extra verbosity.
230 0 0       0 my $meth = $self->can($self->sqitch->verbosity > 1 ? '_run' : '_capture');
231 0         0 $self->$meth( $self->_read($file) );
232             }
233              
234             sub run_handle {
235 1     1 1 1818 my ($self, $fh) = @_;
236 1         6 $self->_spool($fh);
237             }
238              
239             sub run_upgrade {
240 0     0 1   my ($self, $file) = @_;
241 0           my @cmd = $self->sqlite3;
242 0           $cmd[-1] = $self->registry_uri->dbname;
243 0           return $self->sqitch->run( @cmd, $self->_read($file) );
244             }
245              
246             sub _read {
247 0     0     my $self = shift;
248 0           return '.read ' . $self->dbh->quote(shift);
249             }
250              
251             1;
252              
253             __END__
254              
255             =head1 Name
256              
257             App::Sqitch::Engine::sqlite - Sqitch SQLite Engine
258              
259             =head1 Synopsis
260              
261             my $sqlite = App::Sqitch::Engine->load( engine => 'sqlite' );
262              
263             =head1 Description
264              
265             App::Sqitch::Engine::sqlite provides the SQLite storage engine for Sqitch.
266              
267             =head1 Interface
268              
269             =head2 Accessors
270              
271             =head3 C<client>
272              
273             Returns the path to the SQLite client. If C<--client> was passed to C<sqitch>,
274             that's what will be returned. Otherwise, it uses the C<engine.sqlite.client>
275             configuration value, or else defaults to C<sqlite3> (or C<sqlite3.exe> on
276             Windows), which should work if it's in your path.
277              
278             =head2 Instance Methods
279              
280             =head3 C<sqlite3>
281              
282             Returns a list containing the C<sqlite3> client and options to be passed to it.
283             Used internally when executing scripts.
284              
285             =head1 Author
286              
287             David E. Wheeler <david@justatheory.com>
288              
289             =head1 License
290              
291             Copyright (c) 2012-2023 iovation Inc., David E. Wheeler
292              
293             Permission is hereby granted, free of charge, to any person obtaining a copy
294             of this software and associated documentation files (the "Software"), to deal
295             in the Software without restriction, including without limitation the rights
296             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
297             copies of the Software, and to permit persons to whom the Software is
298             furnished to do so, subject to the following conditions:
299              
300             The above copyright notice and this permission notice shall be included in all
301             copies or substantial portions of the Software.
302              
303             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
304             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
305             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
306             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
307             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
308             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
309             SOFTWARE.
310              
311             =cut