#
# $Id: OracleIO.pm,v 1.2 2002/07/20 02:37:37 rsandberg Exp $
#

package DBIx::IO::OracleIO;

use DBIx::IO;

@ISA = qw(DBIx::IO);

use strict;

use DBD::Oracle qw(
    ORA_CLOB
    ORA_BLOB
    ORA_LONG
);

use DBIx::IO::OracleLib ();
use DBIx::IO::GenLib ();

my %long_lob_types = (
    $DBIx::IO::GenLib::LONG_TYPE => ORA_LONG,
    $DBIx::IO::GenLib::BLOB_TYPE => ORA_BLOB,
    $DBIx::IO::GenLib::CLOB_TYPE => ORA_CLOB,
);

my %all_table_col_types;
my %all_table_col_defaults;
my %all_table_col_required;
my %all_table_col_lengths;
my %all_table_col_scale;
my %all_table_cols;
my %all_table_pk;
my %all_table_col_list;

my %datetime_types =
(
    $DBIx::IO::GenLib::DATETIME_TYPE => 1,
    # qualify() will not treat a date type any differently than a datetime type
    $DBIx::IO::GenLib::DATE_TYPE => 1,
    DATE => 1,
);
my %date_types =
(
    $DBIx::IO::GenLib::DATE_TYPE => 1,
    DATE => 1,
);
my %interval_types =
(
    INTERVAL => 1,
    TIMESTAMP => 1,
);
my %numeric_types =
(
    $DBIx::IO::GenLib::NUMERIC_TYPE => 1,
    NUMBER => 1,
    FLOAT => 1,
    BINARY_FLOAT => 1,
    BINARY_DOUBLE => 1,
);
my %char_types =
(
    $DBIx::IO::GenLib::CHAR_TYPE => 1,
    CHAR => 1,
    NCHAR => 1,
    VARCHAR2 => 1,
    NVARCHAR2 => 1,
    RAW => 1,
    'LONG RAW' => 1,
);
my %rowid_types =
(
    $DBIx::IO::GenLib::ROWID_TYPE => 1,
    ROWID => 1,
    UROWID => 1,
);
my %long_types =
(
    $DBIx::IO::GenLib::LONG_TYPE => 1,
    LONG => 1,
);
my %lob_types =
(
    $DBIx::IO::GenLib::LOB_TYPE => 1,
    CLOB => 1,
    BLOB => 1,
    NCLOB => 1,
);
my %ignore_types = (
    BFILE => 1,
);


=head1 NAME

DBIx::IO::OracleIO - DBIx::IO driver for Oracle

=head1 DESCRIPTION

See DBIx::IO.

=head1 METHOD DETAILS

=over 4

See superclass DBIx::IO for more


=cut
sub qualify
{
    my ($self,$val,$field,$date_format,$type) = @_;
    ref($self) || (warn("\$self not an object"),return undef);
    if (defined($type))
    {
        $type = uc($type);
    }
    else
    {
        $field = uc($field);
        my $col_types = $self->column_types();
        $type = $col_types->{$field};
    }
    defined($type) || ($self->_alert("Data type not defined"), return undef);

    if (is_long_type($type))
    {
        return '' unless length($val);
        return $val;
    }

    length($val) || return 'NULL';
    
    if (exists($rowid_types{$type}))
    {
        return "'$val'";
    }
    elsif (exists($numeric_types{$type}))
    {
        return $val;
    }
    elsif (exists($datetime_types{$type}))
    {
        return $val if uc($val) eq 'SYSDATE';
        return $val if $val =~ /^DATE\b/i;
        $date_format ||= $DBIx::IO::OracleLib::NORMAL_DATETIME_FORMAT;
        if ($date_format eq $DBIx::IO::GenLib::UNKNOWN_DATE_FORMAT)
        {
            my $parse_val = DBIx::IO::GenLib::normalize_date($val);
            length($parse_val) || ($self->_alert("The date format of: $val could not be recognized"),return undef);
            $date_format = $DBIx::IO::OracleLib::NORMAL_DATETIME_FORMAT;
            $val = $parse_val;
        }
        return "TO_DATE('$val','$date_format')";
    }
    elsif (exists($interval_types{$type}))
    {
        # Hopefully the operator knows what they're doing in this case
        return $val;
    }
    elsif (exists($char_types{$type}))
    {
        $val =~ s/\000//g;
        length($val) || return 'NULL';
        $val = $self->{dbh}->quote($val);
        return undef if $self->{dbh}->err;
        return $val;
    }
    $self->_alert("Unhandled data type: $type");
    return undef;
}

