package DBIx::Skinny;
use strict;
use warnings;

our $VERSION = '0.0742';

use DBI;
use DBIx::Skinny::Iterator;
use DBIx::Skinny::DBD;
use DBIx::Skinny::Row;
use DBIx::Skinny::Util;
use DBIx::TransactionManager 1.07;
use Carp ();
use Storable ();

sub import {
    my ($class, %opt) = @_;

    return if $class ne 'DBIx::Skinny';

    my $caller = caller;
    my $connect_info = $opt{connect_info};
    if (! $connect_info ) {
        if ( $connect_info = $opt{setup} ) {
            Carp::carp( "use DBIx::Skinny setup => { ... } has been deprecated. Please use connect_info instead" );
        } else {
            $connect_info = {};
        }
    }

    my $profiler = $opt{profiler};
    if (! $profiler ) {
        if ( $profiler = $connect_info->{profiler} ) {
            Carp::carp( "use DBIx::Skinny connect_info => { profiler => ... } has been deprecated. Please use use DBIx::Skinny profiler => ... instead" );
        } elsif ($ENV{SKINNY_TRACE}) {
            require DBIx::Skinny::Profiler::Trace;
            $profiler = DBIx::Skinny::Profiler::Trace->new;
        } elsif ($ENV{SKINNY_PROFILE}) {
            require DBIx::Skinny::Profiler;
            $profiler = DBIx::Skinny::Profiler->new;
        }
    }
                
    my $schema = $opt{schema} || "$caller\::Schema";

    my $driver_name = _guess_driver_name($connect_info);
    my $_attributes = +{
        check_schema    => defined $connect_info->{check_schema} ? $connect_info->{check_schema} : 1,
        dsn             => $connect_info->{dsn},
        username        => $connect_info->{username},
        password        => $connect_info->{password},
        connect_options => $connect_info->{connect_options},
        on_connect_do   => $connect_info->{on_connect_do},
        dbh             => $connect_info->{dbh}||undef,
        driver_name     => $driver_name,
        schema          => $schema,
        profiler        => $profiler,
        klass           => $caller,
        _common_row_class    => '',
        suppress_row_objects => 0,
        last_pid => $$,
    };

    {
        no strict 'refs';
        push @{"${caller}::ISA"}, $class;
        *{"$caller\::_attributes"} = sub { ref $_[0] ? $_[0] : $_attributes };
        *{"$caller\::attribute"} = sub { Carp::carp("attribute has been deprecated."); $_[0]->_attributes };
    }
    $caller->_setup_dbd;

    DBIx::Skinny::Util::load_class($schema);

    strict->import;
    warnings->import;
}

sub new {
    my ($class, $connection_info) = @_;
    my $attr = $class->_attributes;

    my $new_attr;
    for my $key (qw/check_schema dsn username password connect_options driver_name schema profiler klass _common_row_class suppress_row_objects/) {
        $new_attr->{$key} = $attr->{$key};
    }

    my $self = bless $new_attr, $class;
    $new_attr = $self->_attributes;
    $new_attr->{last_pid} = $$;

    # restore.
    for my $key (qw/dbd profiler dbh connect_options on_connect_do/) {
        $new_attr->{$key} = $attr->{$key};
    }

    if ($connection_info) {
        if ( $connection_info->{on_connect_do} ) {
            $new_attr->{on_connect_do} = $connection_info->{on_connect_do};
        }

        $self->connect_info($connection_info);
        if ($connection_info->{dbh}) {
            $self->set_dbh($connection_info->{dbh});
        } else {
            $self->reconnect;
        }
    }

    return $self;
}

my $schema_checked = 0;
sub schema { 
    my $attribute = $_[0]->_attributes;
    my $schema = $attribute->{schema};
    if ( $attribute->{check_schema} && !$schema_checked ) {
        {
            no strict 'refs'; ## no critic..
            unless ( defined *{"@{[ $schema ]}::schema_info"} ) {
                die "Cannot use schema $schema ( is it really loaded? )";
            }
        };
        $schema_checked=1;
    }
    return $schema;
}

sub profiler {
    my ($class, $sql, $bind) = @_;
    my $attr = $class->_attributes;
    if ($attr->{profiler} && $sql) {
        $attr->{profiler}->record_query($sql, $bind);
    }
    return $attr->{profiler};
}

sub suppress_row_objects {
    my ($class, $mode) = @_;
    return $class->_attributes->{suppress_row_objects} unless defined $mode;
    $class->_attributes->{suppress_row_objects} = $mode;
}

#--------------------------------------------------------------------------------
# for transaction

sub txn_manager  {
    my $class = shift;

    my $dbh = $class->dbh;
    $class->_attributes->{txn_manager} ||= DBIx::TransactionManager->new($dbh);
}

