/**
* com.mckoi.database.MasterTableJournal 19 Nov 2000
*
* Mckoi SQL Database ( http://www.mckoi.com/database )
* Copyright (C) 2000, 2001, 2002 Diehl and Associates, Inc.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* Version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License Version 2 for more details.
*
* You should have received a copy of the GNU General Public License
* Version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* Change Log:
*
*
*/
package com.mckoi.database;
import com.mckoi.util.IntegerVector;
import java.io.*;
/**
* A journal of changes that occured to a table in a data conglomerate during
* a transaction.
*
* @author Tobias Downer
*/
final class MasterTableJournal {
/**
* Journal commands.
*/
final static byte TABLE_ADD = 1; // Add a row to a table.
// (params: table_id, row_index)
final static byte TABLE_REMOVE = 2; // Remove a row from a table.
// (params: table_id, row_index)
final static byte TABLE_UPDATE_ADD = 5; // Add a row from an update.
final static byte TABLE_UPDATE_REMOVE = 6; // Remove a row from an update.
/**
* The commit id given to this change when it is committed. This is only
* set when the journal is a committed change to the database.
*/
private long commit_id;
/**
* The master table id.
*/
private int table_id;
/**
* The number of entries in this journal.
*/
private int journal_entries;
/**
* A byte[] array that represents the set of commands a transaction
* performed on this table.
*/
private byte[] command_journal;
/**
* An IntegerVector that is filled with parameters from the command journal.
* For example, a 'TABLE_ADD' journal log will have as parameters the
* row_index that was added to this table.
*/
private IntegerVector command_parameters;
/**
* Constructs the master table journal.
*/
MasterTableJournal(int table_id) {
this.table_id = table_id;
command_journal = new byte[16];
command_parameters = new IntegerVector(32);
}
MasterTableJournal() {
this(-1);
}
/**
* Sets the 'commit_id'. This is only set when this change becomes a
* committed change to the database.
*/
void setCommitID(long commit_id) {
this.commit_id = commit_id;
}
/**
* Returns true if the given command is an addition command.
*/
static boolean isAddCommand(byte command) {
return ((command & 0x03) == TABLE_ADD);
}
/**
* Returns true if the given command is a removal command.
*/
static boolean isRemoveCommand(byte command) {
return ((command & 0x03) == TABLE_REMOVE);
}
/**
* Adds a command to the journal.
*/
private void addCommand(byte command) {
if (journal_entries >= command_journal.length) {
// Resize command array.
int grow_size = Math.min(4000, journal_entries);
grow_size = Math.max(4, grow_size);
byte[] new_command_journal = new byte[journal_entries + grow_size];
System.arraycopy(command_journal, 0, new_command_journal, 0,
journal_entries);
command_journal = new_command_journal;
}
command_journal[journal_entries] = command;
++journal_entries;
}
/**
* Adds a parameter to the journal command parameters.
*/
private void addParameter(int param) {
command_parameters.addInt(param);
}
/**
* Removes the top n entries from the journal.
*/
private void removeTopEntries(int n) {
journal_entries = journal_entries - n;
command_parameters.crop(0, command_parameters.size() - n);
}
/**
* Adds a new command to this journal.
*/
void addEntry(byte command, int row_index) {
addCommand(command);
addParameter(row_index);
}
// ---------- Getters ----------
// These methods assume the journal has been setup and no more entries
// will be made.
/**
* Returns the commit_id that has been set for this journal.
*/
long getCommitID() {
return commit_id;
}
/**
* Returns the table id of the master table this journal is for.
*/
int getTableID() {
return table_id;
}
/**
* Returns the total number of journal entries.
*/
int entries() {
return journal_entries;
}
/**
* Returns the command of the nth entry in the journal.
*/
byte getCommand(int n) {
return command_journal[n];
}
/**
* Returns the row index of the nth entry in the journal.
*/
int getRowIndex(int n) {
return command_parameters.intAt(n);
}
/**
* Returns a normalized list of all rows that were added in this journal,
* but not including those rows also removed. For example, if rows
* 1, 2, and 3 were added and 2 was removed, this will return a list of
* 1 and 3.
*/
int[] normalizedAddedRows() {
IntegerVector list = new IntegerVector();
int size = entries();
for (int i = 0; i < size; ++i) {
byte tc = getCommand(i);
if (tc == TABLE_ADD || tc == TABLE_UPDATE_ADD) {
int row_index = getRowIndex(i);
// If row added, add to list
list.addInt(row_index);
}
else if (tc == TABLE_REMOVE || tc == TABLE_UPDATE_REMOVE) {
// If row removed, if the row is already in the list
// it's removed from the list, otherwise we leave as is.
int row_index = getRowIndex(i);
int found_at = list.indexOf(row_index);
if (found_at != -1) {
list.removeIntAt(found_at);
}
}
else {
throw new Error("Unknown command in journal.");
}
}
return list.toIntArray();
}
/**
* Returns a normalized list of all rows that were removed from this
* journal.
*/
int[] normalizedRemovedRows() {
IntegerVector list = new IntegerVector();
int size = entries();
for (int i = 0; i < size; ++i) {
byte tc = getCommand(i);
if (tc == TABLE_REMOVE || tc == TABLE_UPDATE_REMOVE) {
// If removed add to the list.
int row_index = getRowIndex(i);
list.addInt(row_index);
}
}
return list.toIntArray();
}
/**
* Returns three lists - a list of all rows that were inserted, a list of all
* rows that were deleted, and a list of all updates. All the lists are
* ordered by the order of the command. The update list contains two
* entries per 'update', the row that was removed and the row that was
* added with the updated info.
* <p>
* This method is useful for collecting all modification information on the
* table.
*/
IntegerVector[] allChangeInformation() {
IntegerVector[] lists = new IntegerVector[3];
for (int i = 0; i < 3; ++i) {
lists[i] = new IntegerVector();
}
int size = entries();
for (int i = 0; i < size; ++i) {
byte tc = getCommand(i);
int row_index = getRowIndex(i);
if (tc == TABLE_ADD) {
lists[0].addInt(row_index);
}
else if (tc == TABLE_REMOVE) {
lists[1].addInt(row_index);
}
else if (tc == TABLE_UPDATE_ADD || tc == TABLE_UPDATE_REMOVE) {
lists[2].addInt(row_index);
}
else {
throw new RuntimeException("Don't understand journal command.");
}
}
return lists;
}
/**
* Rolls back the last n entries of this journal. This method takes into
* account the transient nature of rows (all added rows in the journal are
* exclusively referenced by this journal). The algorithm works as follows;
* any rows added are deleted, and rows deleted (that weren't added) are
* removed from the journal.
*/
void rollbackEntries(int n) {
if (n > journal_entries) {
throw new RuntimeException(
"Trying to roll back more journal entries than are in the journal.");
}
IntegerVector to_add = new IntegerVector();
// Find all entries and added new rows to the table
int size = entries();
for (int i = size - n; i < size; ++i) {
byte tc = getCommand(i);
if (tc == TABLE_ADD || tc == TABLE_UPDATE_ADD) {
to_add.addInt(getRowIndex(i));
}
}
// Delete the top entries
removeTopEntries(n);
// Mark all added entries to deleted.
for (int i = 0; i < to_add.size(); ++i) {
addEntry(TABLE_ADD, to_add.intAt(i));
addEntry(TABLE_REMOVE, to_add.intAt(i));
}
}
// ---------- Testing methods ----------
/**
* Throws a transaction clash exception if it detects a clash between
* journal entries. It assumes that this journal is the journal that is
* attempting to be compatible with the given journal. A journal clashes
* when they both contain a row that is deleted.
*/
void testCommitClash(DataTableDef table_def, MasterTableJournal journal)
throws TransactionException {
// Very nasty search here...
// int cost = entries() * journal.entries();
// System.out.print(" CLASH COST = " + cost + " ");
for (int i = 0; i < entries(); ++i) {
byte tc = getCommand(i);
if (isRemoveCommand(tc)) { // command - row remove
int row_index = getRowIndex(i);
// System.out.println("* " + row_index);
for (int n = 0; n < journal.entries(); ++n) {
// System.out.print(" " + journal.getRowIndex(n));
if (isRemoveCommand(journal.getCommand(n)) &&
journal.getRowIndex(n) == row_index) {
throw new TransactionException(
TransactionException.ROW_REMOVE_CLASH,
"Concurrent Serializable Transaction Conflict(1): " +
"Current row remove clash ( row: " + row_index + ", table: " +
table_def.getTableName() + " )");
}
}
// System.out.println();
}
}
}
// ---------- Stream serialization methods ----------
/**
* Reads the journal entries from the given DataInputStream to this object.
* <p>
* This method is only around because we might need it to convert a
* 0.91 era database that stored index data as journals in the file system.
*/
void readFrom(DataInputStream din) throws IOException {
commit_id = din.readInt();
table_id = din.readInt();
journal_entries = din.readInt();
command_journal = new byte[journal_entries];
din.readFully(command_journal, 0, journal_entries);
int size = din.readInt();
for (int i = 0; i < size; ++i) {
command_parameters.addInt(din.readInt());
}
}
/**
* Debugging.
*/
public String toString() {
StringBuffer buf = new StringBuffer();
buf.append("[MasterTableJournal] [");
buf.append(commit_id);
buf.append("] (");
for (int i = 0; i < entries(); ++i) {
byte c = getCommand(i);
int row_index = getRowIndex(i);
buf.append("(");
buf.append(c);
buf.append(")");
buf.append(row_index);
buf.append(" ");
}
buf.append(")");
return new String(buf);
}
}
|