# ==================================================================
# Gossamer Threads Module Library - http://gossamer-threads.com/
#
#   GT::Base
#   CVS Info :                          
#   $Id: Editor.pm,v 1.76 2005/04/27 22:53:24 brewt Exp $
#
# Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
# ==================================================================
#
# Description:
#   Class used to make changes to tables and create tables.
#

package GT::SQL::Editor;
# ==================================================================
use strict;
use vars qw/@ISA $DEBUG $VERSION $ERRORS $error $ERROR_MESSAGE/;
use GT::SQL;
use GT::SQL::Base;
use GT::AutoLoader;

$VERSION       = sprintf "%d.%03d", q$Revision: 1.76 $ =~ /(\d+)\.(\d+)/;
$ERROR_MESSAGE = 'GT::SQL';
@ISA           = qw(GT::SQL::Base);
$DEBUG         = 0;

sub new {
    my $this = shift;
    my $class = ref $this || $this;
    my $self = bless {}, $class;

# Get the arguments
    my $opts = $self->common_param(@_) or return $self->fatal(BADARGS => "$class->new(HASH_REF or HASH)");

    ref $opts->{table} or return $self->fatal(BADARGS => "$class->new(HASH_REF or HASH). 'table' must be specified in the hash. It needs to be the an object from GT::SQL::Table.");
    $self->{_debug}   = $opts->{debug}    || $DEBUG;
    $self->{_err_pkg} = $opts->{_err_pkg} || __PACKAGE__;
    $self->{table}    = $opts->{table};
    $self->{connect}  = $opts->{connect};

# We almost always need to be connected.
    $self->{table}->connect or return;
    return $self;
}


#################################################################
#####                    Editing functions                  #####
#################################################################
##
# $obj->add_col($col_name, 
#           { 
#               size => 20, 
#               type => 'int',
#               view_size => 20, 
#               nice_name => "my col", 
#               regex => 'myregex' 
#           }
#       );
# ------------------------------------
#   
##
$COMPILE{add_col} = __LINE__ . <<'END_OF_SUB';
sub add_col {
    my ($self, $name, $col) = @_;

    $name and ref $col eq 'HASH' or return $self->fatal(BADARGS => '$obj->add_col(COLUMN_NAME, HASH_REF)');
    my $c = $self->{table}->cols;

    # Check the database instead of the def file so that we don't end up with
    # an inability to add a column when the database and def files are out of
    # sync.
    my $exists = $self->{table}->{driver}->column_exists($self->{table}->name, $name);
    $exists and return $self->warn(COLEXISTS => $name);

# You are not permitted to add a not_null column without a default to a table -
# the default is required for existing columns.  You could, if you really want
# it with no default, create it with a default, then alter it to drop the
# default.
    return $self->warn(NOTNULLDEFAULT => $name)
        if $col->{not_null} and (not defined $col->{default} or $col->{default} eq '');

# count file columns
    my %fcols_initial = $self->{table}->_file_cols();

# handle the search indexes
    my $tmp_weight = {};
    $tmp_weight = $self->_get_indexer()->pre_add_column($name, $col) or return if $col->{weight};

# get the column definition
    my $col_props = $self->{table}->{driver}->column_sql($col);
    my $table = $self->{table}->name;

# Auto add a new position number.
    $col->{pos} = keys(%$c) + 1;

# Add the column into the table's column hash, for checking.
# N.B. - everything below this point _must_ reload the table information (i.e.
# via ->reset or ->reload) upon failure
    $c->{$name} = $col;

# Check for conflicts
    $self->{table}->check_schema or return $self->{table}->reset;

    require GT::SQL::Creator;
    GT::SQL::Creator::set_defaults($self, { $name => $col });

# Make the changes
    $self->{table}->{driver}->add_column($table, $name, $col_props) or return $self->{table}->reset;
    $self->{mods}->{$table} = $self->{table};
    $self->save_state;

# Check for file columns
    if (not keys %fcols_initial and uc $col->{form_type} eq 'FILE') {
        require GT::SQL::File;
        my $ftable = GT::SQL::File->new(parent_table => $self->{table}, connect => $self->{connect});
        $ftable->debug_level($self->{_debug});
        $ftable->install({ parent_tablename => $self->{table}->name() });
        $self->{table}->_file_cols(1);
    }

# finish off the search indexes
    if ($col->{weight}) {
        $self->_get_indexer()->post_add_column($name, $col, $tmp_weight) or return;
    }

    1;
}
END_OF_SUB