sub verify_datatype
{
    my ($self,$val,$field,$type) = @_;
    if (defined($type))
    {
        $type = uc($type);
    }
    else
    {
        $field = uc($field);
        my $col_types = $self->column_types();
        $type = $col_types->{$field};
    }
    defined($type) || ($self->_alert("Data type not defined"), return undef);

    if ($numeric_types{$type})
    {
        if ($self->{scale}{$field})
        {
            # This is a real number
            return DBIx::IO::GenLib::isreal($val);
        }
        else
        {
            return -1 unless DBIx::IO::GenLib::isint($val);
        }
    }
    return 1;
}

sub is_long_type
{
    my $type = shift;
    return (exists($lob_types{$type}) || exists($long_types{$type}));
}

sub is_lob_type
{
    my ($self,$type) = @_;
    return exists($lob_types{$type});
}

sub is_ignore_type
{
    my ($self,$type) = @_;
    return (exists($lob_types{$type}) || exists($ignore_types{$type}));
}

sub _assign_table_attrs
{
    my $self = shift;
    my $table_name = shift;
    $table_name = uc($table_name);
    my $rv;
    unless ($rv = $self->SUPER::_assign_table_attrs($table_name,@_))
    {
        return $rv;
    }

    my $table = $self->table_name();
    my $owner;
    ($table,$owner) = $self->_strip_owner($table);
    $self->{sequence_name} = ($owner ? "${owner}." : "") . "SEQ_" . uc($table);

    return 1;
}

sub column_attrs
{
    my ($self,$table) = @_;
    ref($self) || (warn("\$self not an object"),return undef);

    if (exists($all_table_col_types{$table}))
    {
        $self->{scale} = $all_table_col_scale{$table};
        $self->{defaults} = $all_table_col_defaults{$table};
        $self->{required} = $all_table_col_required{$table};
        $self->{lengths} = $all_table_col_lengths{$table};
        $self->{pk} = $all_table_pk{$table};
        $self->{select_cols} = $all_table_cols{$table};
        $self->{col_list} = $all_table_col_list{$table};
        # do not alter this hash ref!!!
        return ($self->{column_types} = $all_table_col_types{$table});
    }

    my $pksth = $self->make_cursor("SELECT cc.column_name FROM user_cons_columns cc,user_constraints c " .
        "WHERE c.constraint_name = cc.constraint_name " .
        "AND c.constraint_type = 'P' " .
        "AND cc.table_name = '$table' " .
        "AND c.status = 'ENABLED'") || return undef;
    my $res = $pksth->fetchall_arrayref() || return undef;
    return undef if $pksth->err();
    my @pk = map($_->[0],@$res);

    my $sth = $self->make_cursor("SELECT utc.column_name,utc.data_type,ut.tablespace_name,utc.data_length, " .
        "utc.data_precision,utc.data_scale,utc.nullable,utc.data_default " .
        "FROM user_tab_columns utc, user_tables ut " .
        "WHERE utc.table_name = '$table' " .
        "AND ut.table_name(+) = utc.table_name") || return undef;
    my ($col,$type,$null,$length,$prec,$scale,$default,$tablespace,%attrs,%defaults,%lengths,%required,%scale,$cols,@cols);
    while (($col,$type,$tablespace,$length,$prec,$scale,$null,$default) = $sth->fetchrow_array)
    {
        $col = uc($col);
        $type = uc($type);
        $type =~ s/\W.*// unless $type eq 'LONG RAW';
        $attrs{$col} = $type;
        $attrs{$DBIx::IO::OracleLib::ROWID_COL_NAME} = $DBIx::IO::GenLib::ROWID_TYPE if $tablespace;
        $default = $1 if $default =~ /\s*\'(.*)\'\s*/;
        $defaults{$col} = $default;
        $required{$col} = uc($null) eq 'N';
        $length = _lengthof($type,$length,$prec,$scale);
        $lengths{$col} = $length;
        $scale{$col} = $scale;
        push(@cols,$col);

        # Build a list of select columns, LOB types error out if selected via DBI so skip them
        next if $lob_types{$type} || $ignore_types{$type} || $type eq 'INTERVAL';
        $cols .= ($datetime_types{$type} ? "TO_CHAR($col,'$DBIx::IO::OracleLib::NORMAL_DATETIME_FORMAT') $col," :
            "$col,");
    }
    return undef if $sth->err;
    %attrs || ($self->_alert("table: $table doesn't seem to exist or have any columns"), return 0);
    chop $cols;

    # do not alter these hash refs!!!
##at subclasses should cache these
    $self->{scale} = $all_table_col_scale{$table} = \%scale;
    $self->{defaults} = $all_table_col_defaults{$table} = \%defaults;
    $self->{required} = $all_table_col_required{$table} = \%required;
    $self->{lengths} = $all_table_col_lengths{$table} = \%lengths;
    $self->{pk} = $all_table_pk{$table} = \@pk;
    $self->{select_cols} = $all_table_cols{$table} = $cols;
    $self->{col_list} = $all_table_col_list{$table} = \@cols;
    return ($self->{column_types} = $all_table_col_types{$table} = \%attrs);
}

