File Coverage

blib/lib/SHARYANTO/SQL/Schema.pm
Criterion Covered Total %
statement 68 91 74.7
branch 28 58 48.2
condition 7 20 35.0
subroutine 6 6 100.0
pod 1 1 100.0
total 110 176 62.5


line stmt bran cond sub pod time code
1             package SHARYANTO::SQL::Schema;
2              
3 1     1   340281 use 5.010001;
  1         4  
  1         43  
4 1     1   6 use strict;
  1         3  
  1         33  
5 1     1   6 use warnings;
  1         2  
  1         33  
6 1     1   73196 use Log::Any '$log';
  1         2884  
  1         6  
7              
8             our $VERSION = '0.09'; # VERSION
9              
10 1     1   84 use Exporter;
  1         3  
  1         1496  
11             our @ISA = qw(Exporter);
12             our @EXPORT_OK = qw(create_or_update_db_schema);
13              
14             our %SPEC;
15              
16             $SPEC{create_or_update_db_schema} = {
17             v => 1.1,
18             summary => 'Routine and convention to create/update '.
19             'your application\'s DB schema',
20             description => <<'_',
21              
22             With this routine (and some convention) you can easily create and update
23             database schema for your application in a simple way using pure SQL.
24              
25             *Version*: version is an integer and starts from 1. Each software release with
26             schema change will bump the version number by 1. Version information is stored
27             in a special table called `meta` (SELECT value FROM meta WHERE
28             name='schema_version').
29              
30             You supply the SQL statements in `spec`. `spec` is a hash which at least must
31             contain the key `latest_v` (an integer) and `install` (a series of SQL
32             statements to create the schema from nothing). It should be the SQL statements
33             to create the latest version of the schema.
34              
35             There should also be zero or more `upgrade_to_v$VERSION` keys, the value of each
36             is a series of SQL statements to upgrade from ($VERSION-1) to $VERSION. So there
37             could be `upgrade_to_v2`, `upgrade_to_v3`, and so on up the latest version.
38              
39             This routine will check the existence of the `meta` table and the current schema
40             version. If `meta` table does not exist yet, the SQL statements in `install`
41             will be executed. The `meta` table will also be created and a row
42             ('schema_version', 1) is added.
43              
44             If `meta` table already exists, schema version will be read from it and one or
45             more series of SQL statements from `upgrade_to_v$VERSION` will be executed to
46             bring the schema to the latest version.
47              
48             Currently only tested on MySQL, Postgres, and SQLite. Postgres is recommended
49             because it can do transactional DDL (a failed upgrade in the middle will not
50             cause the database schema state to be inconsistent, e.g. in-between two
51             versions).
52              
53             _
54             args => {
55             spec => {
56             schema => ['hash*'], # XXX require 'install' & 'latest_v' keys
57             summary => 'SQL statements to create and update schema',
58             req => 1,
59             description => <<'_',
60              
61             Example:
62              
63             {
64             latest_v => 3,
65              
66             install => [
67             'CREATE TABLE IF NOT EXISTS t1 (...)',
68             'CREATE TABLE IF NOT EXISTS t2 (...)',
69             ],
70              
71             upgrade_to_v2 => [
72             'ALTER TABLE t1 ADD COLUMN c5 INT NOT NULL',
73             'CREATE UNIQUE INDEX i1 ON t2(c1)',
74             ],
75              
76             upgrade_to_v3 => [
77             'ALTER TABLE t2 DROP COLUMN c2',
78             ],
79             }
80              
81             _
82             },
83             dbh => {
84             schema => ['obj*'],
85             summary => 'DBI database handle',
86             req => 1,
87             },
88             },
89             "_perinci.sub.wrapper.validate_args" => 0,
90             };
91             sub create_or_update_db_schema {
92 6 50 0 6 1 1803827 my %args = @_; if (!exists($args{'spec'})) { return [400, "Missing argument: spec"] } my $_sahv_dpath = []; my $arg_err; ((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"] } 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"] } # VALIDATE_ARGS
  6 0 0     46  
  0 50 0     0  
  6 0 0     24  
  6 50       12  
  6 50       62  
  6 50       23  
  0 50       0  
  6 0       512  
  0 50       0  
  6 0       96  
  6 50       69  
  6 50       28  
  0 50       0  
93              
94 6         21 my $spec = $args{spec};
95 6         13 my $dbh = $args{dbh};
96              
97 6         217 local $dbh->{RaiseError};
98              
99             # first, check current schema version
100              
101             # XXX check spec: latest_v and upgrade_to_v$V must synchronize
102              
103 6         18 my $v;
104 6         62 my @t = $dbh->tables("", undef, "meta");
105 6 100       5375 if (@t) {
106 2         12 ($v) = $dbh->selectrow_array(
107             "SELECT value FROM meta WHERE name='schema_version'");
108             }
109 6   100     265 $v //= 0;
110              
111 6         14 my $orig_v = $v;
112              
113             # perform schema upgrade atomically per version (at least for db that
114             # supports it like postgres)
115 6         10 my $err;
116              
117 6         16 my $latest_v = $spec->{latest_v};
118 6 50       26 if (!defined($latest_v)) {
119 0         0 $latest_v = 1;
120 0         0 for (keys %$spec) {
121 0 0       0 next unless /^upgrade_to_v(\d+)$/;
122 0 0       0 $latest_v = $1 if $1 > $latest_v;
123             }
124             }
125              
126             STEP:
127 6         33 for my $i (($v+1) .. $latest_v) {
128 10         41 undef $err;
129 10         21 my $last;
130              
131 10         237 $dbh->begin_work;
132              
133 10 100 66     444 if ($v == 0 && !@t) {
134             $dbh->do("CREATE TABLE meta (name VARCHAR(64) NOT NULL PRIMARY KEY, value VARCHAR(255))")
135 4 50       34 or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
136             $dbh->do("INSERT INTO meta (name,value) VALUES ('schema_version',0)")
137 4 50       4347 or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
138             }
139              
140 10 100 100     1396 if ($v == 0 && $spec->{install}) {
141 1         16 $log->debug("Updating database schema from version $v to $i ...");
142 1         6 my $j = 0;
143 1         5 for my $sql (@{ $spec->{install} }) {
  1         7  
144 2 50       18 $dbh->do($sql) or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
145 2         811 $i++;
146             }
147             $dbh->do("UPDATE meta SET value=$latest_v WHERE name='schema_version'")
148 1 50       13 or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
149 1         684 $last++;
150             } else {
151 9         110 $log->debug("Updating database schema from version $v to $i ...");
152             $spec->{"upgrade_to_v$i"}
153 9 50       64 or do { $err = "Error in spec: upgrade_to_v$i not specified"; last STEP };
  0         0  
  0         0  
154 9         18 for my $sql (@{ $spec->{"upgrade_to_v$i"} }) {
  9         37  
155 17 100       3778 $dbh->do($sql) or do { $err = $dbh->errstr; last STEP };
  3         1211  
  3         19  
156             }
157             $dbh->do("UPDATE meta SET value=$i WHERE name='schema_version'")
158 6 50       2467 or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
159             }
160              
161 7 50       344256 $dbh->commit or do { $err = $dbh->errstr; last STEP };
  0         0  
  0         0  
162              
163 7         70 $v = $i;
164             }
165 6 100       65 if ($err) {
166 3         40 $log->error("Can't upgrade schema (from version $v): $err");
167 3         27 $dbh->rollback;
168 3         69 return [500, "Can't upgrade schema (from version $v): $err"];
169             } else {
170 3         134 return [200, "OK (upgraded from v=$orig_v)", {version=>$v}];
171             }
172              
173 0           [200];
174             }
175              
176             1;
177             # ABSTRACT: Routine and convention to create/update your application's DB schema
178              
179             __END__