##
# $obj->drop_col($col_name);
# ---------------------------
#   Drops the column specified by $col_name.
#   If the column is referenced returns an error.
#   If the column is itself an fk reference, the foreign key is dropped.
#
# $obj->drop_col($col_name, "remove");
# -------------------------------------
#   Drops column and all fk references to it.
#
##
$COMPILE{drop_col} = __LINE__ . <<'END_OF_SUB';
sub drop_col {
    my $self = shift;
    my $name = shift || return $self->fatal(BADARGS => '$obj->drop_col(COLUMN_NAME,[ STRING ])');
    exists $self->{table}->cols->{$name} or return $self->warn(NOCOL => $name);
    my $kill = shift;

    my %fcols = $self->{table}->_file_cols();
    my $table = $self->{table}->name;
    if ($self->_is_referenced($table, $name)) {
        if (defined $kill) {
            $self->_remove_references($table, $name);
        }
        else {
            return $self->warn(REFCOL => $name, $table);
        }
    }

    my @fk_tables = grep exists $self->{table}->{fk}->{$_}->{$name}, keys %{$self->{table}->{fk}};
    if (@fk_tables) {
        $self->drop_fk($_, 1);
    }

    my $tmp_weight = {};
    if (($self->{table}->cols->{$name} || {})->{weight}) {
        $tmp_weight = $self->_get_indexer()->pre_delete_column($name, $self->{table}->cols->{$name}) or return
    }

# Columns
    my $old_col = delete $self->{table}->cols->{$name};

# Primary key
    $self->{table}->pk(grep $_ ne $name, $self->{table}->pk);

# Foreign keys
    while (my ($table, $fk) = each %{$self->{table}->fk}) {
        for my $col (keys %$fk) {
            if ($col eq $name) {
                delete $self->{table}->fk->{$_}->{$col}
            }
        }
    }

# Indexes and uniques
    for my $index (qw/index unique/) {
        my $ndx = $self->{table}->$index();
        for (keys %$ndx) {
            my @new = grep $_ ne $name, @{$ndx->{$_}};
            if (@new) {
                $ndx->{$_} = \@new;
            }
            else {
                delete $ndx->{$_};
            }
        }
    }

# Update the positions.
    my $cols = $self->{table}->{cols};
    my $i;
    for my $col (sort { $cols->{$a}->{pos} <=> $cols->{$b}->{pos} } keys %$cols) {
        $cols->{$col}->{pos} = ++$i;
    }

# Check for conflicts
    $self->{table}->check_schema or return $self->{table}->reset;

# File Handling
    if ($fcols{$name}) {
        require GT::SQL::File;
        my $ftable = GT::SQL::File->new(parent_table => $self->{table}, connect => $self->{connect});
        $ftable->debug_level($self->{_debug});
        $ftable->drop_col($name, $fcols{$name}->{file_save_scheme}) or return $self->{table}->reset;
        $self->{table}->_file_cols(1);
    }

# Finish off the index table stuff
    if (($self->{table}->cols->{$name} || {})->{weight}) {
        $tmp_weight = $self->post_delete_column($name, $self->{table}->cols->{$name}, $tmp_weight)
            or return $self->{table}->reset;
    }

# Make the changes - actually drop the column
    $self->{table}->{driver}->drop_column($table, $name, $old_col) or return $self->{table}->reset;
    $self->{mods}->{$table} = $self->{table};
    $self->save_state;

    1;
}
END_OF_SUB

