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   46 use Mojo::Base -base;
  6         15  
  6         47  
3              
4 6     6   1030 use Carp 'croak';
  6         14  
  6         314  
5 6     6   41 use Mojo::File 'path';
  6         14  
  6         266  
6 6     6   39 use Mojo::Loader 'data_section';
  6         21  
  6         307  
7 6     6   40 use Mojo::Util 'decode';
  6         12  
  6         445  
8              
9 6   50 6   42 use constant DEBUG => $ENV{MOJO_MIGRATIONS_DEBUG} || 0;
  6         13  
  6         8849  
10              
11             our $VERSION = '3.008';
12              
13             has name => 'migrations';
14             has sqlite => undef, weak => 1;
15              
16 21     21 1 238 sub active { $_[0]->_active($_[0]->sqlite->db) }
17              
18             sub from_data {
19 8     8 1 1122 my ($self, $class, $name) = @_;
20 8   66     59 return $self->from_string(
      66        
21             data_section($class //= caller, $name // $self->name));
22             }
23              
24 1     1 1 46 sub from_file { shift->from_string(decode 'UTF-8', path(pop)->slurp) }
25              
26             sub from_string {
27 14     14 1 3211 my ($self, $sql) = @_;
28              
29 14         23 my ($version, $way);
30 14         73 my $migrations = $self->{migrations} = {up => {}, down => {}};
31 14   100     169 for my $line (split "\n", $sql // '') {
32 73 100       344 ($version, $way) = ($1, lc $2) if $line =~ /^\s*--\s*(\d+)\s*(up|down)/i;
33 73 50       291 $migrations->{$way}{$version} .= "$line\n" if $version;
34             }
35              
36 14         62 return $self;
37             }
38              
39             sub latest {
40 32 100   32 1 1483 (sort { $a <=> $b } keys %{shift->{migrations}{up}})[-1] || 0;
  71         218  
  32         221  
41             }
42              
43             sub migrate {
44 23     23 1 2436 my ($self, $target) = @_;
45              
46             # Unknown version
47 23         48 my $latest = $self->latest;
48 23   100     88 $target //= $latest;
49 23         40 my ($up, $down) = @{$self->{migrations}}{qw(up down)};
  23         55  
50 23 100 100     269 croak "Version $target has no migration" if $target != 0 && !$up->{$target};
51              
52             # Already the right version (make sure migrations table exists)
53 22         57 my $db = $self->sqlite->db;
54 22 100       1049 return $self if $self->_active($db, 1) == $target;
55              
56             # Lock migrations table and check version again
57 20         75 my $tx = $db->begin;
58 20 50       55 return $self if (my $active = $self->_active($db, 1)) == $target;
59              
60             # Newer version
61 20 100       388 croak "Active version $active is greater than the latest version $latest"
62             if $active > $latest;
63              
64 18         44 my $query = $self->sql_for($active, $target);
65 18         32 warn "-- Migrate ($active -> $target)\n$query\n" if DEBUG;
66 18         46 local $db->dbh->{sqlite_allow_multiple_statements} = 1;
67              
68             # Disable update hook during migrations
69 18         321 my $hook = $db->dbh->sqlite_update_hook(undef);
70              
71             # Catch the error so we can croak it
72 18         109 my ($errored, $error, $result);
73             {
74 18         25 local $@;
  18         28  
75 18 100       29 eval { $result = $db->dbh->do($query); 1 } or $errored = 1;
  18         36  
  17         2535  
76 18 100       51 $error = $@ if $errored;
77             }
78            
79             # Re-enable update hook
80 18         64 $db->dbh->sqlite_update_hook($hook);
81            
82 18 100       250 croak $error if $errored;
83 17 50       68 return $self unless defined $result; # RaiseError disabled
84            
85 17 50       44 $db->query('update mojo_migrations set version = ? where name = ?',
86             $target, $self->name) and $tx->commit;
87              
88 17         127 return $self;
89             }
90              
91             sub sql_for {
92 23     23 1 82 my ($self, $from, $to) = @_;
93              
94             # Up
95 23         43 my ($up, $down) = @{$self->{migrations}}{qw(up down)};
  23         64  
96 23 100       56 if ($from < $to) {
97 12 100       42 my @up = grep { $_ <= $to && $_ > $from } keys %$up;
  34         138  
98 12         58 return join '', @$up{sort { $a <=> $b } @up};
  19         60  
99             }
100              
101             # Down
102 11 100       44 my @down = grep { $_ > $to && $_ <= $from } keys %$down;
  25         107  
103 11         55 return join '', @$down{reverse sort { $a <=> $b } @down};
  12         40  
104             }
105              
106             sub _active {
107 63     63   1125 my ($self, $db, $create) = @_;
108              
109 63         141 my $name = $self->name;
110 63         315 my $results;
111             {
112 63         107 local $db->dbh->{RaiseError} = 0;
  63         133  
113 63         1206 my $query = 'select version from mojo_migrations where name = ?';
114 63         174 $results = $db->query($query, $name);
115             }
116 63 100       316 my $next = $results ? $results->array : undef;
117 63 100 100     206 if ($next || !$create) { return $next->[0] || 0 }
  57   100     278  
118              
119             $db->query(
120 6 100 66     28 '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         70 $db->query('insert into mojo_migrations values (?, ?)', $name, 0);
126              
127 6         30 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