File Coverage

blib/lib/SQL/PatchDAG.pm
Criterion Covered Total %
statement 27 102 26.4
branch 9 44 20.4
condition 9 16 56.2
subroutine 7 21 33.3
pod 15 15 100.0
total 67 198 33.8


line stmt bran cond sub pod time code
1 1     1   70614 use strict; use warnings;
  1     1   3  
  1         30  
  1         5  
  1         2  
  1         47  
2              
3             package SQL::PatchDAG;
4             our $VERSION = '0.110';
5              
6 1     1   6 use File::Spec ();
  1         2  
  1         13  
7 1     1   4 use Fcntl ();
  1         2  
  1         1499  
8              
9             sub new {
10 1     1 1 104 my $class = shift;
11 1         6 my $self = bless { binmode => ':unix', patches => {}, @_ }, $class;
12 1 50       2 my @applied = @{ delete $self->{'applied'} || [] };
  1         13  
13 1         2 @{ $self->{'applied'} }{ @applied } = ();
  1         3  
14 1         3 $self;
15             }
16              
17 0     0 1 0 sub deps_of { @{ $_[0]{'patches'}{ $_[1] } } }
  0         0  
18 0     0 1 0 sub patches { sort keys %{ $_[0]{'patches'} } }
  0         0  
19 0     0 1 0 sub applied { sort keys %{ $_[0]{'applied'} } }
  0         0  
20              
21 0     0 1 0 sub dir { $_[0]{'dir'} }
22 0     0 1 0 sub binmode :method { $_[0]{'binmode'} }
23             sub readdir :method {
24 0     0 1 0 my $dir = shift->dir;
25 0 0       0 opendir my $dh, $dir or die "Couldn't open directory '$dir': $!\n";
26 0         0 File::Spec->no_upwards( sort readdir $dh );
27             }
28             sub open :method {
29 0     0 1 0 my ( $self, $name, $do_rw ) = ( shift, @_ );
30 0 0       0 die "Bad patch name '$name'\n" if $name !~ /\A[a-z0-9_][a-z0-9_-]*\z/;
31 0         0 my $fn = File::Spec->catfile( $self->dir, "$name.sql" );
32 0 0       0 my $mode = $do_rw ? Fcntl::O_RDWR() | Fcntl::O_CREAT() : Fcntl::O_RDONLY();
33 0 0       0 my $fh; eval q{ use open IO => $self->binmode; sysopen $fh, $fn, $mode }
  0         0  
34             or die "Couldn't open '$fn': $!\n";
35 0         0 ( $fn, $fh );
36             }
37              
38             sub from {
39 0     0 1 0 my ( $class, $dir ) = ( shift, shift );
40 0         0 my $self = $class->new( @_, dir => $dir );
41 0         0 my @entry = $self->readdir;
42              
43 0         0 for my $name ( map /(.*)\.sql\z/s, @entry ) {
44 0         0 my ( $fn, $fh ) = $self->open( $name );
45 0 0       0 if ( eof $fh ) { warn "Ignoring empty patch '$fn'\n"; next }
  0         0  
  0         0  
46 0         0 my $dep = readline $fh;
47 0 0       0 $dep =~ s/^-- preceding-patch(?:es)? =(?=(?: \S+)+$)//
48             or die "Bad or missing patch dependecies in '$fn'\n";
49 0         0 $self->{'patches'}{ $name } = [ grep $name ne $_, split ' ', $dep ];
50             }
51              
52 0         0 my @ignore = $self->grep_unknown( map /(.*)\.ignore\z/s, @entry );
53 0         0 delete @{ $self->{'applied'} }{ @ignore };
  0         0  
54              
55 0         0 $self;
56             }
57              
58 0     0 1 0 sub grep_unknown { my $self = shift; grep !exists $self->{'patches'}{ $_ }, @_ }
  0         0  
