NAME
SQL::Schema::Versioned - Routine and convention to create/update your application's DB schema
VERSION
This document describes version 0.239 of SQL::Schema::Versioned (from Perl distribution SQL-Schema-Versioned), released on 2021-08-08.
DESCRIPTION
To use this module, you typically run the "create_or_update_db_schema"() routine at the start of your program/script, e.g.:
use DBI;
use SQL::Schema::Versioned qw(create_or_update_db_schema);
my $spec = { # the schema specification
latest_v => 3,
install => [
"CREATE TABLE t1 (i INT)",
"CREATE TABLE t4 (i INT)",
],
upgrade_to_v1 => [
"CREATE TABLE t1 (i INT)",
"CREATE TABLE t2 (i INT)",
"CREATE TABLE t3 (i INT)",
],
upgrade_to_v2 => [
"CREATE TABLE t4 (i INT)",
"DROP TABLE t3",
],
upgrade_to_v3 => [
"DROP TABLE t2",
],
install_v2 => [
"CREATE TABLE t1 (i INT)",
"CREATE TABLE t2 (i INT)",
"CREATE TABLE t4 (i INT)",
],
};
my $dbh = DBI->connect(...);
my $res = create_or_update_db_schema(dbh=>$dbh, spec=>$spec);
die "Cannot run the application: cannot create/upgrade database schema: $res->[1]"
unless $res->[0] == 200;
This way, your program automatically creates/updates database schema when run. Users need not know anything.
See more elaborate examples in some applications that use this module like App::lcpan or SQLite::Counter::Simple.
BEST PRACTICES
It is recommended that after you create the second and subsequent version (upgrade_to_v2
, upgrade_to_v3
, and so on) you create and keep install_v1
so you can test migration from v1->v2, v2->v3, and so on.
FUNCTIONS
create_or_update_db_schema
Usage:
create_or_update_db_schema(%args) -> [$status_code, $reason, $payload, \%result_meta]
Routine and convention to create/update your application's DB schema.
With this routine (and some convention) you can easily create and update database schema for your application in a simple way using pure SQL.
Version: version is an integer and starts from 1. Each software release with schema change will bump the version number by 1. Version information is stored in a special table called meta
(SELECT value FROM meta WHERE name='schema_version').
You supply the SQL statements in spec
. spec
is a hash which at least must contain the key latest_v
(an integer) and install
(a series of SQL statements to create the schema from nothing to the latest version).
There should also be zero or more upgrade_to_v$VERSION
keys, the value of each is a series of SQL statements to upgrade from ($VERSION-1) to $VERSION. So there could be upgrade_to_v2
, upgrade_to_v3
, and so on up the latest version. This is used to upgrade an existing database from earlier version to the latest.
For testing purposes, you can also add one or more install_v<VERSION>
key, where XXX
is an integer, the lowest version number that you still want to support. So, for example, if latest_v
is 5 and you still want to support from version 2, you can have an install_v2
key containing a series of SQL statements to create the schema at version 2, and upgrade_to_v3
, upgrade_to_v4
, upgrade_to_v5
keys. This way migrations from v2 to v3, v3 to v4, and v4 to v5 can be tested.
You can name install_v1
key as upgrade_to_v1
(to upgrade from 'nothing' a.k.a. v0 to v1), which is basically the same thing.
This routine will check the existence of the meta
table and the current schema version. If meta
table does not exist yet, the SQL statements in install
will be executed. The meta
table will also be created and a row ('schema_version', 1)
is added. The (schema_summary
, <SUMMARY>) row will also be added if your spec specifies a summary
.
If meta
table already exists, schema version will be read from it and one or more series of SQL statements from upgrade_to_v$VERSION
will be executed to bring the schema to the latest version.
Aside from SQL statement, the series can also contain coderefs for more complex upgrade process. Each coderef will be called with $dbh
as argument and must not die (so to signify failure, you can die from inside the coderef).
Currently only tested on MySQL, Postgres, and SQLite. Postgres is recommended because it can do transactional DDL (a failed upgrade in the middle will not cause the database schema state to be inconsistent, e.g. in-between two versions).
Modular schema (components)
This routine supports so-called modular schema, where you can separate your database schema into several components (sets of tables) and then declare dependencies among them.
For example, say you are writing a stock management application. You divide your application into several components: quote
(component that deals with importing stock quotes and querying stock prices), portfolio
(component that deals with computing the market value of your portfolio, calculating gains/losses), trade
(component that connects to your broker API and perform trading by submitting buy/sell orders).
The quote
application component manages these tables: daily_price
, spot_price
. The portfolio
application component manages these tables: account
(list of accounts in stock brokerages), balance
(list of balances), tx
(list of transactions). The trade
application component manages these tables: order
(list of buy/sell orders).
The portfolio
application component requires price information to be able to calculate unrealized gains/losses. The trade
component also needs information from the daily_price
e.g. to calculate 52-week momentum, and writes to the spot_price
to record intraday prices, and reads/writes from the account
and balance
tables. Here are the spec
s for each component:
# spec for the price application component
{
component_name => 'price',
summary => "Price application component",
latest_v => 1,
provides => ['daily_price', 'spot_price'],
install => [...],
...
}
# spec for the portfolio application component
{
component_name => 'portfolio',
summary => "Portfolio application component",
latest_v => 1,
provides => ['account', 'balance', 'tx'],
deps => {
'daily_price' => 1,
'spot_price' => 1,
},
install => [...],
...
}
# spec for the trade application component
{
component_name => 'trade',
summary => "Trade application component",
latest_v => 1,
provides => ['order'],
deps => {
'daily_price' => 1,
'spot_price' => 1,
'account' => 1,
'balance' => 1,
},
install => [...],
...
}
You'll notice that the three keys new here are the component_name
, provides
, and deps
.
When component_name
is set, then instead of the schema_version
key in the meta
table, your component will use the schema_version.<COMPONENT_NAME>
key. When component_name
is not set, it is assumed to be main
and the schema_version
key is used in the meta
table. The component summary
, if specified, will also be written to schema_summary.<COMPONENT_NAME>
key.
provides
is an array of tables to help this routine know which table(s) your component create and maintain. If unset, this routine will try to guess from looking at "CREATE TABLE" SQL statements.
This routine will create table.<TABLE_NAME>
keys in the meta
table to record which components currently maintain which tables. The value of the key is <COMPONENT_NAME>:<VERSION>
. When a component no longer maintain a table in the newest version, the corresponding table.<TABLE_NAME>
row in the meta
will also be removed.
deps
is a hash. The keys are table names that your component requires. The values are integers, meaning the minimum version of the required table (= component version). In the future, more complex dependency relationship and version requirement will be supported.
This function is not exported by default, but exportable.
Arguments ('*' denotes required arguments):
create_from_version => int
Instead of the latest, create from this version.
This can be useful during testing. By default, if given an empty database, this function will use the
install
key of the spec to create the schema from nothing to the latest version. However, if this option is given, function wil use the correspondinginstall_v<VERSION>
key in the spec (which must exist) and then upgrade using theupgrade_to_v<VERSION>
keys to upgrade to the latest version.dbh* => obj
DBI database handle.
spec* => hash
Schema specification, e.g. SQL statements to create and update the schema.
Example:
{ latest_v => 3, # will install version 3 (latest) install => [ 'CREATE TABLE IF NOT EXISTS t1 (...)', 'CREATE TABLE IF NOT EXISTS t2 (...)', 'CREATE TABLE t3 (...)', ], upgrade_to_v2 => [ 'ALTER TABLE t1 ADD COLUMN c5 INT NOT NULL', sub { # this subroutine sets the values of c5 for the whole table my $dbh = shift; my $sth_sel = $dbh->prepare("SELECT c1 FROM t1"); my $sth_upd = $dbh->prepare("UPDATE t1 SET c5=? WHERE c1=?"); $sth_sel->execute; while (my ($c1) = $sth_sel->fetchrow_array) { my $c5 = ...; # calculate c5 value for the row $sth_upd->execute($c5, $c1); } }, 'CREATE UNIQUE INDEX i1 ON t2(c1)', ], upgrade_to_v3 => [ 'ALTER TABLE t2 DROP COLUMN c2', 'CREATE TABLE t3 (...)', ], # provided for testing, so we can test migration from v1->v2, v2->v3 install_v1 => [ 'CREATE TABLE IF NOT EXISTS t1 (...)', 'CREATE TABLE IF NOT EXISTS t2 (...)', ], }
Returns an enveloped result (an array).
First element ($status_code) is an integer containing HTTP-like status code (200 means OK, 4xx caller error, 5xx function error). Second element ($reason) is a string containing error message, or something like "OK" if status is 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 element (%result_meta) is called result metadata and is optional, a hash that contains extra information, much like how HTTP response headers provide additional metadata.
Return value: (any)
FAQ
Why use this module instead of other similar solution?
Mainly simplicity. I write simple application which is often self-contained in a single module/script. This module works with embedded SQL statements instead of having to put SQL in separate files/subdirectory.
How do I see each SQL statement as it is being executed?
Try using Log::ger::DBI::Query, e.g.:
% perl -MLog::ger::DBI::Query -MLog::ger::Output=Screen -MLog::ger::Level::trace yourapp.pl ...
HOMEPAGE
Please visit the project's homepage at https://metacpan.org/release/SQL-Schema-Versioned.
SOURCE
Source repository is at https://github.com/perlancar/perl-SQL-Schema-Versioned.
BUGS
Please report any bugs or feature requests on the bugtracker website https://rt.cpan.org/Public/Dist/Display.html?Name=SQL-Schema-Versioned
When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature.
SEE ALSO
Some other database migration tools on CPAN:
-
Pretty much similar to this module, with support for downgrades. OO style, SQL in separate files/subdirectory.
-
Pretty much similar. OO style, SQL in separate files/subdirectory. Perl scripts can also be executed for each version upgrade. Meta table is configurable (default recommended is 'AppliedMigrations').
-
A more proper database change management tool with dependency resolution and VCS awareness. No numbering. Command-line script and Perl library provided. Looks pretty awesome and something which I hope to use for more complex applications.
AUTHOR
perlancar <perlancar@cpan.org>
CONTRIBUTOR
Steven Haryanto <sharyanto@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2021, 2018, 2017, 2015, 2014, 2013 by perlancar@cpan.org.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.