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 )