File Coverage

blib/lib/DBIx/TempDB.pm
Criterion Covered Total %
statement 205 259 79.1
branch 63 110 57.2
condition 53 103 51.4
subroutine 34 39 87.1
pod 6 6 100.0
total 361 517 69.8


line stmt bran cond sub pod time code
1             package DBIx::TempDB;
2 14     14   191035 use strict;
  14         22  
  14         370  
3 14     14   46 use warnings;
  14         14  
  14         318  
4 14     14   44 use Carp 'confess';
  14         16  
  14         772  
5 14     14   48 use Cwd ();
  14         18  
  14         164  
6 14     14   19574 use DBI;
  14         180581  
  14         742  
7 14     14   95 use File::Basename ();
  14         11  
  14         172  
8 14     14   42 use File::Spec;
  14         14  
  14         185  
9 14     14   7527 use IO::Handle ();
  14         64074  
  14         257  
10 14     14   5832 use Sys::Hostname ();
  14         10330  
  14         237  
11 14     14   6126 use URI::db;
  14         202622  
  14         335  
12 14     14   5174 use URI::QueryParam;
  14         7676  
  14         626  
13              
14 14     14   61 use constant CWD => eval { File::Basename::dirname(Cwd::abs_path($0)) };
  14         20  
  14         15  
  14         2269  
15 14   50 14   59 use constant DEBUG => $ENV{DBIX_TEMP_DB_DEBUG} || 0;
  14         15  
  14         696  
16 14   50 14   50 use constant KILL_SLEEP_INTERVAL => $ENV{DBIX_TEMP_DB_KILL_SLEEP_INTERVAL} || 2;
  14         20  
  14         664  
17 14   50 14   48 use constant MAX_NUMBER_OF_TRIES => $ENV{DBIX_TEMP_DB_MAX_NUMBER_OF_TRIES} || 20;
  14         15  
  14         660  
18 14   50 14   5995 use constant MAX_OPEN_FDS => eval { use POSIX qw(sysconf _SC_OPEN_MAX); sysconf(_SC_OPEN_MAX) } || 1024;
  14     14   61459  
  14         58  
  14         12331  
  14         19  
  14         19  
