# ==================================================================
# Gossamer Threads Module Library - http://gossamer-threads.com/
#
#   GT::SQL::Driver
#   CVS Info :                          
#   $Id: Driver.pm,v 2.5 2005/02/25 03:37:29 jagerman Exp $
#
# Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
# ==================================================================
#
# Description:
#   Overview: This implements a driver class.
#

package GT::SQL::Driver;
# ===============================================================
use strict;
use GT::SQL::Table;
use GT::AutoLoader;
use GT::SQL::Driver::Types;
use GT::SQL::Driver::debug;
use Exporter();
require GT::SQL::Driver::sth;
use vars qw/%CONN @ISA $DEBUG $VERSION $ERROR_MESSAGE $ATTRIBS %QUERY_MAP/;

use constant PROTOCOL => 2;

$ATTRIBS = {
    name    => '',
    schema  => '',
    dbh     => '',
    connect => {}
};
$ERROR_MESSAGE = 'GT::SQL';
$VERSION       = sprintf "%d.%03d", q$Revision: 2.5 $ =~ /(\d+)\.(\d+)/;
$DEBUG         = 0;
@ISA           = qw/GT::SQL::Driver::debug/;

%QUERY_MAP = (
#   QUERY    => METHOD (will be prefixed with '_prepare_' or '_execute_')
    CREATE   => 'create',
    INSERT   => 'insert',
    ALTER    => 'alter',
    SELECT   => 'select',
    UPDATE   => 'update',
    DROP     => 'drop',
    DELETE   => 'delete',
    DESCRIBE => 'describe',
    'SHOW TABLES' => 'show_tables',
    'SHOW INDEX' => 'show_index'
);

$DBI::errstr if 0;

sub load_driver {
# -----------------------------------------------------------------------------
# Loads a sub-driver (i.e. GT::SQL::Driver::MYSQL, GT::SQL::Driver::PG, etc.),
# and creates and returns a new driver object.  The first argument should be
# the name of the driver (e.g. 'PG'), and the remaining arguments are passed to
# new() - which could well be handled by the driver.
#
    my ($class, $driver, @opts) = @_;

# Old GT::SQL versions had an 'ODBC' driver that wasn't an ODBC driver, but an
# MSSQL driver that used ODBC.
    $driver = 'MSSQL' if $driver eq 'ODBC';

    my $pkg = "GT::SQL::Driver::$driver";
    my $lib_path = $INC{'GT/SQL/Driver.pm'};
    $lib_path =~ s|GT/SQL/Driver\.pm$||;
    {
        # Ensure that the driver is loaded from the same location as GT/SQL/Driver.pm
        local @INC = ($lib_path, @INC);
        require "GT/SQL/Driver/$driver.pm";
    }

    my $protocol = $pkg->protocol_version;
    return $class->fatal(DRIVERPROTOCOL => PROTOCOL, $protocol) if $protocol != PROTOCOL;

    return $pkg->new(@opts);
}

sub new {
# -----------------------------------------------------------------------------
# Generic new() method for drivers to inherit; load_driver() should be used
# instead to get a driver object.
#
    my $this    = shift;
    my $class   = ref $this || $this;
    my $self    = bless {}, $class;
    my $opts    = $self->common_param(@_) or return $self->fatal(BADARGS => "$class->new(HASH REF or HASH); invalid parameter: '@_'");

# Otherwise we need to make sure we have a schema.
    $opts->{schema} and ref $opts->{schema} or return $self->fatal(BADARGS => "$class->new(HASH REF or HASH); must specify schema and name");

    $self->{name}     = $opts->{name};
    $self->{schema}   = $opts->{schema};
    $self->{connect}  = $opts->{connect};
    $self->{_debug}   = $opts->{debug}    || $DEBUG;
    $self->{_err_pkg} = $opts->{_err_pkg} || 'GT::SQL';
    $self->{dbh}      = undef;
    $self->{hints}    = { $self->hints };
    $self->debug("New driver object loaded from table: $self->{name}.") if ($self->{_debug} > 2);

    return $self;
}

