The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use warnings;
use strict;

package Jifty::Handle;

=head1 NAME

Jifty::Handle -- A database handle class for Jifty

=head1 DESCRIPTION

A wrapper around Jifty::DBI::Handle which is aware of versions in the
database

=cut

use Jifty::Util;
our @ISA;

=head1 METHODS

=head2 new PARAMHASH

This class method instantiates a new L<Jifty::Handle> object. This
object deals with database handles for the system.  After it is
created, it will be a subclass of L<Jifty::DBI::Handle>.

=cut

# Setup database handle based on config data
sub new {
    my $class = shift;

    my $driver = Jifty->config->framework('Database')->{'Driver'};
    if ( $driver eq 'Oracle' ) {
        $ENV{'NLS_LANG'}  = "AMERICAN_AMERICA.AL32UTF8";
        $ENV{'NLS_NCHAR'} = "AL32UTF8";
    }

    # We do this to avoid Jifty::DBI::Handle's magic reblessing, because
    # it breaks subclass methods.
    my $driver_class = "Jifty::DBI::Handle::" . $driver;
    Jifty::Util->require($driver_class);

    die "No such handle class as $driver_class. ",
        "Check your spelling and check that your Jifty installation and ",
        "related modules (especially Jifty::DBI) are up to date."
        unless $driver_class->can('isa');

    unshift @ISA, $driver_class;
    return $class->SUPER::new();
}

=head2 canonical_database_name

Returns the canonical name of the application's database (the actual name that will
be given to the database driver).  This name is a lower-case version of the C<Database>
argument in the C<Database> section of the framework config.

For SQLite databases (where the database name is actually a filename), this also converts
a relative path into an absolute path based at the application root.

=cut

sub canonical_database_name {
    my $self_or_class = shift;
    my $db_config     = Jifty->config->framework('Database');

    # XXX TODO consider canonicalizing to all-lowercase, once there are no
    # legacy databases
    my $db = $db_config->{'Database'};

    if ( $db_config->{'Driver'} =~ /SQLite/ ) {
        $db = Jifty::Util->absolute_path($db);
        # ':' is not allowed in a (esp.) Win32 path.
        # however, this may break someone's existing setup, so
        # added existence check (Shawn's advice)
        $db =~ s{::}{-}g unless -e $db;
    }

    return $db;
}

=head2 connect ARGS

Like L<Jifty::DBI>'s connect method but pulls the name of the database
from the current L<Jifty::Config>.

=cut

sub connect {
    my $self      = shift;
    my %args      = (@_);
    my %db_config = (
        %{ Jifty->config->framework('Database') },
        Database => $self->canonical_database_name
    );

    my %lc_db_config;

    # Skip the non-dsn keys, but not anything else
    for (
        grep {
            !/^autoupgrade|checkschema|version|forwardcompatible|recordbaseclass|attributes$/i
        } keys %db_config
        )
    {
        $lc_db_config{ lc($_) } = $db_config{$_};
    }
    $self->SUPER::connect( %lc_db_config, %args );
    $self->{db_config} = { %lc_db_config, %args };
    $self->dbh->{LongReadLen} = Jifty->config->framework('MaxAttachmentSize')
        || '10000000';

    # setup attributes
    my $attributes = Jifty->config->framework('Database')->{Attributes} || {};
    for ( keys %$attributes ) {
        $self->dbh->{ lc($_) } = $attributes->{$_};
    }
}

=head2 check_schema_version [pretend => 0|1]

Make sure that we have a recent enough database schema.  If we don't,
then error out.

If C<pretend => 1> is passed, then any auto-upgrade action this might take is
dry-run only.

=cut