19              
20             our $VERSION = '0.14';
21             our %SCHEMA_DATABASE = (pg => 'postgres', mysql => 'mysql');
22             my $N = 0;
23              
24             sub create_database {
25 2 50   2 1 6 return $_[0] if $_[0]->{created};
26 2         3 my $self = shift;
27 2         2 my ($guard, $name);
28              
29 2         2 local $@;
30 2         7 while (++$guard < MAX_NUMBER_OF_TRIES) {
31 2         8 $name = $self->_generate_database_name($N + $guard);
32 2 50       15 eval { $self->_create_database($name) } or next;
  2         7  
33 2         142 $self->{database_name} = $name;
34 2         3 warn "[TempDB:$$] Created temp database $name\n" if DEBUG and !$ENV{DBIX_TEMP_DB_KEEP_DATABASE};
35             warn sprintf "[DBIx::TempDB] Created permanent database %s\n", +($self->dsn)[0]
36 2 50 33     11 if $ENV{DBIX_TEMP_DB_KEEP_DATABASE} and !$ENV{DBIX_TEMP_DB_SILENT};
37 2 50       32 $self->_drop_from_child if $self->{drop_from_child} == 1;
38 2 50       5 $self->_drop_from_double_forked_child if $self->{drop_from_child} == 2;
39 2         4 $self->{created}++;
40 2         15 $self->{url}->dbname($name);
41 2         332 $ENV{DBIX_TEMP_DB_URL} = $self->{url}->uri->as_string;
42 2         36 $N++;
43 2         6 return $self;
44             }
45              
46 0         0 confess "Could not create unique database: '$name'. $@";
47             }
48              
49             sub dsn {
50 7     7 1 1989 my ($self, $url) = @_;
51              
52 7 100 66     36 if (!ref $self and $url) {
53 1 50 33     6 $url = URI::db->new($url) unless ref $url and $url->isa('URI::_db');
54 1 50       60 unless ($url->has_recognized_engine) {
55 0         0 confess "Scheme @{[$url->engine]} is not recognized as a database engine for connection url $url";
  0         0  
56             }
57 1         12 $self->can(sprintf '_dsn_for_%s', $url->canonical_engine)->($self, $url, $url->dbname);
58             }
59             else {
60 6 50       13 confess 'Cannot return DSN before create_database() is called.' unless $self->{database_name};
61 6         12 $self->can(sprintf '_dsn_for_%s', $self->url->canonical_engine)->($self, $self->url, $self->{database_name});
62             }
63             }
64              
65             sub execute {
66 1     1 1 1 my $self = shift;
67 1         4 my $dbh = DBI->connect($self->dsn);
68 1   50 0   6 my $parser = $self->can("_parse_@{[$self->url->canonical_engine]}") || sub { $_[1] };
  0         0  
69 1 50       15 local $dbh->{sqlite_allow_multiple_statements} = 1 if $self->url->canonical_engine eq 'sqlite';
70 1         9 $dbh->do($_) for map { $self->$parser($_) } @_;
  1         2  
71 1         17 $self;
72             }
73              
74             sub execute_file {
75 1     1 1 11 my ($self, $path) = @_;
76              
77 1 50       10 unless (File::Spec->file_name_is_absolute($path)) {
78 1         1 confess "Cannot resolve absolute path to '$path'. Something went wrong with Cwd::abs_path($0)." unless CWD;
79 1         20 $path = File::Spec->catfile(CWD, split '/', $path);
80             }
81              
82 1 50       38 open my $SQL, '<', $path or die "DBIx::TempDB can't open $path: $!";
83 1         4 my $ret = my $sql = '';
84 1         13 while ($ret = $SQL->sysread(my $buffer, 131072, 0)) { $sql .= $buffer }
  1         37  
85 1 50       11 die qq{DBIx::TempDB can't read from file "$path": $!} unless defined $ret;
86 1         1 warn "[TempDB:$$] Execute $path\n" if DEBUG;
87 1         4 $self->execute($sql);
88             }
89              
90             sub new {
91 9     9 1 862 my $class = shift;
92 9   50     104 my $url = URI::db->new(shift || '');
93 9 50       7339 unless ($url->has_recognized_engine) {
94 0         0 confess "Scheme @{[$url->engine]} is not recognized as a database engine for connection url $url";
  0         0  
95             }
96 9         354 my $self = bless {@_, url => $url}, $class;
97 9   50     43 my $dsn_for = sprintf '_dsn_for_%s', $url->canonical_engine || '';
98              
99 9 50       133 unless ($self->can($dsn_for)) {
100 0         0 confess "Cannot generate temp database for '@{[$url->canonical_engine]}'. $class\::$dsn_for() is missing";
  0         0  
101             }
102              
103 9   100     53 $self->{create_database_command} ||= 'create database %d';
104 9   100     33 $self->{drop_database_command} ||= 'drop database %d';
105 9   100     35 $self->{drop_from_child} //= 1;
106 9   66     55 $self->{schema_database} ||= $SCHEMA_DATABASE{$url->canonical_engine};
107 9   100     93 $self->{template} ||= 'tmp_%U_%X_%H%i';
108 9         8 warn "[TempDB:$$] schema_database=$self->{schema_database}\n" if DEBUG;
109              
110 9 50       29 $self->{drop_from_child} = 0 if $ENV{DBIX_TEMP_DB_KEEP_DATABASE};
111              
112 9 100 100     38 return $self->create_database if $self->{auto_create} // 1;
113 7         18 return $self;
114             }
115              
116 25     25 1 153 sub url { shift->{url}->uri }
117              
118             sub DESTROY {
119 9     9   2187 my $self = shift;
120 9 50       33 return close $self->{DROP_PIPE} if $self->{DROP_PIPE};
121 9 50       40 return if $ENV{DBIX_TEMP_DB_KEEP_DATABASE};
122 9 50       23 return if $self->{double_forked};
123 9 100       161 return $self->_cleanup if $self->{created};
124             }
125              
126             sub _cleanup {
127 2     2   3 my $self = shift;
128              
129             eval {
130 2 100       8 if (ref $self->{drop_database_command} eq 'CODE') {
    50          
131 1         3 $self->{drop_database_command}->($self, $self->{database_name});
132             }
133             elsif ($self->url->canonical_engine eq 'sqlite') {
134 1 50       80 unlink $self->{database_name} or die $!;
135             }
136             else {
137 0         0 my $sql = $self->{drop_database_command};
138 0         0 $sql =~ s!\%d!$self->{database_name}!g;
139 0         0 DBI->connect($self->_schema_dsn)->do($sql);
140             }
141 2         29 1;
142 2 50       3 } or do {
143 0         0 die "[$$] Unable to drop $self->{database_name}: $@";
144             };
145             }
146              
147             sub _create_database {
148 2     2   3 my ($self, $name) = @_;
149              
150 2 100       14 if (ref $self->{create_database_command} eq 'CODE') {
    50          
151 1         3 $self->{create_database_command}->($self, $name);
152             }
153             elsif ($self->url->canonical_engine eq 'sqlite') {
154 1         406 require IO::File;
155 14     14   68 use Fcntl qw(O_CREAT O_EXCL O_RDWR);
  14         17  
  14         19558  
156 1 50       1469 IO::File->new->open($name, O_CREAT | O_EXCL | O_RDWR) or die "open $name O_CREAT|O_EXCL|O_RDWR: $!\n";
157             }
158             else {
159 0         0 my $sql = $self->{create_database_command};
160 0         0 $sql =~ s!\%d!$name!g;
161 0         0 DBI->connect($self->_schema_dsn)->do($sql);
162             }
163             }
164              
165             sub _dsn_for_pg {
166 2     2   18 my ($class, $url, $database_name) = @_;
167 2         2 my %opt = %{$url->query_form_hash};
  2         9  
168 2         72 my ($dsn, @userinfo);
169              
170 2         5 $url = URI::db->new($url);
171 2         114 $url->dbname($database_name);
172 2         125 $url->query(undef);
173 2 50       32 if (my $service = delete $opt{service}) { $url->query_param(service => $service) }
  0         0  
174 2         8 $dsn = $url->dbi_dsn;
175 2         257 @userinfo = ($url->user, $url->password);
176              
177 2   100     94 $opt{AutoCommit} //= 1;
178 2   50     7 $opt{AutoInactiveDestroy} //= 1;
179 2   50     5 $opt{PrintError} //= 0;
180 2   50     10 $opt{RaiseError} //= 1;
181              
182 2         27 return $dsn, @userinfo[0, 1], \%opt;
183             }
184              
185             sub _dsn_for_mysql {
186 4     4   76 my ($class, $url, $database_name) = @_;
187 4         7 my %opt = %{$url->query_form_hash};
  4         23  
188 4         178 my ($dsn, @userinfo);
189              
190 4         10 $url = URI::db->new($url);
191 4         288 $url->dbname($database_name);
192 4         251 $url->query(undef);
193 4         56 $dsn = $url->dbi_dsn;
194 4         540 @userinfo = ($url->user, $url->password);
195              
196 4   100     197 $opt{AutoCommit} //= 1;
197 4   50     20 $opt{AutoInactiveDestroy} //= 1;
198 4   50     11 $opt{PrintError} //= 0;
199 4   50     10 $opt{RaiseError} //= 1;
200 4   50     11 $opt{mysql_enable_utf8} //= 1;
201              
202 4         58 return $dsn, @userinfo[0, 1], \%opt;
203             }
204              
205             sub _dsn_for_sqlite {
206 1     1   11 my ($class, $url, $database_name) = @_;
207 1         1 my %opt = %{$url->query_form_hash};
  1         8  
208              
209 1         37 $url = URI::db->new($url);
210 1         50 $url->dbname($database_name);
211 1         33 $url->query(undef);
212 1         16 my $dsn = $url->dbi_dsn;
213              
214 1   50     94 $opt{AutoCommit} //= 1;
215 1   50     4 $opt{AutoInactiveDestroy} //= 1;
216 1   50     4 $opt{PrintError} //= 0;
217 1   50     4 $opt{RaiseError} //= 1;
218 1   50     3 $opt{sqlite_unicode} //= 1;
219              
220 1         26 return $dsn, "", "", \%opt;
221             }
222              
223             sub _generate_database_name {
224 6     6   975 my ($self, $n) = @_;
225 6         8 my $name = $self->{template};
226              
227 6         29 $name =~ s/\%([iHPTUX])/{
228 21 100       18 $1 eq 'i' ? ($n > 0 ? "_$n" : '')
  21 50       201  
    100          
    100          
    100          
    100          
    100          
229             : $1 eq 'H' ? $self->_hostname
230             : $1 eq 'P' ? $$
231             : $1 eq 'T' ? $^T
232             : $1 eq 'U' ? $<
233             : $1 eq 'X' ? File::Basename::basename($0)
234             : "\%$1"
235             }/egx;
236              
237 6 50 33     24 if (63 < length $name and !$self->{keep_too_long_database_name}) {
238             $self->{template} =~ s!\%T!!g
239             or $self->{template} =~ s!\%H!!g
240 0 0 0     0 or $self->{template} =~ s!\%X!!g
      0        
241             or confess "Uable to create shorter database anme.";
242             warn "!!! Database name '$name' is too long! Forcing a shorter template: $self->{template}"
243 0 0 0     0 if !$ENV{HARNESS_ACTIVE} or $ENV{HARNESS_VERBOSE};
244 0         0 return $self->_generate_database_name($n);
245             }
246              
247 6         8 $name =~ s!^/+!!;
248 6         19 $name =~ s!\W!_!g;
249              
250 6 100       14 return $name if $self->url->canonical_engine ne 'sqlite';
251 1         15 return File::Spec->catfile($self->_tempdir, "$name.sqlite");
252             }
253              
254             sub _hostname {
255 4   66 4   15 shift->{hostname} ||= Sys::Hostname::hostname();
256             }
257              
258             sub _drop_from_child {
259 0     0   0 my $self = shift;
260 0         0 my $ppid = $$;
261              
262 0 0       0 pipe my $READ, $self->{DROP_PIPE} or confess "Could not create pipe: $!";
263 0 0       0 defined(my $pid = fork) or confess "Couldn't fork: $!";
264              
265             # parent
266 0 0       0 return $self->{drop_pid} = $pid if $pid;
267              
268             # child
269 0         0 $DB::CreateTTY = 0; # prevent debugger from creating terminals
270 0     0   0 $SIG{$_} = sub { $self->_cleanup; exit; }
  0         0  
271 0         0 for qw(INT QUIT TERM);
272              
273 0         0 for (0 .. MAX_OPEN_FDS - 1) {
274 0 0       0 next if fileno($READ) == $_;
275 0         0 next if DEBUG and fileno(STDERR) == $_;
276 0         0 POSIX::close($_);
277             }
278              
279 0         0 warn "[TempDB:$$] Waiting for $ppid to end\n" if DEBUG;
280 0         0 1 while <$READ>;
281 0         0 $self->_cleanup;
282 0         0 exit 0;
283             }
284              
285             sub _drop_from_double_forked_child {
286 0     0   0 my $self = shift;
287 0         0 my $ppid = $$;
288              
289 0         0 local $SIG{CHLD} = 'DEFAULT';
290              
291 0 0       0 defined(my $pid = fork) or confess "Couldn't fork: $!";
292              
293 0 0       0 if ($pid) {
294              
295             # Wait around until the second fork is done so that when we return from
296             # here there are no new child processes that could mess things up if the
297             # calling process does any process handling.
298 0         0 waitpid $pid, 0;
299 0         0 return $self->{double_forked} = $pid; # could just be a boolean
300             }
301              
302             # Stop the debugger from creating new terminals
303 0         0 $DB::CreateTTY = 0;
304              
305 0         0 $0 = "drop_$self->{database_name}";
306              
307             # Detach completely from parent by creating our own session and process
308             # group, closing all filehandles and forking a second time.
309 0 0       0 POSIX::setsid() != -1 or confess "Couldn't become session leader: $!\n";
310 0         0 POSIX::close($_) for 0 .. MAX_OPEN_FDS - 1;
311 0 0 0     0 POSIX::_exit(0) if fork // confess "Couldn't fork: $!";
312 0         0 sleep KILL_SLEEP_INTERVAL while kill 0, $ppid;
313 0         0 $self->_cleanup;
314 0         0 exit 0;
315             }
316              
317             sub _parse_mysql {
318 2     2   11 my ($self, $sql) = @_;
319 2         4 my ($new, $last, $delimiter) = (0, '', ';');
320 2         1 my @commands;
321              
322 2         5 while (length($sql) > 0) {
323 77         46 my $token;
324              
325 77 100 100     404 if ($sql =~ /^$delimiter/x) {
    100 66        
    100 66        
    100 33        
      33        
      33        
326 5         5 ($new, $token) = (1, $delimiter);
327             }
328             elsif ($sql =~ /^delimiter\s+(\S+)\s*(?:\n|\z)/ip) {
329 1         2 ($new, $token, $delimiter) = (1, ${^MATCH}, $1);
330             }
331             elsif (
332             $sql =~ /^(\s+)/s # whitespace
333             or $sql =~ /^(\w+)/
334             )
335             { # general name
336 55         55 $token = $1;
337             }
338             elsif (
339             $sql =~ /^--.*(?:\n|\z)/p # double-dash comment
340             or $sql =~ /^\#.*(?:\n|\z)/p # hash comment
341             or $sql =~ /^\/\*(?:[^\*]|\*[^\/])*(?:\*\/|\*\z|\z)/p # C-style comment
342             or $sql =~ /^'(?:[^'\\]*|\\(?:.|\n)|'')*(?:'|\z)/p # single-quoted literal text
343             or $sql =~ /^"(?:[^"\\]*|\\(?:.|\n)|"")*(?:"|\z)/p # double-quoted literal text
344             or $sql =~ /^`(?:[^`]*|``)*(?:`|\z)/p
345             )
346             { # schema-quoted literal text
347 5         5 $token = ${^MATCH};
348             }
349             else {
350 11         10 $token = substr($sql, 0, 1);
351             }
352              
353             # chew token
354 77         63 substr $sql, 0, length($token), '';
355              
356 77 100       63 if ($new) {
357 6 50       15 push @commands, $last if $last !~ /^\s*$/s;
358 6         10 ($new, $last) = (0, '');
359             }
360             else {
361 71         104 $last .= $token;
362             }
363             }
364              
365 2 100       7 push @commands, $last if $last !~ /^\s*$/s;
366 2         3 return map { s/^\s+//; $_ } @commands;
  7         10  
  7         19  
367             }
368              
369             sub _schema_dsn {
370 0     0   0 my $self = shift;
371 0         0 local $self->{database_name} = $self->{schema_database};
372 0         0 return $self->dsn;
373             }
374              
375             sub _tempdir {
376 1   33 1   23 shift->{tempdir} ||= File::Spec->tmpdir;
377             }
378              
379             1;
380              
381             =encoding utf8
382              
383             =head1 NAME
384              
385             DBIx::TempDB - Create a temporary database
386              
387             =head1 VERSION
388              
389             0.14
390              
391             =head1 SYNOPSIS
392              
393             use Test::More;
394             use DBIx::TempDB;
395             use DBI;
396              
397             # provide credentials with environment variables
398             plan skip_all => 'TEST_PG_DSN=postgresql://postgres@localhost' unless $ENV{TEST_PG_DSN};
399              
400             # create a temp database
401             my $tmpdb = DBIx::TempDB->new($ENV{TEST_PG_DSN});
402              
403             # print complete url to db server with database name
404             diag $tmpdb->url;
405              
406             # useful for reading in fixtures
407             $tmpdb->execute("create table users (name text)");
408             $tmpdb->execute_file("path/to/file.sql");
409              
410             # connect to the temp database
411             my $db = DBI->connect($tmpdb->dsn);
412              
413             # run tests...
414              
415             done_testing;
416             # database is cleaned up when test exit
417              
418             =head1 DESCRIPTION
419              
420             L is a module which allows you to create a temporary database,
421             which only lives as long as your process is alive. This can be very
422             convenient when you want to run tests in parallel, without messing up the
423             state between tests.
424              
425             This module currently support PostgreSQL, MySQL and SQLite by installing the optional
426             L, L and/or L modules.
427              
428             Please create an L
429             or pull request for more backend support.
430              
431             =head1 CAVEAT
432              
433             Creating a database is easy, but making sure it gets clean up when your
434             process exit is a totally different ball game. This means that
435             L might fill up your server with random databases, unless
436             you choose the right "drop strategy". Have a look at the L
437             parameter you can give to L and test the different values and select
438             the one that works for you.
439              
440             =head1 ENVIRONMENT VARIABLES
441              
442             =head2 DBIX_TEMP_DB_KEEP_DATABASE
443              
444             Setting this variable will disable the core feature in this module:
445             A unique database will be created, but it will not get dropped/deleted.
446              
447             =head2 DBIX_TEMP_DB_URL
448              
449             This variable is set by L and contains the complete
450             URL pointing to the temporary database.
451              
452             Note that calling L on different instances of
453             L will overwrite C.
454              
455             =head1 METHODS
456              
457             =head2 create_database
458              
459             $self = $self->create_database;
460              
461             This method will create a temp database for the current process. Calling this
462             method multiple times will simply do nothing. This method is normally
463             automatically called by L.
464              
465             The database name generate is defined by the L parameter passed to
466             L, but normalization will be done to make it work for the given database.
467              
468             =head2 dsn
469              
470             ($dsn, $user, $pass, $attrs) = $self->dsn;
471             ($dsn, $user, $pass, $attrs) = DBIx::TempDB->dsn($url);
472              
473             Will parse L or C<$url>, and return a list of arguments suitable for
474             L.
475              
476             Note that this method cannot be called as an object method before
477             L is called. You can on the other hand call it as a class
478             method, with a L or URL string as input.
479              
480             =head2 execute
481              
482             $self = $self->execute(@sql);
483              
484             This method will execute a list of C<@sql> statements in the temporary
485             SQL server.
486              
487             =head2 execute_file
488              
489             $self = $self->execute_file("relative/to/executable.sql");
490             $self = $self->execute_file("/absolute/path/stmt.sql");
491              
492             This method will read the contents of a file and execute the SQL statements
493             in the temporary server.
494              
495             This method is a thin wrapper around L.
496              
497             =head2 new
498              
499             $self = DBIx::TempDB->new($url, %args);
500             $self = DBIx::TempDB->new("mysql://127.0.0.1");
501             $self = DBIx::TempDB->new("postgresql://postgres@db.example.com");
502             $self = DBIx::TempDB->new("sqlite:");
503              
504             Creates a new object after checking the C<$url> is valid. C<%args> can be:
505              
506             =over 4
507              
508             =item * auto_create
509              
510             L will be called automatically, unless C is
511             set to a false value.
512              
513             =item * create_database_command
514              
515             Can be set to a custom create database command in the database. The default is
516             "create database %d", where %d will be replaced by the generated database name.
517              
518             For even more control, you can set this to a code ref which will be called like
519             this:
520              
521             $self->$cb($database_name);
522              
523             The default is subject to change.
524              
525             =item * drop_database_command
526              
527             Can be set to a custom drop database command in the database. The default is
528             "drop database %d", where %d will be replaced by the generated database name.
529              
530             For even more control, you can set this to a code ref which will be called like
531             this:
532              
533             $self->$cb($database_name);
534              
535             The default is subject to change.
536              
537             =item * drop_from_child
538              
539             Setting "drop_from_child" to a true value will create a child process which
540             will remove the temporary database, when the main process ends. There are two
541             possible values:
542              
543             C (the default) will create a child process which monitor
544             the L object with a pipe. This will then DROP the temp database
545             if the object goes out of scope or if the process ends.
546              
547             C will create a child process detached from the parent,
548             which monitor the parent with C.
549              
550             The double fork code is based on a paste contributed by
551             L, Knut Arne Bjørndal.
552              
553             =item * template
554              
555             Customize the generated database name. Default template is "tmp_%U_%X_%H%i".
556             Possible variables to expand are:
557              
558             %i = The number of tries if tries are higher than 0. Example: "_3"
559             %H = Hostname
560             %P = Process ID ($$)
561             %T = Process start time ($^T)
562             %U = UID of current user
563             %X = Basename of executable
564              
565             The default is subject to change!
566              
567             =back
568              
569             =head2 url
570              
571             $url = $self->url;
572              
573             Returns the input URL as L compatible object. This URL will have
574             the L part set to the database from L,
575             but not I after L is actually called.
576              
577             The URL returned can be passed directly to modules such as L
578             and L.
579              
580             =head1 COPYRIGHT AND LICENSE
581              
582             Copyright (C) 2015, Jan Henning Thorsen
583              
584             This program is free software, you can redistribute it and/or modify it under
585             the terms of the Artistic License version 2.0.
586              
587             =head1 AUTHOR
588              
589             Jan Henning Thorsen - C
590              
591             =cut