# This method is designed to be subclassed to provide "hints" for simple, small
# differences between drivers, which simplifies the code over using a subclass.
# It returns a hash of hints, with values of "1" unless otherwise indicated.
# Currently supported hints are:
#   case_map            # Corrects ->fetchrow_hashref column case when the database doesn't
#   prefix_indexes      # Indexes will be prefixed with the table name (including the table's prefix)
#   fix_index_dbprefix  # Look for erroneous (db_prefix)(index) when dropping indexes
#   now                 # Specifies an SQL value to use instead of NOW() (for 'time_check' columns, among other things)
#   bind                # An array ref of: [\%BIND_HASH, ('COLUMNTYPE' => $bind_type, 'TYPE2' => $bind_type2, ...)] for drivers that need special placeholder binding for certain column types
#   ai                  # Contains a string to use for an AI column; or a code reference that is passed ($table, $column) and returns the string, or an array reference of queries to run to create the ai sequence after the column/table has been created
#   drop_pk_constraint  # use ALTER TABLE ... DROP CONSTRAINT pkeyname to drop a primary key
sub hints { () }
# Removing the () breaks under 5.00404, as it will return @_ in list context

$COMPILE{protocol_version} = __LINE__ . <<'END_OF_SUB';
sub protocol_version {
# -----------------------------------------------------------------------------
# This checks the GT::SQL::Driver protocol, and dies if the versions aren't
# equal.  The protocol version only changes for major driver changes such as
# the v2.000 version of this module, which had the drivers do their own queries
# (as opposed to the previous hack of having drivers trying to return alternate
# versions of MySQL's queries).  All protocol v2 and above drivers are required
# to override this - any driver that does not is, by definition, a protocol v1
# driver.
#
# The current protocol version is defined by the PROTOCOL constant - but
# drivers that haven't overridden protocol_version() are, by definition, v1.
#
    1;
}
END_OF_SUB

sub available_drivers {
# -----------------------------------------------------------------------------
# Returns a list of available GT::SQL::Driver::* drivers
#
    my $driver_path = $INC{'GT/SQL/Driver.pm'};
    $driver_path =~ s/\.pm$//;
    my $dh = \do { local *DH; *DH };
    my @drivers;
    opendir $dh, $driver_path or return ();
    while (defined(my $driver = readdir $dh)) {
        # By convention, only all-uppercase modules are accepted as GT::SQL drivers
        next unless $driver =~ /^([A-Z_][A-Z0-9_]*)\.pm$/;
        push @drivers, $1;
    }
    @drivers;
}

sub connect {
# -------------------------------------------------------------------
# Returns the current database handle.
#
    my $self    = shift;
    $self->{dbh} and return $self->{dbh};

    eval { require DBI };
    if ($@) {
        return $self->warn(CANTCONNECT => "DBI module not installed. You must install the perl database module DBI from: http://www.perl.com/CPAN/modules/by-module/DBI");
    }

# Make sure we have a database, otherwise probably an error.
    exists $self->{connect}->{database} or return $self->fatal(CANTCONNECT => "No connection string passed to tbl->connect, make sure your table object got a connection hash.");
    keys %{$self->{schema}}             or return $self->fatal(CANTCONNECT => "Unable to connect to database without a valid schema.");

    my $dsn = $self->dsn($self->{connect});
    my $conn_key = "$dsn\0$self->{connect}->{login}\0$self->{connect}->{password}";
    if (defined $CONN{$conn_key}) {
        $self->{dbh} = $CONN{$conn_key};
        $self->debug("Using stored connection: $dsn") if ($self->{_debug} > 1);
        return $CONN{$conn_key};
    }

# Connect to the database.
    $self->debug("Connecting to database with: '$dsn', '$self->{connect}->{login}', '******'") if ($self->{_debug} > 1);
    my $res = eval {
        $CONN{$conn_key} = DBI->connect($dsn, $self->{connect}->{login}, $self->{connect}->{password}, { RaiseError => $self->{connect}->{RaiseError}, PrintError => $self->{connect}->{PrintError}, AutoCommit => 1 })
            or die "$DBI::errstr\n";
        1;
    };
    $res or return $self->warn(CANTCONNECT => "$@");

    $self->{dbh} = $CONN{$conn_key};
    $self->debug("Connected successfully to database.") if $self->{_debug} > 1;

    return $self->{dbh};
}