##
# $obj->alter_col($column_name, \%new_defs);
# -------------------------------------------
#   
$COMPILE{alter_col} = __LINE__ . <<'END_OF_SUB';
sub alter_col {
    my ($self, $col, $defs) = @_;

    ref $defs eq 'HASH' or return $self->fatal(BADARGS => '$obj->alter_col(COLUMN_NAME, HASH_REF)');
    exists $self->{table}->{schema}->{cols}->{$col} or return $self->warn(NOCOL => $col);

    my %fcols = $self->{table}->_file_cols();

# Can't change the position, force it to what it was before.
    my $orig = $self->{table}->{schema}->{cols}->{$col};
    my $table = $self->{table}->{name};

# Set the position, can't be changed.
    $defs->{pos} = $orig->{pos};

# Check to see if we need to update the SQL.
    my $orig_sql = $self->{table}->{driver}->column_sql($orig);
    my $new_sql  = $self->{table}->{driver}->column_sql($defs);
    my $change   = $orig_sql ne $new_sql;

# If we've changed, check the keys.
    if ($change) {
        return $self->warn(REFCOL => $col, $table) if $self->_is_referenced($table, $col);
        return $self->warn(COLREF => $col, $table) if exists $self->{table}->fk->{$col};
    }

# Check for conflicts
    my $old_col = $self->{table}->{schema}->{cols}->{$col};
    $self->{table}->{schema}->{cols}->{$col} = $defs;
    $self->{table}->check_schema or return $self->{table}->reset;

# adding a file column
    if (not keys %fcols and $defs->{form_type} and lc $defs->{form_type} eq 'file') {

        require GT::SQL::File;
        my $ftable = GT::SQL::File->new({ parent_table => $self->{table}, connect => $self->{connect} });
        $ftable->debug_level($self->{_debug});
        $ftable->install({parent_tablename => $self->{table}->name() });
    }

# removing a file column
    elsif ($fcols{$col} and not ($defs->{form_type} and lc $defs->{form_type} eq 'file')) {
        require GT::SQL::File;
        my $ftable = GT::SQL::File->new({ parent_table => $self->{table}, connect => $self->{connect} });
        $ftable->drop_col($col);
    }

# Make the changes
    if ($change) {
        $self->{table}->{driver}->alter_column($table, $col, $new_sql, $old_col) or return $self->{table}->reset;
    }
    $self->{mods}->{$table} = $self->{table};
    $self->save_state or return;

# finish off the file column setup
    if ($defs->{form_type} and lc $defs->{form_type} eq 'file') {
        $self->{table}->update({ $col => '' });
        $self->{table}->_file_cols(1);
    }

    1;
}
END_OF_SUB

##
# $obj->add_index($index_name => [ field1, field2 .. ]);
# --------------------
#   Add a index to the table specified by 
#   $index_name. The array should contain fields
#   that will be part of the index.
##
$COMPILE{add_index} = __LINE__ . <<'END_OF_SUB';
sub add_index {
    my ($self, $index_name, $columns) = @_;
    ref $columns eq 'ARRAY' or return $self->fatal(BADARGS => '$obj->add_index(INDEX_NAME => ARRAY_REF)');

# Do the columns exist?
    for (@$columns) {
        return $self->warn(NOCOL => $_) unless exists $self->{table}->cols->{$_};
    }

    exists $self->{table}->{schema}->{index}->{$index_name}
        and return $self->warn(INDXEXISTS => $index_name);

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

# Check for conflicts
    $self->{table}->{schema}->{index}->{$index_name} = $columns;
    $self->{table}->check_schema or return $self->{table}->reset;

# Make the changes
    $self->{table}->{driver}->create_index($table, $index_name, @$columns) or return $self->{table}->reset;
    $self->{mods}->{$table} = $self->{table};
    $self->save_state;
    1;
}
END_OF_SUB

##
# $obj->drop_index($index_name);
# --------------------------------
#   Drops an index by the name $index_name.
##
$COMPILE{drop_index} = __LINE__ . <<'END_OF_SUB';
sub drop_index {
    my ($self, $index_name) = @_;
    $index_name or return $self->fatal(BADARGS => '$obj->drop_index(INDEX_NAME)');
    exists $self->{table}->index->{$index_name} or return $self->warn(NOINDEX => $index_name);

# Check for conflicts
    delete $self->{table}->index->{$index_name};
    $self->{table}->check_schema or return $self->{table}->reset;

# Make the changes
    my $table = $self->{table}->name;
    $self->{table}->{driver}->drop_index($table, $index_name) or return $self->{table}->reset;
    $self->{mods}->{$table} = $self->{table};
    $self->save_state;
    1;
}
END_OF_SUB