sub in_transaction_check {
    my $class = shift;

    return unless $class->_attributes->{txn_manager};

    return $class->_attributes->{txn_manager}->in_transaction;
}

sub txn_scope {
    my @caller = caller();
    $_[0]->txn_manager->txn_scope(caller => \@caller);
}
sub txn_begin    { $_[0]->txn_manager->txn_begin    }
sub txn_rollback { $_[0]->txn_manager->txn_rollback }
sub txn_commit   { $_[0]->txn_manager->txn_commit   }
sub txn_end      { $_[0]->txn_manager->txn_end      }

#--------------------------------------------------------------------------------
# db handling
sub connect_info {
    my ($class, $connect_info) = @_;

    my $attr = $class->_attributes;

    if ($connect_info) {
        $attr->{dsn} = $connect_info->{dsn};
        $attr->{username} = $connect_info->{username};
        $attr->{password} = $connect_info->{password};
        $attr->{connect_options} = $connect_info->{connect_options};

        $class->_setup_dbd($connect_info);
        return;
    } else {
        return +{
            dsn             => $attr->{dsn},
            username        => $attr->{username},
            password        => $attr->{password},
            connect_options => $attr->{connect_options},
        };
    }
}

sub connect {
    my $class = shift;

    $class->connect_info(@_) if scalar @_ >= 1;

    my $attr = $class->_attributes;
    my $do_connected=0;
    if ( !$attr->{dbh} ) {
        $do_connected=1;
    }
    $attr->{dbh} ||= DBI->connect(
        $attr->{dsn},
        $attr->{username},
        $attr->{password},
        { RaiseError => 1, PrintError => 0, AutoCommit => 1, %{ $attr->{connect_options} || {} } }
    ) or Carp::croak("Connection error: " . $DBI::errstr);

    if ( $do_connected && $attr->{on_connect_do} ) {
        $class->do_on_connect;
    }

    $attr->{dbh};
}

sub reconnect {
    my $class = shift;

    if ( my $info = $class->in_transaction_check ) {
        my $caller = $info->{caller};
        my $pid    = $info->{pid};
        Carp::confess("Detected transaction during a reconnect operation (last known transaction at $caller->[1] line $caller->[2], pid $pid). Refusing to proceed at");
    }

    $class->disconnect();
    $class->connect(@_);
}

sub do_on_connect {
    my $class = shift;

    my $on_connect_do = $class->_attributes->{on_connect_do};
    if (not ref($on_connect_do)) {
        $class->do($on_connect_do);
    } elsif (ref($on_connect_do) eq 'CODE') {
        $on_connect_do->($class);
    } elsif (ref($on_connect_do) eq 'ARRAY') {
        $class->do($_) for @$on_connect_do;
    } else {
        Carp::croak('Invalid on_connect_do: '.ref($on_connect_do));
    }
}

sub disconnect {
    my $class = shift;
    delete $class->_attributes->{txn_manager};
    if (my $dbh = delete $class->_attributes->{dbh}) {
        $dbh->disconnect;
    }
}

sub set_dbh {
    my ($class, $dbh) = @_;
    $class->_attributes->{dbh} = $dbh;
    $class->_setup_dbd({dbh => $dbh});
}

sub _setup_dbd {
    my ($class, $args) = @_;
    my $driver_name = $args ? _guess_driver_name($args) : $class->_attributes->{driver_name};
    $class->_attributes->{driver_name} = $driver_name;
    $class->_attributes->{dbd} = $driver_name ? DBIx::Skinny::DBD->new($driver_name) : undef;
}

sub _guess_driver_name {
    my $args = shift;
    if ($args->{dbh}) {
        return $args->{dbh}->{Driver}->{Name};
    } elsif ($args->{dsn}) {
        my (undef, $driver_name,) = DBI->parse_dsn($args->{dsn}) or Carp::croak "can't parse DSN: @{[ $args->{dsn} ]}";
        return $driver_name
    }
}

sub dbd {
    $_[0]->_attributes->{dbd} or do {
        require Data::Dumper;
        Carp::croak("Attribute 'dbd' is not defined. Either we failed to connect, or the connection has gone away. Current attribute dump: @{[ Data::Dumper::Dumper($_[0]->_attributes) ]}");
    };
}

sub _verify_pid {
    my $class = shift;

    my $attr = $class->_attributes;
    if ( $attr->{last_pid} != $$ and my $dbh = $attr->{dbh}) {
        $attr->{last_pid} = $$;
        $dbh->{InactiveDestroy} = 1;
        if ( my $info = $class->in_transaction_check ) {
            my $pid    = $info->{pid};
            my $caller = $info->{caller};
            Carp::confess("Detected transaction while processing forked child (last known transaction at $caller->[1] line $caller->[2], pid $pid). Refusing to proceed at");
        }
        $attr->{txn_manager} = undef;
        $attr->{dbh}         = undef;
    }
}

