File Coverage

blib/lib/SQL/PatchDAG.pm
Criterion Covered Total %
statement 27 102 26.4
branch 9 46 19.5
condition 9 16 56.2
subroutine 7 21 33.3
pod 15 15 100.0
total 67 200 33.5


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