package net.sourceforge.mayfly.parser; import static net.sourceforge.mayfly.parser.TokenType.CLOSE_PAREN; import static net.sourceforge.mayfly.parser.TokenType.EQUAL; import static net.sourceforge.mayfly.parser.TokenType.IDENTIFIER; import static net.sourceforge.mayfly.parser.TokenType.KEYWORD_character; import static net.sourceforge.mayfly.parser.TokenType.KEYWORD_column; import static net.sourceforge.mayfly.parser.TokenType.KEYWORD_like; import static net.sourceforge.mayfly.parser.TokenType.KEYWORD_on; import static net.sourceforge.mayfly.parser.TokenType.KEYWORD_set; import static net.sourceforge.mayfly.parser.TokenType.KEYWORD_to; import static net.sourceforge.mayfly.parser.TokenType.KEYWORD_values; import net.sourceforge.mayfly.MayflyException; import net.sourceforge.mayfly.MayflyInternalException; import net.sourceforge.mayfly.Options; import net.sourceforge.mayfly.UnimplementedException; import net.sourceforge.mayfly.datastore.BinaryCell; import net.sourceforge.mayfly.datastore.Cell; import net.sourceforge.mayfly.datastore.Column; import net.sourceforge.mayfly.datastore.ColumnNames; import net.sourceforge.mayfly.datastore.LongCell; import net.sourceforge.mayfly.datastore.Position; import net.sourceforge.mayfly.datastore.constraint.Action; import net.sourceforge.mayfly.datastore.constraint.Cascade; import net.sourceforge.mayfly.datastore.constraint.NoAction; import net.sourceforge.mayfly.datastore.constraint.SetDefault; import net.sourceforge.mayfly.datastore.constraint.SetNull; import net.sourceforge.mayfly.datastore.types.BinaryDataType; import net.sourceforge.mayfly.datastore.types.DataType; import net.sourceforge.mayfly.datastore.types.DateDataType; import net.sourceforge.mayfly.datastore.types.DecimalDataType; import net.sourceforge.mayfly.datastore.types.IntegerDataType; import net.sourceforge.mayfly.datastore.types.StringDataType; import net.sourceforge.mayfly.datastore.types.TimestampDataType; import net.sourceforge.mayfly.evaluation.Aggregator; import net.sourceforge.mayfly.evaluation.Expression; import net.sourceforge.mayfly.evaluation.GroupBy; import net.sourceforge.mayfly.evaluation.GroupItem; import net.sourceforge.mayfly.evaluation.NoGroupBy; import net.sourceforge.mayfly.evaluation.ResultRow; import net.sourceforge.mayfly.evaluation.Value; import net.sourceforge.mayfly.evaluation.ValueList; import net.sourceforge.mayfly.evaluation.command.AddColumn; import net.sourceforge.mayfly.evaluation.command.AddConstraint; import net.sourceforge.mayfly.evaluation.command.ChangeColumn; import net.sourceforge.mayfly.evaluation.command.Command; import net.sourceforge.mayfly.evaluation.command.CreateIndex; import net.sourceforge.mayfly.evaluation.command.CreateSchema; import net.sourceforge.mayfly.evaluation.command.CreateTable; import net.sourceforge.mayfly.evaluation.command.Delete; import net.sourceforge.mayfly.evaluation.command.DropColumn; import net.sourceforge.mayfly.evaluation.command.DropConstraint; import net.sourceforge.mayfly.evaluation.command.DropForeignKey; import net.sourceforge.mayfly.evaluation.command.DropIndex; import net.sourceforge.mayfly.evaluation.command.DropTable; import net.sourceforge.mayfly.evaluation.command.Insert; import net.sourceforge.mayfly.evaluation.command.LastIdentity; import net.sourceforge.mayfly.evaluation.command.ModifyColumn; import net.sourceforge.mayfly.evaluation.command.NoopCommand; import net.sourceforge.mayfly.evaluation.command.RenameTable; import net.sourceforge.mayfly.evaluation.command.SetClause; import net.sourceforge.mayfly.evaluation.command.SetSchema; import net.sourceforge.mayfly.evaluation.command.SubselectedInsert; import net.sourceforge.mayfly.evaluation.command.UnresolvedCheckConstraint; import net.sourceforge.mayfly.evaluation.command.UnresolvedConstraint; import net.sourceforge.mayfly.evaluation.command.UnresolvedForeignKey; import net.sourceforge.mayfly.evaluation.command.UnresolvedPrimaryKey; import net.sourceforge.mayfly.evaluation.command.UnresolvedTableReference; import net.sourceforge.mayfly.evaluation.command.UnresolvedUniqueConstraint; import net.sourceforge.mayfly.evaluation.command.Update; import net.sourceforge.mayfly.evaluation.condition.And; import net.sourceforge.mayfly.evaluation.condition.Condition; import net.sourceforge.mayfly.evaluation.condition.Equal; import net.sourceforge.mayfly.evaluation.condition.Greater; import net.sourceforge.mayfly.evaluation.condition.In; import net.sourceforge.mayfly.evaluation.condition.IsNull; import net.sourceforge.mayfly.evaluation.condition.LessEqual; import net.sourceforge.mayfly.evaluation.condition.Like; import net.sourceforge.mayfly.evaluation.condition.Not; import net.sourceforge.mayfly.evaluation.condition.NotEqual; import net.sourceforge.mayfly.evaluation.condition.Or; import net.sourceforge.mayfly.evaluation.condition.SubselectedIn; import net.sourceforge.mayfly.evaluation.expression.Average; import net.sourceforge.mayfly.evaluation.expression.Concatenate; import net.sourceforge.mayfly.evaluation.expression.Count; import net.sourceforge.mayfly.evaluation.expression.CountAll; import net.sourceforge.mayfly.evaluation.expression.CurrentTimestampExpression; import net.sourceforge.mayfly.evaluation.expression.DefaultValue; import net.sourceforge.mayfly.evaluation.expression.Divide; import net.sourceforge.mayfly.evaluation.expression.Function; import net.sourceforge.mayfly.evaluation.expression.Maximum; import net.sourceforge.mayfly.evaluation.expression.Minimum; import net.sourceforge.mayfly.evaluation.expression.Minus; import net.sourceforge.mayfly.evaluation.expression.Multiply; import net.sourceforge.mayfly.evaluation.expression.NullExpression; import net.sourceforge.mayfly.evaluation.expression.Plus; import net.sourceforge.mayfly.evaluation.expression.RealTimeSource; import net.sourceforge.mayfly.evaluation.expression.ScalarSubselect; import net.sourceforge.mayfly.evaluation.expression.SearchedCase; import net.sourceforge.mayfly.evaluation.expression.SingleColumn; import net.sourceforge.mayfly.evaluation.expression.SpecifiedDefaultValue; import net.sourceforge.mayfly.evaluation.expression.Sum; import net.sourceforge.mayfly.evaluation.expression.TimeSource; import net.sourceforge.mayfly.evaluation.expression.literal.CellExpression; import net.sourceforge.mayfly.evaluation.expression.literal.DecimalLiteral; import net.sourceforge.mayfly.evaluation.expression.literal.IntegerLiteral; import net.sourceforge.mayfly.evaluation.expression.literal.Literal; import net.sourceforge.mayfly.evaluation.expression.literal.LongLiteral; import net.sourceforge.mayfly.evaluation.expression.literal.QuotedString; import net.sourceforge.mayfly.evaluation.from.From; import net.sourceforge.mayfly.evaluation.from.FromElement; import net.sourceforge.mayfly.evaluation.from.FromTable; import net.sourceforge.mayfly.evaluation.from.InnerJoin; import net.sourceforge.mayfly.evaluation.from.LeftJoin; import net.sourceforge.mayfly.evaluation.select.ColumnOrderItem; import net.sourceforge.mayfly.evaluation.select.Limit; import net.sourceforge.mayfly.evaluation.select.OrderBy; import net.sourceforge.mayfly.evaluation.select.OrderItem; import net.sourceforge.mayfly.evaluation.select.ReferenceOrderItem; import net.sourceforge.mayfly.evaluation.select.Select; import net.sourceforge.mayfly.evaluation.what.AliasedExpression; import net.sourceforge.mayfly.evaluation.what.All; import net.sourceforge.mayfly.evaluation.what.AllColumnsFromTable; import net.sourceforge.mayfly.evaluation.what.What; import net.sourceforge.mayfly.evaluation.what.WhatElement; import net.sourceforge.mayfly.util.ImmutableList; import java.io.Reader; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** @internal * Hand-written recursive descent parser. * So far this has brought far fewer headaches than ANTLR (which might * mean I just don't understand ANTLR). It is also nicer to unit test * this parser, there is no crazy build.xml junk like with ANTLR, and * who knows what other benefits. */ public class Parser { private static final ResultRow PSEUDO_ROW_FOR_VALUE_CONSTRUCTOR = new ResultRow() { @Override public SingleColumn findColumn( String tableOrAlias, String originalTableOrAlias, String columnName, Location location) { throw new MayflyException( "values clause may not refer to column: " + Column.displayName(originalTableOrAlias, columnName), location ); } }; private List tokens; private Token currentToken; private final boolean allowParameters; private final TimeSource timeSource; private final Options options; public Parser(String sql) { this(new Lexer(sql).tokens()); } /** * Create a parser which reads input from a Reader. * The caller is responsible for closing the Reader. */ public Parser(Reader sql, Options options) { this(new Lexer(sql).tokens(), false, new RealTimeSource(), options); } /** * Create a parser from a list of tokens. The parser * will mutate the list as it parses. * The list must end with an end of file token. */ public Parser(List tokens) { this(tokens, false); } public Parser(List tokens, boolean allowParameters) { this(tokens, allowParameters, new RealTimeSource()); } public Parser(List tokens, boolean allowParameters, TimeSource timeSource) { this(tokens, allowParameters, timeSource, new Options()); } public Parser(List tokens, boolean allowParameters, TimeSource timeSource, Options options) { this.tokens = tokens; this.currentToken = tokens.isEmpty() ? null : (Token) tokens.get(0); this.allowParameters = allowParameters; this.timeSource = timeSource; this.options = options; } public List parseCommands() { List commands = new ArrayList(); while (true) { if (consumeIfMatches(TokenType.END_OF_FILE)) { return commands; } else if (consumeIfMatches(TokenType.SEMICOLON)) { } else { commands.add(parseCommand()); if (currentToken.type != TokenType.END_OF_FILE && currentToken.type != TokenType.SEMICOLON) { throw new ParserException("end of command", currentToken); } } } } public Command parse() { Command command = parseCommand(); expectAndConsume(TokenType.END_OF_FILE); return command; } Command parseCommand() { if (currentToken.type == TokenType.KEYWORD_select) { return parseSelect(); } else if (consumeNonReservedWordIfMatches("call")) { return parseCall(); } else if (consumeIfMatches(TokenType.KEYWORD_drop)) { if (consumeIfMatches(TokenType.KEYWORD_table)) { return parseDropTable(); } else if (consumeIfMatches(TokenType.KEYWORD_index)) { return parseDropIndex(); } else { throw new ParserException("drop command", currentToken); } } else if (consumeIfMatches(TokenType.KEYWORD_create)) { if (consumeIfMatches(TokenType.KEYWORD_schema)) { return parseCreateSchema(); } else if (consumeIfMatches(TokenType.KEYWORD_table)) { return parseCreateTable(); } else if (consumeIfMatches(TokenType.KEYWORD_index)) { return parseCreateIndex(false); } else if (consumeIfMatches(TokenType.KEYWORD_unique)) { expectAndConsume(TokenType.KEYWORD_index); return parseCreateIndex(true); } else { throw new ParserException("create command", currentToken); } } else if (consumeIfMatches(TokenType.KEYWORD_alter)) { if (consumeIfMatches(TokenType.KEYWORD_table)) { return parseAlterTable(); } else if (consumeIfMatches(TokenType.KEYWORD_schema)) { return parseAlterSchema(); } else { throw new ParserException("alter command", currentToken); } } else if (currentToken.type == KEYWORD_set) { return parseSetSchema(); } else if (currentToken.type == TokenType.KEYWORD_insert) { return parseInsert(); } else if (currentToken.type == TokenType.KEYWORD_update) { return parseUpdate(); } else if (currentToken.type == TokenType.KEYWORD_delete) { return parseDelete(); } else { throw new ParserException("command", currentToken); } } private Command parseCreateIndex(boolean unique) { String indexName = consumeIdentifier(); expectAndConsume(TokenType.KEYWORD_on); UnresolvedTableReference table = parseTableReference(); ColumnNames columns = parseColumnNamesForIndex(); return new CreateIndex(table, indexName, columns, unique); } private Command parseCall() { expectNonReservedWord("identity"); expectAndConsume(TokenType.OPEN_PAREN); expectAndConsume(CLOSE_PAREN); return new LastIdentity(); } private Command parseSetSchema() { expectAndConsume(KEYWORD_set); expectAndConsume(TokenType.KEYWORD_schema); return new SetSchema(consumeIdentifier()); } private Command parseInsert() { Location start = currentToken.location; expectAndConsume(TokenType.KEYWORD_insert); expectAndConsume(TokenType.KEYWORD_into); UnresolvedTableReference table = parseTableReference(); if (consumeIfMatches(KEYWORD_set)) { List names = new ArrayList(); ValueList values = new ValueList(currentToken.location); do { names.add(consumeIdentifier()); expectAndConsume(EQUAL); values = values.with(parseAndEvaluate()); } while (consumeIfMatches(TokenType.COMMA)); return new Insert(table, new ImmutableList(names), values, start.combine(values.location)); } ImmutableList columnNames = parseColumnNamesForInsert(); if (currentToken.type == TokenType.KEYWORD_select) { Select subselect = parseSelect(); return new SubselectedInsert(table, columnNames, subselect); } else if (currentToken.type == KEYWORD_values) { ValueList values = parseValueConstructor(); return new Insert(table, columnNames, values, start.combine(values.location)); } else { throw new ParserException("VALUES or SELECT", currentToken); } } private Command parseUpdate() { expectAndConsume(TokenType.KEYWORD_update); UnresolvedTableReference table = parseTableReference(); expectAndConsume(KEYWORD_set); List setClauses = parseSetClauseList(); Condition where = parseOptionalWhere(); return new Update(table, setClauses, where); } private Command parseDelete() { expectAndConsume(TokenType.KEYWORD_delete); expectAndConsume(TokenType.KEYWORD_from); UnresolvedTableReference table = parseTableReference(); Condition where = parseOptionalWhere(); return new Delete(table, where); } private List parseSetClauseList() { List clauses = new ArrayList(); do { clauses.add(parseSetClause()); } while (consumeIfMatches(TokenType.COMMA)); return clauses; } private SetClause parseSetClause() { String column = consumeIdentifier(); expectAndConsume(EQUAL); Expression value = parseExpressionOrNull(); return new SetClause(column, value); } private ImmutableList parseColumnNamesForInsert() { if (currentToken.type == TokenType.OPEN_PAREN) { expectAndConsume(TokenType.OPEN_PAREN); List columnNames = new ArrayList(); if (currentToken.type != CLOSE_PAREN) { do { columnNames.add(consumeIdentifier()); } while (consumeIfMatches(TokenType.COMMA)); } expectAndConsume(CLOSE_PAREN); return new ImmutableList(columnNames); } else { return null; } } private ColumnNames parseColumnNamesForIndex() { expectAndConsume(TokenType.OPEN_PAREN); List columnNames = new ArrayList(); do { columnNames.add(consumeIdentifier()); if (consumeIfMatches(TokenType.OPEN_PAREN)) { expectAndConsume(TokenType.NUMBER); expectAndConsume(CLOSE_PAREN); } } while (consumeIfMatches(TokenType.COMMA)); expectAndConsume(CLOSE_PAREN); return new ColumnNames(new ImmutableList(columnNames)); } private ColumnNames parseColumnNames() { expectAndConsume(TokenType.OPEN_PAREN); List columnNames = new ArrayList(); do { columnNames.add(consumeIdentifier()); } while (consumeIfMatches(TokenType.COMMA)); expectAndConsume(CLOSE_PAREN); return new ColumnNames(new ImmutableList(columnNames)); } ValueList parseValueConstructor() { Location start = expectAndConsume(TokenType.KEYWORD_values).location; ValueList values = new ValueList(start); expectAndConsume(TokenType.OPEN_PAREN); if (currentToken.type != CLOSE_PAREN) { do { values = values.with(parseAndEvaluate()); } while (consumeIfMatches(TokenType.COMMA)); } Location end = expectAndConsume(CLOSE_PAREN).location; return values.with(end); } private Value parseAndEvaluate() { Expression expression = parseExpressionOrNull(); if (expression == null) { // default value return new Value(null, Location.UNKNOWN); } else { Cell cell = expression.evaluate( PSEUDO_ROW_FOR_VALUE_CONSTRUCTOR); return new Value(cell, expression.location); } } Expression parseExpressionOrNull() { Location start = currentToken.location; if (consumeIfMatches(TokenType.KEYWORD_null)) { return new NullExpression(start); } else if (consumeIfMatches(TokenType.KEYWORD_default)) { return null; } else { try { return parseExpression().asNonBoolean(); } catch (FoundNullLiteral e) { throw new MayflyException( "Specify a null literal rather than an expression containing one", start.combine(e.location())); } } } private Command parseDropTable() { boolean ifExists = false; if (consumeIfMatches(TokenType.KEYWORD_if)) { expectAndConsume(TokenType.KEYWORD_exists); ifExists = true; } UnresolvedTableReference table = parseTableReference(); if (consumeIfMatches(TokenType.KEYWORD_if)) { expectAndConsume(TokenType.KEYWORD_exists); ifExists = true; } return new DropTable(table, ifExists); } private Command parseDropIndex() { String indexName = consumeIdentifier(); if (consumeIfMatches(KEYWORD_on)) { UnresolvedTableReference table = parseTableReference(); return new DropIndex(table, indexName); } else { return new DropIndex(null, indexName); } } private CreateSchema parseCreateSchema() { String schemaName = consumeIdentifier(); if (consumeIfMatches(TokenType.KEYWORD_authorization)) { String user = consumeIdentifier(); if (!user.equalsIgnoreCase("dba")) { throw new MayflyException("Can only specify user dba in create schema but was " + user); } } CreateSchema schema = new CreateSchema(schemaName); while (consumeIfMatches(TokenType.KEYWORD_create)) { expectAndConsume(TokenType.KEYWORD_table); CreateTable createTable = parseCreateTable(); schema.add(createTable); } return schema; } private Command parseAlterSchema() { consumeIdentifier(); expectAndConsume(TokenType.KEYWORD_character); expectAndConsume(TokenType.KEYWORD_set); consumeIdentifier(); return new NoopCommand(); } private CreateTable parseCreateTable() { String tableName = consumeIdentifier(); CreateTable table = new CreateTable(tableName); expectAndConsume(TokenType.OPEN_PAREN); do { parseTableElement(table); } while (consumeIfMatches(TokenType.COMMA)); expectAndConsume(CLOSE_PAREN); parseTableTypeIfPresent(); parseCharacterSetIfPresent(); return table; } private void parseTableTypeIfPresent() { if (consumeNonReservedWordIfMatches("engine")) { expectAndConsume(EQUAL); Token token = expectAndConsume(IDENTIFIER); String tableType = token.getText(); if ("innodb".equalsIgnoreCase(tableType) || "myisam".equalsIgnoreCase(tableType)) { // For now, ignore the type } else { throw new ParserException("unrecognized table type " + tableType, token.location); } } } private void parseCharacterSetIfPresent() { if (consumeIfMatches(KEYWORD_character)) { expectAndConsume(KEYWORD_set); consumeIdentifier(); } } private Command parseAlterTable() { UnresolvedTableReference table = parseTableReference(); if (consumeIfMatches(TokenType.KEYWORD_drop)) { // optional according to SQL92 but does anyone omit it? if (consumeIfMatches(KEYWORD_column)) { String column = consumeIdentifier(); return new DropColumn(table, column); } else if (consumeIfMatches(TokenType.KEYWORD_foreign)) { expectAndConsume(TokenType.KEYWORD_key); String constraintName = consumeIdentifier(); return new DropForeignKey(table, constraintName); } else if (consumeIfMatches(TokenType.KEYWORD_constraint)) { String constraintName = consumeIdentifier(); return new DropConstraint(table, constraintName); } else { throw new ParserException( "alter table drop action", currentToken); } } else if (consumeIfMatches(TokenType.KEYWORD_add)) { // optional according to SQL92 but does anyone omit it? if (consumeIfMatches(KEYWORD_column)) { return parseAddColumn(table); } else if (lookingAtConstraint()) { UnresolvedConstraint key = parseConstraint(); return new AddConstraint(table, key); } else { throw new ParserException( "alter table add action", currentToken); } } else if (consumeNonReservedWordIfMatches("modify")) { expectAndConsume(KEYWORD_column); Column newColumn = parseColumnDisallowingMostConstraints(table); return new ModifyColumn(table, newColumn); } else if (consumeNonReservedWordIfMatches("change")) { expectAndConsume(KEYWORD_column); String oldName = consumeIdentifier(); Column newColumn = parseColumnDisallowingMostConstraints(table); return new ChangeColumn(table, oldName, newColumn); } else if (matchesNonReservedWord("engine")) { parseTableTypeIfPresent(); return new NoopCommand(); } else if (consumeNonReservedWordIfMatches("rename")) { expectAndConsume(KEYWORD_to); String newName = consumeIdentifier(); return new RenameTable(table, newName); } else if (currentToken.type == KEYWORD_character) { parseCharacterSetIfPresent(); return new NoopCommand(); } else { throw new ParserException("alter table action", currentToken); } } private Command parseAddColumn(UnresolvedTableReference table) { Position position = Position.LAST; Column newColumn = parseColumnDisallowingMostConstraints(table); if (consumeNonReservedWordIfMatches("after")) { Token column = expectAndConsume(IDENTIFIER); position = Position.after(column.getText(), column.location); } else if (consumeNonReservedWordIfMatches("first")) { /* first is a keyword in SQL92, but I guess we'll make it one here when we need to. */ position = Position.FIRST; } return new AddColumn(table, newColumn, position); } /** * @internal * Parse column and throw exception if a constraint is * present. * The exceptions are constraints which are stored in * the {@link Column} itself (currently NOT NULL). */ private Column parseColumnDisallowingMostConstraints( UnresolvedTableReference table) { CreateTable constraintCollector = new CreateTable(table.tableName()); Column newColumn = parseColumnDefinition(constraintCollector); if (constraintCollector.hasConstraints()) { throw new MayflyException( "constraints are not yet supported in ALTER TABLE"); } return newColumn; } void parseTableElement(CreateTable table) { if (currentToken.type == IDENTIFIER) { table.addColumn(parseColumnDefinition(table)); } else if (lookingAtConstraint()) { table.addConstraint(parseConstraint()); } else if (consumeIfMatches(TokenType.KEYWORD_index)) { String name; if (currentToken.type == IDENTIFIER) { name = consumeIdentifier(); } else { name = null; } ColumnNames columns = parseColumnNames(); table.addIndex(name, columns); } else { throw new ParserException( "column or table constraint", currentToken); } } private boolean lookingAtConstraint() { return currentToken.type == TokenType.KEYWORD_primary || currentToken.type == TokenType.KEYWORD_unique || currentToken.type == TokenType.KEYWORD_foreign || currentToken.type == TokenType.KEYWORD_constraint || currentToken.type == TokenType.KEYWORD_check; } private UnresolvedConstraint parseConstraint() { String constraintName; if (consumeIfMatches(TokenType.KEYWORD_constraint)) { constraintName = consumeIdentifier(); } else { constraintName = null; } if (consumeIfMatches(TokenType.KEYWORD_primary)) { expectAndConsume(TokenType.KEYWORD_key); return new UnresolvedPrimaryKey( parseColumnNames(), constraintName); } else if (consumeIfMatches(TokenType.KEYWORD_unique)) { return new UnresolvedUniqueConstraint( parseColumnNames(), constraintName); } else if (currentToken.type == TokenType.KEYWORD_foreign) { return parseForeignKeyConstraint(constraintName); } else if (consumeIfMatches(TokenType.KEYWORD_check)) { expectAndConsume(TokenType.OPEN_PAREN); Condition condition = parseCondition().asBoolean(); expectAndConsume(CLOSE_PAREN); return new UnresolvedCheckConstraint(condition, constraintName); } else { throw new MayflyInternalException( "expected constraint but got " + currentToken.describe()); } } private UnresolvedForeignKey parseForeignKeyConstraint(String constraintName) { Location start = currentToken.location; expectAndConsume(TokenType.KEYWORD_foreign); expectAndConsume(TokenType.KEYWORD_key); expectAndConsume(TokenType.OPEN_PAREN); String referencingColumn = consumeIdentifier(); expectAndConsume(CLOSE_PAREN); expectAndConsume(TokenType.KEYWORD_references); UnresolvedTableReference targetTable = parseTableReference(); expectAndConsume(TokenType.OPEN_PAREN); String targetColumn = consumeIdentifier(); Token end = expectAndConsume(CLOSE_PAREN); Actions actions = parseActions(); return new UnresolvedForeignKey( referencingColumn, targetTable, targetColumn, actions.onDelete, actions.onUpdate, constraintName, start.combine(end.location) ); } Actions parseActions() { Actions actions = new Actions(); if (consumeIfMatches(TokenType.KEYWORD_on)) { if (consumeIfMatches(TokenType.KEYWORD_delete)) { actions.onDelete = parseForeignKeyAction(); if (consumeIfMatches(TokenType.KEYWORD_on)) { expectAndConsume(TokenType.KEYWORD_update); actions.onUpdate = parseForeignKeyAction(); } } else if (consumeIfMatches(TokenType.KEYWORD_update)) { actions.onUpdate = parseForeignKeyAction(); if (consumeIfMatches(TokenType.KEYWORD_on)) { expectAndConsume(TokenType.KEYWORD_delete); actions.onDelete = parseForeignKeyAction(); } } else { throw new ParserException( "UPDATE or DELETE", currentToken); } } return actions; } class Actions { Action onDelete; Action onUpdate; public Actions() { onDelete = new NoAction(); onUpdate = new NoAction(); } } private Action parseForeignKeyAction() { if (consumeNonReservedWordIfMatches("no")) { expectNonReservedWord("action"); return new NoAction(); } else if (consumeNonReservedWordIfMatches("cascade")) { return new Cascade(); } else if (consumeIfMatches(KEYWORD_set)) { if (consumeIfMatches(TokenType.KEYWORD_null)) { return new SetNull(); } else if (consumeIfMatches(TokenType.KEYWORD_default)) { return new SetDefault(); } else { throw new ParserException("expected ON DELETE action " + " but got SET " + currentToken.describe(), currentToken.location); } } else { throw new ParserException("ON DELETE action", currentToken); } } Column parseColumnDefinition(CreateTable table) { String name = consumeIdentifier(); ParsedDataType parsed = parseDataType(); boolean isAutoIncrement = parsed.isAutoIncrement; boolean isSequence = parsed.isSequence; DefaultValue defaultValue = parseDefaultClause(name); Expression onUpdateValue = parseOnUpdateValue(name); if (currentToken.type == IDENTIFIER) { String text = currentToken.getText(); if (text.equalsIgnoreCase("auto_increment")) { consumeIdentifier(); isAutoIncrement = true; } else if (text.equalsIgnoreCase("generated")) { consumeIdentifier(); expectAndConsume(TokenType.KEYWORD_by); expectAndConsume(TokenType.KEYWORD_default); expectAndConsume(TokenType.KEYWORD_as); expectIdentifier("identity"); if (consumeIfMatches(TokenType.OPEN_PAREN)) { expectIdentifier("start"); expectAndConsume(TokenType.KEYWORD_with); defaultValue = new SpecifiedDefaultValue(parseDefaultValue(name)); expectAndConsume(CLOSE_PAREN); } isSequence = true; } else { // Need to leave the identifier unconsumed, as it // might be AFTER or FIRST from an ALTER TABLE } } if ((isAutoIncrement || isSequence) && !(defaultValue.isSpecified())) { defaultValue = new SpecifiedDefaultValue(new LongCell(1)); } boolean isNotNull = parseColumnConstraints(table, name); return new Column(name, defaultValue, onUpdateValue, isAutoIncrement, isSequence, parsed.type, isNotNull); } private DefaultValue parseDefaultClause(String name) { if (consumeIfMatches(TokenType.KEYWORD_default)) { return new SpecifiedDefaultValue(parseDefaultValue(name)); } else { return DefaultValue.NOT_SPECIFIED; } } private Expression parseOnUpdateValue(String columnName) { if (consumeIfMatches(TokenType.KEYWORD_on)) { expectAndConsume(TokenType.KEYWORD_update); return parseDefaultValue(columnName); } else { return null; } } Expression parseDefaultValue(String columnName) { Location start = currentToken.location; if (currentToken.type == TokenType.NUMBER || currentToken.type == TokenType.PERIOD) { return parseNumber(start).asNonBoolean(); } else if (consumeIfMatches(TokenType.PLUS)) { return parseNumber(start).asNonBoolean(); } else if (consumeIfMatches(TokenType.MINUS)) { return parseNegativeNumber(start).asNonBoolean(); } else if (currentToken.type == TokenType.QUOTED_STRING) { return parseQuotedString().asNonBoolean(); } else if (consumeIfMatches(TokenType.KEYWORD_null)) { return new NullExpression(start); } else if (consumeIfMatches(TokenType.KEYWORD_current_timestamp)) { return new CurrentTimestampExpression(start, timeSource); } else { throw new ParserException( "default value for column " + columnName, currentToken); } } private boolean parseColumnConstraints(CreateTable table, String column) { boolean isNotNull = false; while (true) { if (consumeIfMatches(TokenType.KEYWORD_primary)) { expectAndConsume(TokenType.KEYWORD_key); table.addConstraint(new UnresolvedPrimaryKey(column)); } else if (consumeIfMatches(TokenType.KEYWORD_unique)) { table.addConstraint(new UnresolvedUniqueConstraint(column)); } else if (consumeIfMatches(TokenType.KEYWORD_not)) { expectAndConsume(TokenType.KEYWORD_null); isNotNull = true; } else { break; } } return isNotNull; } ParsedDataType parseDataType() { boolean isAutoIncrement = false; boolean isSequence = false; DataType type; if (consumeIfMatches(TokenType.KEYWORD_integer) || consumeIfMatches(TokenType.KEYWORD_int)) { type = new IntegerDataType(TokenType.KEYWORD_integer.description()); } else if (consumeIfMatches(TokenType.KEYWORD_smallint)) { type = new IntegerDataType(TokenType.KEYWORD_smallint.description()); } else if (consumeIfMatches(TokenType.KEYWORD_varchar)) { expectAndConsume(TokenType.OPEN_PAREN); long size = consumeLong(); expectAndConsume(CLOSE_PAREN); type = new StringDataType( TokenType.KEYWORD_varchar.description() + "(" + size + ")"); } else if (consumeIfMatches(TokenType.KEYWORD_decimal)) { expectAndConsume(TokenType.OPEN_PAREN); int precision = consumeInteger(); expectAndConsume(TokenType.COMMA); int scale = consumeInteger(); expectAndConsume(CLOSE_PAREN); type = new DecimalDataType(precision, scale); } else if (currentToken.type == IDENTIFIER) { // These shouldn't be reserved if they are not in the // SQL standard, seems like. Token token = expectAndConsume(IDENTIFIER); String currentText = token.getText(); if (currentText.equalsIgnoreCase("tinyint")) { type = new IntegerDataType("TINYINT"); } else if (currentText.equalsIgnoreCase("bigint")) { type = new IntegerDataType("BIGINT"); } else if (currentText.equalsIgnoreCase("text")) { type = new StringDataType("TEXT"); } else if (currentText.equalsIgnoreCase("blob")) { if (consumeIfMatches(TokenType.OPEN_PAREN)) { long size = consumeLong(); //expectAndConsume(TokenType.NUMBER); expectAndConsume(CLOSE_PAREN); type = new BinaryDataType(size); } else { type = new BinaryDataType(); } } else if (currentText.equalsIgnoreCase("date")) { // This is a reserved word in SQL92, I think for the // DATE '2003-04-22' syntax for literals. // We could follow that lead if need be... type = new DateDataType(); } else if (currentText.equalsIgnoreCase("timestamp")) { // Reserved word in SQL92; maybe we should too... type = new TimestampDataType(); } else if (currentText.equalsIgnoreCase("identity")) { isAutoIncrement = true; type = new IntegerDataType("INTEGER"); } else if (currentText.equalsIgnoreCase("serial")) { isSequence = true; type = new IntegerDataType("INTEGER"); } else { throw new ParserException("data type", token); } } else { throw new ParserException("data type", currentToken); } return new ParsedDataType(isAutoIncrement, isSequence, type); } class ParsedDataType { final boolean isAutoIncrement; final boolean isSequence; final DataType type; public ParsedDataType(boolean isAutoIncrement, boolean isSequence, DataType type) { this.isAutoIncrement = isAutoIncrement; this.isSequence = isSequence; this.type = type; } } Select parseSelect() { Location start = currentToken.location; expectAndConsume(TokenType.KEYWORD_select); boolean distinct; if (consumeIfMatches(TokenType.KEYWORD_all)) { distinct = false; } else if (consumeIfMatches(TokenType.KEYWORD_distinct)) { distinct = true; } else { distinct = false; } What what = parseWhat(); expectAndConsume(TokenType.KEYWORD_from); From from = parseFromItems(); Condition where = parseOptionalWhere(); Aggregator groupBy = parseGroupBy(); OrderBy orderBy = parseOrderBy(); Limit limit = parseLimit(); if (consumeIfMatches(TokenType.KEYWORD_for)) { expectAndConsume(TokenType.KEYWORD_update); /* Until we try to do transactions, or at least multiple threads, this can be a noop */ } return new Select(what, from, where, groupBy, distinct, orderBy, limit, start); } private Condition parseOptionalWhere() { if (consumeIfMatches(TokenType.KEYWORD_where)) { return parseWhere(); } else { return Condition.TRUE; } } public What parseWhat() { if (consumeIfMatches(TokenType.ASTERISK)) { return new What(new All()); } What what = new What(); do { what = what.with(parseWhatElement()); } while (consumeIfMatches(TokenType.COMMA)); return what; } public WhatElement parseWhatElement() { if (currentToken.type == IDENTIFIER && ((Token) tokens.get(1)).type == TokenType.PERIOD && ((Token) tokens.get(2)).type == TokenType.ASTERISK) { String firstIdentifier = consumeIdentifier(); expectAndConsume(TokenType.PERIOD); expectAndConsume(TokenType.ASTERISK); return new AllColumnsFromTable(firstIdentifier); } Expression expression = parseExpression().asNonBoolean(); if (consumeIfMatches(TokenType.KEYWORD_as)) { String aliasedColumn = consumeIdentifier(); return new AliasedExpression(aliasedColumn, expression); } return expression; } public ParserExpression parseExpression() { ParserExpression left = parseFactor(); while (currentToken.type == TokenType.MINUS || currentToken.type == TokenType.PLUS ) { Token token = consume(); if (token.type == TokenType.MINUS) { left = new NonBooleanParserExpression(new Minus(left.asNonBoolean(), parseFactor().asNonBoolean())); } else if (token.type == TokenType.PLUS) { left = new NonBooleanParserExpression(new Plus(left.asNonBoolean(), parseFactor().asNonBoolean())); } else { throw new MayflyInternalException("Didn't expect token " + token.describe()); } } return left; } private ParserExpression parseFactor() { ParserExpression left = parsePrimary(); while (currentToken.type == TokenType.CONCATENATE || currentToken.type == TokenType.DIVIDE || currentToken.type == TokenType.ASTERISK ) { Token token = consume(); if (token.type == TokenType.CONCATENATE) { left = new NonBooleanParserExpression( new Concatenate(left.asNonBoolean(), parsePrimary().asNonBoolean()) ); } else if (token.type == TokenType.DIVIDE) { left = new NonBooleanParserExpression( new Divide(left.asNonBoolean(), parsePrimary().asNonBoolean()) ); } else if (token.type == TokenType.ASTERISK) { left = new NonBooleanParserExpression( new Multiply(left.asNonBoolean(), parsePrimary().asNonBoolean()) ); } else { throw new MayflyInternalException("Didn't expect token " + token.describe()); } } return left; } public Condition parseWhere() { return parseCondition().asBoolean(); } public ParserExpression parseCondition() { ParserExpression expression = parseBooleanTerm(); while (currentToken.type == TokenType.KEYWORD_or) { expectAndConsume(TokenType.KEYWORD_or); Condition right = parseBooleanTerm().asBoolean(); expression = new BooleanParserExpression(new Or(expression.asBoolean(), right)); } return expression; } private ParserExpression parseBooleanTerm() { ParserExpression expression = parseBooleanFactor(); while (currentToken.type == TokenType.KEYWORD_and) { expectAndConsume(TokenType.KEYWORD_and); Condition right = parseBooleanFactor().asBoolean(); expression = new BooleanParserExpression(new And(expression.asBoolean(), right)); } return expression; } private ParserExpression parseBooleanFactor() { return parseBooleanPrimary(); } private ParserExpression parseBooleanPrimary() { if (consumeIfMatches(TokenType.KEYWORD_not)) { Condition expression = parseBooleanPrimary().asBoolean(); return new BooleanParserExpression(new Not(expression)); } ParserExpression left = parseExpression(); if (consumeIfMatches(EQUAL)) { Expression right = parsePrimary().asNonBoolean(); return new BooleanParserExpression(new Equal(left.asNonBoolean(), right)); } if (currentToken.type == TokenType.KEYWORD_like) { return new BooleanParserExpression(parseLike(left)); } else if (consumeIfMatches(TokenType.LESS_GREATER)) { Expression right = parsePrimary().asNonBoolean(); return new BooleanParserExpression(NotEqual.construct(left.asNonBoolean(), right)); } else if (consumeIfMatches(TokenType.BANG_EQUAL)) { Expression right = parsePrimary().asNonBoolean(); return new BooleanParserExpression(NotEqual.construct(left.asNonBoolean(), right)); } else if (consumeIfMatches(TokenType.GREATER)) { Expression right = parsePrimary().asNonBoolean(); return new BooleanParserExpression(new Greater(left.asNonBoolean(), right)); } else if (consumeIfMatches(TokenType.LESS)) { Expression right = parsePrimary().asNonBoolean(); return new BooleanParserExpression(new Greater(right, left.asNonBoolean())); } else if (consumeIfMatches(TokenType.LESS_EQUAL)) { Expression right = parsePrimary().asNonBoolean(); return new BooleanParserExpression( new LessEqual(left.asNonBoolean(), right)); } else if (consumeIfMatches(TokenType.GREATER_EQUAL)) { Expression right = parsePrimary().asNonBoolean(); return new BooleanParserExpression( new LessEqual(right, left.asNonBoolean())); } else if (consumeIfMatches(TokenType.KEYWORD_not)) { return parseNotOperator(left); } else if (currentToken.type == TokenType.KEYWORD_in) { return new BooleanParserExpression(parseIn(left.asNonBoolean())); } else if (consumeIfMatches(TokenType.KEYWORD_is)) { if (consumeIfMatches(TokenType.KEYWORD_not)) { return new BooleanParserExpression(new Not(parseIs(left.asNonBoolean()))); } return new BooleanParserExpression(parseIs(left.asNonBoolean())); } else { return left; } } private Like parseLike(ParserExpression left) { expectAndConsume(KEYWORD_like); Expression right = parsePrimary().asNonBoolean(); return new Like(left.asNonBoolean(), right); } /* * x NOT IN y, x NOT LIKE y, etc. */ private BooleanParserExpression parseNotOperator(ParserExpression left) { if (currentToken.type == TokenType.KEYWORD_in) { return new BooleanParserExpression(new Not(parseIn(left.asNonBoolean()))); } else if (currentToken.type == KEYWORD_like) { return new BooleanParserExpression(new Not(parseLike(left))); } else { throw new ParserException("IN or LIKE", currentToken); } } private Condition parseIs(Expression left) { expectAndConsume(TokenType.KEYWORD_null); return new IsNull(left); } private Condition parseIn(Expression left) { Condition result; expectAndConsume(TokenType.KEYWORD_in); expectAndConsume(TokenType.OPEN_PAREN); if (currentToken.type == TokenType.KEYWORD_select) { Select subselect = parseSelect(); result = new SubselectedIn(left, subselect); } else { ImmutableList<Expression> expressions = parseExpressionList(); result = new In(left, expressions); } expectAndConsume(CLOSE_PAREN); return result; } private ImmutableList<Expression> parseExpressionList() { List<Expression> expressions = new ArrayList<Expression>(); do { expressions.add(parseExpression().asNonBoolean()); } while (consumeIfMatches(TokenType.COMMA)); return new ImmutableList<Expression>(expressions); } public ParserExpression parsePrimary() { AggregateArgumentParser argumentParser = new AggregateArgumentParser(); Location start = currentToken.location; if (currentToken.type == IDENTIFIER) { return new NonBooleanParserExpression(parsePrimaryIdentifier()); } else if (currentToken.type == TokenType.NUMBER || currentToken.type == TokenType.PERIOD) { return parseNumber(start); } else if (consumeIfMatches(TokenType.PLUS)) { return parseNumber(start); } else if (consumeIfMatches(TokenType.MINUS)) { return parseNegativeNumber(start); } else if (currentToken.type == TokenType.KEYWORD_case) { return parseCase(); } else if (currentToken.type == TokenType.QUOTED_STRING) { return parseQuotedString(); } else if (currentToken.type == TokenType.PARAMETER) { return new NonBooleanParserExpression( new IntegerLiteral(parameterDummy())); } else if (consumeIfMatches(TokenType.KEYWORD_null)) { throw new FoundNullLiteral(start); } else if (currentToken.type == TokenType.BINARY) { Token token = expectAndConsume(TokenType.BINARY); return new NonBooleanParserExpression( new CellExpression(new BinaryCell(token.getBytes()), start)); } else if (argumentParser.parse(TokenType.KEYWORD_max, false)) { return new NonBooleanParserExpression(new Maximum( argumentParser.expression, argumentParser.functionName, argumentParser.distinct, argumentParser.location)); } else if (argumentParser.parse(TokenType.KEYWORD_min, false)) { return new NonBooleanParserExpression(new Minimum( argumentParser.expression, argumentParser.functionName, argumentParser.distinct, argumentParser.location)); } else if (argumentParser.parse(TokenType.KEYWORD_sum, false)) { return new NonBooleanParserExpression(new Sum( argumentParser.expression, argumentParser.functionName, argumentParser.distinct, argumentParser.location )); } else if (argumentParser.parse(TokenType.KEYWORD_avg, false)) { return new NonBooleanParserExpression(new Average( argumentParser.expression, argumentParser.functionName, argumentParser.distinct, argumentParser.location )); } else if (argumentParser.parse(TokenType.KEYWORD_count, true)) { if (argumentParser.gotAsterisk) { return new NonBooleanParserExpression( new CountAll(argumentParser.functionName, argumentParser.location) ); } else { return new NonBooleanParserExpression(new Count( argumentParser.expression, argumentParser.functionName, argumentParser.distinct, argumentParser.location )); } } else if (consumeIfMatches(TokenType.OPEN_PAREN)) { ParserExpression expression; if (currentToken.type == TokenType.KEYWORD_select) { expression = parseScalarSubselect(); } else { expression = parseCondition(); } expectAndConsume(CLOSE_PAREN); return expression; } else { /* I'm pretty sure the distinction between "expression", "primary", "term", etc, are just for the grammar - that users will be happy thinking of it all as expressions. (these are basically concepts introduced so we can write a grammar, not really the way that programmers think of precedence). */ /* Do we use the word expression for both boolean and non-boolean? */ throw new ParserException("expression", currentToken); } } private int parameterDummy() { if (allowParameters) { expectAndConsume(TokenType.PARAMETER); /* We are just checking the syntax, so this value won't be used anywhere */ return 0; } else { throw new MayflyException( "Attempt to specify '?' outside a prepared statement", currentToken.location); } } private ParserExpression parseScalarSubselect() { return new NonBooleanParserExpression( new ScalarSubselect(parseSelect())); } private ParserExpression parseCase() { Location start = currentToken.location; SearchedCase result = new SearchedCase(); expectAndConsume(TokenType.KEYWORD_case); do { expectAndConsume(TokenType.KEYWORD_when); Condition condition = parseWhere(); expectAndConsume(TokenType.KEYWORD_then); // This will get more complicated when/if we allow NULL Expression thenValue = parseExpression().asNonBoolean(); result = result.withCase(condition, thenValue); } while (currentToken.type == TokenType.KEYWORD_when); if (consumeIfMatches(TokenType.KEYWORD_else)) { result = result.withElse(parseExpression().asNonBoolean()); } Token end = expectAndConsume(TokenType.KEYWORD_end); return new NonBooleanParserExpression( result.withLocation(start.combine(end.location)) ); } private ParserExpression parseQuotedString() { Token literal = expectAndConsume(TokenType.QUOTED_STRING); return new NonBooleanParserExpression( new QuotedString(literal.getText(), literal.location)); } private ParserExpression parseNumber(Location start) { Literal number = parseNumericLiteral(start); return new NonBooleanParserExpression(number); } private ParserExpression parseNegativeNumber(Location start) { Literal number = parseNumericLiteral(start); return new NonBooleanParserExpression(makeNegativeLiteral(number)); } private Literal makeNegativeLiteral(Literal number) { Location location = number.location; // Probably should have more unit tests for this, especially // the edge cases (-2^31 is one). if (number instanceof IntegerLiteral) { IntegerLiteral integer = (IntegerLiteral) number; return new IntegerLiteral(- integer.value, location); } else if (number instanceof LongLiteral) { LongLiteral longLiteral = (LongLiteral) number; return new LongLiteral(- longLiteral.value, location); } else if (number instanceof DecimalLiteral) { DecimalLiteral decimal = (DecimalLiteral) number; return new DecimalLiteral(decimal.value.negate(), location); } else { throw new UnimplementedException("don't yet handle big integers"); } } Literal parseNumericLiteral(Location start) { if (consumeIfMatches(TokenType.PERIOD)) { Token number = expectAndConsume(TokenType.NUMBER); String secondInteger = number.getText(); return new DecimalLiteral("." + secondInteger, start.combine(number.location) ); } Token number = expectAndConsume(TokenType.NUMBER); String firstInteger = number.getText(); if (currentToken.type == TokenType.PERIOD) { Token period = expectAndConsume(TokenType.PERIOD); if (currentToken.type == TokenType.NUMBER) { Token secondNumber = expectAndConsume(TokenType.NUMBER); String secondInteger = secondNumber.getText(); return new DecimalLiteral( firstInteger + "." + secondInteger, start.combine(secondNumber.location)); } else { // Might need to look into semantics for this. Should it be // BigDecimal or integer? return new DecimalLiteral( firstInteger + ".", start.combine(period.location) ); } } return integerToObject(firstInteger, start.combine(number.location)); } class AggregateArgumentParser { Expression expression; String functionName; boolean gotAsterisk; boolean distinct; Location location; boolean parse(TokenType aggregateTokenType, boolean allowAsterisk) { if (currentToken().type == aggregateTokenType) { Token function = expectAndConsume(aggregateTokenType); functionName = function.getText(); expectAndConsume(TokenType.OPEN_PAREN); if (allowAsterisk && consumeIfMatches(TokenType.ASTERISK)) { gotAsterisk = true; } else { if (consumeIfMatches(TokenType.KEYWORD_all)) { } else if (consumeIfMatches(TokenType.KEYWORD_distinct)) { distinct = true; } expression = parseExpression().asNonBoolean(); } Token end = expectAndConsume(CLOSE_PAREN); location = function.location.combine(end.location); return true; } return false; } } abstract public class ParserExpression { abstract public Expression asNonBoolean(); abstract public Condition asBoolean(); } public class NonBooleanParserExpression extends ParserExpression { private final Expression expression; public NonBooleanParserExpression(Expression expression) { this.expression = expression; } @Override public Condition asBoolean() { throw new ParserException( "expected boolean expression but got non-boolean expression", expression.location); } @Override public Expression asNonBoolean() { return expression; } } public class BooleanParserExpression extends ParserExpression { private final Condition condition; public BooleanParserExpression(Condition expression) { this.condition = expression; } @Override public Condition asBoolean() { return condition; } @Override public Expression asNonBoolean() { throw new ParserException( "expected non-boolean expression but got boolean expression"); } } private Expression parsePrimaryIdentifier() { Token firstIdentifier = expectAndConsume(IDENTIFIER); if (consumeIfMatches(TokenType.PERIOD)) { Token column = expectAndConsume(IDENTIFIER); return new SingleColumn(firstIdentifier.getText(), column.getText(), firstIdentifier.location.combine(column.location), options ); } else if (consumeIfMatches(TokenType.OPEN_PAREN)) { ImmutableList<Expression> arguments = parseExpressionList(); Token close = expectAndConsume(CLOSE_PAREN); return Function.create(firstIdentifier.getText(), arguments, firstIdentifier.location.combine(close.location), options); } else { return new SingleColumn(firstIdentifier.getText(), firstIdentifier.location, options); } } private SingleColumn parseColumnReference() { Expression expression = parseExpression().asNonBoolean(); if (expression instanceof SingleColumn) { return (SingleColumn) expression; } else { throw new ParserException( "expected column reference in ORDER BY but got " + expression.displayName(), expression.location); } } public From parseFromItems() { From from = new From(); do { from = from.with(parseFromItem()); } while (consumeIfMatches(TokenType.COMMA)); return from; } private FromElement parseFromItem() { FromElement left; if (consumeIfMatches(TokenType.OPEN_PAREN)) { left = parseFromItem(); expectAndConsume(CLOSE_PAREN); } else { left = parseFromTable(); } while (true) { if (consumeIfMatches(TokenType.KEYWORD_cross)) { expectAndConsume(TokenType.KEYWORD_join); FromElement right = parseFromItem(); left = new InnerJoin(left, right, Condition.TRUE); } else if (consumeIfMatches(TokenType.KEYWORD_inner)) { expectAndConsume(TokenType.KEYWORD_join); FromElement right = parseFromItem(); expectAndConsume(TokenType.KEYWORD_on); Condition condition = parseWhere(); left = new InnerJoin(left, right, condition); } else if (consumeIfMatches(TokenType.KEYWORD_left)) { if (currentToken.type == TokenType.KEYWORD_outer) { expectAndConsume(TokenType.KEYWORD_outer); } expectAndConsume(TokenType.KEYWORD_join); FromElement right = parseFromItem(); expectAndConsume(TokenType.KEYWORD_on); Condition condition = parseWhere(); left = new LeftJoin(left, right, condition); } else { return left; } } } public FromTable parseFromTable() { String firstIdentifier = consumeIdentifier(); String table; if (currentToken.type == TokenType.PERIOD) { expectAndConsume(TokenType.PERIOD); table = consumeIdentifier(); } else { table = firstIdentifier; } if (currentToken.type == IDENTIFIER) { String alias = consumeIdentifier(); return new FromTable(table, alias); } else { return new FromTable(table); } } public UnresolvedTableReference parseTableReference() { Token first = expectAndConsume(IDENTIFIER); if (consumeIfMatches(TokenType.PERIOD)) { Token table = expectAndConsume(IDENTIFIER); return new UnresolvedTableReference( first.getText(), table.getText(), first.location.combine(table.location), options); } else { return new UnresolvedTableReference( null, first.getText(), first.location, options); } } private Aggregator parseGroupBy() { if (consumeIfMatches(TokenType.KEYWORD_group)) { expectAndConsume(TokenType.KEYWORD_by); List<GroupItem> items = new ArrayList<GroupItem>(); do { items.add(parseGroupItem()); } while (consumeIfMatches(TokenType.COMMA)); Condition having; if (consumeIfMatches(TokenType.KEYWORD_having)) { having = parseCondition().asBoolean(); } else { having = Condition.TRUE; } return new GroupBy(new ImmutableList<GroupItem>(items), having); } else { if (currentToken.type == TokenType.KEYWORD_having) { throw new ParserException( "can't specify HAVING without GROUP BY", currentToken.location); } return new NoGroupBy(); } } private GroupItem parseGroupItem() { return new GroupItem(parseExpression().asNonBoolean()); } private OrderBy parseOrderBy() { if (consumeIfMatches(TokenType.KEYWORD_order)) { expectAndConsume(TokenType.KEYWORD_by); OrderBy orderBy = new OrderBy(); do { orderBy = orderBy.with(parseOrderItem()); } while (consumeIfMatches(TokenType.COMMA)); return orderBy; } else { return new OrderBy(); } } OrderItem parseOrderItem() { if (currentToken.type == TokenType.NUMBER) { int reference = consumeInteger(); boolean ascending = parseAscending(); return new ReferenceOrderItem(reference, ascending); } else { SingleColumn column = parseColumnReference(); boolean ascending = parseAscending(); return new ColumnOrderItem(column, ascending); } } private boolean parseAscending() { if (consumeIfMatches(TokenType.KEYWORD_asc)) { return true; } else if (consumeIfMatches(TokenType.KEYWORD_desc)) { return false; } else { return true; } } Limit parseLimit() { if (currentToken.type == TokenType.KEYWORD_limit) { expectAndConsume(TokenType.KEYWORD_limit); int count = consumeIntegerOrParameter(); if (consumeNonReservedWordIfMatches("offset")) { int offset = consumeIntegerOrParameter(); return new Limit(count, offset); } return new Limit(count, Limit.NO_OFFSET); } else { return Limit.NONE; } } private int consumeIntegerOrParameter() { if (currentToken.type == TokenType.NUMBER) { return consumeInteger(); } else if (currentToken.type == TokenType.PARAMETER) { return parameterDummy(); } else { throw new ParserException("number", currentToken); } } public String remainingTokens() { StringBuilder result = new StringBuilder(); boolean first = true; Iterator iter = tokens.iterator(); while (iter.hasNext()) { Token token = (Token) iter.next(); if (token.type == TokenType.END_OF_FILE) { break; } if (first) { first = false; } else { result.append(" "); } result.append(token.describe()); } return result.toString(); } private String consumeIdentifier() { Token token = expectAndConsume(IDENTIFIER); return token.getText(); } private void expectIdentifier(String expectedIdentifier) { Token token = expectAndConsume(IDENTIFIER); String text = token.getText(); if (!text.equalsIgnoreCase(expectedIdentifier)) { throw new ParserException(expectedIdentifier, token); } } int consumeInteger() { Token number = expectAndConsume(TokenType.NUMBER); String text = number.getText(); try { return Integer.parseInt(text); } catch (NumberFormatException e) { // Out of range. Most (all?) other cases are prevented in the lexer. throw new ParserException(text + " is out of range", number.location); } } long consumeLong() { Token number = expectAndConsume(TokenType.NUMBER); String text = number.getText(); try { return Long.parseLong(text); } catch (NumberFormatException e) { // Out of range. Most (all?) other cases are prevented in the lexer. throw new ParserException(text + " is out of range", number.location); } } private Literal integerToObject(String text, Location location) { try { return new IntegerLiteral(Integer.parseInt(text), location); } catch (NumberFormatException e) { // Out of range. Most (all?) other cases are prevented in the lexer. } try { return new LongLiteral(Long.parseLong(text), location); } catch (NumberFormatException e) { } throw new UnimplementedException("don't yet handle BigInteger " + text); } boolean consumeIfMatches(TokenType type) { if (currentToken.type == type) { expectAndConsume(type); return true; } return false; } private boolean consumeNonReservedWordIfMatches(String word) { if (currentToken.type != IDENTIFIER) { return false; } String currentText = currentToken.getText(); if (word.equalsIgnoreCase(currentText)) { expectAndConsume(IDENTIFIER); return true; } return false; } private boolean matchesNonReservedWord(String word) { if (currentToken.type != IDENTIFIER) { return false; } String currentText = currentToken.getText(); if (word.equalsIgnoreCase(currentText)) { return true; } return false; } private void expectNonReservedWord(String word) { if (!consumeNonReservedWordIfMatches(word)) { throw new ParserException(word, currentToken); } } Token expectAndConsume(TokenType expectedType) { Token token = currentToken; if (token.type != expectedType) { throw new ParserException( describeExpectation(expectedType), token ); } return consume(); } /** * @internal * Consume the current token. This is a slightly dangerous operation, * in the sense that it is easy to be careless about whether you are * consuming the token type that you think. * So call {@link #expectAndConsume(TokenType)} * instead where feasible. */ private Token consume() { Token consumedToken = (Token) tokens.remove(0); this.currentToken = tokens.isEmpty() ? null : (Token) tokens.get(0); return consumedToken; } Token currentToken() { return currentToken; } private String describeExpectation(TokenType expectedType) { return expectedType.description(); } }