$COMPILE{dsn} = __LINE__ . <<'END_OF_SUB';
sub dsn {
# -------------------------------------------------------------------
# Creates the data source name used by DBI to connect to the database.
# Since this is database-dependant, this is just a stub.
#
    require Carp;
    Carp::croak("Driver has no dsn()");
}
END_OF_SUB

$COMPILE{prepare_raw} = __LINE__ . <<'END_OF_SUB';
sub prepare_raw {
# ---------------------------------------------------------------
# Returns a raw sth object.
# WARNING: MAKE SURE YOUR SQL IS PORTABLE AS NO ALTERATIONS WILL
# BE MADE! ALSO YOU MUST MANUALLY CALL ->finish ON THESE!
#
    my ($self, $query) = @_;
    $self->debug("Preparing RAW query: $query") if $self->{_debug} > 1;
    my $sth = $self->{dbh}->prepare($query) or return $self->warn(CANTPREPARE => $query);
    $self->debug("RAW STH is prepared: $query") if $self->{_debug} > 1;
    return $sth;
}
END_OF_SUB

$COMPILE{prepare} = __LINE__ . <<'END_OF_SUB';
sub prepare {
# ---------------------------------------------------------------
# We can override whatever type of queries we need to alter by replacing
# the _prepare_* functions.
#
    my ($self, $query) = @_;
    if (! defined $query) {
        return $self->warn(CANTPREPARE => "", "Empty Query");
    }

    # For any drivers that need hacked-in limit support (currently MS SQL and Oracle):
    delete @$self{qw/_limit _lim_offset _lim_rows/};

    if (my $now = $self->{hints}->{now}) {
        $query =~ s/\bNOW\(\)/$now/g;
    }

    if ($query =~ /^\s*SHOW\s+TABLES\s*(?:;\s*)?$/i) {
        $self->{do} = 'SHOW TABLES';
    }
    elsif ($query =~ /^\s*SHOW\s+INDEX\s+FROM\s+\w+\s*(?:;\s*)?$/i) {
        # See 'Driver-specific notes' below
        $self->{do} = 'SHOW INDEX';
    }
    else {
        $self->{do} = uc +($query =~ /(\w+)/)[0];
    }
    if (my $meth = $QUERY_MAP{$self->{do}}) {
        $meth = "_prepare_$meth";
        $query = $self->$meth($query) or return;
    }

    $self->{query} = $query;
    $self->debug("Preparing query: $query") if $self->{_debug} > 1;

    $self->{sth} = $self->{dbh}->prepare($query)
        or return $self->warn(CANTPREPARE => $query, $DBI::errstr);

    my $pkg = ref($self) . '::sth';
    $self->debug("CREATING $pkg OBJECT") if $self->{_debug} > 2;
    return $pkg->new($self);
}
END_OF_SUB

# Define one generic prepare, and alias all the specific _prepare_* functions to it
sub _generic_prepare { $_[1] }
for (*_prepare_create, *_prepare_insert, *_prepare_alter, *_prepare_select, *_prepare_update, *_prepare_drop, *_prepare_delete, *_prepare_describe) {
    $_ = \&_generic_prepare;
}
# Driver-specific notes:
# 'SHOW TABLES'
# The driver should return single-column rows of non-system tables in the
# database.  The name of the column is not important, and users of SHOW TABLE
# should not depend on it (i.e. do not use ->fetchrow_hashref)
*_prepare_show_tables = \&_generic_prepare;
# 'SHOW INDEX FROM table'
# Drivers should return one row per column per index, having at least the keys:
#   - index_name: the name of the index
#   - index_column: the name of the column
#   - index_unique: 1 if the index is unique, 0 otherwise
#   - index_primary: 1 if the column is a primary key, 0 otherwise
#
# The rows must be grouped by index, and ordered by the position of the column
# within said groupings.
#
# So, for a unique index named 'unique1' on columns 'col1', 'col2', 'col3', and
# a normal index named 'index1' on 'col3', 'col4', and a primary key on
# 'colpk', you should get (at a minimum; extra columns are permitted):
# +------------+--------------+--------------+---------------+
# | index_name | index_column | index_unique | index_primary |
# +------------+--------------+--------------+---------------+
# | unique1    | col1         |            1 |             0 |
# | unique1    | col2         |            1 |             0 |
# | unique1    | col3         |            1 |             0 |
# | index1     | col3         |            0 |             0 |
# | index1     | col4         |            0 |             0 |
# | PRIMARY    | colpk        |            1 |             1 |
# +------------+--------------+--------------+---------------+
# 'PRIMARY' above should be changed by drivers whose databases have named
# primary keys, otherwise using 'PRIMARY' for the primary key is recommended.
#
# Any other information may be returned; users of this query mapping should
# always use ->fetchrow_hashref, and access the above four keys for
# portability.
#
# Note that index_primary results may overlap other indexes for some databases
# - Oracle, in particular, will bind a primary key onto an existing index if
# possible.  In such a case, you'll get the index indicated normally, but some
# of the columns may make up the primary key.  For example, the following
# result would indicate that there is one index on col1, col2, col3, and that
# there is a primary key made up of (col1, col2):
#
# +------------+--------------+--------------+---------------+
# | index_name | index_column | index_unique | index_primary |
# +------------+--------------+--------------+---------------+
# | index1     | col1         |            0 |             1 |
# | index1     | col2         |            0 |             1 |
# | index1     | col3         |            0 |             0 |
# +------------+--------------+--------------+---------------+
#
# Currently, results such as the above are known to occur in Oracle databases
# where a primary key was added to an already-indexed column after creating the
# table - other databases give primary keys an independant index.
#
# Although _prepare_show_index is defined here, no drivers actually satisfy the
# above without some query result remapping, and as such all currently override
# either this or _execute_show_index.
*_prepare_show_index = \&_generic_prepare;