sub check_schema_version {
    my $self = shift;
    my %opts = ( pretend => 0, @_ );

    require Jifty::Model::Metadata;
    my $autoup = delete Jifty->config->framework('Database')->{'AutoUpgrade'};

    # Application db version check
    {
        my $appv = Jifty->config->framework('Database')->{'Version'};
        my $dbv = $self->_fetch_dbv;

        unless (defined $dbv) {
            my $error = "Application schema has no version in the database.\n";

            # we can just create it and we'll be up to date
            if ( $autoup ) {
                warn $error;
                warn "Automatically creating your database.\n";
                $self->_create_original_database();
                return 1;
            }

            die "${error}Please run `bin/jifty schema --setup` to create the database.\n";
        }

        unless ( version->new($appv) == version->new($dbv) ) {

        # if app version is older than db version, but we are still compatible
            my $compat = delete Jifty->config->framework('Database')
                ->{'ForwardCompatible'} || $appv;
            if (   version->new($appv) > version->new($dbv)
                || version->new($compat) < version->new($dbv) )
            {
                my $error = 
                    "Application schema version in database ($dbv) doesn't match application schema version ($appv)\n";
                if ( $autoup ) {
                    warn $error;
                    warn
                        "Automatically upgrading your database to match the current application schema.\n";
                    $self->_upgrade_schema('print' => $opts{pretend});
                } else {
                    die "${error}Please run `bin/jifty schema --setup` to upgrade the database.\n";
                }
            }
        }
    }

    # Jifty db version check
    {

        # If we got here, the application had a version (somehow) so
        # this is an upgrade.  If $dbv is undef, it's because it's
        # from before when the _jifty_metadata table existed.
        my $dbv
            = version->new( Jifty::Model::Metadata->load("jifty_db_version")
                || '0.60426' );
        my $appv = version->new($Jifty::VERSION);
        unless ( $appv == $dbv ) {
            my $error =
                "Internal jifty schema version in database ($dbv) doesn't match running jifty version ($appv)\n";
            if ($autoup) {
                warn $error;
                warn
                    "Automatically upgrading your database to match the current Jifty schema\n";
                $self->_upgrade_schema('print' => $opts{pretend});
            } else {
                die "${error}Please run `bin/jifty schema --setup` to upgrade the database.\n";
            }
        }
    }

    # Plugin version check
    for my $plugin ( Jifty->plugins ) {
        my $plugin_class = ref $plugin;

        my $dbv
            = Jifty::Model::Metadata->load( $plugin_class . '_db_version' );
        my $appv = version->new( $plugin->version );

        if ( not defined $dbv ) {
            my $error =
                "$plugin_class plugin isn't installed in database\n";
            if ($autoup) {
                warn $error;
                warn
                    "Automatically upgrading your database to match the current plugin schema\n";
                $self->_upgrade_schema('print' => $opts{pretend});
            } else {
                die "${error}Please run `bin/jifty schema --setup` to upgrade the database.\n";
            }
        } elsif (version->new($dbv) < $appv) {
            my $error =
                "$plugin_class plugin version in database ($dbv) doesn't match running plugin version ($appv)\n";
            if ($autoup) {
                warn $error;
                warn
                    "Automatically upgrading your database to match the current plugin schema\n";
                $self->_upgrade_schema('print' => $opts{pretend});
            } else {
                die "${error}Please run `bin/jifty schema --setup` to upgrade the database.\n";
            }
        }
    }

}

=head2 create_database MODE

C<MODE> is either "print" or "execute".

This method either prints the commands necessary to create the
database or actually creates it, depending on the value of MODE.
Returns undef on failure.

=cut

sub create_database {
    my $self     = shift;
    my $mode     = shift || 'execute';
    my $database = $self->canonical_database_name;
    my $driver   = Jifty->config->framework('Database')->{'Driver'};
    my $query    = "CREATE DATABASE $database";
    $query .= " TEMPLATE template0" if $driver =~ /Pg/;
    if ( $mode eq 'print' ) {
        print "$query;\n";
    } elsif ( $driver =~ /SQLite/ ) {
        return 1; # Kinda an optimistic hack
    } else {
        return $self->simple_query($query);
    } 
}

=head2 drop_database MODE

C<MODE> is either "print" or "execute".

This method either prints the commands necessary to drop the database
or actually drops it, depending on the value of MODE.  Returns undef
on failure.

=cut

sub drop_database {
    my $self     = shift;
    my $mode     = shift || 'execute';
    my $database = $self->canonical_database_name;
    my $driver   = Jifty->config->framework('Database')->{'Driver'};
    if ( $mode eq 'print' ) {
        print "DROP DATABASE $database;\n";
    } elsif ( $driver =~ /SQLite/ ) {

        # Win32 complains when you try to unlink open DB
        $self->disconnect if $^O eq 'MSWin32';
        return unlink($database);
    } else {
        local $SIG{__WARN__}
            = sub { warn $_[0] unless $_[0] =~ /exist|couldn't execute/i };
        return $self->simple_query("DROP DATABASE $database");
    }
}

sub _create_original_database {
    my $self = shift;

    my $hack = {};
    require Jifty::Script::Schema;
    bless $hack, "Jifty::Script::Schema";
    $hack->create_all_tables;

    # reconnect for consistency
    # SQLite complains about the schema being changed
    $self->disconnect;
    $self->connect;
}

sub _upgrade_schema {
    my $self = shift;
    my $hack = { @_ };
    require Jifty::Script::Schema;
    bless $hack, "Jifty::Script::Schema";
    $hack->run_upgrades;
}

sub _fetch_dbv {
    my $self = shift;

    my $dbv = Jifty::Model::Metadata->load("application_db_version");
    return $dbv if defined $dbv;

    # First layer of backwards compatibility -- it used to be in _db_version
    eval {
        local $SIG{__WARN__} = sub { };
        my @v = Jifty->handle->fetch_result(
            "SELECT major, minor, rev FROM _db_version");
        return join( ".", @v ) if @v == 3;
    };

    # Second layer -- it was also called the 'key' column, not the data_key column
    eval {
        local $SIG{__WARN__} = sub { };
        return Jifty->handle->fetch_result(
            "SELECT value FROM _jifty_metadata WHERE key = 'application_db_version'"
        );
    };

    # most likely no database exists
    return undef;
}

=head1 AUTHOR

Various folks at BestPractical Solutions, LLC.

=cut

1;