sub _lengthof
{
    my ($type,$length,$prec,$scale) = @_;
    return 50 if exists($interval_types{$type});
    return 25 if $type eq 'BINARY_DOUBLE';
    return 15 if $type eq 'BINARY_FLOAT';
    return $length unless !$length || $type eq 'NUMBER' || $type eq 'FLOAT';
    my $ll = abs($prec) + abs($scale);
    if ($type eq 'NUMBER' && $prec)
    {
        # 1 for optional (-) and 1 for optional '.'
        return abs($prec) + 1 + ($scale > 0);
    }
    elsif ($type eq 'NUMBER' || $type eq 'FLOAT')
    {
        return 126;
    }
    return 255;
}

sub insert_hash
{
    my ($self,$orig_insert,$date_format) = @_;
    ref($self) || (warn("\$self not an object"),return undef);
    ref($orig_insert) || ($self->_alert("\$insert_hash not a hash ref"),return undef);
    my $insert = { %$orig_insert };
    my $attrs = $self->column_types();
    my $dbh = $self->{dbh};
    my $table = $self->table_name();
    my $pkname = $self->key_name();
    my $pk;
    if (exists($attrs->{$pkname}) && !exists($insert->{$pkname}))
    {
        $pk = $self->key_nextval();
        defined($pk) || ($self->_alert("Can't generate key for $table"), return undef);
        $insert->{$pkname} = $pk;
    }
    else
    {
        $pk = $insert->{$pkname};
    }

    %$insert || return -1.1;
    delete $insert->{ROWID};

    my ($fields,$values,$field,$qual_val,%bind);
    foreach $field (keys %$insert)
    {
        $field = uc($field);
        $fields .= "$field,";
        $qual_val = $self->qualify($insert->{$field},$field,$date_format);
        if (is_long_type($attrs->{$field}))
        {
            $bind{":$field"} = [ $field,$qual_val ];
            $qual_val = ":$field";
        }
        unless (defined($qual_val))
        {
            $self->_alert("Unable to qualify insert value: qualify($insert->{$field},$field,$date_format)");
            return undef;
        }
        $values .= "$qual_val,";
    }
    chop($fields);
    chop($values);
    my $sql = "INSERT INTO $table ($fields) VALUES ($values)";
    my $sth = $dbh->prepare($sql) || ($self->_alert("Can't prepare $sql"), return undef);
    my ($bind_field,$bind_val);
    while (($bind_field,$bind_val) = each %bind)
    {
        my ($field,$val) = @$bind_val;
        my $type = $long_lob_types{$attrs->{$field}};
        $sth->bind_param($bind_field,$val,{ ora_type => $type, ora_field => $field }) || ($self->_alert("Error binding $bind_field"), return undef);
    }
    my $rv = $sth->execute();
    unless ($rv)
    {
        if ($sth->err == 1)
        {
            return -1.4;
        }
        else
        {
            return undef;
        }
    }

    return (length($pk) ? $pk : -1.2);
}


=pod

=item C<sequence_name>

 $sequence_name = $io->sequence_name([$sequence_name]);

Get/set the name of the sequence that generates key values
for inserts. Defaults to the name of the table prepended with "SEQ_".

=cut
sub sequence_name
{
    my ($self,$sequence_name) = @_;
    if (defined($sequence_name))
    {
        return $self->{sequence_name} = $sequence_name;
    }
    return $self->{sequence_name};
}


=pod

=item C<key_nextval>

 $next_seq_val = $io->key_nextval([$seq_name]);

Returns the next value in the Oracle sequence object named
$seq_name or the table name prepended with "SEQ_"
All sequence statement handles are cached per $dbh for performance
reasons. A new $sth will be prepared unless the object that calls
this method has previously called it with the same sequence request.

Returns undef if error.

