16

Is it possible to predict the operations that follow a DELETE CASCADE automatically? In my software I would like to give the user a warning with details about the data that would be deleted then.

user694971
  • 421
  • 1
  • 4
  • 16
  • 2
    Good question, i am interested in the answer too. – Alp May 17 '11 at 10:12
  • The only reasonable approach I can think of currently is `SHOW FULL COLUMNS FROM ` and then traverse into the corresponding tables... (hardcodes fkey_names => table_names...) – user694971 May 20 '11 at 14:53

3 Answers3

4

You can make a copy of the database and put triggers on the after delete

DELIMITER $$

CREATE TRIGGER ad_table1_each AFTER DELETE ON table1 FOR EACH ROW
BEGIN
  INSERT INTO log VALUES (null                 /*autoinc id*/
        , 'table1'                             /*tablename*/
        , old.id                               /*tableid*/
        , concat_ws(',',old.field1,old.field2  /*CSV's of fields*/
        , NOW()                                /*timestamp*/
        , 'delete');                           /*what action*/


  REPLACE INTO restore_table1 VALUES (old.id,
        , old.field1
        , old.field2
        , ... );

END $$

DELIMITER ;

The log table is just a table with the following fields:

id            integer autoincrement primary key
tablename     varchar(45)
table_id      integer
fields        varchar(6000)
delete_time   timestamp
action        enum('insert','update','delete')

If you do a SELECT @last_id:= max(id) FROM log before the delete cascade on the copy.
Then you can do a SELECT * FROM log WHERE id > @last_id
and get all the rows that will be deleted in the cascade.

After that you can use the restore_table1 to recreate the rows that were deleted in the cascade in the copy database.

Johan
  • 74,508
  • 24
  • 191
  • 319
1

I think you could use Johan's trigger solution in combination with a transaction that you roll back. This avoids both the need for a second database and for the manual restore of the deleted entries.

  • add the trigger and the log table
  • for each attempted deletion start a transaction and delete the entries
  • present the information from the log to your user for approval
  • if the user agrees commit the transaction, otherwise rollback
Stefan
  • 53
  • 1
  • 7
  • The only problem is, if the engine that you log into also supports transactions, than the logging will be rolled back as well, so you need to use a non-transactional engine for the logging, like MyISAM. – Johan Sep 26 '11 at 17:38
  • The transaction idea is great. In fact I would neither use triggers nor a log for that. I would just simulate the delete and afterwards rollback. – user694971 Sep 26 '11 at 18:18
  • 1
    @user694971: I think you need the log if you want to show the deleted entries to the user. Without it you can only show the remaining entries, unless your application logic can work that out. – Stefan Sep 28 '11 at 15:25
0

I wrote a very quick hack that does exactly what you need in PHP, since I wanted to do the exact same thing and haven't found any resources for that online.

It might be too late for you, but it may help others.

function get_referencing_foreign_keys ($database, $table) {
    $query = 'SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, REFERENCED_COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = "'.$database.'" AND REFERENCED_TABLE_NAME = '.esc($table);
    $result = rquery($query);
    $foreign_keys = array();
    while ($row = mysql_fetch_row($result)) {
        $foreign_keys[] = array('database' => $row[0], 'table' => $row[1], 'column' => $row[2], 'reference_column' => $row[3]);
    }

    return $foreign_keys;
}

function get_foreign_key_deleted_data_html ($database, $table, $where) {
    $data = get_foreign_key_deleted_data ($database, $table, $where);

    $html = '';
    foreach ($data as $key => $this_data) {
        $html .= "<h2>$key</h2>\n";

        $html .= "<table>\n";
        $i = 0;
        foreach ($this_data as $value) {
            if($i == 0) {
                $html .= "\t<tr>\n";
                foreach ($value as $column => $column_value) {
                    $html .= "\t\t<th>".htmlentities($column)."</th>\n";
                }
                $html .= "\t</tr>\n";
            }
            $html .= "\t<tr>\n";
            foreach ($value as $column => $column_value) {
                $html .= "\t\t<td>".htmlentities($column_value)."</td>\n";
            }
            $html .= "\t</tr>\n";
            $i++;
        }
        $html .= "</table>\n";
    }

    return $html;
}

function get_foreign_key_deleted_data ($database, $table, $where) {
    $GLOBALS['get_data_that_would_be_deleted'] = array();
    $data = get_data_that_would_be_deleted($database, $table, $where);
    $GLOBALS['get_data_that_would_be_deleted'] = array();
    return $data;
}

function get_data_that_would_be_deleted ($database, $table, $where, $recursion = 100) {
    if($recursion <= 0) {
        die("Deep recursion!");
    }

    if($recursion == 100) {
        $GLOBALS['get_data_that_would_be_deleted'] = array();
    }

    if($table) {
        if(is_array($where)) {
            $foreign_keys = get_referencing_foreign_keys($database, $table);
            $data = array();

            $query = 'SELECT * FROM `'.$table.'`';
            if(count($where)) {
                $query .= ' WHERE 1';
                foreach ($where as $name => $value) {
                    $query .= " AND `$name` = ".esc($value);
                }
            }
            $result = rquery($query);

            $to_check = array();

            while ($row = mysql_fetch_row($result)) {
                $new_row = array();
                $i = 0;
                foreach ($row as $this_row) {
                    $field_info = mysql_fetch_field($result, $i);
                    $new_row[$field_info->name] = $this_row;
                    foreach ($foreign_keys as $this_foreign_key) {
                        if($this_foreign_key['reference_column'] == $field_info->name) {
                            $to_check[] = array('value' => $this_row, 'foreign_key' => array('table' => $this_foreign_key['table'], 'column' => $this_foreign_key['column'], 'database' => $this_foreign_key['database']));
                        }
                    }
                    $i++;
                }
                $GLOBALS['get_data_that_would_be_deleted'][$table][] = $new_row;
            }
            foreach ($to_check as $this_to_check) {
                if(isset($this_to_check['value']) && !is_null($this_to_check['value'])) {
                    get_data_that_would_be_deleted($database, $this_to_check['foreign_key']['table'], array($this_to_check['foreign_key']['column'] => $this_to_check['value']), $recursion - 1);;
                }
            }

            $data = $GLOBALS['get_data_that_would_be_deleted'];

            return $data;
        } else {
            die("\$where needs to be an array with column_name => value pairs");
        }
    } else {
        die("\$table was not defined!");
    }
}

Imagine I have a table called "table" in the database "db" and I want to delete the one with the id 180, then I'd call:

print(get_foreign_key_deleted_data_html('db', 'table', array('id' => 180)));

and it prints a full table with all the rows and all the values that would be deleted.

But as I've said, this is a very, very quick and dirty hack. I'd be glad for any bug-report (and there surely are a lot of them!).

Norman
  • 1