sub DESTROY {
    my $self = shift;
    $self->_verify_pid;
}

sub dbh {
    my $class = shift;

    $class->_verify_pid;

    my $dbh = $class->connect;
    unless ($dbh->FETCH('Active') && $dbh->ping) {
        $class->reconnect;
    }
    $class->_attributes->{dbh};
}

#--------------------------------------------------------------------------------
# schema trigger call
sub call_schema_trigger {
    my ($class, $trigger, $schema, $table, $args) = @_;
    $schema->call_trigger($class, $table, $trigger, $args);
}

#--------------------------------------------------------------------------------
sub do {
    my ($class, $sql, $attr, @bind_vars) = @_;
    $class->profiler($sql, @bind_vars ? \@bind_vars : undef);
    my $ret;
    eval { $ret = $class->dbh->do($sql, $attr, @bind_vars) };
    if ($@) {
        $class->_stack_trace('', $sql, @bind_vars ? \@bind_vars : '', $@);
    }
    $ret;
}

sub count {
    my ($class, $table, $column, $where) = @_;

    $column ||= '*';
    my $rs = $class->resultset(
        {
            from   => [$table],
        }
    );

    $rs->add_select("COUNT($column)" =>  'cnt');
    if ($where) {
        $class->_add_where($rs, $where);
    }

    $rs->retrieve->first->cnt;
}

sub resultset {
    my ($class, $args) = @_;
    $args->{skinny} = $class;
    $class->dbd->query_builder_class->new($args);
}

sub search {
    my ($class, $table, $where, $opt) = @_;

    $class->search_rs($table, $where, $opt)->retrieve;
}

sub search_rs {
    my ($class, $table, $where, $opt) = @_;

    my $cols = $opt->{select} || do {
        my $column_info = $class->schema->schema_info->{$table};
        unless ( $column_info ) {
            Carp::croak("schema_info does not exist for table '$table'");
        }
        $column_info->{columns};
    };

    my $rs = $class->resultset(
        {
            select => $cols,
            from   => [$table],
        }
    );

    if ( $where ) {
        $class->_add_where($rs, $where);
    }

    $rs->limit(  $opt->{limit}  ) if $opt->{limit};
    $rs->offset( $opt->{offset} ) if $opt->{offset};

    if (my $terms = $opt->{order_by}) {
        $terms = [$terms] unless ref($terms) eq 'ARRAY';
        my @orders;
        for my $term (@{$terms}) {
            my ($col, $case);
            if (ref($term) eq 'HASH') {
                ($col, $case) = each %$term;
            } else {
                $col  = $term;
                $case = 'ASC';
            }
            push @orders, { column => $col, desc => $case };
        }
        $rs->order(\@orders);
    }

    if (my $terms = $opt->{having}) {
        for my $col (keys %$terms) {
            $rs->add_having($col => $terms->{$col});
        }
    }

    $rs->for_update(1) if $opt->{for_update};

    return $rs;
}

sub single {
    my ($class, $table, $where, $opt) = @_;
    $opt->{limit} = 1;
    $class->search_rs($table, $where, $opt)->retrieve->first;
}

sub search_named {
    my ($class, $sql, $args, $opts, $opt_table_info) = @_;

    $sql = sprintf($sql, @{$opts||[]});
    my %named_bind = %{$args};
    my @bind;
    $sql =~ s{:([A-Za-z_][A-Za-z0-9_]*)}{
        Carp::croak("$1 does not exists in hash") if !exists $named_bind{$1};
        if ( ref $named_bind{$1} && ref $named_bind{$1} eq "ARRAY" ) {
            push @bind, @{ $named_bind{$1} };
            my $tmp = join ',', map { '?' } @{ $named_bind{$1} };
            "( $tmp )";
        } else {
            push @bind, $named_bind{$1};
            '?'
        }
    }ge;

    $class->search_by_sql($sql, \@bind, $opt_table_info);
}

sub search_by_sql {
    my ($class, $sql, $bind, $opt_table_info) = @_;

    my $sth = $class->_execute($sql, $bind);
    return $class->_get_sth_iterator($sql, $sth, $opt_table_info);
}

sub find_or_new {
    my ($class, $table, $args) = @_;
    $class->single($table, $args) or do {
        $class->hash_to_row($table, $args);
    };
}

sub hash_to_row {
    my ($class, $table, $hash) = @_;

    my $row_class = $class->_get_row_class($table, $table);
    my $row = $row_class->new(
        {
            sql            => undef,
            row_data       => $hash,
            skinny         => $class,
            opt_table_info => $table,
        }
    );
    $row->setup;
    $row;
}

sub _get_sth_iterator {
    my ($class, $sql, $sth, $opt_table_info) = @_;

    return DBIx::Skinny::Iterator->new(
        skinny         => $class,
        sth            => $sth,
        sql            => $sql,
        row_class      => $class->_get_row_class($sql, $opt_table_info),
        opt_table_info => $opt_table_info,
        suppress_objects => $class->suppress_row_objects,
    );
}

