package ModSQL;
import java.io.*;
import java.sql.*;
import java.util.ArrayList;

/* $Id: Update.java,v 1.9 2003/09/24 19:59:34 cvs Exp $
 *
 * Copyright (c) 2003 Chris Studholme <chris.studholme@utoronto.ca>
 *
 * May be copied or modified under the terms of the GNU General Public
 * License.  See COPYING for more information.
 */

/**
 * <p>Update will parse and execute an SQL UPDATE query.
 *
 * @author chris.studholme@utoronto.ca
 */
final class Update implements Query {

  /** Name of table. */
  private String tablename=null;
  /** Array of column names to update. */
  private String[] set_column=null;
  /** New values for columns. */
  private Function[] set_value=null;
  /** Where clause used to choose rows. */
  private Function where=null;

  /** Reader used to fetch rows (set by optimize). */
  private TableReader reader=null;
  /** Database table to update (set by optimize). */
  private DatabaseTable table=null;
  /** Array of indices of columns to update (set by optimize). */
  private int[] column_index=null;
  /** Array of column types (set by optimize). */
  private int[] column_type=null;

  /** Table manager used to open tables. */
  protected transient DatabaseManager tablemanager;

  /**
   * Contructor to parse query.
   *
   * @param tokenizer StreamTokenizer that SQL tokens should be read from
   * @param manager manager to use when looking up tables
   * @throws SQLException if an error occurs
   * @throws IOException if there is a problem reading the query
   */
  protected Update(StreamTokenizer tokenizer, DatabaseManager manager) 
    throws SQLException, IOException {
    tablemanager = manager;
    ParseUpdate(tokenizer);
  }

  /**
   * Close query and free resources in use.
   */
  public void close() {
  }

  /**
   * Returns human-readable string version of query (with surrounding 
   * brackets).
   *
   * @return String representation of query
   */
  public String toString() {
    return toString(true);
  }

  /**
   * Returns human-readable string version of query.
   *
   * @param with_brackets true to include surrounding brackets
   * @return String representation of query
   */
  public String toString(boolean with_brackets) {
    StringBuffer result = new StringBuffer();
    if (with_brackets) result.append('(');
    result.append("UPDATE ");
    result.append(tablename!=null ? tablename : "[UNKNOWN]");
    if (set_column!=null && set_value!=null) {
      result.append(" SET ");
      result.append(set_column[0]);
      result.append("=");
      result.append(set_value[0]);
      for (int i=1; i<set_column.length; ++i) {
	result.append(", ");
	result.append(set_column[i]);
	result.append("=");
	result.append(set_value[i]);
      }
    }
    if (where!=null) {
      result.append(" WHERE ");
      result.append(where);
    }
    if (with_brackets) result.append(')');
    return result.toString();
  }
  
  /**
   * Parse update query of the form:<br>
   *   <blockquote><code>
   *   UPDATE tablename SET columns [ WHERE condition ]
   *   </code></blockquote>
   *
   * @param tokenizer StreamTokenizer that SQL tokens should be read from
   * @throws SQLException if an error occurs
   * @throws IOException if there is a problem reading the query
   */   
  private void ParseUpdate(StreamTokenizer tokenizer)
    throws SQLException, IOException {
    
    tablename=null;
    set_column=null;
    set_value=null;
    where=null;
     
    if ((tokenizer.ttype!=StreamTokenizer.TT_WORD)||
	(!tokenizer.sval.equals("update")))
      throw new SQLException("'UPDATE' expected");
    tokenizer.nextToken();

    // table name
    if (tokenizer.ttype!=StreamTokenizer.TT_WORD)
      throw new SQLException("table name expected at or before "+tokenizer);
    tablename=tokenizer.sval;
    tokenizer.nextToken();
    
    if (tokenizer.ttype!=StreamTokenizer.TT_WORD ||
	!tokenizer.sval.equals("set"))
      throw new SQLException("'SET' expected");

    // column list
    ArrayList names = new ArrayList();
    ArrayList values = new ArrayList();
    do {
      tokenizer.nextToken();
      if (tokenizer.ttype!=StreamTokenizer.TT_WORD)
	throw new SQLException("column name expected at or before "+tokenizer);
      names.add(tokenizer.sval);
      if (tokenizer.nextToken()!='=')
	throw new SQLException("'=' expected at or before "+tokenizer);
      tokenizer.nextToken();
      values.add(Expression.parseExpression(tokenizer,tablemanager));
    } while (tokenizer.ttype==',');
    set_column = new String[names.size()];
    set_column = (String[])names.toArray(set_column);
    set_value = new Function[values.size()];
    set_value = (Function[])values.toArray(set_value);

    // where statement
    if (tokenizer.ttype==StreamTokenizer.TT_WORD) {
      if (!tokenizer.sval.equals("where"))
	throw new SQLException("'WHERE' expected");
      tokenizer.nextToken();
      where = Expression.parseExpression(tokenizer,tablemanager);
    }
  }

  /**
   * Optimize query.
   *
   * @throws SQLException if an error occurs
   */
  public void optimize() throws SQLException {
    if (tablename==null || set_column==null || set_value==null)
      throw new SQLException("cannot execute UPDATE statement");

    DatabaseTable[] tables = new DatabaseTable[1];
    tables[0] = table = tablemanager.openTable(tablename,false);
    String[] aliases = new String[1];
    reader = new TableReader(tables,aliases,null);

    // lookup column names
    if (where!=null) 
      where.registerWith(reader);
    for (int i=0; i<set_value.length; ++i)
      set_value[i].registerWith(reader);
    column_index = new int[set_column.length];
    column_type = new int[set_column.length];
    for (int i=0; i<set_column.length; ++i) {
      column_index[i] = table.findColumn(set_column[i]);
      if (column_index[i]<0)
	throw new SQLException("column '"+set_column[i]+"' not found");
      column_type[i] = table.getColumnType(column_index[i]);
    }

    // optimize
    if (where!=null) {
      reader.optimizeParameterOrder(where);
      where.optimize();
      if (where.isAggregate())
	throw new SQLException("WHERE is an aggregate");
    }
    for (int i=0; i<set_value.length; ++i) {
      set_value[i].optimize();
      if (set_value[i].isAggregate())
	throw new SQLException("value is an aggregate");
    }
  }

  /**
   * Execute the query.
   *
   * @return number of rows updated
   * @throws SQLException if an error occurs
   */
  public int execute() throws SQLException {
    // read all rows
    Boolean TRUE_VALUE = new Boolean(true);
    int count=0;
    while (true) {
      try {
	// advance to next row
	Object result;
	do {
	  reader.next();
	  if (where==null) break;
	  result = where.evaluate(Function.MATCH_EQU,TRUE_VALUE);
	} while (result==null || ((Boolean)result).booleanValue()!=true);
	reader.readAllTables();  
	for (int i=0; i<set_value.length; ++i) {
	  Object value = set_value[i].evaluate(false);
	  value = Insert.getCompatibleType(value,column_type[i]);
	  reader.updateColumnValue(0,column_index[i],value);
	}
	reader.commitUpdates();
	++count;
      }
      catch (EndOfTable e) {
	if (reader!=e.getReader())
	  throw new SQLExceptionWithCause("SOFTWARE BUG",e);
	if (reader.finishTable())
	  break;
      }
    }
    return count;
  }

};
