File Coverage

blib/lib/DBIx/TempDB/Util.pm
Criterion Covered Total %
statement 115 169 68.0
branch 26 52 50.0
condition 35 60 58.3
subroutine 20 26 76.9
pod 3 3 100.0
total 199 310 64.1


line stmt bran cond sub pod time code
1             package DBIx::TempDB::Util;
2 16     16   142576 use strict;
  16         52  
  16         564  
3 16     16   129 use warnings;
  16         34  
  16         520  
4 16     16   78 use Exporter 'import';
  16         30  
  16         595  
5              
6 16     16   88 use Carp qw(confess croak);
  16         30  
  16         950  
7 16     16   8264 use IO::Select;
  16         27956  
  16         815  
8 16     16   8357 use POSIX;
  16         106365  
  16         87  
9 16     16   46244 use Scalar::Util 'blessed';
  16         39  
  16         1004  
10 16     16   8491 use URI::db;
  16         313857  
  16         561  
11 16     16   7157 use URI::QueryParam;
  16         12963  
  16         818  
12              
13 16   50 16   117 use constant DEBUG => $ENV{DBIX_TEMP_DB_DEBUG} || 0;
  16         56  
  16         2020  
14 16   100 16   134 use constant KILL_SLEEP_INTERVAL => $ENV{DBIX_TEMP_DB_KILL_SLEEP_INTERVAL} || 2;
  16         39  
  16         33595  