sub data2itr {
    my ($class, $table, $data) = @_;

    return DBIx::Skinny::Iterator->new(
        skinny         => $class,
        data           => $data,
        row_class      => $class->_get_row_class($table, $table),
        opt_table_info => $table,
        suppress_objects => $class->suppress_row_objects,
    );
}

sub _guess_table_name {
    my ($class, $sql) = @_;

    if ($sql =~ /\sfrom\s+["`]?([\w]+)["`]?\s*/si) {
        return $1;
    }
    return;
}

sub _get_row_class {
    my ($class, $sql, $table) = @_;

    $table ||= $class->_guess_table_name($sql)||'';
    if ($table && $class->schema->schema_info->{$table}) {
        return $class->schema->schema_info->{$table}->{row_class};
    } else {
        return $class->_attributes->{_common_row_class} ||= do {
            my $row_class = join '::', $class->_attributes->{klass}, 'Row';
            DBIx::Skinny::Util::load_class($row_class) or do {
                no strict 'refs'; @{"$row_class\::ISA"} = ('DBIx::Skinny::Row');
            };
            $row_class;
        };
    }
}

sub _quote {
    my ($label, $quote, $name_sep) = @_;

    return $label if $label eq '*';
    return $quote . $label . $quote if !defined $name_sep;
    return join $name_sep, map { $quote . $_ . $quote } split /\Q$name_sep\E/, $label;
}

sub bind_params {
    my($class, $table, $columns, $sth) = @_;

    my $schema = $class->schema;
    my $dbd    = $class->dbd;
    my $i = 1;
    for my $column (@{ $columns }) {
        my($col, $val) = @{ $column };
        my $type = $schema->column_type($table, $col);
        my $attr = $type ? $dbd->bind_param_attributes($type) : undef;

        my $ref = ref $val;
        if ($ref eq 'ARRAY') {
            $sth->bind_param($i++, $_, $attr) for @$val;
        } elsif (not $ref) {
            $sth->bind_param($i++, $val, $attr);
        } else {
            die "you can't set bind value, arrayref or scalar. you set $ref ref value.";
        }
    }
}

sub _set_columns {
    my ($class, $args, $insert) = @_;

    my $schema = $class->schema;
    my $dbd = $class->dbd;
    my $quote = $dbd->quote;
    my $name_sep = $dbd->name_sep;

    my (@columns, @bind_columns, @quoted_columns);
    for my $col (keys %{ $args }) {
        my $quoted_col = _quote($col, $quote, $name_sep);
        if (ref($args->{$col}) eq 'SCALAR') {
            push @columns, ($insert ? ${ $args->{$col} } :"$quoted_col = " . ${ $args->{$col} });
        } else {
            push @columns, ($insert ? '?' : "$quoted_col = ?");
            push @bind_columns, [$col, $schema->utf8_off($col, $args->{$col})];
        }
        push @quoted_columns, $quoted_col;
    }

    return (\@columns, \@bind_columns, \@quoted_columns);
}

sub _insert_or_replace {
    my ($class, $is_replace, $table, $args) = @_;

    my $schema = $class->schema;

    # deflate
    for my $col (keys %{$args}) {
        $args->{$col} = $schema->call_deflate($col, $args->{$col});
    }

    my ($columns, $bind_columns, $quoted_columns) = $class->_set_columns($args, 1);

    my $sql = $is_replace ? 'REPLACE' : 'INSERT';
    $sql .= " INTO $table\n";
    $sql .= '(' . join(', ', @$quoted_columns) .')' . "\n" .
            'VALUES (' . join(', ', @$columns) . ')' . "\n";

    my $sth = $class->_execute($sql, $bind_columns, $table);
    $class->_close_sth($sth);

    my $pk = $class->schema->schema_info->{$table}->{pk};

    if (defined $pk && not(ref $pk) && not(defined $args->{$pk})) {
        $args->{$pk} = $class->_last_insert_id($table);
    }

    return $args if $class->suppress_row_objects;

    my $row_class = $class->_get_row_class($sql, $table);
    if (! $row_class) {
        Carp::croak( "Could not find row definition for table '$table' (did you make sure to install_table()?)" );
    }

    my $obj = $row_class->new(
        {
            row_data       => $args,
            skinny         => $class,
            opt_table_info => $table,
        }
    );
    $obj->setup;

    $obj;
}

sub _last_insert_id {
    my ($class, $table) = @_;

    my $dbh = $class->dbh;
    my $driver = $class->_attributes->{driver_name};
    if ( $driver eq 'mysql' ) {
        return $dbh->{mysql_insertid};
    } elsif ( $driver eq 'Pg' ) {
        return $dbh->last_insert_id( undef, undef, undef, undef,{ sequence => join( '_', $table, 'id', 'seq' ) } );
    } elsif ( $driver eq 'SQLite' ) {
        return $dbh->func('last_insert_rowid');
    } elsif ( $driver eq 'Oracle' ) {
        return;
    } else {
        Carp::croak "Don't know how to get last insert id for $driver";
    }
}

*create = \*insert;
sub insert {
    my ($class, $table, $args) = @_;

    my $schema = $class->schema;
    $class->call_schema_trigger('pre_insert', $schema, $table, $args);

    my $obj = $class->_insert_or_replace(0, $table, $args);

    $class->call_schema_trigger('post_insert', $schema, $table, $obj);

    $obj;
}

sub replace {
    my ($class, $table, $args) = @_;

    my $schema = $class->schema;
    $class->call_schema_trigger('pre_insert', $schema, $table, $args);

    my $obj = $class->_insert_or_replace(1, $table, $args);

    $class->call_schema_trigger('post_insert', $schema, $table, $obj);

    $obj;
}

sub bulk_insert {
    my ($class, $table, $args) = @_;

    my $code = $class->_attributes->{dbd}->can('bulk_insert') or Carp::croak "dbd don't provide bulk_insert method";
    $code->($class, $table, $args);
}

sub update {
    my ($class, $table, $args, $where) = @_;

    my $schema = $class->schema;
    $class->call_schema_trigger('pre_update', $schema, $table, $args);

    my $values = {};
    for my $col (keys %{$args}) {
       $values->{$col} = $schema->call_deflate($col, $args->{$col});
    }

    my ($columns, $bind_columns, undef) = $class->_set_columns($values, 0);

    my $stmt = $class->resultset;
    $class->_add_where($stmt, $where);
    my @where_values = map {[$_ => $stmt->where_values->{$_}]} @{$stmt->bind_col};

    push @{$bind_columns}, @where_values;

    my $sql = "UPDATE $table SET " . join(', ', @$columns) . ' ' . $stmt->as_sql_where;
    my $sth = $class->_execute($sql, $bind_columns, $table);

    my $rows = $sth->rows;

    $class->_close_sth($sth);
    $class->call_schema_trigger('post_update', $schema, $table, $rows);

    return $rows;
}

sub update_by_sql {
    my ($class, $sql, $bind) = @_;

    Carp::carp( 'update_by_sql has been deprecated. Please use $skinny->do($sql, undef, @bind)' );
    $class->do($sql, undef, @$bind);
}

sub delete {
    my ($class, $table, $where) = @_;

    my $schema = $class->schema;
    $class->call_schema_trigger('pre_delete', $schema, $table, $where);

    my $stmt = $class->resultset(
        {
            from => [$table],
        }
    );

    $class->_add_where($stmt, $where);

    my $sql = "DELETE " . $stmt->as_sql;
    my @where_values = map {[$_ => $stmt->where_values->{$_}]} @{$stmt->bind_col};
    my $sth = $class->_execute($sql, \@where_values, $table);
    my $rows = $sth->rows;

    $class->call_schema_trigger('post_delete', $schema, $table, $rows);

    $class->_close_sth($sth);
    $rows;
}

sub delete_by_sql {
    my ($class, $sql, $bind) = @_;

    Carp::carp( 'delete_by_sql has been deprecated. Please use $skinny->do($sql, undef, @bind)' );
    $class->do($sql, undef, @$bind);
}

*find_or_insert = \*find_or_create;
sub find_or_create {
    my ($class, $table, $args) = @_;
    my $row = $class->single($table, $args);
    return $row if $row;
    $class->insert($table, $args)->refetch;
}

sub _add_where {
    my ($class, $stmt, $where) = @_;
    for my $col (keys %{$where}) {
        $stmt->add_where($col => $where->{$col});
    }
}

sub _execute {
    my ($class, $stmt, $args, $table) = @_;

    my ($sth, $bind);
    if ($table) {
        $bind = [map {(ref $_->[1]) eq 'ARRAY' ? @{$_->[1]} : $_->[1]} @$args];
        $class->profiler($stmt, $bind);
        eval {
            $sth = $class->dbh->prepare($stmt) or die $class->dbh->errstr;
            $class->bind_params($table, $args, $sth);
            $sth->execute;
        };
    } else {
        $bind = $args;
        $class->profiler($stmt, $bind);
        eval {
            $sth = $class->dbh->prepare($stmt) or die $class->dbh->errstr;
            $sth->execute(@{$args});
        };
    }

    if ($@) {
        $class->_stack_trace($sth, $stmt, $bind, $@);
    }
    return $sth;
}

# stack trace
sub _stack_trace {
    my ($class, $sth, $stmt, $bind, $reason) = @_;
    require Data::Dumper;

    if ($sth) {
        $class->_close_sth($sth);
    }

    $stmt =~ s/\n/\n          /gm;
    Carp::croak sprintf <<"TRACE", $reason, $stmt, Data::Dumper::Dumper($bind);
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ DBIx::Skinny 's Exception @@@@@
Reason  : %s
SQL     : %s
BIND    : %s
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
TRACE
}

sub _close_sth {
    my ($class, $sth) = @_;
    $sth->finish;
    undef $sth;
}

1;

__END__
=head1 NAME

DBIx::Skinny - simple DBI wrapper/ORMapper

=head1 SYNOPSIS

create your db model base class.

    package Your::Model;
    use DBIx::Skinny connect_info => {
        dsn => 'dbi:SQLite:',
        username => '',
        password => '',
    };
    1;
    
create your db schema class.
See DBIx::Skinny::Schema for docs on defining schema class.

    package Your::Model::Schema;
    use DBIx::Skinny::Schema;
    
    install_table user => schema {
        pk 'id';
        columns qw/
            id
            name
        /;
    };
    1;
    
in your script.

    use Your::Model;
    
    my $skinny = Your::Model->new;
    # insert new record.
    my $row = $skinny->insert('user',
        {
            id   => 1,
        }
    );
    $row->update({name => 'nekokak'});

    $row = $skinny->search_by_sql(q{SELECT id, name FROM user WHERE id = ?}, [ 1 ]);
    $row->delete('user');

=head1 DESCRIPTION

DBIx::Skinny is simple DBI wrapper and simple O/R Mapper.
It aims to be lightweight, with minimal dependencies so it's easier to install. 

=head1 ARCHITECTURE

DBIx::Skinny classes are comprised of three distinct components:

=head2 MODEL

The C<model> is where you say 

    package MyApp::Model;
    use DBIx::Skinny;

This is the entry point to using DBIx::Skinny. You connect, insert, update, delete, select stuff using this object.

=head2 SCHEMA

The C<schema> is a simple class that describes your table definitions. Note that this is different from DBIx::Class terms. DBIC's schema is equivalent to DBIx::Skinny's model + schema, where the actual schema information is scattered across the result classes.

In DBIx::Skinny, you simply use DBIx::Skinny::Schema's domain specific languaage to define a set of tables

    package MyApp::Model::Schema;
    use DBIx::Skinny::Schema;

    install_table $table_name => schema {
        pk $primary_key_column;
        columns qw(
            column1
            column2
            column3
        );
    }

    ... and other tables ...

=head2 ROW

Unlike DBIx::Class, you don't need to have a set of classes that represent a row type (i.e. "result" classes in DBIC terms). In DBIx::Skinny, the row objects are blessed into anonymous classes that inherit from DBIx::Skinny::Row, so you don't have to create these classes if you just want to use some simple queries.

If you want to define methods to be performed by your row objects, simply create a row class like so:

    package MyApp::Model::Row::CamelizedTableName;
    use base qw(DBIx::Skinny::Row);

Note that your table name will be camelized using String::CamelCase.

=head1 METHODS

DBIx::Skinny provides a number of methods to all your classes, 

=over

=item $skinny->new([\%connection_info])

create your skinny instance.
It is possible to use it even by the class method.

$connection_info is optional argment.

When $connection_info is specified,
new method connect new DB connection from $connection_info.

When $connection_info is not specified,
it becomes use already setup connection or it doesn't do at all.

example:

    my $db = Your::Model->new;

or

    # connect new database connection.
    my $db = Your::Model->new(+{
        dsn      => $dsn,
        username => $username,
        password => $password,
        connect_options => $connect_options,
    });

or

    my $dbh = DBI->connect();
    my $db = Your::Model->new(+{
        dbh => $dbh,
    });

=item $skinny->insert($table_name, \%row_data)

insert new record and get inserted row object.

if insert to table has auto increment then return $row object with fill in key column by last_insert_id.

example:

    my $row = Your::Model->insert('user',{
        id   => 1,
        name => 'nekokak',
    });
    say $row->id; # show last_insert_id()

or

    my $db = Your::Model->new;
    my $row = $db->insert('user',{
        id   => 1,
        name => 'nekokak',
    });

=item $skinny->create($table_name, \%row_data)

insert method alias.

=item $skinny->replace($table_name, \%row_data)

The data that already exists is replaced. 

example:

    Your::Model->replace('user',{
        id   => 1,
        name => 'tokuhirom',
    });

or 

    my $db = Your::Model->new;
    my $row = $db->replace('user',{
        id   => 1,
        name => 'tokuhirom',
    });

=item $skinny->bulk_insert($table_name, \@rows_data)

Accepts either an arrayref of hashrefs.
each hashref should be a structure suitable
forsubmitting to a Your::Model->insert(...) method.

insert many record by bulk.

example:

    Your::Model->bulk_insert('user',[
        {
            id   => 1,
            name => 'nekokak',
        },
        {
            id   => 2,
            name => 'yappo',
        },
        {
            id   => 3,
            name => 'walf443',
        },
    ]);

=item $skinny->update($table_name, \%update_row_data, [\%update_condition])

$update_condition is optional argment.

update record.

example:

    my $update_row_count = Your::Model->update('user',{
        name => 'nomaneko',
    },{ id => 1 });

or 

    # see) DBIx::Skinny::Row's POD
    my $row = Your::Model->single('user',{id => 1});
    $row->update({name => 'nomaneko'});

=item $skinny->update_by_sql($sql, [\@bind_values])

update record by specific sql. return update row count.

example:

    my $update_row_count = Your::Model->update_by_sql(
        q{UPDATE user SET name = ?},
        ['nomaneko']
    );

=item $skinny->delete($table, \%delete_condition)

delete record. return delete row count.

example:

    my $delete_row_count = Your::Model->delete('user',{
        id => 1,
    });

or

    # see) DBIx::Skinny::Row's POD
    my $row = Your::Model->single('user', {id => 1});
    $row->delete

=item $skinny->delete_by_sql($sql, \@bind_values)

delete record by specific sql. return delete row count.

example:

    my $delete_row_count = Your::Model->delete_by_sql(
        q{DELETE FROM user WHERE id = ?},
        [1]
    });

=item $skinny->find_or_create($table, \%values)

create record if not exsists record.

return DBIx::Skinny::Row's instance object.

example:

    my $row = Your::Model->find_or_create('usr',{
        id   => 1,
        name => 'nekokak',
    });

NOTICE: find_or_create has bug.

reproduction example:

    my $row = Your::Model->find_or_create('user',{
        id   => 1,
        name => undef,
    });

In this case, it becomes an error by insert.

If you want to do the same thing in this case,

    my $row = Your::Model->single('user', {
        id   => 1,
        name => \'IS NULL',
    })
    unless ($row) {
        Your::Model->insert('user', {
            id => 1,
        });
    }

Because the interchangeable rear side is lost, it doesn't mend. 

=item $skinny->find_or_insert($table, \%values)

find_or_create method alias.

=item $skinny->search($table_name, [\%search_condition, [\%search_attr]])

simple search method.
search method get DBIx::Skinny::Iterator's instance object.

see L<DBIx::Skinny::Iterator>

get iterator:

    my $itr = Your::Model->search('user',{id => 1},{order_by => 'id'});

get rows:

    my @rows = Your::Model->search('user',{id => 1},{order_by => 'id'});

See L</ATTRIBUTES> for more information for \%search_attr.

=item $skinny->search_rs($table_name, [\%search_condition, [\%search_attr]])

simple search method.
search_rs method always get DBIx::Skinny::Iterator's instance object.

This method does the same exact thing as search() except it will always return a iterator, even in list context.

=item $skinny->single($table_name, \%search_condition)

get one record.
give back one case of the beginning when it is acquired plural records by single method.

    my $row = Your::Model->single('user',{id =>1});

=item $skinny->resultset(\%options)

resultset case:

    my $rs = Your::Model->resultset(
        {
            select => [qw/id name/],
            from   => [qw/user/],
        }
    );
    $rs->add_where('name' => {op => 'like', value => "%neko%"});
    $rs->limit(10);
    $rs->offset(10);
    $rs->order({ column => 'id', desc => 'DESC' });
    my $itr = $rs->retrieve;

=item $skinny->count($table_name, $target_column, [\%search_condition])

get simple count

    my $cnt = Your::Model->count('user' => 'id', {age => 30});

=item $skinny->search_named($sql, [\%bind_values, [\@sql_parts, [$table_name]]])

execute named query

    my $itr = Your::Model->search_named(q{SELECT * FROM user WHERE id = :id}, {id => 1});

If you give ArrayRef to value, that is expanded to "(?,?,?,?)" in SQL.
It's useful in case use IN statement.

    # SELECT * FROM user WHERE id IN (?,?,?);
    # bind [1,2,3]
    my $itr = Your::Model->search_named(q{SELECT * FROM user WHERE id IN :ids}, {ids => [1, 2, 3]});

If you give \@sql_parts,

    # SELECT * FROM user WHERE id IN (?,?,?) AND unsubscribed_at IS NOT NULL;
    # bind [1,2,3]
    my $itr = Your::Model->search_named(q{SELECT * FROM user WHERE id IN :ids %s}, {ids => [1, 2, 3]}, ['AND unsubscribed_at IS NOT NULL']);

If you give table_name. It is assumed the hint that makes DBIx::Skinny::Row's Object.

=item $skinny->search_by_sql($sql, [\@bind_vlues, [$table_name]])

execute your SQL

    my $itr = Your::Model->search_by_sql(q{
        SELECT
            id, name
        FROM
            user
        WHERE
            id = ?
    },[ 1 ]);

If $opt_table_info is specified, it set table infomation to result iterator.
So, you can use table row class to search_by_sql result.

=item $skinny->txn_scope

get transaction scope object.

    do {
        my $txn = Your::Model->txn_scope;

        $row->update({foo => 'bar'});

        $txn->commit;
    }

An alternative way of transaction handling based on
L<DBIx::Skinny::Transaction>.

If an exception occurs, or the guard object otherwise leaves the scope
before C<< $txn->commit >> is called, the transaction will be rolled
back by an explicit L</txn_rollback> call. In essence this is akin to
using a L</txn_begin>/L</txn_commit> pair, without having to worry
about calling L</txn_rollback> at the right places. Note that since there
is no defined code closure, there will be no retries and other magic upon
database disconnection.

=item $skinny->hash_to_row($table_name, $row_data_hash_ref)

make DBIx::Skinny::Row's class from hash_ref.

    my $row = Your::Model->hash_to_row('user',
        {
            id   => 1,
            name => 'lestrrat',
        }
    );

=item $skinny->data2itr($table_name, \@rows_data)

DBIx::Skinny::Iterator is made based on \@rows_data.

    my $itr = Your::Model->data2itr('user',[
        {
            id   => 1,
            name => 'nekokak',
        },
        {
            id   => 2,
            name => 'yappo',
        },
        {
            id   => 3,
            name => 'walf43',
        },
    ]);

    my $row = $itr->first;
    $row->insert; # inser data.

=item $skinny->find_or_new($table_name, \%row_data)

Find an existing record from database.

If none exists, instantiate a new row object and return it.

The object will not be saved into your storage until you call "insert" in DBIx::Skinny::Row on it.

    my $row = Your::Model->find_or_new('user',{name => 'nekokak'});

=item $skinny->do($sql, [$option, $bind_values])

execute your query.

See) L<http://search.cpan.org/dist/DBI/DBI.pm#do>

=item $skinny->dbh

get database handle.

=item $skinny->connect([\%connection_info])

connect database handle.

If you give \%connection_info, create new database connection.

=item $skinny->reconnect(\%connection_info)

re connect database handle.

If you give \%connection_info, create new database connection.

=item $skinny->disconnect()

Disconnects from the currently connected database.

=item $skinny->suppress_row_objects($flag)

set row object creation mode.

=back

=head1 ATTRIBUTES

=over

=item order_by

    { order_by => [ { id => 'desc' } ] }
    # or
    { order_by => { id => 'desc' } }
    # or 
    { order_by => 'name' }

=item for_update

    { for_update => 1 }

=back

=head1 ENVIRONMENT VARIABLES

=head2 SKINNY_PROFILE

for debugging sql.

see L<DBIx::Skinny::Profile>

        $ SKINNY_PROFILE=1 perl ./your_script.pl

=head2 SKINNY_TRACE

for debugging sql.

see L<DBIx::Skinny::Profiler::Trace>

    $ SKINNY_TRACE=1 perl ./your_script.pl

=head2 TRIGGER

    my $row = $db->insert($args);
    # pre_insert: ($db, $args, $table_name)
    # post_insert: ($db, $row, $table_name)

    my $updated_rows_count = $db->update($args);
    my $updated_rows_count = $row->update(); # example $args: +{ id => $row->id }
    # pre_update: ($db, $args, $table_name)
    # post_update: ($db, $updated_rows_count, $table_name)

    my $deleted_rows_count = $db->delete($args);
    my $deleted_rows_count = $row->delete(); # example $args: +{ id => $row->id }
    # pre_delete: ($db, $args, $table_name)
    # post_delete: ($db, $deleted_rows_count, $table_name)

=head1 BUGS AND LIMITATIONS

No bugs have been reported.

=head1 AUTHOR

Atsushi Kobayashi  C<< <nekokak __at__ gmail.com> >>

=head1 CONTRIBUTORS

walf443 : Keiji Yoshimi

TBONE : Terrence Brannon

nekoya : Ryo Miyake

oinume: Kazuhiro Oinuma

fujiwara: Shunichiro Fujiwara

pjam: Tomoyuki Misonou

magicalhat

Makamaka Hannyaharamitu

nihen: Masahiro Chiba

lestrrat: Daisuke Maki

tokuhirom: Tokuhiro Matsuno

=head1 SUPPORT

  irc: #dbix-skinny@irc.perl.org

  ML: http://groups.google.com/group/dbix-skinny

=head1 REPOSITORY

  git clone git://github.com/nekokak/p5-dbix-skinny.git  

=head1 LICENCE AND COPYRIGHT

Copyright (c) 2010, Atsushi Kobayashi C<< <nekokak __at__ gmail.com> >>. All rights reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L<perlartistic>.