$COMPILE{extract_index_name} = __LINE__ . <<'END_OF_SUB';
sub extract_index_name {
# -----------------------------------------------------------------------------
# Takes an table name and database index name (which could be prefixed, if the
# database uses prefixes) and returns the GT::SQL index name (i.e. without
# prefix).
    my ($self, $table, $index) = @_;
    if ($self->{hints}->{prefix_indexes}) {
        $index =~ s/^\Q$table\E(?=.)//i;
    }
    $index;
}
END_OF_SUB

sub disconnect {
# -------------------------------------------------------------------
# Disconnect from the database.
#
    my $self = shift;
    $self->{dbh} and $self->{dbh}->disconnect;
}

sub reset_env {
# -------------------------------------------------------------------
# Remove all database connections that aren't still alive
#
    @GT::SQL::Driver::debug::QUERY_STACK = ();
    for my $dsn (keys %CONN) {
        next if ($CONN{$dsn} and $CONN{$dsn}->ping);
        $CONN{$dsn}->disconnect if ($CONN{$dsn});
        delete $CONN{$dsn};
    }
}

sub do {
# -------------------------------------------------------------------
# Do a query.
#
    my $self = shift;
    ($self->prepare(@_) or return)->execute;
}

$COMPILE{do_raw_transaction} = __LINE__ . <<'END_OF_SUB';
sub do_raw_transaction {
# -----------------------------------------------------------------------------
# Do a series of queries as a single transaction - note that this is only
# supported under DBI >= 1.20; older versions of DBI result in the queries
# being performed without a transaction.
# This subroutine should be passed a list of queries; the queries will be run
# in order.  Each query may optionally be an array reference where the first
# element is the query, and remaining elements are placeholders to use when
# executing the query.  Furthermore, you may pass a reference to the string
# or array reference to specify a non-critical query.
#
# For example:
# $self->do_raw_transaction(
#     "QUERY1",
#     \["QUERY2 ?", $value],
#     \"QUERY3",
#     ["QUERY4 ?, ?", $value1, $value2]
# );
#
# This will attempt to run the 4 queries, and will abort if query 1 or 4 do not
# succeed.
#
# Also note that this is ONLY meant to be used by individual drivers as it
# assumes the queries passed in are ready to run without any rewriting.  As
# such, any use outside of individual drivers should be considered an error.
#
# Returns '1' on success, undef on failure of any query (excepting non-critical
# queries, see above).
#
    my ($self, @queries) = @_;

    my $transaction = $DBI::VERSION >= 1.20;
    $self->{dbh}->begin_work if $transaction;

    $self->debug("Begin query transaction") if $self->{_debug};
    $self->debug("Transaction not possible; DBI version < 1.20") if $self->{_debug} and not $transaction;

    my $time;
    $time = Time::HiRes::time() if $self->{_debug} and exists $INC{"Time/HiRes.pm"};
    for (@queries) {
        my $critical = not(ref eq 'SCALAR' or ref eq 'REF');
        my $q = $critical ? $_ : $$_;
        my ($query, @ph) = ref $q ? @$q : $q;
        if ($self->{_debug}) {
            my $debugquery = GT::SQL::Driver::debug->replace_placeholders($query, @ph);
            $self->debug("Executing query $debugquery");
        }
        my $did = $self->{dbh}->do($query, undef, @ph);
        if (!$did and $critical) {
            $self->warn(CANTEXECUTE => $query => $DBI::errstr);
            $self->debug("Critical query failed, transaction aborted; performing transaction rollback")
                if $self->{_debug} and $transaction;
            $self->{dbh}->rollback if $transaction;
            return undef;
        }
    }

    $self->debug("Transaction complete; committing") if $self->{_debug};
    $self->{dbh}->commit if $transaction;

    if ($self->{_debug} and exists $INC{"Time/HiRes.pm"}) {
        my $elapsed = Time::HiRes::time() - $time;
        $self->debug(sprintf("Transaction execution took: %.6fs", $elapsed));
    }

    1;
}
END_OF_SUB