15              
16             our @EXPORT_OK = qw(dsn_for on_process_end parse_sql);
17              
18             sub dsn_for {
19 15     15 1 7970 my ($url, $database_name) = @_;
20 15 100       81 $url = URI::db->new($url) unless blessed $url;
21 15 50       3022 croak "Unknown engine for $url" unless $url->has_recognized_engine;
22              
23 15         279 my $engine = $url->canonical_engine;
24 15   100     210 $database_name //= $url->dbname;
25 15 100       774 return _dsn_for_mysql($url, $database_name) if $engine eq 'mysql';
26 7 100       40 return _dsn_for_pg($url, $database_name) if $engine eq 'pg';
27 1 50       6 return _dsn_for_sqlite($url, $database_name) if $engine eq 'sqlite';
28 0         0 croak "Can't create DSN for engine $engine.";
29             }
30              
31             sub on_process_end {
32 1     1 1 2 my $code = pop;
33 1   50     4 my $mode = shift // 'fork';
34              
35 1 50       5 return _on_process_end_fork($code) if $mode eq 'fork';
36 1 50       4 return _on_process_end_double_fork($code) if $mode eq 'double_fork';
37 1         9 return DBIx::TempDB::Guard->new($code, $$);
38             }
39              
40             sub parse_sql {
41 2     2 1 88 my ($type, $sql) = @_;
42 2 50       10 $type = $type->canonical_engine if blessed $type;
43 2 50       25 return _parse_mysql($sql) if $type eq 'mysql';
44 0         0 return $sql;
45             }
46              
47             sub _dsn_for_mysql {
48 8     8   18 my ($url, $database_name) = @_;
49 8         11 my %opt = %{$url->query_form_hash};
  8         34  
50 8         428 my ($dsn, @userinfo);
51              
52 8         24 $url = URI::db->new($url);
53 8         1303 $url->dbname($database_name);
54 8         601 $url->query(undef);
55 8         188 $dsn = $url->dbi_dsn;
56 8         1407 @userinfo = ($url->user, $url->password);
57              
58 8   100     671 $opt{AutoCommit} //= 1;
59 8   50     36 $opt{AutoInactiveDestroy} //= 1;
60 8   100     30 $opt{PrintError} //= 0;
61 8   50     44 $opt{RaiseError} //= 1;
62 8   50     31 $opt{mysql_enable_utf8} //= 1;
63              
64 8         61 return $dsn, @userinfo[0, 1], \%opt;
65             }
66              
67             sub _dsn_for_pg {
68 6     6   14 my ($url, $database_name) = @_;
69 6         10 my %opt = %{$url->query_form_hash};
  6         27  
70 6         368 my ($dsn, @userinfo);
71              
72 6         17 $url = URI::db->new($url);
73 6         1026 $url->dbname($database_name);
74 6         455 $url->query(undef);
75 6 50       122 if (my $service = delete $opt{service}) { $url->query_param(service => $service) }
  0         0  
76 6         39 $dsn = $url->dbi_dsn;
77 6         1231 @userinfo = ($url->user, $url->password);
78              
79 6   100     506 $opt{AutoCommit} //= 1;
80 6   100     35 $opt{AutoInactiveDestroy} //= 1;
81 6   50     27 $opt{PrintError} //= 0;
82 6   50     25 $opt{RaiseError} //= 1;
83              
84 6         44 return $dsn, @userinfo[0, 1], \%opt;
85             }
86              
87             sub _dsn_for_sqlite {
88 1     1   3 my ($url, $database_name) = @_;
89 1         2 my %opt = %{$url->query_form_hash};
  1         5  
90              
91 1         62 $url = URI::db->new($url);
92 1         189 $url->dbname($database_name);
93 1         57 $url->query(undef);
94 1         26 my $dsn = $url->dbi_dsn;
95              
96 1   50     177 $opt{AutoCommit} //= 1;
97 1   50     17 $opt{AutoInactiveDestroy} //= 1;
98 1   50     16 $opt{PrintError} //= 0;
99 1   50     6 $opt{RaiseError} //= 1;
100 1   50     6 $opt{sqlite_unicode} //= 1;
101              
102 1         10 return $dsn, "", "", \%opt;
103             }
104              
105             sub _on_process_end_double_fork {
106 0     0   0 my $code = shift;
107 0         0 my $ppid = $$;
108              
109 0         0 warn "[TempDB:$$] Watching process using double fork.\n" if DEBUG;
110 0         0 local $SIG{CHLD} = 'DEFAULT';
111 0 0       0 pipe(my ($READER), my ($WRITER)) or confess "Couldn't create pipe: $!";
112              
113             # Parent
114 0 0 0     0 if (my $pid_1 = fork // confess "Couldn't fork: $!") {
115 0         0 my $pid_2;
116              
117             # Wait around until the second fork is done so that when we return from
118             # here there are no new child processes that could mess things up if the
119             # calling process does any process handling.
120 0         0 close $WRITER;
121 0         0 $pid_2 = <$READER>;
122 0 0       0 $pid_2 = $pid_2 =~ m!(\d+)! ? $1 : undef;
123 0         0 waitpid $pid_1, 0;
124 0 0       0 confess "Couldn't get pid_2 from $pid_1." unless $pid_2;
125 0         0 warn "[TempDB:$$] Double forked from $$ to $pid_1 to $pid_2\n" if DEBUG;
126 0     0   0 return DBIx::TempDB::Guard->new(sub { kill TERM => $pid_2 }, $pid_2);
  0         0  
127             }
128              
129             # Child #1
130             # Detach completely from parent by creating our own session and process
131             # group, closing all filehandles and forking a second time.
132 0         0 $0 = "dbix-on-process-end-$ppid";
133 0         0 close $READER;
134 0         0 $DB::CreateTTY = 0;
135 0 0       0 POSIX::setsid() != -1 or die "[TempDB:$$] Couldn't become session leader: $!\n";
136              
137 0 0 0     0 if (my $pid_2 = fork // die "[TempDB:$$] Couldn't fork: $!") {
138 0         0 print $WRITER "$pid_2\n";
139 0         0 close $WRITER;
140 0         0 POSIX::_exit(0);
141             }
142              
143             # Child #2
144 0         0 warn "[TempDB:$ppid/$$] Double fork waiting on signals or parent to go away.\n" if DEBUG;
145 0         0 _on_process_signals($code);
146 0         0 sleep KILL_SLEEP_INTERVAL while kill 0, $ppid;
147 0         0 local $ENV{DBIX_TEMP_DB_SIGNAL} = 'parent';
148 0         0 $code->();
149 0         0 exit;
150             }
151              
152             sub _on_process_end_fork {
153 0     0   0 my $code = shift;
154 0         0 my $ppid = $$;
155              
156             # Parent
157 0         0 warn "[TempDB:$$] Watching process using single fork.\n" if DEBUG;
158 0 0       0 pipe(my ($READER), my ($WRITER)) or confess "Couldn't create pipe: $!";
159 0 0       0 defined(my $pid = fork) or confess "Couldn't fork: $!";
160 0 0   0   0 return DBIx::TempDB::Guard->new(sub { close $WRITER }, $pid) if $pid;
  0         0  
161              
162             # Child
163 0         0 $0 = "dbix-on-process-end-$ppid";
164 0         0 $DB::CreateTTY = 0;
165 0         0 close $WRITER;
166 0         0 warn "[TempDB:$ppid/$$] Fork waiting on signals or pipe to go away.\n" if DEBUG;
167 0         0 _on_process_signals($code);
168 0         0 IO::Select->new($READER)->can_read;
169 0         0 local $ENV{DBIX_TEMP_DB_SIGNAL} = 'pipe';
170 0         0 $code->();
171 0         0 exit;
172             }
173              
174             sub _on_process_signals {
175 0     0   0 my $code = shift;
176 0         0 for my $name (qw(INT QUIT TERM)) {
177 0     0   0 $SIG{$name} = sub { local $ENV{DBIX_TEMP_DB_SIGNAL} = $name; $code->(); exit; };
  0         0  
  0         0  
  0         0  
178             }
179             }
180              
181             sub _parse_mysql {
182 2     2   5 my $sql = shift;
183 2         6 my ($new, $last, $delimiter) = (0, '', ';');
184 2         4 my @commands;
185              
186 2         6 while (length($sql) > 0) {
187 77         93 my $token;
188              
189 77 100 100     480 if ($sql =~ /^$delimiter/x) {
    100 66        
    100 66        
    100 33        
      33        
      33        
190 5         9 ($new, $token) = (1, $delimiter);
191             }
192             elsif ($sql =~ /^delimiter\s+(\S+)\s*(?:\n|\z)/ip) {
193 1         4 ($new, $token, $delimiter) = (1, ${^MATCH}, $1);
194             }
195             elsif ($sql =~ /^(\s+)/s or $sql =~ /^(\w+)/) { # general name
196 55         102 $token = $1;
197             }
198             elsif (
199             $sql =~ /^--.*(?:\n|\z)/p # double-dash comment
200             or $sql =~ /^\#.*(?:\n|\z)/p # hash comment
201             or $sql =~ /^\/\*(?:[^\*]|\*[^\/])*(?:\*\/|\*\z|\z)/p # C-style comment
202             or $sql =~ /^'(?:[^'\\]*|\\(?:.|\n)|'')*(?:'|\z)/p # single-quoted literal text
203             or $sql =~ /^"(?:[^"\\]*|\\(?:.|\n)|"")*(?:"|\z)/p # double-quoted literal text
204             or $sql =~ /^`(?:[^`]*|``)*(?:`|\z)/p
205             )
206             { # schema-quoted literal text
207 5         11 $token = ${^MATCH};
208             }
209             else {
210 11         22 $token = substr($sql, 0, 1);
211             }
212              
213             # chew token
214 77         112 substr $sql, 0, length($token), '';
215              
216 77 100       118 if ($new) {
217 6 50       22 push @commands, $last if $last !~ /^\s*$/s;
218 6         14 ($new, $last) = (0, '');
219             }
220             else {
221 71         150 $last .= $token;
222             }
223             }
224              
225 2 100       10 push @commands, $last if $last !~ /^\s*$/s;
226 2         5 return map { s/^\s+//; $_ } @commands;
  7         15  
  7         26  
227             }
228              
229             package DBIx::TempDB::Guard;
230 1     1   2 sub new { my $class = shift; bless [@_], $class }
  1         7  
231 1     1   2308 sub DESTROY { shift->[0]->() }
232              
233             package DBIx::TempDB::Util;
234             1;
235              
236             =encoding utf8
237              
238             =head1 NAME
239              
240             DBIx::TempDB::Util - Utility functions for DBIx::TempDB
241              
242             =head1 SYNOPSIS
243              
244             use DBIx::TempDB::Util qw(dsn_for parse_sql);
245              
246             my $url = URI::db->new("postgresql://postgres@localhost");
247             print join ", ", dsn_for($url);
248              
249             my $guard = on_process_end sub { ... };
250             undef $guard; # call the code block earlier
251              
252             print $_ for parse_sql("mysql", "delimiter //\ncreate table y (bar varchar(255))//\n");
253              
254             =head1 DESCRIPTION
255              
256             L contains some utility functions for L.
257              
258             =head1 FUNCTIONS
259              
260             =head2 dsn_for
261              
262             @dsn = dsn_for +URI::db->new("postgresql://postgres@localhost");
263             @dsn = dsn_for "postgresql://postgres@localhost";
264              
265             L takes either a string or L object and returns a list of
266             arguments suitable for L.
267              
268             =head2 on_process_end
269              
270             $guard = on_process_end sub { ... };
271             $guard = on_process_end $mode => sub { ... };
272             $guard = on_process_end destroy => sub { ... };
273             $guard = on_process_end double_fork => sub { ... };
274             $guard = on_process_end fork => sub { ... };
275              
276             Used to set up a code block to be called when the process ends. The default
277             C<$mode> is "fork". The C<$guard> value can be used to call the code block
278             before the process ends:
279              
280             undef $guard; # call sub { ... }
281              
282             =over 2
283              
284             =item * destroy
285              
286             This mode will call the callback when the current process ends normally. This
287             means that if the process is killed with SIGKILL (9) or another untrapped
288             signal, then the callback will I be called.
289              
290             =item * double_fork
291              
292             This mode will create a process that is detached from the parent process. The
293             double forked process will check if the parent is running by sending C
294             every two seconds. This mode might not be supported by all operating systems.
295              
296             =item * fork
297              
298             This mode will create a process with a pipe connected to the parent process.
299             Once the pipe is closed (when the parents ends) the callback will be called.
300             This should work in most processes, but will not work if a the process group
301             receives an unhandled signal.
302              
303             =back
304              
305             =head2 parse_sql
306              
307             @statements = parse_sql $type, $sql;
308             @statements = parse_sql $uri_db, $sql;
309             @statements = parse_sql "mysql", "insert into ...";
310              
311             Takes either a string or an L object and a string containing SQL and
312             splits the SQL into a list of individual statements.
313              
314             Currently only "mysql" is a supported type, meaning any other type will simply
315             return the input C<$sql>.
316              
317             This is not required for SQLite though, you can do this instead:
318              
319             local $dbh->{sqlite_allow_multiple_statements} = 1;
320             $dbh->do($sql);
321              
322             =head1 SEE ALSO
323              
324             L.
325              
326             =cut