##
# $obj->add_unique($index_name => [ field1, field2 .. ]);
# --------------------
#   Add a unique index to the table specified by 
#   $index_name. The array should contain fields
#   that will be part of the index.
##
$COMPILE{add_unique} = __LINE__ . <<'END_OF_SUB';
sub add_unique {
    my ($self, $index_name, $indexes) = @_;

    $index_name and ref $indexes eq 'ARRAY' or return $self->fatal(BADARGS => '$obj->add_unique(INDEX_NAME => ARRAY_REF)');
# Do the columns exist?
    for (@$indexes) {
        exists $self->{table}->cols->{$_} or return $self->warn(NOCOL => $_);
    }
    exists $self->{table}->unique->{$index_name} and return $self->warn(INDXEXISTS => $index_name);

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

# Do the new fields have unique data in them?
    my $in = join ", " => @{$indexes};
    my $query = "SELECT $in, COUNT(*) AS hits FROM $table GROUP BY $in HAVING ";
    $query .= lc $self->{table}->{connect}->{driver} eq 'mysql' ? 'hits' : 'COUNT(*)';
    $query .= ' > 1';
    $self->debug($query) if $self->{_debug};

    my $sth = $self->{table}->do($query) or return;
    $sth->fetchrow and return $self->warn(NOTUNIQUE => $index_name);

# Check for conflicts
    $self->{table}->unique->{$index_name} = $indexes;
    $self->{table}->check_schema or return $self->{table}->reset;

# Make the changes
    $self->{table}->{driver}->create_unique($table, $index_name, @$indexes) or return $self->{table}->reset;
    $self->{mods}->{$table} = $self->{table};
    $self->save_state;
    1;
}
END_OF_SUB

##
# $obj->drop_unique($index_name);
# --------------------------------
#   Drops an index by the name $index_name.
##
$COMPILE{drop_unique} = __LINE__ . <<'END_OF_SUB';
sub drop_unique {
    my ($self, $index_name) = @_;

    $index_name or return $self->fatal(BADARGS => '$obj->drop_unique(INDEX_NAME)');
    exists $self->{table}->unique->{$index_name} or return $self->warn(NOUNIQUE => $index_name);

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

# Check for conflicts
    delete $self->{table}->unique->{$index_name};
    $self->{table}->check_schema or return $self->{table}->reset;

# Make the changes
    $self->{table}->{driver}->drop_index($table, $index_name) or return $self->{table}->reset;
    $self->{mods}->{$table} = $self->{table};
    $self->save_state;
    1;
}
END_OF_SUB

##
# $obj->add_pk($field1, $field2, ...);
# -------------------------------------
#   Addes primary keys specified by list. If there is already a primary key it
#   drops it and adds all the keys at the same time.  If there is no primary
#   keys this makes sure the data in the primary keys is unique.
##
$COMPILE{add_pk} = __LINE__ . <<'END_OF_SUB';
sub add_pk {
    my ($self, @fields) = @_;

    @fields or return $self->fatal(BADARGS => '$obj->add_pk(COLUMN1, COLUMN2, ...)');
    for (@fields) {
        exists $self->{table}->cols->{$_} or return $self->warn(NOCOL => $_);
    }

    my ($table, %add) = $self->{table}->name;
    if ($self->{table}->pk) {
        $self->{table}->{driver}->drop_pk($table) or return;
        %add = map { $_ => 1 } @{delete $self->{table}->{schema}->{pk}};
    }

# Check for conflicts
    for (@fields) { $add{$_} = 1 }
    $self->{table}->{schema}->{pk} = [keys %add];
    $self->{table}->check_schema or return $self->{table}->reset;

# Make the changes
    $self->{table}->{driver}->create_pk($table, keys %add) or return $self->{table}->reset;
    $self->{mods}->{$table} = $self->{table};
    $self->save_state;
    1;
}
END_OF_SUB

##
# $obj->drop_pk;
# --------------
#   Drops the current primary key.
##
$COMPILE{drop_pk} = __LINE__ . <<'END_OF_SUB';
sub drop_pk {
    my $self = shift;
    $self->{table}->pk or return $self->warn('NOPK');

# Check for conflicts
    $self->{table}->{schema}->{pk} = [];
    $self->{table}->check_schema or return $self->{table}->reset;

# Make the changes
    my $table = $self->{table}->name;
    $self->{table}->{driver}->drop_pk($table) or return $self->{table}->reset;
    1;
}
END_OF_SUB

##
# $obj->add_fk( RELATION_NAME, { SOURCE_FIELD_1 => TARGET_FIELD });
# ------------------------------------------------------------------
#   Sets the foreign key relations for one relation.
#
#   this structure introduces a limitations: a table cannot
#   refer two schemas in the same target table, which should
#   really not be a problem.
##
$COMPILE{add_fk} = __LINE__ . <<'END_OF_SUB';
sub add_fk {
    my $self = shift;
    $self->{table}->fk(@_) or return;
    1;
}
END_OF_SUB