sub quote {
# -----------------------------------------------------------
# This subroutines quotes (or not) a value.
#
    my $val = pop;
    return 'NULL' if not defined $val;
    return $$val if ref $val eq 'SCALAR' or ref $val eq 'LVALUE';
    (values %CONN)[0]->quote($val);
}

$COMPILE{create_table} = __LINE__ . <<'END_OF_SUB';
sub create_table {
# -------------------------------------------------------------------
# Creates a table.
#
    my $self = shift;
    $self->connect or return;

    my $table = $self->{name};

# Figure out the order of the create, and then build the create statement.
    my %pos = map { $_ => $self->{schema}->{cols}->{$_}->{pos} } keys %{$self->{schema}->{cols}};
    my (@field_defs, $ai_queries);
    for my $field (sort { $pos{$a} <=> $pos{$b} } keys %{$self->{schema}->{cols}}) {
        my %field_def = map { defined $self->{schema}->{cols}->{$field}->{$_} ? ($_ => $self->{schema}->{cols}->{$field}->{$_}) : () } keys %{$self->{schema}->{cols}->{$field}};
        my $is_ai = $self->{schema}->{ai} && $field eq $self->{schema}->{ai};
        delete $field_def{default} if $is_ai;
        my $def = $field . ' ' . ($self->column_sql(\%field_def) or return);
        if ($is_ai) {
            my $ai = $self->{hints}->{ai} || 'AUTO_INCREMENT';
            $ai = $ai->($table, $field) if ref $ai eq 'CODE';
            if (ref $ai eq 'ARRAY') {
                $ai_queries = $ai;
            }
            else {
                $def .= " $ai";
            }
        }
        push @field_defs, $def;
    }

# Add the primary key.
    if (@{$self->{schema}->{pk}}) {
        push @field_defs, "PRIMARY KEY (" .  join(",", @{$self->{schema}->{pk}}) . ")";
    }

# Create the table
    my $create_query = "\n\tCREATE TABLE $table (\n\t\t";
    $create_query .= join ",\n\t\t", @field_defs;
    $create_query .= "\n\t)";

    $self->do($create_query) or return;

# If the database needs separate queries to set up the auto-increment, run them
    if ($ai_queries) {
        for (@$ai_queries) {
            $self->do($_);
        }
    }

# Create the table's indexes
    for my $type (qw/index unique/) {
        my $create_index = "create_$type";
        while (my ($index_name, $index) = each %{$self->{schema}->{$type}}) {
            $self->$create_index($table => $index_name => @$index) if @$index;
        }
    }

    1;
}
END_OF_SUB

