File Coverage

blib/lib/Mojo/SQLite/Migrations.pm
Criterion Covered Total %
statement 84 84 100.0
branch 32 36 88.8
condition 20 24 83.3
subroutine 14 14 100.0
pod 7 7 100.0
total 157 165 95.1


line stmt bran cond sub pod time code
1             package Mojo::SQLite::Migrations;
2 6     6   40 use Mojo::Base -base;
  6         12  
  6         42  
3              
4 6     6   1018 use Carp 'croak';
  6         12  
  6         291  
5 6     6   46 use Mojo::File 'path';
  6         11  
  6         322  
6 6     6   38 use Mojo::Loader 'data_section';
  6         9  
  6         251  
7 6     6   33 use Mojo::Util 'decode';
  6         34  
  6         356  
8              
9 6   50 6   36 use constant DEBUG => $ENV{MOJO_MIGRATIONS_DEBUG} || 0;
  6         16  
  6         7795  
10              
11             our $VERSION = '3.007';
12              
13             has name => 'migrations';
14             has sqlite => undef, weak => 1;
15              
16 21     21 1 222 sub active { $_[0]->_active($_[0]->sqlite->db) }
17              
18             sub from_data {
19 8     8 1 827 my ($self, $class, $name) = @_;
20 8   66     44 return $self->from_string(
      66        
21             data_section($class //= caller, $name // $self->name));
22             }
23              
24 1     1 1 40 sub from_file { shift->from_string(decode 'UTF-8', path(pop)->slurp) }
25              
26             sub from_string {
27 14     14 1 2551 my ($self, $sql) = @_;
28              
29 14         28 my ($version, $way);
30 14         58 my $migrations = $self->{migrations} = {up => {}, down => {}};
31 14   100     94 for my $line (split "\n", $sql // '') {
32 73 100       281 ($version, $way) = ($1, lc $2) if $line =~ /^\s*--\s*(\d+)\s*(up|down)/i;
33 73 50       225 $migrations->{$way}{$version} .= "$line\n" if $version;
34             }
35              
36 14         46 return $self;
37             }
38              
39             sub latest {
40 32 100   32 1 906 (sort { $a <=> $b } keys %{shift->{migrations}{up}})[-1] || 0;
  71         189  
  32         193  
41             }
42              
43             sub migrate {
44 23     23 1 2110 my ($self, $target) = @_;
45              
46             # Unknown version
47 23         49 my $latest = $self->latest;
48 23   100     77 $target //= $latest;
49 23         33 my ($up, $down) = @{$self->{migrations}}{qw(up down)};
  23         49  
50 23 100 100     252 croak "Version $target has no migration" if $target != 0 && !$up->{$target};
51              
52             # Already the right version (make sure migrations table exists)
53 22         50 my $db = $self->sqlite->db;
54 22 100       867 return $self if $self->_active($db, 1) == $target;
55              
56             # Lock migrations table and check version again
57 20         68 my $tx = $db->begin;
58 20 50       42 return $self if (my $active = $self->_active($db, 1)) == $target;
59              
60             # Newer version
61 20 100       245 croak "Active version $active is greater than the latest version $latest"
62             if $active > $latest;
63              
64 18         46 my $query = $self->sql_for($active, $target);
65 18         26 warn "-- Migrate ($active -> $target)\n$query\n" if DEBUG;
66 18         42 local $db->dbh->{sqlite_allow_multiple_statements} = 1;
67              
68             # Disable update hook during migrations
69 18         282 my $hook = $db->dbh->sqlite_update_hook(undef);
70              
71             # Catch the error so we can croak it
72 18         104 my ($errored, $error, $result);
73             {
74 18         27 local $@;
  18         20  
75 18 100       27 eval { $result = $db->dbh->do($query); 1 } or $errored = 1;
  18         29  
  17         2443  
76 18 100       42 $error = $@ if $errored;
77             }
78            
79             # Re-enable update hook
80 18         50 $db->dbh->sqlite_update_hook($hook);
81            
82 18 100       237 croak $error if $errored;
83 17 50       38 return $self unless defined $result; # RaiseError disabled
84            
85 17 50       39 $db->query('update mojo_migrations set version = ? where name = ?',
86             $target, $self->name) and $tx->commit;
87              
88 17         107 return $self;
89             }
90              
91             sub sql_for {
92 23     23 1 70 my ($self, $from, $to) = @_;
93              
94             # Up
95 23         33 my ($up, $down) = @{$self->{migrations}}{qw(up down)};
  23         59  
96 23 100       44 if ($from < $to) {
97 12 100       40 my @up = grep { $_ <= $to && $_ > $from } keys %$up;
  34         122  
98 12         53 return join '', @$up{sort { $a <=> $b } @up};
  18         53  
99             }
100              
101             # Down
102 11 100       41 my @down = grep { $_ > $to && $_ <= $from } keys %$down;
  25         101  
103 11         48 return join '', @$down{reverse sort { $a <=> $b } @down};
  14         36  
104             }
105              
106             sub _active {
107 63     63   914 my ($self, $db, $create) = @_;
108              
109 63         142 my $name = $self->name;
110 63         276 my $results;
111             {
112 63         79 local $db->dbh->{RaiseError} = 0;
  63         109  
113 63         968 my $query = 'select version from mojo_migrations where name = ?';
114 63         161 $results = $db->query($query, $name);
115             }
116 63 100       252 my $next = $results ? $results->array : undef;
117 63 100 100     177 if ($next || !$create) { return $next->[0] || 0 }
  57   100     233  
118              
119             $db->query(
120 6 100 66     62 'create table if not exists mojo_migrations (
121             name text not null primary key,
122             version integer not null check (version >= 0)
123             )'
124             ) if !$results or $results->sth->err;
125 6         71 $db->query('insert into mojo_migrations values (?, ?)', $name, 0);
126              
127 6         22 return 0;
128             }
129              
130             1;
131              
132             =encoding utf8
133              
134             =head1 NAME
135              
136             Mojo::SQLite::Migrations - Migrations
137              
138             =head1 SYNOPSIS
139              
140             use Mojo::SQLite::Migrations;
141              
142             my $migrations = Mojo::SQLite::Migrations->new(sqlite => $sql);
143             $migrations->from_file('/home/dbook/migrations.sql')->migrate;
144              
145             =head1 DESCRIPTION
146              
147             L is used by L to allow database
148             schemas to evolve easily over time. A migration file is just a collection of
149             sql blocks, with one or more statements, separated by comments of the form
150             C<-- VERSION UP/DOWN>.
151              
152             -- 1 up
153             create table messages (message text);
154             insert into messages values ('I ♥ Mojolicious!');
155             -- 1 down
156             drop table messages;
157              
158             -- 2 up (...you can comment freely here...)
159             create table stuff (whatever integer);
160             -- 2 down
161             drop table stuff;
162              
163             The idea is to let you migrate from any version, to any version, up and down.
164             Migrations are very safe, because they are performed in transactions and only
165             one can be performed at a time. If a single statement fails, the whole
166             migration will fail and get rolled back. Every set of migrations has a
167             L, which is stored together with the currently active version in an
168             automatically created table named C.
169              
170             =head1 ATTRIBUTES
171              
172             L implements the following attributes.
173              
174             =head2 name
175              
176             my $name = $migrations->name;
177             $migrations = $migrations->name('foo');
178              
179             Name for this set of migrations, defaults to C.
180              
181             =head2 sqlite
182              
183             my $sql = $migrations->sqlite;
184             $migrations = $migrations->sqlite(Mojo::SQLite->new);
185              
186             L object these migrations belong to. Note that this attribute is
187             weakened.
188              
189             =head1 METHODS
190              
191             L inherits all methods from L and
192             implements the following new ones.
193              
194             =head2 active
195              
196             my $version = $migrations->active;
197              
198             Currently active version.
199              
200             =head2 from_data
201              
202             $migrations = $migrations->from_data;
203             $migrations = $migrations->from_data('main');
204             $migrations = $migrations->from_data('main', 'file_name');
205              
206             Extract migrations from a file in the DATA section of a class with
207             L, defaults to using the caller class and
208             L.
209              
210             __DATA__
211             @@ migrations
212             -- 1 up
213             create table messages (message text);
214             insert into messages values ('I ♥ Mojolicious!');
215             -- 1 down
216             drop table messages;
217              
218             =head2 from_file
219              
220             $migrations = $migrations->from_file('/home/dbook/migrations.sql');
221              
222             Extract migrations from a file.
223              
224             =head2 from_string
225              
226             $migrations = $migrations->from_string(
227             '-- 1 up
228             create table foo (bar integer);
229             -- 1 down
230             drop table foo;'
231             );
232              
233             Extract migrations from string.
234              
235             =head2 latest
236              
237             my $version = $migrations->latest;
238              
239             Latest version available.
240              
241             =head2 migrate
242              
243             $migrations = $migrations->migrate;
244             $migrations = $migrations->migrate(3);
245              
246             Migrate from L to a different version, up or down, defaults to using
247             L. All version numbers need to be positive, with version C<0>
248             representing an empty database.
249              
250             # Reset database
251             $migrations->migrate(0)->migrate;
252              
253             =head2 sql_for
254              
255             my $sql = $migrations->sql_for(5, 10);
256              
257             Get SQL to migrate from one version to another, up or down.
258              
259             =head1 DEBUGGING
260              
261             You can set the C environment variable to get some
262             advanced diagnostics information printed to C.
263              
264             MOJO_MIGRATIONS_DEBUG=1
265              
266             =head1 BUGS
267              
268             Report any issues on the public bugtracker.
269              
270             =head1 AUTHOR
271              
272             Dan Book, C
273              
274             =head1 COPYRIGHT AND LICENSE
275              
276             Copyright 2015, Dan Book.
277              
278             This library is free software; you may redistribute it and/or modify it under
279             the terms of the Artistic License version 2.0.
280              
281             =head1 SEE ALSO
282              
283             L