App-JESP
view release on metacpan or search on metacpan
lib/App/JESP.pm view on Meta::CPAN
package App::JESP;
$App::JESP::VERSION = '0.016';
use Moose;
use App::JESP::Plan;
use App::JESP::Colorizer;
use JSON;
use Class::Load;
use DBI;
use DBIx::Simple;
use File::Spec;
use IO::Interactive;
use Log::Any qw/$log/;
use String::Truncate;
# Settings
## DB Connection attrbutes.
has 'dsn' => ( is => 'ro', isa => 'Str', required => 1 );
has 'username' => ( is => 'ro', isa => 'Maybe[Str]', required => 1);
has 'password' => ( is => 'ro', isa => 'Maybe[Str]', required => 1);
has 'home' => ( is => 'ro', isa => 'Str', required => 1 );
## JESP Attributes
has 'prefix' => ( is => 'ro', isa => 'Str', default => 'jesp_' );
has 'driver_class' => ( is => 'ro', isa => 'Str', lazy_build => 1);
# Operational stuff
has 'get_dbh' => ( is => 'ro', isa => 'CodeRef', default => sub{
my ($self) = @_;
return sub{
return DBI->connect( $self->dsn(), $self->username(), $self->password(),
{ RaiseError => 1,
PrintError => 0,
AutoCommit => 1,
});
};
});
has 'dbix_simple' => ( is => 'ro', isa => 'DBIx::Simple', lazy_build => 1);
has 'patches_table_name' => ( is => 'ro', isa => 'Str' , lazy_build => 1);
has 'meta_patches' => ( is => 'ro', isa => 'ArrayRef[HashRef]',
lazy_build => 1 );
has 'plan' => ( is => 'ro', isa => 'App::JESP::Plan', lazy_build => 1);
has 'driver' => ( is => 'ro', isa => 'App::JESP::Driver', lazy_build => 1 );
has 'interactive' => ( is => 'ro' , isa => 'Bool' , lazy_build => 1 );
has 'colorizer' => ( is => 'ro', isa => 'App::JESP::Colorizer', lazy_build => 1 );
has json => ( is => "ro", lazy_build => 1 );
sub _build_json { JSON->new->relaxed(1) }
sub _build_driver{
my ($self) = @_;
return $self->driver_class()->new({ jesp => $self });
}
sub _build_driver_class{
my ($self) = @_;
my $db_name = $self->dbix_simple()->dbh()->{Driver}->{Name};
my $driver_class = 'App::JESP::Driver::'.$db_name;
$log->info("Loading driver ".$driver_class);
Class::Load::load_class( $driver_class );
return $driver_class;
}
sub _build_plan{
my ($self) = @_;
my $file = File::Spec->catfile( $self->home(), 'plan.json' );
unless( ( -e $file ) && ( -r $file ) ){
die "File $file does not exists or is not readable\n";
}
return App::JESP::Plan->new({ file => $file, jesp => $self });
}
sub _build_dbix_simple{
my ($self) = @_;
my $dbh = $self->get_dbh()->();
my $db = DBIx::Simple->connect($dbh);
}
sub _build_patches_table_name{
my ($self) = @_;
return $self->prefix().'patch';
}
# Building the meta patches, in SQLite compatible format.
sub _build_meta_patches{
my ($self) = @_;
return [
lib/App/JESP.pm view on Meta::CPAN
# Runs the code to return a DBIx::Simple::Result
# or die with the given error message (for humans)
#
# Mainly this is for testing that a table exists by attemtpting to select from
# it. Do NOT use that in any other cases.
sub _protect_select{
my ( $self, $code , $message) = @_;
my $result = eval{ $code->(); };
if( my $err = $@ || $result->isa('DBIx::Simple::Dummy') ){
$log->trace("Error doing select: ".( $err || $self->dbix_simple()->error() ) );
die $message."\n";
}
return $result;
}
sub _apply_meta_patch{
my ($self, $meta_patch) = @_;
$log->debug("Applying meta patch ".$meta_patch->{id});
my $sql = $meta_patch->{sql};
my $db = $self->dbix_simple();
$log->debug("Doing ".$sql);
eval{
$db->begin_work();
$db->dbh->do( $sql ) or die "Cannot do '$sql':".$db->dbh->errstr()."\n";
$db->insert( $self->patches_table_name() , { id => $meta_patch->{id} } );
$db->commit();
};
if( my $err = $@ ){
$log->error("Got error $err. ROLLING BACK");
$db->rollback();
die "ERROR APPLYING META PATCH ".$meta_patch->{id}.": $err. ABORTING\n";
};
}
__PACKAGE__->meta->make_immutable();
1;
__END__
=head1 NAME
App::JESP - Just Enough SQL Patches
=cut
=head1 SYNOPSIS
Use the command line utility:
jesp
Or use from your own program (in Perl):
my $jesp = App::JESP->new({
interactive => 0, # No ANSI color
home => 'path/to/jesphome',
dsn => ...,
username => ...,
password => ...
});
$jesp->install();
$jesp->deploy();
=cut
=head1 CONFIGURATION
All JESP configuration must live in a JESP home directory.
This home directory must contain a plan.json file, containing the patching
plan for your DB. See plan.json section below for the format of this file.
=head2 plan.json
This file MUST live in your JESP home directory. It has to contain
a json datastructure like this:
{
"patches": [
{ "id":"foobar_sql", "sql": "CREATE TABLE foobar(id INT PRIMARY KEY)"},
{ "id":"foobar_rel", "file": "patches/morefoobar.sql" }
{ "id":"foobar_abs", "file": "/absolute/path/to/patches/evenmore.sql" },
{ "id":"a_backfill_script", "file": "path/to/executable/file.sh" },
],
}
Patches MUST have a unique C<id> in all the plan, and they can either contain raw SQL
(C<sql> key), or point to a C<file> (absolute, or relative to the JESP home) containing
the SQL.
The C<id> is a VARCHAR(512). While it doesn't indicate any ordering, a simple and useful
way to keep the IDs unique is to provide a date/timestamp (of when the patch was
I<authored>) plus a free form description of the change.
The L<JSON> file is parsed with the relaxed flag, which means it can contain trailing
commas (and # comments). The trailing commas are particularly useful, since commit diffs
and merge conflicts will be contained to the new line that was added.
You are encouraged to look in L<https://github.com/jeteve/App-JESP/tree/master/t> for examples.
=head1 PATCH TYPES
=head1 sql
Simply add the SQL statement to execute in your patch structure:
{
"patches": [
...
{ "id":"2017-11-02: create table foobar", "sql": "CREATE TABLE foobar(id INT PRIMARY KEY)"}
...
}
This is convenient for very short SQL statment.
=head1 sql files
Point to a file that contains SQL statement(s) to be executed. The filename can be either absolute
( run in 0.787 second using v1.01-cache-2.11-cpan-f56aa216473 )