$COMPILE{column_sql} = __LINE__ . <<'END_OF_SUB';
sub column_sql {
# -----------------------------------------------------------------------------
# Converts a column definition into an SQL string used in the create table
# statement, and (for some drivers) when adding a new column to a table.
#
    my ($self, $opts) = @_;

    ref $opts eq 'HASH' or return $self->fatal(BADARGS => '$obj->column_sql (HASH_REF)');
    $opts->{type}       or return $self->fatal(BADARGS => 'Column definition does not have a SQL type defined');

    my $pkg = ref($self) . '::Types';
    my $type = uc $opts->{type};

    if ($pkg->can($type)) {
        $self->debug("Using driver specific SQL for type $opts->{type}") if $self->{_debug} > 1;
    }
    elsif (GT::SQL::Driver::Types->can($type)) {
        $pkg = 'GT::SQL::Driver::Types';
    }
    else {
        return $self->fatal(BADTYPE => $opts->{type});
    }
    $pkg->$type({%$opts});
}
END_OF_SUB

$COMPILE{insert} = __LINE__ . <<'END_OF_SUB';
sub insert {
# -----------------------------------------------------------------------------
# This subroutine, using a couple driver hints, handles insertions for every
# driver currently supported.
#
    my ($self, $input) = @_;

    my (@names, @values, @placeholders, @binds);
    my %got;
    my $ai = $self->{schema}->{ai};
    my $bind = $self->{hints}->{bind};
    my $cols = $self->{schema}->{cols};
    while (my ($col, $val) = each %$input) {
        ++$got{$col};
        next if $ai and $col eq $ai and !$val;
        push @names, $col;
        my $def = $cols->{$col};
        if ($def->{time_check} and (not defined $val or $val eq '' or $val eq 'NOW()')) {
            push @values, $self->{hints}->{now} || 'NOW()';
        }
        elsif ($def->{type} =~ /DATE/ and (not defined $val or $val eq '')) {
            push @values, 'NULL';
        }
        elsif (ref $val eq 'SCALAR' or ref $val eq 'LVALUE') {
            push @values, $$val;
        }
        else {
            push @placeholders, $val;
            push @values, '?';
            if ($bind and defined $val) {
                for (my $i = 1; $i < @$bind; $i += 2) {
                    if ($def->{type} =~ /$bind->[$i]/) {
                        push @binds, [scalar @placeholders, $col, $bind->[$i+1]];
                        last;
                    }
                }
            }
        }
    }

# Update any timestamp columns to current time.
    for my $col (keys %$cols) {
        next unless not $got{$col} and $cols->{$col}->{time_check};
        push @names, $col;
        push @values, $self->{hints}->{now} || 'NOW()';
        $got{$col} = 1;
    }

# Add an auto increment field if required
    if ($ai and not $input->{$ai}) {
        my @ai_insert = $self->ai_insert($ai);
        if (@ai_insert) {
            push @names,  $ai_insert[0];
            push @values, $ai_insert[1];
        }
    }

# Fill in any missing defaults 
    for my $col (keys %$cols) {
        next if $ai and $col eq $ai
             or $got{$col}
             or not exists $cols->{$col}->{default};
        my $val = $cols->{$col}->{default};
        push @names, $col;
        push @values, '?';
        push @placeholders, $val;
        $got{$col} = 1;
        if ($bind and defined $val) {
            my $def = $cols->{$col};
            for (my $i = 1; $i < @$bind; $i += 2) {
                if ($def->{type} =~ /$bind->[$i]/) {
                    push @binds, [scalar @placeholders, $col, $bind->[$i+1]];
                    last;
                }
            }
        }
    }

# Create the SQL and statement handle.
    my $query = "INSERT INTO $self->{name} (";
    $query .= join ',', @names;
    $query .= ") VALUES (";
    $query .= join ',', @values;
    $query .= ")";

    $bind->[0]->{$query} = \@binds if $bind;

    my $sth = $self->prepare($query) or return;
    $sth->execute(@placeholders) or return;
    $sth;
}
END_OF_SUB

sub ai_insert {
# -----------------------------------------------------------------------------
# Returns a column name and value to use for the AI column when inserting a
# row.  If this returns an empty list, no value will be inserted.  This will
# only be called when the table has an auto-increment column, so checking is
# not necessary.  The sole argument passed in is the name of the column.
#
    my ($self, $ai) = @_;
    return $ai, 'NULL';
}