##
# $obj->drop_fk(RELATION_NAME [, SKIPSAVE]);
# ---------------------------------------------
#   Drops the foreign key relation for a given relation.  If a second parameter
#   is passed, and true, the state of the current table will not be saved (any
#   other changed tables are, however).
##
$COMPILE{drop_fk} = __LINE__ . <<'END_OF_SUB';
sub drop_fk {
    my ($self, $tbl, $nosave) = @_;
    my $table = $self->{connect}->{PREFIX} . $tbl;
    delete $self->{table}->{schema}->{fk}->{$table}
        or return $self->warn(FKNOEXISTS => $tbl, $self->{table}->{name});
    my $remote = $self->new_table($table);
    my $rfk = $remote->fk_tables || [];
    $remote->fk_tables([grep $_ ne $self->{table}->{name}, @$rfk]);
    $remote->save_state;
    $self->{table}->save_state unless $nosave;
}
END_OF_SUB

##
# $obj->add_tree(ARGS);
# ---------------------
#   Create a tree table for the current table.
#   'ARGS' is a hash or hash reference consisting of the following:
#       father => 'father_id_column',
#       root => 'root_id_column',
#       depth => 'depth_column'
#   where 'father_id_column', 'root_id_column', and 'depth_column' are the names
#   of the columns you will use for keeping track of the father record, root
#   record, and the depth from the root record, respectively. All of these
#   columns should already exist - an error will occur if they do not.
#
#   Any other arguments passed in will be passed straight through to
#   GT::SQL::Tree->create
##
$COMPILE{add_tree} = __LINE__ . <<'END_OF_SUB';
sub add_tree {
    my $self = shift;

    my $input = $self->common_param(@_) or return $self->fatal(BADARGS => 'GT::SQL::Editor->add_tree(HASH or HASH REF)');

    return $self->warn(TREEEXISTS => $self->{table}->{name}) if $self->{table}->{schema}->{tree} and ($input->{force} || 'force') eq 'check';

    $input->{father} or return $self->fatal(BADARGS => 'GT::SQL::Editor->add_tree(..., father => \'father_col\', ...)');
    $input->{root}   or return $self->fatal(BADARGS => 'GT::SQL::Editor->add_tree(..., root => \'root_col\', ...)');
    $input->{depth}  or return $self->fatal(BADARGS => 'GT::SQL::Editor->add_tree(..., depth => \'depth_col\', ...)');

    require GT::SQL::Tree;
    GT::SQL::Tree->create(debug => $self->{_debug}, %$input, table => $self->{table});
}
END_OF_SUB

$COMPILE{drop_tree} = __LINE__ . <<'END_OF_SUB';
sub drop_tree {
    my $self = shift;
    my $tree = $self->{table}->tree or return;
    $tree->destroy;
}
END_OF_SUB

$COMPILE{load_data} = __LINE__ . <<'END_OF_SUB';
sub load_data {
# ---------------------------------------------------------------
# imports the contents of a file with validation.
#
    my ($self, $file, $options) = @_;
    -f $file and -r _ or return $self->fatal(FILENOEXISTS => $file);
    $self->{table}->connect or return;

    my $delim = $options->{delim} || '|';
    my @cols  = ref $options->{cols} ? @{$options->{cols}} : @{$self->{table}->ordered_columns};

    local *FILE;
    open FILE, $file or return $self->warn(CANTOPEN => $file, "$!");
    while (<FILE>) {
        chomp;
        my $i = 0;
        my %fields = map { $cols[$i++] => $_ } split /(?<!\\)\Q$delim\E/;
        $self->{table}->insert(\%fields, 1) or print "Line $. skipped - validation failed:\n$GT::SQL::error\n\n";
   }
   close FILE;
   1;
}
END_OF_SUB

$COMPILE{export_data} = __LINE__ . <<'END_OF_SUB';
sub export_data {
# ---------------------------------------------------------------
# Dumps the contents of a table to a file.
#
    my $self = shift;
    my $opt  = shift;
    ref $opt eq 'HASH' or return $self->fatal(BADARGS => '$obj->export_data(HASHREF)');

    my $order  = $opt->{order};
    my $delim  = $opt->{delim}  || '|';
    my $file   = $opt->{file}   || undef;
    my $header = $opt->{header} || undef;
    my $table  = $self->{table}->name;

    my @order = $order
        ? ref $order eq 'ARRAY' ? @$order : $order
        : $self->{table}->ordered_columns;

    my ($offset, $limit) = (0, 1000);

    local *FILE;
    if ($file) {
        open FILE, "> $file" or return $self->warn(CANTOPEN => $file, "$!");
    }
    while () {
        $self->{table}->select_options("LIMIT $limit OFFSET " . ($offset++ * $limit));
        my $sth = $self->{table}->select(\@order);

        if ($header) {
            print FILE join($delim, @order), "\n";
            $header = undef;
        }
        my $count = 0;
        while (my $arr = $sth->fetchrow_arrayref) {
            ++$count;
            for (@$arr) {
                y/\r//d;
                s/\Q$delim\E/``/g;
                s/\n/~~/g;
            }
            my $joined = join $delim, @$arr;
            $file
                ? print FILE $joined, "\n"
                : print $joined, "\n";
        }
        last unless $count;
    }
    1;
}
END_OF_SUB

