File Coverage

blib/lib/App/Sqitch/Engine/sqlite.pm
Criterion Covered Total %
statement 59 88 67.0
branch 2 6 33.3
condition 6 9 66.6
subroutine 26 39 66.6
pod 10 10 100.0
total 103 152 67.7


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