File Coverage

blib/lib/DBIx/TempDB.pm
Criterion Covered Total %
statement 206 260 79.2
branch 63 110 57.2
condition 53 103 51.4
subroutine 34 39 87.1
pod 6 6 100.0
total 362 518 69.8


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