##
# $obj->drop_search_driver
# -----------------
#   Drops current search driver
##
$COMPILE{drop_search_driver} = __LINE__ . <<'END_OF_SUB';
sub drop_search_driver {
    my $self    = shift;

    require GT::SQL::Search;
    if ($self->{table}->search_driver) {
        my $indexer = GT::SQL::Search->load_indexer(table => $self->{table}, _debug => $self->{_debug}) or return;
        $indexer->drop_search_driver or return;
    }
    $self->{table}->search_driver('NONINDEXED');
    $self->{table}->save_state;
    1;
}
END_OF_SUB

##
# $obj->add_search_driver
# -----------------
#   Adds new search driver
##
$COMPILE{add_search_driver} = __LINE__ . <<'END_OF_SUB';
sub add_search_driver {
    my $self      = shift;
    my $olddriver = $self->{table}->search_driver();
    my $newdriver = shift or return;

    require GT::SQL::Search;

# check and see if driver is ok
    GT::SQL::Search->driver_ok($newdriver, { table => $self->{table} }) or return;

# load the driver
    my $indexer = GT::SQL::Search->load_indexer(table => $self->{table}, _debug => $self->{_debug}, driver => $newdriver) or return;
    $indexer->add_search_driver or return;

    $self->{table}->search_driver($newdriver);
    $self->{table}->save_state or return;
    1;
}
END_OF_SUB

##
# $obj->change_search_driver
# -----------------
#   Adds new search driver
##
$COMPILE{change_search_driver} = __LINE__ . <<'END_OF_SUB';
sub change_search_driver {
    my $self      = shift;
    my $newdriver = uc shift or return;
    my $driver    = $self->{table}->search_driver;
    $driver eq $newdriver and return $self->warn(SAMEDRIVER => $driver);

    $self->drop_search_driver() or return;
    $self->add_search_driver($newdriver) or return;

    1;
}
END_OF_SUB

##
# $obj->drop_table;
# -----------------
#   Drops the current table.
##
$COMPILE{drop_table} = __LINE__ . <<'END_OF_SUB';
sub drop_table {
    my $self  = shift;
    my $rm_fk = lc(shift or '') eq 'remove';
    my $table = $self->{table}->name;
    my $tmp   = $self->{table}->fk_tables() || [];
    @$tmp and !$rm_fk and return $self->warn(TABLEREFD => $table);

    my $tmp_weights = {};
    if ($self->_uses_weights) {
        $tmp_weights = $self->_get_indexer->pre_drop_table() or return
    }

    $self->{table}->{driver}->drop_table($table) or return;

    delete $GT::SQL::OBJ_CACHE{"TABLE\0$table\0$self->{connect}->{def_path}"};

# If this table has a tree, drop it:
    $self->drop_tree if $self->{table}->{schema}->{tree};

    unlink "$self->{connect}->{def_path}/$table.def";

    for (keys %{$self->{table}->{schema}->{fk}}) {
        next if $_ eq $table;
        my $t = $self->new_table($_);
        $t->{schema}->{fk_tables} = [grep $_ ne $table, @{$t->{schema}->{fk_tables}}];
        $t->save_state();
    }

    $self->_file_drop_tables();
    $self->_uses_weights and ($self->_get_indexer->post_drop_table($tmp_weights) or return);
    $rm_fk and $self->_drop_related_fk_entries($table);

    1;
}
END_OF_SUB

$COMPILE{_file_drop_tables} = __LINE__ . <<'END_OF_SUB';
sub _file_drop_tables {
    my $self    = shift;
    if ( $self->{table}->_file_cols() ) {
        require GT::SQL::File;
        GT::SQL::File->new({ parent_table => $self->{table}, connect => $self->{connect} })->drop_table();
    }
}
END_OF_SUB