$COMPILE{insert_multiple} = __LINE__ . <<'END_OF_SUB';
sub insert_multiple {
# -----------------------------------------------------------------------------
# Performs a multiple-insertion.  By default, this is simply done as multiple
# executes on a single insertion, and as a single transaction if under
# DBI >= 1.20.
#
    my ($self, $cols, $args) = @_;
    $self->{dbh}->begin_work if $DBI::VERSION >= 1.20;
    my $count;
    for my $val (@$args) {
        my %set;
        for my $i (0 .. $#$cols) {
            $set{$cols->[$i]} = $val->[$i];
        }
        ++$count if $self->insert(\%set);
    }
    $self->{dbh}->commit if $DBI::VERSION >= 1.20;
    $count;
}
END_OF_SUB

sub update {
# -------------------------------------------------------------------
    my ($self, $set, $where) = @_;

    my $c = $self->{schema}->{cols};
    my %set;

    for my $cond (@{$set->{cond}}) {
        if (ref $cond eq 'ARRAY') {
            $set{$cond->[0]}++ if exists $c->{$cond->[0]} and $c->{$cond->[0]}->{time_check};
        }
    }
    for my $col (keys %$c) {
        next unless not $set{$col} and $c->{$col}->{time_check};
        $set->add($col, '=', \($self->{hints}->{now} || 'NOW()'));
    }

    my ($sql_set, $set_vals, $set_cols) = $set->sql(1);
    my ($sql_where, $where_vals, $where_cols) = $where->sql(1);
    my $i = 1;

    # Set up binds, if necessary
    my @binds;
    my $bind = $self->{hints}->{bind};
    if ($bind) {
        for my $col (@$set_cols) {
            next unless exists $c->{$col};
            for (my $j = 1; $j < @$bind; $j += 2) {
                if ($c->{$col}->{type} =~ /$bind->[$j]/) {
                    push @binds, [scalar $i, $col, $bind->[$j+1]];
                    last;
                }
            }
            $i++;
        }
    }

    my $query = "UPDATE $self->{name} SET $sql_set";
    $query .= " WHERE $sql_where" if $sql_where;

    $bind->[0]->{$query} = \@binds if $bind;

    my $sth = $self->prepare($query) or return;
    $sth->execute(@$set_vals, @$where_vals) or return;
    $sth;
}

sub delete {
# -------------------------------------------------------------------
    my ($self, $where) = @_;
    my ($sql_where, $where_vals) = $where ? $where->sql(1) : ();
    my $sql = "DELETE FROM $self->{name}";
    $sql .= " WHERE $sql_where" if $sql_where;

    my $sth = $self->prepare($sql) or return;
    $sth->execute(@$where_vals) or return;
    $sth;
}

sub select {
# -------------------------------------------------------------------
    my ($self, $field_arr, $where, $opts) = @_;

    my ($fields, $opt_clause) = ('', '');
    if (ref $field_arr and @$field_arr) {
        $fields = join ",", @$field_arr;
    }
    else {
        $fields = '*';
    }
    my ($sql_where, $where_vals) = $where->sql(1);
    $sql_where and ($sql_where = " WHERE $sql_where");
    if ($opts) {
        for my $opt (@$opts) {
            next if (! defined $opt);
            $opt_clause .= " $opt";
        }
    }
    my $sql = "SELECT $fields FROM " . $self->{name};
    $sql .= $sql_where if $sql_where;
    $sql .= $opt_clause if $opt_clause;
    my $sth = $self->prepare($sql) or return;
    $sth->execute(@$where_vals) or return;
    $sth;
}

$COMPILE{drop_table} = __LINE__ . <<'END_OF_SUB';
sub drop_table {
# -------------------------------------------------------------------
# Drops the table passed in.
#
    my ($self, $table) = @_;
    $self->do("DROP TABLE $table");
}
END_OF_SUB

$COMPILE{column_exists} = __LINE__ . <<'END_OF_SUB';
sub column_exists {
# -----------------------------------------------------------------------------
# Returns true or false value depending on whether the column exists in the
# table.  This defaults to a DESCRIBE of the table, then looks for the column
# in the DESCRIBE results - but many databases probably have a much more
# efficient alternative.
#
    my ($self, $table, $column) = @_;
    my $sth = $self->prepare("DESCRIBE $table") or return;
    $sth->execute or return;
    my $found;
    while (my ($col) = $sth->fetchrow) {
        $found = 1, last if $col eq $column;
    }
    $found;
}
END_OF_SUB

