File Coverage

blib/lib/SHARYANTO/SQL/Schema.pm
Criterion Covered Total %
statement 84 119 70.5
branch 45 90 50.0
condition 2 20 10.0
subroutine 6 6 100.0
pod 1 1 100.0
total 138 236 58.4


line stmt bran cond sub pod time code
1             package SHARYANTO::SQL::Schema;
2              
3             our $DATE = '2014-09-06'; # DATE
4             our $VERSION = '0.10'; # VERSION
5              
6 1     1   102320 use 5.010001;
  1         4  
  1         38  
7 1     1   6 use strict;
  1         2  
  1         151  
8 1     1   6 use warnings;
  1         2  
  1         34  
9 1     1   1006 use Log::Any '$log';
  1         2396  
  1         5  
10              
11 1     1   67 use Exporter;
  1         3  
  1         1692  
12             our @ISA = qw(Exporter);
13             our @EXPORT_OK = qw(
14             create_or_update_db_schema
15             );
16              
17             our %SPEC;
18              
19             $SPEC{create_or_update_db_schema} = {
20             v => 1.1,
21             summary => 'Routine and convention to create/update '.
22             'your application\'s DB schema',
23             description => <<'_',
24              
25             With this routine (and some convention) you can easily create and update
26             database schema for your application in a simple way using pure SQL.
27              
28             *Version*: version is an integer and starts from 1. Each software release with
29             schema change will bump the version number by 1. Version information is stored
30             in a special table called `meta` (SELECT value FROM meta WHERE
31             name='schema_version').
32              
33             You supply the SQL statements in `spec`. `spec` is a hash which at least must
34             contain the key `latest_v` (an integer) and `install` (a series of SQL
35             statements to create the schema from nothing to the latest version).
36              
37             There should also be zero or more `upgrade_to_v$VERSION` keys, the value of each
38             is a series of SQL statements to upgrade from ($VERSION-1) to $VERSION. So there
39             could be `upgrade_to_v2`, `upgrade_to_v3`, and so on up the latest version. This
40             is used to upgrade an existing database from earlier version to the latest.
41              
42             For testing purposes, you can also add one or more `install_v<VERSION>` key,
43             where `XXX` is an integer, the lowest version number that you still want to
44             support. So, for example, if `latest_v` is 5 and you still want to support from
45             version 2, you can have an `install_v2` key containing a series of SQL
46             statements to create the schema at version 2, and `upgrade_to_v3`,
47             `upgrade_to_v4`, `upgrade_to_v5` keys. This way migrations from v2 to v3, v3 to
48             v4, and v4 to v5 can be tested.
49              
50             This routine will check the existence of the `meta` table and the current schema
51             version. If `meta` table does not exist yet, the SQL statements in `install`
52             will be executed. The `meta` table will also be created and a row
53             `('schema_version', 1)` is added.
54              
55             If `meta` table already exists, schema version will be read from it and one or
56             more series of SQL statements from `upgrade_to_v$VERSION` will be executed to
57             bring the schema to the latest version.
58              
59             Currently only tested on MySQL, Postgres, and SQLite. Postgres is recommended
60             because it can do transactional DDL (a failed upgrade in the middle will not
61             cause the database schema state to be inconsistent, e.g. in-between two
62             versions).
63              
64             _
65             args => {
66             spec => {
67             schema => ['hash*'], # XXX require 'install' & 'latest_v' keys
68             summary => 'SQL statements to create and update schema',
69             req => 1,
70             description => <<'_',
71              
72             Example:
73              
74             {
75             latest_v => 3,
76              
77             # will install version 3 (latest)
78             install => [
79             'CREATE TABLE IF NOT EXISTS t1 (...)',
80             'CREATE TABLE IF NOT EXISTS t2 (...)',
81             'CREATE TABLE t3 (...)',
82             ],
83              
84             upgrade_to_v2 => [
85             'ALTER TABLE t1 ADD COLUMN c5 INT NOT NULL',
86             'CREATE UNIQUE INDEX i1 ON t2(c1)',
87             ],
88              
89             upgrade_to_v3 => [
90             'ALTER TABLE t2 DROP COLUMN c2',
91             'CREATE TABLE t3 (...)',
92             ],
93              
94             # provided for testing, so we can test migration from v1->v2, v2->v3
95             install_v1 => [
96             'CREATE TABLE IF NOT EXISTS t1 (...)',
97             'CREATE TABLE IF NOT EXISTS t2 (...)',
98             ],
99             }
100              
101             _
102             },
103             dbh => {
104             schema => ['obj*'],
105             summary => 'DBI database handle',
106             req => 1,
107             },
108             create_from_version => {
109             schema => ['int*'],
110             summary => 'Instead of the latest, create from this version',
111             description => <<'_',
112              
113             This can be useful during testing. By default, if given an empty database, this
114             function will use the `install` key of the spec to create the schema from
115             nothing to the latest version. However, if this option is given, function wil
116             use the corresponding `install_v<VERSION>` key in the spec (which must exist)
117             and then upgrade using the `upgrade_to_v<VERSION>` keys to upgrade to the latest
118             version.
119              
120             _
121             },
122             },
123             "x.perinci.sub.wrapper.disable_validate_args" => 1,
124             };
125             sub create_or_update_db_schema {
126 7 0 0 7 1 459063 my %args = @_; require Scalar::Util::Numeric;my $_sahv_dpath = []; my $arg_err; if (exists($args{'create_from_version'})) { ((defined($args{'create_from_version'})) ? 1 : (($arg_err //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Required input not specified"),0)) && ((Scalar::Util::Numeric::isint($args{'create_from_version'})) ? 1 : (($arg_err //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Input is not of type integer"),0)); if ($arg_err) { return [400, "Invalid argument value for create_from_version: $arg_err"] } }if (!exists($args{'dbh'})) { return [400, "Missing argument: dbh"] } require Scalar::Util;((defined($args{'dbh'})) ? 1 : (($arg_err //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Required input not specified"),0)) && ((Scalar::Util::blessed($args{'dbh'})) ? 1 : (($arg_err //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Input is not of type object"),0)); if ($arg_err) { return [400, "Invalid argument value for dbh: $arg_err"] } if (!exists($args{'spec'})) { return [400, "Missing argument: spec"] } ((defined($args{'spec'})) ? 1 : (($arg_err //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Required input not specified"),0)) && ((ref($args{'spec'}) eq 'HASH') ? 1 : (($arg_err //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Input is not of type hash"),0)); if ($arg_err) { return [400, "Invalid argument value for spec: $arg_err"] } # VALIDATE_ARGS
  7 50 0     2573  
  7 0 0     1667  
  7 50 0     31  
  7 50 0     43  
  1 50 0     28  
  1 100       6  
  0 50       0  
  7 0       34  
  0 50       0  
  7 0       56  
  7 50       425  
  7 50       24  
  0 50       0  
  7 50       28  
  0 0       0  
  7 50       56  
  7 0       19  
  0 50       0  
    50          
    50          
127              
128 7         15 my $spec = $args{spec};
129 7         15 my $dbh = $args{dbh};
130 7         18 my $from_v = $args{create_from_version};
131              
132 7         741 local $dbh->{RaiseError};
133              
134             # first, check current schema version
135              
136             # XXX check spec: latest_v and upgrade_to_v$V must synchronize
137              
138 7         21 my $current_v;
139 7         88 my @has_meta_table = $dbh->tables("", undef, "meta");
140 7 100       6400 if (@has_meta_table) {
141 2         20 ($current_v) = $dbh->selectrow_array(
142             "SELECT value FROM meta WHERE name='schema_version'");
143             }
144 7   100     389 $current_v //= 0;
145              
146 7         13 my $orig_v = $current_v;
147              
148             # perform schema upgrade atomically per version (at least for db that
149             # supports atomic DDL like postgres)
150 7         17 my $err;
151              
152 7         18 my $latest_v = $spec->{latest_v};
153 7 50       22 if (!defined($latest_v)) {
154 0         0 $latest_v = 1;
155 0         0 for (keys %$spec) {
156 0 0       0 next unless /^upgrade_to_v(\d+)$/;
157 0 0       0 $latest_v = $1 if $1 > $latest_v;
158             }
159             }
160              
161             STEP:
162 7         10 while (1) {
163 15 100       66 last if $current_v >= $latest_v;
164              
165 11         284 $dbh->begin_work;
166              
167             # install
168 11 100       294 if ($current_v == 0) {
169             # create 'meta' table if not exists
170 5 50       16 unless (@has_meta_table) {
171             $dbh->do("CREATE TABLE meta (name VARCHAR(64) NOT NULL PRIMARY KEY, value VARCHAR(255))")
172 5 50       39 or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
173             $dbh->do("INSERT INTO meta (name,value) VALUES ('schema_version',0)")
174 5 50       15228 or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
175             }
176              
177 5 100       855 if ($from_v) {
178             # install from a specific version
179 1 50       8 if ($spec->{"install_v$from_v"}) {
180 1         11 $log->debug("Creating version $from_v of database schema ...");
181 1         4 for my $sql (@{ $spec->{"install_v$from_v"} }) {
  1         5  
182 3 50       419 $dbh->do($sql) or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
183             }
184             $dbh->do("UPDATE meta SET value=$from_v WHERE name='schema_version'")
185 1 50       215 or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
186 1 50       30722 $dbh->commit or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
187 1         6 $current_v = $from_v;
188 1         6 next STEP;
189             } else {
190 0         0 $err = "Error in spec: Can't find 'install_v$from_v' key in spec";
191 0         0 last STEP;
192             }
193             } else {
194             # install directly the latest version
195 4 100       22 if ($spec->{install}) {
    50          
196 1         11 $log->debug("Creating latest version of database schema ...");
197 1         4 for my $sql (@{ $spec->{install} }) {
  1         4  
198 2 50       197 $dbh->do($sql) or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
199             }
200             $dbh->do("UPDATE meta SET value=$latest_v WHERE name='schema_version'")
201 1 50       199 or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
202 1 50       13471 $dbh->commit or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
203 1         11 last STEP;
204             } elsif ($spec->{upgrade_to_v1}) {
205             # there is no 'install' but 'upgrade_to_v1', so we upgrade
206             # from v1 to latest
207 3         136 goto UPGRADE;
208             } else {
209 0         0 $err = "Error in spec: Can't find 'install' key in spec";
210 0         0 last STEP;
211             }
212             }
213             }
214              
215             UPGRADE:
216 9         27 my $next_v = $current_v + 1;
217 9         105 $log->debug("Updating database schema from version $current_v to $next_v ...");
218             $spec->{"upgrade_to_v$next_v"}
219 9 50       67 or do { $err = "Error in spec: upgrade_to_v$next_v not specified"; last STEP };
  0         0  
  0         0  
220 9         46 for my $sql (@{ $spec->{"upgrade_to_v$next_v"} }) {
  9         46  
221 17 100       3231 $dbh->do($sql) or do { $err = $dbh->errstr; last STEP };
  2         1016  
  2         12  
222             }
223             $dbh->do("UPDATE meta SET value=$next_v WHERE name='schema_version'")
224 7 50       3032 or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
225 7 50       179035 $dbh->commit or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
226 7         52 $current_v = $next_v;
227             }
228 7 100       33 if ($err) {
229 2         18 $log->error("Can't upgrade schema (from version $orig_v): $err");
230 2         19 $dbh->rollback;
231 2         46 return [500, "Can't upgrade schema (from version $orig_v): $err"];
232             } else {
233 5         233 return [200, "OK (upgraded from version $orig_v to $latest_v)", {version=>$latest_v}];
234             }
235             }
236              
237             1;
238             # ABSTRACT: Routine and convention to create/update your application's DB schema
239              
240             __END__
241              
242             =pod
243              
244             =encoding UTF-8
245              
246             =head1 NAME
247              
248             SHARYANTO::SQL::Schema - Routine and convention to create/update your application's DB schema
249              
250             =head1 VERSION
251              
252             This document describes version 0.10 of SHARYANTO::SQL::Schema (from Perl distribution SHARYANTO-SQL-Schema), released on 2014-09-06.
253              
254             =head1 DESCRIPTION
255              
256             This module uses L<Log::Any> for logging.
257              
258             To use this module, you typically run the create_or_update_db_schema() routine
259             at the start of your program/script, e.g.:
260              
261             use DBI;
262             use SHARYANTO::SQL::Schema qw(create_or_update_db_schema);
263             my $spec = {...}; # the schema specification
264             my $dbh = DBI->connect(...);
265             my $res = create_or_update_db_schema(dbh=>$dbh, spec=>$spec);
266             die "Cannot run the application: cannot create/upgrade database schema: $res->[1]"
267             unless $res->[0] == 200;
268              
269             This way, your program automatically creates/updates database schema when run.
270             Users need not know anything.
271              
272             =head1 FUNCTIONS
273              
274              
275             =head2 create_or_update_db_schema(%args) -> [status, msg, result, meta]
276              
277             Routine and convention to create/update your application's DB schema.
278              
279             With this routine (and some convention) you can easily create and update
280             database schema for your application in a simple way using pure SQL.
281              
282             I<Version>: version is an integer and starts from 1. Each software release with
283             schema change will bump the version number by 1. Version information is stored
284             in a special table called C<meta> (SELECT value FROM meta WHERE
285             name='schema_version').
286              
287             You supply the SQL statements in C<spec>. C<spec> is a hash which at least must
288             contain the key C<latest_v> (an integer) and C<install> (a series of SQL
289             statements to create the schema from nothing to the latest version).
290              
291             There should also be zero or more C<upgrade_to_v$VERSION> keys, the value of each
292             is a series of SQL statements to upgrade from ($VERSION-1) to $VERSION. So there
293             could be C<upgrade_to_v2>, C<upgrade_to_v3>, and so on up the latest version. This
294             is used to upgrade an existing database from earlier version to the latest.
295              
296             For testing purposes, you can also add one or more C<< install_vE<lt>VERSIONE<gt> >> key,
297             where C<XXX> is an integer, the lowest version number that you still want to
298             support. So, for example, if C<latest_v> is 5 and you still want to support from
299             version 2, you can have an C<install_v2> key containing a series of SQL
300             statements to create the schema at version 2, and C<upgrade_to_v3>,
301             C<upgrade_to_v4>, C<upgrade_to_v5> keys. This way migrations from v2 to v3, v3 to
302             v4, and v4 to v5 can be tested.
303              
304             This routine will check the existence of the C<meta> table and the current schema
305             version. If C<meta> table does not exist yet, the SQL statements in C<install>
306             will be executed. The C<meta> table will also be created and a row
307             C<('schema_version', 1)> is added.
308              
309             If C<meta> table already exists, schema version will be read from it and one or
310             more series of SQL statements from C<upgrade_to_v$VERSION> will be executed to
311             bring the schema to the latest version.
312              
313             Currently only tested on MySQL, Postgres, and SQLite. Postgres is recommended
314             because it can do transactional DDL (a failed upgrade in the middle will not
315             cause the database schema state to be inconsistent, e.g. in-between two
316             versions).
317              
318             Arguments ('*' denotes required arguments):
319              
320             =over 4
321              
322             =item * B<create_from_version> => I<int>
323              
324             Instead of the latest, create from this version.
325              
326             This can be useful during testing. By default, if given an empty database, this
327             function will use the C<install> key of the spec to create the schema from
328             nothing to the latest version. However, if this option is given, function wil
329             use the corresponding C<< install_vE<lt>VERSIONE<gt> >> key in the spec (which must exist)
330             and then upgrade using the C<< upgrade_to_vE<lt>VERSIONE<gt> >> keys to upgrade to the latest
331             version.
332              
333             =item * B<dbh>* => I<obj>
334              
335             DBI database handle.
336              
337             =item * B<spec>* => I<hash>
338              
339             SQL statements to create and update schema.
340              
341             Example:
342              
343             {
344             latest_v => 3,
345            
346             # will install version 3 (latest)
347             install => [
348             'CREATE TABLE IF NOT EXISTS t1 (...)',
349             'CREATE TABLE IF NOT EXISTS t2 (...)',
350             'CREATE TABLE t3 (...)',
351             ],
352            
353             upgrade_to_v2 => [
354             'ALTER TABLE t1 ADD COLUMN c5 INT NOT NULL',
355             'CREATE UNIQUE INDEX i1 ON t2(c1)',
356             ],
357            
358             upgrade_to_v3 => [
359             'ALTER TABLE t2 DROP COLUMN c2',
360             'CREATE TABLE t3 (...)',
361             ],
362            
363             # provided for testing, so we can test migration from v1->v2, v2->v3
364             install_v1 => [
365             'CREATE TABLE IF NOT EXISTS t1 (...)',
366             'CREATE TABLE IF NOT EXISTS t2 (...)',
367             ],
368             }
369              
370             =back
371              
372             Return value:
373              
374             Returns an enveloped result (an array).
375              
376             First element (status) is an integer containing HTTP status code
377             (200 means OK, 4xx caller error, 5xx function error). Second element
378             (msg) is a string containing error message, or 'OK' if status is
379             200. Third element (result) is optional, the actual result. Fourth
380             element (meta) is called result metadata and is optional, a hash
381             that contains extra information.
382              
383             (any)
384              
385             =head1 FAQ
386              
387             =head2 Why use this module instead of other similar solution?
388              
389             Mainly simplicity. I write simple application which is often self-contained in a
390             single module/script. This module works with embedded SQL statements instead of
391             having to put SQL in separate files/subdirectory.
392              
393             =head2 How do I see each SQL statement as it is being executed?
394              
395             Try using L<Log::Any::For::DBI>, e.g.:
396              
397             % TRACE=1 perl -MLog::Any::For::DBI -MLog::Any::App yourapp.pl ...
398              
399             =head1 TODO
400              
401             =over
402              
403             =item * Configurable meta table name?
404              
405             =item * Reversion/downgrade?
406              
407             Something which does not come up often yet in my case.
408              
409             =back
410              
411             =head1 SEE ALSO
412              
413             L<SHARYANTO>
414              
415             Some other database migration tools on CPAN:
416              
417             =over
418              
419             =item * L<DBIx::Migration>
420              
421             Pretty much similar to this module, with support for downgrades. OO style, SQL
422             in separate files/subdirectory.
423              
424             =item * L<Database::Migrator>
425              
426             Pretty much similar. OO style, SQL in separate files/subdirectory. Perl scripts
427             can also be executed for each version upgrade. Meta table is configurable
428             (default recommended is 'AppliedMigrations').
429              
430             =item * L<sqitch>
431              
432             A more proper database change management tool with dependency resolution and VCS
433             awareness. No numbering. Command-line script and Perl library provided. Looks
434             pretty awesome and something which I hope to use for more complex applications.
435              
436             =back
437              
438             =head1 HOMEPAGE
439              
440             Please visit the project's homepage at L<https://metacpan.org/release/SHARYANTO-SQL-Schema>.
441              
442             =head1 SOURCE
443              
444             Source repository is at L<https://github.com/sharyanto/perl-SHARYANTO-SQL-Schema>.
445              
446             =head1 BUGS
447              
448             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=SHARYANTO-SQL-Schema>
449              
450             When submitting a bug or request, please include a test-file or a
451             patch to an existing test-file that illustrates the bug or desired
452             feature.
453              
454             =head1 AUTHOR
455              
456             perlancar <perlancar@cpan.org>
457              
458             =head1 COPYRIGHT AND LICENSE
459              
460             This software is copyright (c) 2014 by perlancar@cpan.org.
461              
462             This is free software; you can redistribute it and/or modify it under
463             the same terms as the Perl 5 programming language system itself.
464              
465             =cut