package edu.hawaii.ics.yucheng;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
enum SQLType {
CREATEORDROP, SELECT, JOIN;
}
/**
* A static class that parses the join sql statement.
*
* @author Cheng Jade
* @assignment ICS 421 Project
* @date Mar 19, 2010
* @bugs None
*/
public class SQLStructure {
/** the overall statement and its type */
public final String sql;
public final SQLType type;
/** the overall qualifications and two for selection push down. */
ArrayList<String> table1QualificationColumns = new ArrayList<String>();
ArrayList<String> table2QualificationColumns = new ArrayList<String>();
/** target columns to display in the join. */
String[] targetColumns;
/** columns for projection push down. */
ArrayList<String> table1RelevantColumns = new ArrayList<String>();
ArrayList<String> table2RelevantColumns = new ArrayList<String>();
/** target tables. */
public final String tableName1;
String tableName2;
/** where clause */
String whereClause;
private void toStringHelper(final StringBuilder builder, final Iterable<String> list) {
for (final String item : list)
builder.append(" " + item);
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("sql: " + this.sql + "\n");
if (this.tableName1 != null) {
builder.append("tableName1: ");
builder.append(this.tableName1 + "\n");
}
if (this.tableName2 != null) {
builder.append("tableName2: ");
builder.append(this.tableName2 + "\n");
}
if (this.whereClause != null) {
builder.append("whereClause: ");
builder.append(this.whereClause + "\n");
}
if (this.table1QualificationColumns.size() != 0) {
builder.append("table1QualificationColumns: \n");
this.toStringHelper(builder, this.table1QualificationColumns);
}
if (this.table2QualificationColumns.size() != 0) {
builder.append("\ntable2QualificationColumns: \n");
this.toStringHelper(builder, this.table2QualificationColumns);
}
if (this.targetColumns != null) {
builder.append("\ntargetColumns: \n");
this.toStringHelper(builder, Arrays.asList(this.targetColumns));
}
if (this.table1RelevantColumns.size() != 0) {
builder.append("\ntable1RelevantColumns: \n");
this.toStringHelper(builder, this.table1RelevantColumns);
}
if (this.table2RelevantColumns.size() != 0) {
builder.append("\ntable2RelevantColumns: \n");
this.toStringHelper(builder, this.table2RelevantColumns);
}
return builder.toString();
}
/**
* Parse the input file containing the sql statement into desired fields.
*
* @param path
* The path that contains the sql file.
*
* @throw ProgramException Fail to parse due to different formating reasons.
*
*/
public SQLStructure(final String sql) throws ProgramException {
assert null != sql;
this.sql = sql;
// if sql is a creat or drop statement, no extra parsing is needed.
if (sql.toUpperCase().startsWith("CREATE") || sql.toUpperCase().startsWith("DROP")) {
this.type = SQLType.CREATEORDROP;
this.tableName1 = null;
return;
}
// grab key word each line.
final int selectIndex = (sql.indexOf("SELECT") != -1) ? sql.indexOf("SELECT") : sql.indexOf("select");
final int fromIndex = (sql.indexOf("FROM") != -1) ? sql.indexOf("FROM") : sql.indexOf("from");
final int whereIndex = (sql.indexOf("WHERE") != -1) ? sql.indexOf("WHERE") : sql.indexOf("where");
if (selectIndex == -1)
throw new ProgramException("Invalid SQL, missing the SELECT clause");
if (fromIndex == -1)
throw new ProgramException("Invalid SQL, missing the from clause");
// set up table names and their aliases.
final String from;
if (whereIndex != -1)
from = sql.substring(fromIndex, whereIndex).trim().substring(4).trim();
else
from = sql.substring(fromIndex).trim().substring(4).trim();
final String[] tableNames = this.parseCommaSeparatedString(from);
// parse table 1's name and alias.
final TableNamePair table1 = this.tableNameAlias(tableNames[0]);
this.tableName1 = table1.tableName;
final String table1Alias = table1.alias;
// if there's only one table then it will be treated as a select
// statement. In this case, no extra parsing is needed.
final int tableCount = tableNames.length;
if (tableCount == 1) {
this.type = SQLType.SELECT;
return;
}
this.type = SQLType.JOIN;
// otherwise, statement will be treated as a join statement.
if (tableCount != 2)
throw new ProgramException("Invalid SQL, '" + from + "' program supports one or two tables");
final TableNamePair table2 = this.tableNameAlias(tableNames[1]);
this.tableName2 = table2.tableName;
final String table2Alias = table2.alias;
// replace aliases in select and where clauses.
String select = sql.substring(selectIndex, fromIndex).trim().substring(6).trim();
select = select.replaceAll(table1Alias + ".", this.tableName1 + ".");
select = select.replaceAll(table2Alias + ".", this.tableName2 + ".");
String where = sql.substring(whereIndex).trim().substring(5).trim();
where = where.replaceAll(table1Alias + ".", this.tableName1 + ".");
where = where.replaceAll(table2Alias + ".", this.tableName2 + ".");
// set up table qualification columns and parse to each node.
this.targetColumns = this.parseCommaSeparatedString(select);
String modifiedWhere = where.replaceAll("( (a|A)(n|N)(d|D)) | ((o|O)(r|R) )", " , ");
this.separateQualifications(this.parseCommaSeparatedString(modifiedWhere));
this.whereClause = "WHERE " + where;
// set up relevant columns.
modifiedWhere = modifiedWhere.replace("=", " , ");
final ArrayList<String> allColumns = new ArrayList<String>();
final String[] allColumnsWhere = this.parseCommaSeparatedString(modifiedWhere);
for (final String item : allColumnsWhere)
allColumns.add(item);
for (final String item : this.targetColumns)
allColumns.add(item);
this.separateRelevantColumns(allColumns);
}
/**
* Use the CSV parser to parse a comma separated string and return an array.
*
* @param key
* The string to be parsed.
*
* @throw ProgramException Fail to parse the string.
*
* @return An array of strings.
*
*/
private String[] parseCommaSeparatedString(final String key) throws ProgramException {
final Reader stream = new StringReader(key);
return CSVParser.parse(stream);
}
/**
* Append a column to either table1RelevantColumns or table2RelevantColumns
* based on whether they contain tableName1 or tableName2.
*
* @param columns
* The list of columns to be examined.
*
*/
private void separateRelevantColumns(final ArrayList<String> columns) {
for (final String item : columns) {
if (item.contains(this.tableName1))
this.table1RelevantColumns.add(item);
if (item.contains(this.tableName2))
this.table2RelevantColumns.add(item);
}
}
/**
* Append a qualification entry to either table1QualificationColumns or
* table2QualificationColumns based on whether they only contain tableName1
* or tableName2. An qualification entry might be in neither
* table1QualificationColumns nor table2QualificationColumns. In this case,
* the qualification should not be used for selection push down since it
* involves more than one table.
*
* @param qualifications
* The list of qualifications to be examined.
*
*/
private void separateQualifications(final String[] qualifications) {
for (final String item : qualifications) {
if (item.contains(this.tableName1) && !item.contains(this.tableName2))
this.table1QualificationColumns.add(item);
if (item.contains(this.tableName2) && !item.contains(this.tableName1))
this.table2QualificationColumns.add(item);
}
}
/**
* A private class that group table name and its alias together.
*/
private class TableNamePair {
final String tableName;
final String alias;
public TableNamePair(final String tableName, final String alias) {
this.tableName = tableName;
this.alias = alias;
}
}
/**
* Method takes a table name in the format "tableName an alias" or
* "tableName alias" and return a TableNamePair that contains "tableName"
* and "alias".
*
* @param tableName
* The table name in the format of "tableName as alias" or
* "tableName alias".
*
* @return A TableNamePair that contains "tableName" and "alias".
*/
private TableNamePair tableNameAlias(String tableName) throws ProgramException {
final String[] result = tableName.split(" ");
final int size = result.length;
if (size > 3)
throw new ProgramException("Invaild SQL, '" + tableName + "' table name can have only one alias");
if (size == 1)
return new TableNamePair(tableName, null);
tableName = result[0].trim();
if (size == 2)
return new TableNamePair(tableName, result[1].trim());
if (!result[1].equalsIgnoreCase("as"))
throw new ProgramException("Invaild SQL, '" + tableName + "' invalid alias syntax");
return new TableNamePair(tableName, result[2].trim());
}
/**
* A test main method to test the implementation.
*
* @param args
* The command line arguments.
*/
public static void main(final String[] args) {
// call the parse and print each field.
try {
final SQLStructure sql = new SQLStructure("SELECT S.sailorname " + "FROM Sailors S , Reserves R WHERE S.sid=R.rid " + "And R.day ='2008-08-12'");
System.out.println(sql);
} catch (final ProgramException e) {
System.err.println(e.getMessage());
System.exit(1);
return;
}
}
}