$COMPILE{add_column} = __LINE__ . <<'END_OF_SUB';
sub add_column {
# -------------------------------------------------------------------
# Adds a column to a table.
#
    my ($self, $table, $column, $def) = @_;
    $self->do("ALTER TABLE $table ADD $column $def");
}
END_OF_SUB

$COMPILE{drop_column} = __LINE__ . <<'END_OF_SUB';
sub drop_column {
# -------------------------------------------------------------------
# Drops a column from a table.
#
    my ($self, $table, $column) = @_;
    $self->do("ALTER TABLE $table DROP $column");
}
END_OF_SUB

$COMPILE{alter_column} = __LINE__ . <<'END_OF_SUB';
sub alter_column {
# -----------------------------------------------------------------------------
# Changes a column.  Takes table name, column name, definition for the new
# column (string), and the old column definition (hash ref).  The new column
# definition should already be set in the table object
# ($self->{table}->{schema}->{cols}->{$column_name}).
#
    my ($self, $table, $column, $new_def, $old_col) = @_;
    $self->do("ALTER TABLE $table CHANGE $column $column $new_def");
}
END_OF_SUB

$COMPILE{create_index} = __LINE__ . <<'END_OF_SUB';
sub create_index {
# -----------------------------------------------------------------------------
# Adds an index - checks driver hints for whether or not to prefix the index
# with the prefixed table name.
#
    my ($self, $table, $index_name, @index_cols) = @_;
    $index_name = $table . $index_name if $self->{hints}->{prefix_indexes};
    $self->do("CREATE INDEX $index_name ON $table (" . join(",", @index_cols) . ")");
}
END_OF_SUB

$COMPILE{create_unique} = __LINE__ . <<'END_OF_SUB';
sub create_unique {
# -----------------------------------------------------------------------------
# Adds a unique index to a table, using the prefixed table name as a prefix.
#
    my ($self, $table, $unique_name, @unique_cols) = @_;
    $unique_name = $table . $unique_name if $self->{hints}->{prefix_indexes};
    $self->do("CREATE UNIQUE INDEX $unique_name ON $table (" . join(",", @unique_cols) . ")");
}
END_OF_SUB

$COMPILE{drop_index} = __LINE__ . <<'END_OF_SUB';
sub drop_index {
# -----------------------------------------------------------------------------
# Drops an index.
#
    my ($self, $table, $index_name) = @_;
    $index_name = $table . $index_name if $self->{hints}->{prefix_indexes};
    my $dropped = $self->do("DROP INDEX $index_name");
    $dropped ||= $self->do("DROP INDEX $self->{connect}->{PREFIX}$index_name") if $self->{hints}->{fix_index_dbprefix};
    $dropped;
}
END_OF_SUB

$COMPILE{create_pk} = __LINE__ . <<'END_OF_SUB';
sub create_pk {
# -------------------------------------------------------------------
# Adds a primary key to a table.
#
    my ($self, $table, @cols) = @_;
    $self->do("ALTER TABLE $table ADD PRIMARY KEY (" . join(",", @cols) . ")");
}
END_OF_SUB

$COMPILE{drop_pk} = __LINE__ . <<'END_OF_SUB';
sub drop_pk {
# -------------------------------------------------------------------
# Drop a primary key.
#
    my ($self, $table) = @_;
    my $do;
    if ($self->{hints}->{drop_pk_constraint}) {
        # To drop a primary key in ODBC or Pg, you drop the primary key
        # constraint, which implicitly drops the index implicitly created by a
        # primary key.
        my $sth = $self->prepare("SHOW INDEX FROM $table") or return;
        $sth->execute or return;

        my $pk_constraint;
        while (my $index = $sth->fetchrow_hashref) {
            if ($index->{index_primary}) {
                $pk_constraint = $index->{index_name};
                last;
            }
        }

        $pk_constraint or return $self->warn(CANTEXECUTE => "ALTER TABLE $table DROP PRIMARY KEY" => "No primary key found for $table");

        $do = "ALTER TABLE $table DROP CONSTRAINT $pk_constraint";
    }
    else {
        $do = "ALTER TABLE $table DROP PRIMARY KEY";
    }
    $self->do($do);
}
END_OF_SUB

1;
