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