59 0     0 1 0 sub grep_unapplied { my $self = shift; grep !exists $self->{'applied'}{ $_ }, @_ }
  0         0  
60              
61             sub die_if_not_matching {
62 0     0 1 0 my ( $self, $skip_missing ) = ( shift, @_ );
63 0         0 my @prob;
64 0 0       0 if ( my @u = $self->grep_unknown ( $self->applied ) ) { push @prob, "extraneous: @u" }
  0         0  
65 0 0       0 if ( my @u = $skip_missing ? () : $self->grep_unapplied( $self->patches ) ) { push @prob, "missing: @u" }
  0 0       0  
66 0 0       0 ( not @prob ) or die sprintf "Database schema does not match patches (%s)\n", join '; ', @prob;
67             }
68              
69             sub get_next_unapplied {
70 0     0 1 0 my $self = shift;
71              
72 0         0 $self->die_if_not_matching( 'skip_missing' );
73 0 0       0 ( my @missing = $self->grep_unapplied( $self->patches ) ) or return;
74              
75 0         0 for my $name ( @missing ) {
76 0 0       0 if ( not $self->grep_unapplied( $self->deps_of( $name ) ) ) {
77 0         0 $self->{'applied'}{ $name } = undef;
78 0         0 my ( $fn, $fh ) = $self->open( $name );
79 0         0 return ( $name, $fn, do { local $/; readline $fh } );
  0         0  
  0         0  
80             }
81             }
82              
83 0         0 die "All missing patches have unsatisfied dependencies: @missing\n";
84             }
85              
86             sub create {
87 0     0 1 0 my ( $self, $name, $do_recreate ) = ( shift, @_ );
88 0         0 my $patches = $self->{'patches'};
89              
90             die sprintf "Patch '%s' %s\n", $name, $do_recreate ? 'does not exist' : 'already exists'
91 0 0 0     0 if exists $patches->{ $name } xor !!$do_recreate;
    0          
92              
93 0         0 my ( $fn, $fh ) = $self->open( $name, 'rw' );
94 0         0 my @content = "\n/* remove this comment and put your DDL statements and other SQL here */\n";
95              
96 0 0       0 if ( $do_recreate ) {
97 0         0 @content = readline $fh;
98 0 0 0     0 shift @content if @content and $content[0] =~ m!^-- preceding-patch(?:es)? = !;
99 0         0 delete $patches->{ $name };
100             }
101              
102 0         0 my %depended = map +( $_, undef ), map @$_, values %$patches;
103 0         0 my @dep = sort grep !exists $depended{ $_ }, keys %$patches;
104 0         0 $patches->{ $name } = \@dep;
105              
106 0 0       0 @dep = $name if not @dep;
107 0         0 seek $fh, 0, 0;
108 0         0 print $fh "-- preceding-patches = @dep\n", @content;
109 0         0 truncate $fh, tell $fh;
110              
111 0         0 $fn;
112             }
113              
114             sub run {
115 18     18 1 12260 my $self = shift;
116              
117 18 100 100     207 my ( $fn )
    100 100        
    100 100        
118             = @_ == 1 && $_[0] !~ /^-/ ? $self->create( $_[0] )
119             : @_ == 2 && $_[0] eq '-r' ? $self->create( $_[1], 'recreate' )
120             : @_ == 2 && $_[0] eq '-e' ? $self->open( $_[1] )
121             : die "usage: $0 [ -r | -e ] \n";
122              
123             die "No editor to run, EDITOR environment variable unset\n"
124 1 100   1   9 if do { no warnings 'uninitialized'; '' eq $ENV{'EDITOR'} };
  1         2  
  1         130  
  4         23  
  4         18  
125              
126 3         10 $self->_exec( $ENV{'EDITOR'}, $fn );
127             }
128              
129 0     0     sub _exec { shift; exec { $_[0] } @_ }
  0            
  0            
130              
131             1;
132              
133             __END__