$COMPILE{_drop_related_fk_entries} = __LINE__ . <<'END_OF_SUB';
sub _drop_related_fk_entries {
    my $self        = shift;
    my $table_name  = shift or return;

    my $fk          = $self->{table}->fk() or return;
    my $prefix      = $self->{connect}->{PREFIX};
    for my $related_name ( keys %{$fk} ) {
        my $table     = $self->{table}->new_table($related_name);
        my $fk_tables = $table->fk_tables() or next;
        $fk_tables    = [ grep { $_ ne $table_name  } @{$fk_tables} ];
        $table->fk_tables( $fk_tables );
        $table->save_state();
    }

    1;
}
END_OF_SUB

###########################################################################
#####                       Private Functions                         #####
###########################################################################

$COMPILE{_is_referenced} = __LINE__ . <<'END_OF_SUB';
sub _is_referenced {
    my ($self, $mytable, $mycol) = @_;
    for my $table (@{$self->{table}->fk_tables}) {
        my $fk = $self->new_table($table)->fk;
        if (exists $fk->{$mytable}) {
            for my $key (keys %{$fk->{$mytable}}) {
                if ($mycol eq $fk->{$mytable}->{$key}) {
                    return 1;
                }
            }
        }
    }
    0;
}
END_OF_SUB

$COMPILE{_remove_referenced} = __LINE__ . <<'END_OF_SUB';
sub _remove_referenced {
    my ($self, $mytable, $mycol) = @_;
    for my $table (@{$self->{table}->fk_tables}) {
        my $new_table = $self->{mods}->{$table} || $self->new_table($table);
        my $fk = $new_table->fk;
        if (exists $fk->{$mytable}) {
            for my $key (keys %{$fk->{$mytable}}) {
                if ($mycol eq $fk->{$mytable}->{$key}) {
                    delete $fk->{$mytable}->{$key};
                    $self->{mods}->{$table} ||= $new_table;
                }
                if (not keys %{$fk->{$mytable}}) {
                    delete $fk->{$mytable};
                    $self->{mods}->{$table} ||= $new_table;
                }
            }
        }
    }
    1;
}
END_OF_SUB

$COMPILE{_remove_references} = __LINE__ . <<'END_OF_SUB';
sub _remove_references {
    my ($self, $mytable, $mycol) = @_;
    for my $table (keys %{$self->{table}->fk}) {
        if ($self->{table}->fk->{$table}->{$mycol}) {
            delete $self->{table}->fk->{$table}->{$mycol};
        }
        next if keys %{$self->{table}->fk->{$table}};
        my $t = $self->{mods}->{$table} || $self->new_table($table);
        $t->{schema}->{fk_table} = [grep $_ ne $mytable, @{$t->fk_tables}];
        $self->{mods}->{$table} = $t;
    }
    1;
}
END_OF_SUB

sub save_state {
    my $self = shift;
    for my $table (keys %{$self->{mods}}) {
        my $new_table = $self->{mods}->{$table};
        $new_table->save_state or return;
        delete $self->{mods}->{$new_table};
    }
    1;
}

sub _uses_weights {
#-------------------------------------------------------------------------------
    return keys %{$_[0]->{table}->weight()}
}

$COMPILE{_get_indexer} = __LINE__ . <<'END_OF_SUB';
sub _get_indexer {
#-------------------------------------------------------------------------------
    my $self = shift;
    require GT::SQL::Search;
    my $indexer = GT::SQL::Search->load_indexer(
        table  => $self->{table},
        debug  => $self->{_debug}
    );
    return $indexer;
}
END_OF_SUB

1;

__END__

=head1 NAME

GT::SQL::Editor - an interface to modify an SQL table.

=head1 SYNOPSIS

    my $editor = $DB->editor('Table');
    $editor->add_col(Foo => { size => 20, type => 'int' });
    $editor->export_data('/tmp/foo.txt');

=head1 DESCRIPTION

GT::SQL::Editor is an easy way to do a lot of table maintenance
functions like:

* Adding columns
* Dropping columns
* Changing columns
* Altering keys
* Importing data
* Dropping data

To get an editor object, you simply call C<editor> from a
GT::SQL object, and specify the tablename you want to edit:

    $editor = $db->editor('TableName');

Note: You can not use Editor with relations, only tables.

=head2 add_col

This method allows you to add a column to the current table.
All attributes for the column are passed in a single hash. 

    $editor->add_col($col_name, 
                        { 
                            size => 20, 
                            type => 'int',
                            view_size => 20, 
                            nice_name => "my col", 
                            regex => 'myregex' 
                        }
                    );

