package edu.hawaii.ics.yucheng;
import java.io.FileReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Scanner;
/**
* A static class that parses the join sql statement.
*
* @author Cheng Jade
* @assignment ICS 421 Assignment 2-2
* @date Mar 19, 2010
* @bugs None
*/
public class ParseSQL {
/** the overall statement */
final String sql;
/** the overall qualifications and two for selection push down. */
final ArrayList<String> table1QualificationColumns = new ArrayList<String>();
final ArrayList<String> table2QualificationColumns = new ArrayList<String>();
/** target columns to display in the join. */
final String[] targetColumns;
/** columns for projection push down. */
final ArrayList<String> table1RelevantColumns = new ArrayList<String>();
final ArrayList<String> table2RelevantColumns = new ArrayList<String>();
/** target tables. */
final String tableName1;
final String tableName2;
/** where clause */
final String whereClause;
/**
* 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 ParseSQL(final String path) throws ProgramException {
assert null != path;
try {
final FileReader reader = new FileReader(path);
final Scanner scanner = new Scanner(reader).useDelimiter("\\Z");
final String sqlFile = scanner.next().replaceAll("\n|\r", " ");
final String[] sqls = sqlFile.split(";");
if (sqls.length != 1)
throw new ProgramException("Invalid SQL, program supports only one sql statement");
sql = sqls[0];
// 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 = sql.substring(fromIndex, whereIndex).trim().substring(4).trim();
final String[] tableNames = parseQualifications(from);
if (tableNames.length != 2)
throw new ProgramException("Invalid SQL, '" + from + "' program supports two tables");
final TableNamePair table1 = tableNameAlias(tableNames[0]);
final TableNamePair table2 = tableNameAlias(tableNames[1]);
tableName1 = table1.tableName;
tableName2 = table2.tableName;
String table1Alias = table1.alias;
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 + ".", tableName1 + ".");
select = select.replaceAll(table2Alias + ".", tableName2 + ".");
String where = sql.substring(whereIndex).trim().substring(5).trim();
where = where.replaceAll(table1Alias + ".", tableName1 + ".");
where = where.replaceAll(table2Alias + ".", tableName2 + ".");
// set up table qualification columns and parse to each node.
targetColumns = parseQualifications(select);
String modifiedWhere = where.replaceAll("( (a|A)(n|N)(d|D)) | ((o|O)(r|R) )", " , ");
separateQualifications(parseQualifications(modifiedWhere));
whereClause = "WHERE " + where;
// set up relevant columns.
modifiedWhere = modifiedWhere.replace("=", " , ");
final ArrayList<String> allColumns = new ArrayList<String>();
String[] allColumnsWhere = parseQualifications(modifiedWhere);
for (String item : allColumnsWhere)
allColumns.add(item);
for (String item : targetColumns)
allColumns.add(item);
separateRelevantColumns(allColumns);
} catch (final Exception e) {
throw new ProgramException(e.getMessage(), e);
}
}
/**
* 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[] parseQualifications(final String key) throws ProgramException {
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(ArrayList<String> columns) {
for (String item : columns) {
if (item.contains(tableName1))
table1RelevantColumns.add(item);
if (item.contains(tableName2))
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(String[] qualifications) {
for (String item : qualifications) {
if (item.contains(tableName1) && !item.contains(tableName2))
table1QualificationColumns.add(item);
if (item.contains(tableName2) && !item.contains(tableName1))
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 {
String[] result = tableName.split(" ");
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(String[] args) {
assert null != args;
// Print usage information, if wrong number of arguments were used.
if (args.length != 1) {
final String name = ParseSQL.class.getSimpleName();
System.err.println("Usage: java " + name + " <path>");
System.err.println(" <path> path to a SQL file");
System.exit(1);
return;
}
// call the parse and print each field.
try {
ParseSQL sql = new ParseSQL(args[0]);
System.out.println("table1RelevantColumns");
for (String item : sql.table1RelevantColumns)
System.out.println("\t'" + item + "'");
System.out.println("table2RelevantColumns");
for (String item : sql.table2RelevantColumns)
System.out.println("\t'" + item + "'");
System.out.println("table1QualificationColumns");
for (String item : sql.table1QualificationColumns)
System.out.println("\t'" + item + "'");
System.out.println("table2QualificationColumns");
for (String item : sql.table2QualificationColumns)
System.out.println("\t'" + item + "'");
System.out.println("targetColumns");
for (String item : sql.targetColumns)
System.out.println("\t'" + item + "'");
System.out.println("tableName1: '" + sql.tableName1 + "'");
System.out.println("tableName2: '" + sql.tableName2 + "'");
System.out.println("whereClause: '" + sql.whereClause + "'");
} catch (final ProgramException e) {
System.err.println(e.getMessage());
System.exit(1);
return;
}
}
}