=cut
sub key_nextval
{
    my ($self,$seq) = @_;
    ref($self) || (warn("\$self not an object"),return undef);
    my $dbh = $self->{dbh};
    $seq ||= $self->{sequence_name};
##at DBI version requirement prepare_cached
    my $crs = $dbh->prepare_cached("SELECT $seq.NEXTVAL FROM DUAL") || return undef;
    $crs->execute() || return undef;
    return (($crs->fetchrow_array)[0]);
}

sub update_hash
{
    my ($self,$update,$key,$date_format,$hint) = @_;
    ref($self) || (warn("\$self not an object"),return undef);
    ref($update) || ($self->_alert("\$update not a hash ref"), return undef);
    %$update || return -1;
    my $dbh = $self->dbh();
    my $table = $self->table_name();
    unless (ref($key))
    {
        $key = { $self->key_name() => $key };
    }

    my $where = $self->_build_where_clause($key) || return undef;

    my $set_sql;
    my $attrs = $self->column_types();
    my %bind;
    my ($col,$val);
    while (($col,$val) = each %$update)
    {
        $col = uc($col);
##at does insert implement it's optional $hint feature?
        $val = $self->qualify($val,$col,$date_format);
        if (is_long_type($attrs->{$col}))
        {
            $bind{":$col"} = [ $col,$val ];
            $val = ":$col";
        }
        unless (defined($val))
        {
            $self->_alert("Unable to qualify insert value: qualify($val,$col,$date_format)");
            return undef;
        }
        $set_sql .= "$col = $val,";
    }
    chop($set_sql);
    my ($bind_field,$bind_val);
    my $sql = "UPDATE $hint $table SET $set_sql $where";
    my $sth = $dbh->prepare($sql) || ($self->_alert("Can't prepare $sql"), return undef);
    while (($bind_field,$bind_val) = each %bind)
    {
        my ($field,$val) = @$bind_val;
        my $type = $long_lob_types{$attrs->{$field}};
        $sth->bind_param($bind_field,$val,{ ora_type => $type, ora_field => $field }) || ($self->_alert("Error binding $bind_field"), return undef);
    }
    return $sth->execute();
}

##at should normalize the data types, e.g. $io->{column_types}{$column} = $DBIx::IO::GenLib::NORMAL_DATETIME_TYPE

=pod

=item C<existing_table_names>

 $sorted_arrayref = DBIx::IO::OracleIO->existing_table_names([$dbh]);

Return a sorted arrayref of table names found in the
data dictionary.

Class or object method.
$dbh is required if called as a class method.

Return undef if db error.

=cut
sub existing_table_names
{
    my ($caller,$dbh) = @_;
    $dbh ||= $caller->dbh();
    my $rv = $dbh->selectcol_arrayref('SELECT table_name FROM user_tables ORDER BY table_name');
    return undef if $dbh->err;
    return $rv;
}


=pod

=item C<is_datetime>

 $bool = $io->is_datetime($column_name);

Determine if $column_name is of a datetime type.

=cut
sub is_datetime
{
    my ($self,$column_name) = @_;
    my $types = $self->column_types();
    return $datetime_types{$types->{$column_name}};
}

=pod

=item C<is_date>

 $bool = $io->is_date($column_name);

Determine if $column_name is of a date type.

=cut
sub is_date
{
    my ($self,$column_name) = @_;
    my $types = $self->column_types();
    return $date_types{$types->{$column_name}};
}

=pod

=item C<is_char>

 $bool = $io->is_char($column_name);

Determine if $column_name is of a character type.

=cut
sub is_char
{
    my ($self,$column_name) = @_;
    my $types = $self->column_types();
    return $char_types{$types->{$column_name}};
}

=pod

=item C<limit>

 $sql = $io->limit($sql,$limit);

Modify the given $sql to return a limited set
of records.

=cut
sub limit
{
    my ($self,$sql,$limit,$where) = @_;
    return "$sql $where ROWNUM < ($limit + 1)";
}

=pod

=item C<lc_func>

 $function = $io->lc_func($column);

Apply the function
for modifying $column to lower case.

=cut
sub lc_func
{
    my ($self,$column) = @_;
    return "LOWER($column)";
}

=pod

=back

=cut


1;

__END__

=head1 BUGS

No known bugs.

=head1 SEE ALSO

L<DBIx::IO::Table>, L<DBIx::IO::Search>, L<DBIx::IO>, L<DBIx::IO::OracleLIB>

=head1 AUTHOR

Reed Sandberg, E<lt>reed_sandberg Ӓ yahooE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2000-2008 Reed Sandberg

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

The full text of the license can be found in the
LICENSE file included with this module.