The same rules apply to this method that apply when you
define a column for creating a table. You must specify the
type.

=head2 drop_col

This method drops a column from the current table. Checks
are made to ensure the column is not linked to by a foreign
key relation. 

    $editor->drop_col($col_name);

-or-

    $editor->drop_col($col_name, "remove");

If you just specify the column name C<drop_col> will check if
the column is referenced in a foreign key relation. If it
is C<drop_col> will return undef and set the error message in
$GT::SQL::error. If it is not the column will be dropped.

If you specify "remove" C<drop_col> will remove all foreign
key relations that point to the specified column.

If the specified column is itself a foreign key relation, the relation will be
dropped.

=head2 alter_col

This allows you to make changes to a columns type, null status,
etc..

    $editor->alter_col($column_name,
                                { 
                                    size => 20, 
                                    type => 'int' 
                                });

The first argument is the column name the second is the definitions.
The column definitions are exactly the same as the column
definitions from the create. The type must be specified.

You can not add attributes to the column in this way.
You must specify the original definitions along with the
changes you need to make.

=head2 add_unique

This allows you to add a unique index to the current table.
If the name of the unique index is the same as another
index you C<add_unique> will return undef and set the error
in $GT::SQL::error.

    $editor->add_unique($index_name => [ $field1, $field2 .. ]);

The name of the new index is the first argument. The second argument
is an array reference containing the columns that will be indexed.
The order of the columns are maintained for the unique index.
If you specify an index that has data in it that is not unique
(yes we do a select on the database) C<add_unique> will return
an error and set the error in $GT::SQL::error.

=head2 drop_unique

This method allows you to drop a unique index for the current
table. If the unique index does not exist C<drop_unique> will
return undef and set the error in $GT::SQL::error. C<drop_unique>
will also check to make sure dropping the unique index will not
cause problems for the database structure. If dropping the unique
index will cause a problem C<drop_unique> will return undef and set
the error in $GT::SQL::error.

    $editor->drop_unique($index_name);

$index_name should be the name of the unique index to drop.

=head2 add_index

This takes the same arguments as C<add_unique> and return the same thing.
The only difference is C<add_index> has no reason to check the content of
the current table because indexes are not unique. unique indexes are  :)

    $editor->add_index($index_name => [ $field1, $field2 .. ]);

=head2 drop_index

This method drops the specified index from the current table.
C<drop_index> will check to make sure no problems are caused from
dropping the index. If there are C<drop_index> will return undef
and set the error in $GT::SQL::error.

    $editor->drop_index($index_name);

$index_name should be the name of the index to drop.

=head2 add_pk

This method allows you to add a primary key to the current 
database. 

    $editor->add_pk($field1, $field2, ...);

If there is already a primary key in the database C<add_pk>
will drop the key and add the this new one. The table
will be check to make sure this change does not create problems
for the table. I problem is auto increment not being the primary
key anymore. If there is a problem this function returns undef 
and stores the error in $GT::SQL::error.

=head2 drop_pk

This method drops the current primary key. If there is no primary
key to drop it returns undef and sets the error in $GT::SQL::error.

    $editor->drop_pk;

If dropping the primary key will cause problems for the database
this method will return undef and set the error in $GT::SQL::error.

=head2 add_fk

This method allows you to add foreign key relations to the current 
table.

    $editor->add_fk($RELATION_NAME, { $SOURCE_FIELD_1 => $TARGET_FIELD });

You can not link your foreign key to tables that do not exist. Also the
columns types and lengths for the two columns must be the same.
Circularity is not allowed either. That is a set of foreign keys can not
end up pointing back at the same table they started at. All of these things
are checked when this is added. If anything does not match this method returns
undef and sets the error in $GT::SQL::error.

=head2 drop_fk

This method drops the specified foreign key relation. 

    $editor->drop_fk($table);

$table should be the name of the foreign table the foreign
key points to.

=head2 drop_table

This method drops the current table. If there are any foreign keys
pointing to this table this method will fail and return undef. The error
will be set in $GT::SQL::error.

    $editor->drop_table;

-or-

    $editor->drop_table("remove");

If the first argument to this method is remove it will remove all
the foreign key relations that point to this table.

=head1 COPYRIGHT

Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
http://www.gossamer-threads.com/

=head1 VERSION

Revision: $Id: Editor.pm,v 1.76 2005/04/27 22:53:24 brewt Exp $

=cut
