File Coverage

blib/lib/SQL/Schema/Versioned.pm
Criterion Covered Total %
statement 192 245 78.3
branch 99 154 64.2
condition 14 30 46.6
subroutine 14 14 100.0
pod 1 1 100.0
total 320 444 72.0


line stmt bran cond sub pod time code
1             package SQL::Schema::Versioned;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2021-08-08'; # DATE
5             our $DIST = 'SQL-Schema-Versioned'; # DIST
6             our $VERSION = '0.239'; # VERSION
7              
8 3     3   332271 use 5.010001;
  3         41  
9 3     3   17 use strict;
  3         6  
  3         79  
10 3     3   14 use warnings;
  3         8  
  3         84  
11 3     3   5641 use Log::ger;
  3         166  
  3         18  
12              
13 3     3   806 use Exporter qw(import);
  3         7  
  3         2055  
14             our @EXPORT_OK = qw(
15             create_or_update_db_schema
16             );
17              
18             our %SPEC;
19              
20             sub _sv_key {
21 24   50 24   63 my $comp = $_[0] // 'main';
22 24 50       257 $comp =~ /\A\w+\z/
23             or die "Invalid component '$comp', please stick to ".
24             "letters/numbers/underscores";
25 24 100       132 "schema_version" . ($comp eq 'main' ? '' : ".$comp");
26             }
27              
28             sub _extract_created_tables_from_sql_statements {
29 18     18   152 my @sql = @_;
30              
31 18         33 my @res;
32 18         51 for my $sql (@sql) {
33 31 100       228 if ($sql =~ /\A\s*
34             create\s+table(?:\s+if\s+not\s+exists)?\s+
35             (?:
36             `([^`]+)` |
37             "([^"]+)" |
38             (\S+)
39             )
40             /isx) {
41 29   100     240 push @res, ($1 // $2 // $3);
      66        
42             }
43             }
44 18         91 @res;
45             }
46              
47             sub _get_provides_from_db {
48 12     12   1957 my ($dbh) = @_;
49              
50 12         19 my %provides;
51              
52 12         60 my $sth = $dbh->prepare(
53             "SELECT name, value FROM meta WHERE name LIKE 'table.%'");
54 12         1453 $sth->execute;
55 12         208 while (my @row = $sth->fetchrow_array) {
56 16         112 my ($table) = $row[0] =~ /table\.(.+)/;
57 16 50       104 my ($provcomp, $provver) = $row[1] =~ /\A(\w+):(\d*)\z/
58             or return [500, "Corrupt information in `meta` table: $row[0] -> $row[1]"];
59 16         182 $provides{$table} = [$provcomp, $provver];
60             }
61 12         167 %provides;
62             }
63              
64             $SPEC{create_or_update_db_schema} = {
65             v => 1.1,
66             summary => 'Routine and convention to create/update '.
67             'your application\'s DB schema',
68             description => <<'_',
69              
70             With this routine (and some convention) you can easily create and update
71             database schema for your application in a simple way using pure SQL.
72              
73             *Version*: version is an integer and starts from 1. Each software release with
74             schema change will bump the version number by 1. Version information is stored
75             in a special table called `meta` (SELECT value FROM meta WHERE
76             name='schema_version').
77              
78             You supply the SQL statements in `spec`. `spec` is a hash which at least must
79             contain the key `latest_v` (an integer) and `install` (a series of SQL
80             statements to create the schema from nothing to the latest version).
81              
82             There should also be zero or more `upgrade_to_v$VERSION` keys, the value of each
83             is a series of SQL statements to upgrade from ($VERSION-1) to $VERSION. So there
84             could be `upgrade_to_v2`, `upgrade_to_v3`, and so on up the latest version. This
85             is used to upgrade an existing database from earlier version to the latest.
86              
87             For testing purposes, you can also add one or more `install_v<VERSION>` key,
88             where `XXX` is an integer, the lowest version number that you still want to
89             support. So, for example, if `latest_v` is 5 and you still want to support from
90             version 2, you can have an `install_v2` key containing a series of SQL
91             statements to create the schema at version 2, and `upgrade_to_v3`,
92             `upgrade_to_v4`, `upgrade_to_v5` keys. This way migrations from v2 to v3, v3 to
93             v4, and v4 to v5 can be tested.
94              
95             You can name `install_v1` key as `upgrade_to_v1` (to upgrade from 'nothing'
96             a.k.a. v0 to v1), which is basically the same thing.
97              
98             This routine will check the existence of the `meta` table and the current schema
99             version. If `meta` table does not exist yet, the SQL statements in `install`
100             will be executed. The `meta` table will also be created and a row
101             `('schema_version', 1)` is added. The (`schema_summary`, <SUMMARY>) row will
102             also be added if your spec specifies a `summary`.
103              
104             If `meta` table already exists, schema version will be read from it and one or
105             more series of SQL statements from `upgrade_to_v$VERSION` will be executed to
106             bring the schema to the latest version.
107              
108             Aside from SQL statement, the series can also contain coderefs for more complex
109             upgrade process. Each coderef will be called with `$dbh` as argument and must
110             not die (so to signify failure, you can die from inside the coderef).
111              
112             Currently only tested on MySQL, Postgres, and SQLite. Postgres is recommended
113             because it can do transactional DDL (a failed upgrade in the middle will not
114             cause the database schema state to be inconsistent, e.g. in-between two
115             versions).
116              
117             ### Modular schema (components)
118              
119             This routine supports so-called modular schema, where you can separate your
120             database schema into several *components* (sets of tables) and then declare
121             dependencies among them.
122              
123             For example, say you are writing a stock management application. You divide your
124             application into several components: `quote` (component that deals with
125             importing stock quotes and querying stock prices), `portfolio` (component that
126             deals with computing the market value of your portfolio, calculating
127             gains/losses), `trade` (component that connects to your broker API and perform
128             trading by submitting buy/sell orders).
129              
130             The `quote` application component manages these tables: `daily_price`,
131             `spot_price`. The `portfolio` application component manages these tables:
132             `account` (list of accounts in stock brokerages), `balance` (list of balances),
133             `tx` (list of transactions). The `trade` application component manages these
134             tables: `order` (list of buy/sell orders).
135              
136             The `portfolio` application component requires price information to be able to
137             calculate unrealized gains/losses. The `trade` component also needs information
138             from the `daily_price` e.g. to calculate 52-week momentum, and writes to the
139             `spot_price` to record intraday prices, and reads/writes from the `account` and
140             `balance` tables. Here are the `spec`s for each component:
141              
142             # spec for the price application component
143             {
144             component_name => 'price',
145             summary => "Price application component",
146             latest_v => 1,
147             provides => ['daily_price', 'spot_price'],
148             install => [...],
149             ...
150             }
151              
152             # spec for the portfolio application component
153             {
154             component_name => 'portfolio',
155             summary => "Portfolio application component",
156             latest_v => 1,
157             provides => ['account', 'balance', 'tx'],
158             deps => {
159             'daily_price' => 1,
160             'spot_price' => 1,
161             },
162             install => [...],
163             ...
164             }
165              
166             # spec for the trade application component
167             {
168             component_name => 'trade',
169             summary => "Trade application component",
170             latest_v => 1,
171             provides => ['order'],
172             deps => {
173             'daily_price' => 1,
174             'spot_price' => 1,
175             'account' => 1,
176             'balance' => 1,
177             },
178             install => [...],
179             ...
180             }
181              
182             You'll notice that the three keys new here are the `component_name`, `provides`,
183             and `deps`.
184              
185             When `component_name` is set, then instead of the `schema_version` key in the
186             `meta` table, your component will use the `schema_version.<COMPONENT_NAME>` key.
187             When `component_name` is not set, it is assumed to be `main` and the
188             `schema_version` key is used in the `meta` table. The component `summary`, if
189             specified, will also be written to `schema_summary.<COMPONENT_NAME>` key.
190              
191             `provides` is an array of tables to help this routine know which table(s) your
192             component create and maintain. If unset, this routine will try to guess from
193             looking at "CREATE TABLE" SQL statements.
194              
195             This routine will create `table.<TABLE_NAME>` keys in the `meta` table to record
196             which components currently maintain which tables. The value of the key is
197             `<COMPONENT_NAME>:<VERSION>`. When a component no longer maintain a table in the
198             newest version, the corresponding `table.<TABLE_NAME>` row in the `meta` will
199             also be removed.
200              
201             `deps` is a hash. The keys are table names that your component requires. The
202             values are integers, meaning the minimum version of the required table (=
203             component version). In the future, more complex dependency relationship and
204             version requirement will be supported.
205              
206             _
207             args => {
208             spec => {
209             schema => ['hash*'], # XXX require 'install' & 'latest_v' keys
210             summary => 'Schema specification, e.g. SQL statements '.
211             'to create and update the schema',
212             req => 1,
213             description => <<'_',
214              
215             Example:
216              
217             {
218             latest_v => 3,
219              
220             # will install version 3 (latest)
221             install => [
222             'CREATE TABLE IF NOT EXISTS t1 (...)',
223             'CREATE TABLE IF NOT EXISTS t2 (...)',
224             'CREATE TABLE t3 (...)',
225             ],
226              
227             upgrade_to_v2 => [
228             'ALTER TABLE t1 ADD COLUMN c5 INT NOT NULL',
229             sub {
230             # this subroutine sets the values of c5 for the whole table
231             my $dbh = shift;
232             my $sth_sel = $dbh->prepare("SELECT c1 FROM t1");
233             my $sth_upd = $dbh->prepare("UPDATE t1 SET c5=? WHERE c1=?");
234             $sth_sel->execute;
235             while (my ($c1) = $sth_sel->fetchrow_array) {
236             my $c5 = ...; # calculate c5 value for the row
237             $sth_upd->execute($c5, $c1);
238             }
239             },
240             'CREATE UNIQUE INDEX i1 ON t2(c1)',
241             ],
242              
243             upgrade_to_v3 => [
244             'ALTER TABLE t2 DROP COLUMN c2',
245             'CREATE TABLE t3 (...)',
246             ],
247              
248             # provided for testing, so we can test migration from v1->v2, v2->v3
249             install_v1 => [
250             'CREATE TABLE IF NOT EXISTS t1 (...)',
251             'CREATE TABLE IF NOT EXISTS t2 (...)',
252             ],
253             }
254              
255             _
256             },
257             dbh => {
258             schema => ['obj*'],
259             summary => 'DBI database handle',
260             req => 1,
261             },
262             create_from_version => {
263             schema => ['int*'],
264             summary => 'Instead of the latest, create from this version',
265             description => <<'_',
266              
267             This can be useful during testing. By default, if given an empty database, this
268             function will use the `install` key of the spec to create the schema from
269             nothing to the latest version. However, if this option is given, function wil
270             use the corresponding `install_v<VERSION>` key in the spec (which must exist)
271             and then upgrade using the `upgrade_to_v<VERSION>` keys to upgrade to the latest
272             version.
273              
274             _
275             },
276             },
277             "x.perinci.sub.wrapper.disable_validate_args" => 1,
278             };
279             sub create_or_update_db_schema {
280 3 50 0 3 1 26 my %args = @_; my $arg_err; { no warnings ('void');require Scalar::Util::Numeric;if (exists($args{'create_from_version'})) { ((defined($args{'create_from_version'})) ? 1 : (($arg_err //= "Required but not specified"),0)) && ((Scalar::Util::Numeric::isint($args{'create_from_version'})) ? 1 : (($arg_err //= "Not of type integer"),0)); if ($arg_err) { return [400, "Invalid argument value for create_from_version: $arg_err"] } }no warnings ('void');require Scalar::Util;if (exists($args{'dbh'})) { ((defined($args{'dbh'})) ? 1 : (($arg_err //= "Required but not specified"),0)) && ((Scalar::Util::blessed($args{'dbh'})) ? 1 : (($arg_err //= "Not of type object"),0)); if ($arg_err) { return [400, "Invalid argument value for dbh: $arg_err"] } }if (!exists($args{'dbh'})) { return [400, "Missing argument: dbh"] } no warnings ('void');if (exists($args{'spec'})) { ((defined($args{'spec'})) ? 1 : (($arg_err //= "Required but not specified"),0)) && ((ref($args{'spec'}) eq 'HASH') ? 1 : (($arg_err //= "Not of type hash"),0)); if ($arg_err) { return [400, "Invalid argument value for spec: $arg_err"] } }if (!exists($args{'spec'})) { return [400, "Missing argument: spec"] } } # VALIDATE_ARGS
  3 50 0 3   7  
  3 50 0 3   447  
  3 50 0 24   31  
  3 100 0     6  
  3 50 0     372  
  3 50       21  
  3 50       6  
  3 50       6349  
  24 50       295790  
  24 50       62  
  24 50       77  
  24 50       1663  
  24 50       1829  
  1 50       28  
  1 50       15  
  0 50       0  
  24         118  
  24         114  
  24         223  
  24         86  
  0         0  
  24         68  
  0         0  
  24         64  
  24         134  
  24         62  
  0         0  
  24         70  
  0         0  
281              
282 24         47 my $spec = $args{spec};
283 24         50 my $dbh = $args{dbh};
284 24         49 my $from_v = $args{create_from_version};
285              
286 24   100     98 my $comp = $spec->{component_name} // 'main';
287 24         75 my $sv_key = _sv_key($comp);
288              
289 24         406 local $dbh->{RaiseError};
290              
291             # first, check current schema version
292              
293             # XXX check spec: latest_v and upgrade_to_v$V must synchronize
294              
295 24         541 my $current_v;
296 24         147 my @has_meta_table = $dbh->tables("", undef, "meta");
297 24 100       17669 if (@has_meta_table) {
298 9         82 ($current_v) = $dbh->selectrow_array(
299             "SELECT value FROM meta WHERE name='$sv_key'");
300             }
301 24   100     1278 $current_v //= 0;
302              
303 24         52 my %provides; # list of tables provided by all components
304 24 100       68 if (@has_meta_table) {
305 9         35 %provides = _get_provides_from_db($dbh);
306             }
307              
308             # list of tables provided by this component
309 24         48 my @provides;
310             GET_PROVIDES:
311             {
312 24 100       45 if ($spec->{provides}) {
  24 100       94  
313 2         3 @provides = @{ $spec->{provides} };
  2         5  
314             } elsif ($spec->{install}) {
315             @provides = _extract_created_tables_from_sql_statements(
316 17         32 @{ $spec->{install} });
  17         65  
317             } else {
318 5 100       22 if ($comp ne 'main') {
319             return [
320 1         22 412, "Both `provides` and `install` spec are not ".
321             "specified, can't get list of tables managed by ".
322             "this component"];
323             }
324             }
325             }
326              
327             CHECK_DEPS:
328             {
329 23 100       51 my $deps = $spec->{deps} or last;
  23         83  
330              
331 3         16 for my $table (sort keys %$deps) {
332 3         7 my $reqver = $deps->{$table};
333 3         5 my $prov = $provides{$table};
334 3 100       30 defined $prov or return [
335             412,
336             "Dependency fails: ".
337             "This component ('$comp') requires table '$table' ".
338             "(version $reqver) which has not been provided by ".
339             "any other component. Perhaps you should install the ".
340             "missing component first."
341             ];
342 2         6 my ($provcomp, $provver) = @$prov;
343 2 100       25 $provver >= $reqver or return [
344             412,
345             "Dependency fails: ".
346             "This component ('$comp') requires table '$table' ".
347             "version $reqver but the database currently only has ".
348             "version $provver (from component '$provcomp'). Perhaps ".
349             "you should upgrade the '$provcomp' component first."
350             ];
351             }
352             } # CHECK_DEPS
353              
354             CHECK_PROVIDES:
355             {
356 21         38 for my $t (@provides) {
  21         50  
357 25         53 my $prov = $provides{$t};
358 25 100       58 next unless $prov;
359 7         17 my ($provcomp, $provver) = @$prov;
360 7 100       33 $provcomp eq $comp or return [
361             412,
362             "Component conflict: ".
363             "This component ('$comp') provides table '$t' ".
364             "but another component ($provcomp version $provver) also ".
365             "provides this table. Perhaps you should update ".
366             "either one or both components first?"
367             ];
368             }
369             } # CHECK_PROVIDES
370              
371 20         41 my $orig_v = $current_v;
372              
373             # perform schema upgrade atomically per version (at least for db that
374             # supports atomic DDL like postgres)
375              
376 20         38 my $latest_v = $spec->{latest_v};
377 20 50       54 if (!defined($latest_v)) {
378 0         0 $latest_v = 1;
379 0         0 for (keys %$spec) {
380 0 0       0 next unless /^upgrade_to_v(\d+)$/;
381 0 0       0 $latest_v = $1 if $1 > $latest_v;
382             }
383             }
384              
385 20 100       68 if ($current_v > $latest_v) {
386 1         44 die "Database schema version ($current_v) is newer than the spec's ".
387             "latest version ($latest_v), you probably need to upgrade ".
388             "the application first\n";
389             }
390              
391             my $code_update_provides = sub {
392 16     16   32 my @k;
393 16         91 for my $t (sort keys %provides) {
394 6         14 my $prov = $provides{$t};
395 6 100       18 next unless $prov->[0] eq $comp;
396 5         10 delete $provides{$t};
397 5         17 push @k, "table.$t";
398             }
399 16 100       50 if (@k) {
400             $dbh->do("DELETE FROM meta WHERE name IN (".
401 3 50       7 join(",", map { $dbh->quote($_) } @k).")")
  5         46  
402             or return $dbh->errstr;
403             }
404 16         346 for my $t (@provides) {
405 22         1080 $provides{$t} = [$comp, $latest_v];
406 22 50       153 $dbh->do("INSERT INTO meta (name, value) VALUES (?, ?)",
407             {},
408             "table.$t",
409             "$comp:$latest_v",
410             ) or return $dbh->errstr;
411             }
412             # success
413 16         1644 "";
414 19         154 };
415              
416             my $code_update_summary = sub {
417 6 100   6   27 my $key = "schema_summary".($comp eq 'main' ? '' : ".$comp");
418 6         57 my ($cur_summary) = $dbh->selectrow_array(
419             "SELECT value FROM meta WHERE name=?", {}, $key);
420 6   50     458 $cur_summary //= "";
421 6   50     26 my $new_summary = $spec->{summary} // "";
422 6 50       32 return "" if $cur_summary eq $new_summary;
423 0 0       0 $dbh->do("REPLACE INTO meta (name, value) VALUES (?, ?)",
424             {},
425             $key,
426             $new_summary,
427             ) or return $dbh->errstr;
428             # success
429 0         0 "";
430 19         81 };
431              
432 19         42 my $begun;
433             my $res;
434              
435 19         40 my $db_state = 'committed';
436              
437             SETUP:
438 19         31 while (1) {
439 31 100       124 last if $current_v >= $latest_v;
440              
441             # we should only begin writing to the database from this step, because
442             # we want to do things atomically (when the database supports it). when
443             # we want to bail out, we don't return() directly but set $res and last
444             # SETUP so we can rollback.
445 25         282 $dbh->begin_work;
446 25         559 $db_state = 'begun';
447              
448             # install
449 25 100       94 if ($current_v == 0) {
450             # create 'meta' table if not exists
451 15 100       46 unless (@has_meta_table) {
452             $dbh->do("CREATE TABLE meta (name VARCHAR(64) NOT NULL PRIMARY KEY, value VARCHAR(255))")
453 14 50       69 or do { $res = [500, "Couldn't create meta table: ".$dbh->errstr]; last SETUP };
  0         0  
  0         0  
454             }
455 15         5713 my $sv_row = $dbh->selectrow_hashref("SELECT * FROM meta WHERE name='$sv_key'");
456 15 50       1774 unless ($sv_row) {
457             $dbh->do("INSERT INTO meta (name,value) VALUES ('$sv_key',0)")
458 15 50       98 or do { $res = [500, "Couldn't insert to meta table: ".$dbh->errstr]; last SETUP };
  0         0  
  0         0  
459             }
460              
461 15 100       782 if ($from_v) {
462             # install from a specific version
463 1 50       8 if ($spec->{"install_v$from_v"}) {
464 1         12 log_debug("Creating version $from_v of database schema (component $comp) ...");
465 1         4 my $i = 0;
466 1         3 for my $step (@{ $spec->{"install_v$from_v"} }) {
  1         5  
467 3         200 $i++;
468 3 50       10 if (ref($step) eq 'CODE') {
469 0 0       0 eval { $step->($dbh) }; if ($@) { $res = [500, "Died when executing code from install_v$from_v\'s step #$i: $@"]; last SETUP }
  0         0  
  0         0  
  0         0  
  0         0  
470             } else {
471 3 50       14 $dbh->do($step) or do { $res = [500, "Failed executing SQL statement from install_v$from_v\'s step #$i: ".$dbh->errstr." (SQL statement: $step)"]; last SETUP };
  0         0  
  0         0  
472             }
473             }
474 1         88 $current_v = $from_v;
475             $dbh->do("UPDATE meta SET value=$from_v WHERE name='$sv_key'")
476 1 50       11 or do { $res = [500, "Couldn't set $sv_key in meta table: ".$dbh->errstr]; last SETUP };
  0         0  
  0         0  
477              
478 1 50       46 if ($current_v == $latest_v) {
479 0 0       0 if (my $up_res = $code_update_provides->()) { $res = [500, "Couldn't update provides information: $up_res"]; last SETUP }
  0         0  
  0         0  
480             }
481              
482 1 50       9018 $dbh->commit or do { $res = [500, "Couldn't commit: ".$dbh->errstr]; last SETUP };
  0         0  
  0         0  
483 1         16 $db_state = 'committed';
484              
485 1         8 next SETUP;
486             } else {
487 0         0 $res = [400, "Error in spec: Can't find 'install_v$from_v' key in spec"];
488 0         0 last SETUP;
489             }
490             } else {
491             # install directly the latest version
492 14 100       50 if ($spec->{install}) {
    50          
493 10         72 log_debug("Creating latest version of database schema (component $comp) ...");
494 10         32 my $i = 0;
495 10         23 for my $step (@{ $spec->{install} }) {
  10         33  
496 15         504 $i++;
497 15 100       47 if (ref($step) eq 'CODE') {
498 1 50       3 eval { $step->($dbh) }; if ($@) { $res = [500, "Died when executing code from install's step #$i: $@"]; last SETUP }
  1         24  
  1         7  
  0         0  
  0         0  
499             } else {
500 14 50       61 $dbh->do($step) or do { $res = [500, "Failed executing SQL statement from install's step #$i: ".$dbh->errstr." (SQL statement: $step)"]; last SETUP };
  0         0  
  0         0  
501             }
502             }
503             $dbh->do("UPDATE meta SET value=$latest_v WHERE name='$sv_key'")
504 10 50       1043 or do { $res = [500, "Couldn't update $sv_key in meta table: ".$dbh->errstr]; last SETUP };
  0         0  
  0         0  
505              
506 10 50       410 if (my $up_res = $code_update_provides->()) { $res = [500, "Couldn't update provides information: $up_res"]; last SETUP }
  0         0  
  0         0  
507              
508 10 50       94421 $dbh->commit or do { $res = [500, "Couldn't commit: ".$dbh->errstr]; last SETUP };
  0         0  
  0         0  
509 10         91 $db_state = 'committed';
510              
511 10         49 last SETUP;
512             } elsif ($spec->{upgrade_to_v1}) {
513             # there is no 'install' but 'upgrade_to_v1', so we upgrade
514             # from v1 to latest
515 4         162 goto UPGRADE;
516             } else {
517 0         0 $res = [400, "Error in spec: Can't find 'install' key in spec"];
518 0         0 last SETUP;
519             }
520             }
521             } # install
522              
523             UPGRADE:
524 14         37 my $next_v = $current_v + 1;
525 14         144 log_debug("Updating database schema (component $comp) from version $current_v to $next_v ...");
526             $spec->{"upgrade_to_v$next_v"}
527 14 50       104 or do { $res = [400, "Error in spec: upgrade_to_v$next_v not specified"]; last SETUP };
  0         0  
  0         0  
528 14         36 my $i = 0;
529 14         64 for my $step (@{ $spec->{"upgrade_to_v$next_v"} }) {
  14         61  
530 25         2507 $i++;
531 25 100       84 if (ref($step) eq 'CODE') {
532 1 50       7 eval { $step->($dbh) }; if ($@) { $res = [500, "Died when executing code from upgrade_to_v$next_v\'s step #$i: $@"]; last SETUP }
  1         9  
  1         29  
  1         7  
  1         5  
533             } else {
534 24 100       143 $dbh->do($step) or do { $res = [500, "Failed executing SQL statement from upgrade_to_v$next_v\'s step #$i: ".$dbh->errstr." (SQL statement: $step)"]; last SETUP };
  2         527  
  2         14  
535             }
536             }
537 11         2808 $current_v = $next_v;
538             $dbh->do("UPDATE meta SET value=$next_v WHERE name='$sv_key'")
539 11 50       94 or do { $res = [500, "Couldn't set $sv_key in meta table: ".$dbh->errstr]; last SETUP };
  0         0  
  0         0  
540              
541 11 100       778 if ($current_v == $latest_v) {
542 6 50       21 if (my $up_res = $code_update_provides->()) { $res = [500, "Couldn't update provides information: $up_res"]; last SETUP }
  0         0  
  0         0  
543 6 50       33 if (my $us_res = $code_update_summary->()) { $res = [500, "Couldn't update summary: $us_res"]; last SETUP }
  0         0  
  0         0  
544             }
545              
546 11 50       106501 $dbh->commit or do { $res = [500, "Couldn't commit: ".$dbh->errstr]; last SETUP };
  0         0  
  0         0  
547 11         136 $db_state = 'committed';
548              
549             } # SETUP
550              
551 19   100     286 $res //= [200, "OK (upgraded from version $orig_v to $latest_v)", {version=>$latest_v}];
552              
553 19 100       88 if ($res->[0] != 200) {
554 3         16 log_error("Failed creating/upgrading schema: %s", $res);
555 3 50       111 $dbh->rollback unless $db_state eq 'committed';
556             } else {
557 16 50       51 $dbh->commit unless $db_state eq 'committed';
558             }
559              
560 19         785 $res;
561             }
562              
563             1;
564             # ABSTRACT: Routine and convention to create/update your application's DB schema
565              
566             __END__
567              
568             =pod
569              
570             =encoding UTF-8
571              
572             =head1 NAME
573              
574             SQL::Schema::Versioned - Routine and convention to create/update your application's DB schema
575              
576             =head1 VERSION
577              
578             This document describes version 0.239 of SQL::Schema::Versioned (from Perl distribution SQL-Schema-Versioned), released on 2021-08-08.
579              
580             =head1 DESCRIPTION
581              
582             To use this module, you typically run the L</"create_or_update_db_schema">()
583             routine at the start of your program/script, e.g.:
584              
585             use DBI;
586             use SQL::Schema::Versioned qw(create_or_update_db_schema);
587             my $spec = { # the schema specification
588             latest_v => 3,
589              
590             install => [
591             "CREATE TABLE t1 (i INT)",
592             "CREATE TABLE t4 (i INT)",
593             ],
594              
595             upgrade_to_v1 => [
596             "CREATE TABLE t1 (i INT)",
597             "CREATE TABLE t2 (i INT)",
598             "CREATE TABLE t3 (i INT)",
599             ],
600             upgrade_to_v2 => [
601             "CREATE TABLE t4 (i INT)",
602             "DROP TABLE t3",
603             ],
604             upgrade_to_v3 => [
605             "DROP TABLE t2",
606             ],
607              
608             install_v2 => [
609             "CREATE TABLE t1 (i INT)",
610             "CREATE TABLE t2 (i INT)",
611             "CREATE TABLE t4 (i INT)",
612             ],
613             };
614             my $dbh = DBI->connect(...);
615             my $res = create_or_update_db_schema(dbh=>$dbh, spec=>$spec);
616             die "Cannot run the application: cannot create/upgrade database schema: $res->[1]"
617             unless $res->[0] == 200;
618              
619             This way, your program automatically creates/updates database schema when run.
620             Users need not know anything.
621              
622             See more elaborate examples in some applications that use this module like
623             L<App::lcpan> or L<SQLite::Counter::Simple>.
624              
625             =head1 BEST PRACTICES
626              
627             It is recommended that after you create the second and subsequent version
628             (C<upgrade_to_v2>, C<upgrade_to_v3>, and so on) you create and keep
629             C<install_v1> so you can test migration from v1->v2, v2->v3, and so on.
630              
631             =head1 FUNCTIONS
632              
633              
634             =head2 create_or_update_db_schema
635              
636             Usage:
637              
638             create_or_update_db_schema(%args) -> [$status_code, $reason, $payload, \%result_meta]
639              
640             Routine and convention to createE<sol>update your application's DB schema.
641              
642             With this routine (and some convention) you can easily create and update
643             database schema for your application in a simple way using pure SQL.
644              
645             I<Version>: version is an integer and starts from 1. Each software release with
646             schema change will bump the version number by 1. Version information is stored
647             in a special table called C<meta> (SELECT value FROM meta WHERE
648             name='schema_version').
649              
650             You supply the SQL statements in C<spec>. C<spec> is a hash which at least must
651             contain the key C<latest_v> (an integer) and C<install> (a series of SQL
652             statements to create the schema from nothing to the latest version).
653              
654             There should also be zero or more C<upgrade_to_v$VERSION> keys, the value of each
655             is a series of SQL statements to upgrade from ($VERSION-1) to $VERSION. So there
656             could be C<upgrade_to_v2>, C<upgrade_to_v3>, and so on up the latest version. This
657             is used to upgrade an existing database from earlier version to the latest.
658              
659             For testing purposes, you can also add one or more C<< install_vE<lt>VERSIONE<gt> >> key,
660             where C<XXX> is an integer, the lowest version number that you still want to
661             support. So, for example, if C<latest_v> is 5 and you still want to support from
662             version 2, you can have an C<install_v2> key containing a series of SQL
663             statements to create the schema at version 2, and C<upgrade_to_v3>,
664             C<upgrade_to_v4>, C<upgrade_to_v5> keys. This way migrations from v2 to v3, v3 to
665             v4, and v4 to v5 can be tested.
666              
667             You can name C<install_v1> key as C<upgrade_to_v1> (to upgrade from 'nothing'
668             a.k.a. v0 to v1), which is basically the same thing.
669              
670             This routine will check the existence of the C<meta> table and the current schema
671             version. If C<meta> table does not exist yet, the SQL statements in C<install>
672             will be executed. The C<meta> table will also be created and a row
673             C<('schema_version', 1)> is added. The (C<schema_summary>, <SUMMARY>) row will
674             also be added if your spec specifies a C<summary>.
675              
676             If C<meta> table already exists, schema version will be read from it and one or
677             more series of SQL statements from C<upgrade_to_v$VERSION> will be executed to
678             bring the schema to the latest version.
679              
680             Aside from SQL statement, the series can also contain coderefs for more complex
681             upgrade process. Each coderef will be called with C<$dbh> as argument and must
682             not die (so to signify failure, you can die from inside the coderef).
683              
684             Currently only tested on MySQL, Postgres, and SQLite. Postgres is recommended
685             because it can do transactional DDL (a failed upgrade in the middle will not
686             cause the database schema state to be inconsistent, e.g. in-between two
687             versions).
688              
689             =head3 Modular schema (components)
690              
691             This routine supports so-called modular schema, where you can separate your
692             database schema into several I<components> (sets of tables) and then declare
693             dependencies among them.
694              
695             For example, say you are writing a stock management application. You divide your
696             application into several components: C<quote> (component that deals with
697             importing stock quotes and querying stock prices), C<portfolio> (component that
698             deals with computing the market value of your portfolio, calculating
699             gains/losses), C<trade> (component that connects to your broker API and perform
700             trading by submitting buy/sell orders).
701              
702             The C<quote> application component manages these tables: C<daily_price>,
703             C<spot_price>. The C<portfolio> application component manages these tables:
704             C<account> (list of accounts in stock brokerages), C<balance> (list of balances),
705             C<tx> (list of transactions). The C<trade> application component manages these
706             tables: C<order> (list of buy/sell orders).
707              
708             The C<portfolio> application component requires price information to be able to
709             calculate unrealized gains/losses. The C<trade> component also needs information
710             from the C<daily_price> e.g. to calculate 52-week momentum, and writes to the
711             C<spot_price> to record intraday prices, and reads/writes from the C<account> and
712             C<balance> tables. Here are the C<spec>s for each component:
713              
714             # spec for the price application component
715             {
716             component_name => 'price',
717             summary => "Price application component",
718             latest_v => 1,
719             provides => ['daily_price', 'spot_price'],
720             install => [...],
721             ...
722             }
723            
724             # spec for the portfolio application component
725             {
726             component_name => 'portfolio',
727             summary => "Portfolio application component",
728             latest_v => 1,
729             provides => ['account', 'balance', 'tx'],
730             deps => {
731             'daily_price' => 1,
732             'spot_price' => 1,
733             },
734             install => [...],
735             ...
736             }
737            
738             # spec for the trade application component
739             {
740             component_name => 'trade',
741             summary => "Trade application component",
742             latest_v => 1,
743             provides => ['order'],
744             deps => {
745             'daily_price' => 1,
746             'spot_price' => 1,
747             'account' => 1,
748             'balance' => 1,
749             },
750             install => [...],
751             ...
752             }
753              
754             You'll notice that the three keys new here are the C<component_name>, C<provides>,
755             and C<deps>.
756              
757             When C<component_name> is set, then instead of the C<schema_version> key in the
758             C<meta> table, your component will use the C<< schema_version.E<lt>COMPONENT_NAMEE<gt> >> key.
759             When C<component_name> is not set, it is assumed to be C<main> and the
760             C<schema_version> key is used in the C<meta> table. The component C<summary>, if
761             specified, will also be written to C<< schema_summary.E<lt>COMPONENT_NAMEE<gt> >> key.
762              
763             C<provides> is an array of tables to help this routine know which table(s) your
764             component create and maintain. If unset, this routine will try to guess from
765             looking at "CREATE TABLE" SQL statements.
766              
767             This routine will create C<< table.E<lt>TABLE_NAMEE<gt> >> keys in the C<meta> table to record
768             which components currently maintain which tables. The value of the key is
769             C<< E<lt>COMPONENT_NAMEE<gt>:E<lt>VERSIONE<gt> >>. When a component no longer maintain a table in the
770             newest version, the corresponding C<< table.E<lt>TABLE_NAMEE<gt> >> row in the C<meta> will
771             also be removed.
772              
773             C<deps> is a hash. The keys are table names that your component requires. The
774             values are integers, meaning the minimum version of the required table (=
775             component version). In the future, more complex dependency relationship and
776             version requirement will be supported.
777              
778             This function is not exported by default, but exportable.
779              
780             Arguments ('*' denotes required arguments):
781              
782             =over 4
783              
784             =item * B<create_from_version> => I<int>
785              
786             Instead of the latest, create from this version.
787              
788             This can be useful during testing. By default, if given an empty database, this
789             function will use the C<install> key of the spec to create the schema from
790             nothing to the latest version. However, if this option is given, function wil
791             use the corresponding C<< install_vE<lt>VERSIONE<gt> >> key in the spec (which must exist)
792             and then upgrade using the C<< upgrade_to_vE<lt>VERSIONE<gt> >> keys to upgrade to the latest
793             version.
794              
795             =item * B<dbh>* => I<obj>
796              
797             DBI database handle.
798              
799             =item * B<spec>* => I<hash>
800              
801             Schema specification, e.g. SQL statements to create and update the schema.
802              
803             Example:
804              
805             {
806             latest_v => 3,
807            
808             # will install version 3 (latest)
809             install => [
810             'CREATE TABLE IF NOT EXISTS t1 (...)',
811             'CREATE TABLE IF NOT EXISTS t2 (...)',
812             'CREATE TABLE t3 (...)',
813             ],
814            
815             upgrade_to_v2 => [
816             'ALTER TABLE t1 ADD COLUMN c5 INT NOT NULL',
817             sub {
818             # this subroutine sets the values of c5 for the whole table
819             my $dbh = shift;
820             my $sth_sel = $dbh->prepare("SELECT c1 FROM t1");
821             my $sth_upd = $dbh->prepare("UPDATE t1 SET c5=? WHERE c1=?");
822             $sth_sel->execute;
823             while (my ($c1) = $sth_sel->fetchrow_array) {
824             my $c5 = ...; # calculate c5 value for the row
825             $sth_upd->execute($c5, $c1);
826             }
827             },
828             'CREATE UNIQUE INDEX i1 ON t2(c1)',
829             ],
830            
831             upgrade_to_v3 => [
832             'ALTER TABLE t2 DROP COLUMN c2',
833             'CREATE TABLE t3 (...)',
834             ],
835            
836             # provided for testing, so we can test migration from v1->v2, v2->v3
837             install_v1 => [
838             'CREATE TABLE IF NOT EXISTS t1 (...)',
839             'CREATE TABLE IF NOT EXISTS t2 (...)',
840             ],
841             }
842              
843              
844             =back
845              
846             Returns an enveloped result (an array).
847              
848             First element ($status_code) is an integer containing HTTP-like status code
849             (200 means OK, 4xx caller error, 5xx function error). Second element
850             ($reason) is a string containing error message, or something like "OK" if status is
851             200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
852             element (%result_meta) is called result metadata and is optional, a hash
853             that contains extra information, much like how HTTP response headers provide additional metadata.
854              
855             Return value: (any)
856              
857             =head1 FAQ
858              
859             =head2 Why use this module instead of other similar solution?
860              
861             Mainly simplicity. I write simple application which is often self-contained in a
862             single module/script. This module works with embedded SQL statements instead of
863             having to put SQL in separate files/subdirectory.
864              
865             =head2 How do I see each SQL statement as it is being executed?
866              
867             Try using L<Log::ger::DBI::Query>, e.g.:
868              
869             % perl -MLog::ger::DBI::Query -MLog::ger::Output=Screen -MLog::ger::Level::trace yourapp.pl ...
870              
871             =head1 HOMEPAGE
872              
873             Please visit the project's homepage at L<https://metacpan.org/release/SQL-Schema-Versioned>.
874              
875             =head1 SOURCE
876              
877             Source repository is at L<https://github.com/perlancar/perl-SQL-Schema-Versioned>.
878              
879             =head1 BUGS
880              
881             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=SQL-Schema-Versioned>
882              
883             When submitting a bug or request, please include a test-file or a
884             patch to an existing test-file that illustrates the bug or desired
885             feature.
886              
887             =head1 SEE ALSO
888              
889             Some other database migration tools on CPAN:
890              
891             =over
892              
893             =item * L<DBIx::Migration>
894              
895             Pretty much similar to this module, with support for downgrades. OO style, SQL
896             in separate files/subdirectory.
897              
898             =item * L<Database::Migrator>
899              
900             Pretty much similar. OO style, SQL in separate files/subdirectory. Perl scripts
901             can also be executed for each version upgrade. Meta table is configurable
902             (default recommended is 'AppliedMigrations').
903              
904             =item * L<sqitch>
905              
906             A more proper database change management tool with dependency resolution and VCS
907             awareness. No numbering. Command-line script and Perl library provided. Looks
908             pretty awesome and something which I hope to use for more complex applications.
909              
910             =back
911              
912             =head1 AUTHOR
913              
914             perlancar <perlancar@cpan.org>
915              
916             =head1 CONTRIBUTOR
917              
918             =for stopwords Steven Haryanto
919              
920             Steven Haryanto <sharyanto@cpan.org>
921              
922             =head1 COPYRIGHT AND LICENSE
923              
924             This software is copyright (c) 2021, 2018, 2017, 2015, 2014, 2013 by perlancar@cpan.org.
925              
926             This is free software; you can redistribute it and/or modify it under
927             the same terms as the Perl 5 programming language system itself.
928              
929             =cut