/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.modeshape.sequencer.ddl.dialect.derby;

import static org.modeshape.sequencer.ddl.StandardDdlLexicon.ALL_PRIVILEGES;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.COLUMN_ATTRIBUTE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.DDL_EXPRESSION;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.DDL_LENGTH;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.DDL_START_CHAR_INDEX;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.DDL_START_COLUMN_NUMBER;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.DDL_START_LINE_NUMBER;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.GRANTEE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.GRANT_PRIVILEGE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.NEW_NAME;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.PROPERTY_VALUE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.SQL;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_ALTER_COLUMN_DEFINITION;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_ALTER_TABLE_STATEMENT;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_COLUMN_DEFINITION;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_COLUMN_REFERENCE;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_CREATE_TABLE_STATEMENT;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_DROP_COLUMN_DEFINITION;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_DROP_TABLE_CONSTRAINT_DEFINITION;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_GRANT_ON_TABLE_STATEMENT;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_SIMPLE_PROPERTY;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.TYPE_STATEMENT_OPTION;
import static org.modeshape.sequencer.ddl.StandardDdlLexicon.VALUE;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.IS_TABLE_TYPE;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.ORDER;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.PARAMETER_STYLE;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.ROLE_NAME;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TABLE_NAME;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_CREATE_FUNCTION_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_CREATE_INDEX_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_CREATE_PROCEDURE_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_CREATE_ROLE_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_CREATE_SYNONYM_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_CREATE_TRIGGER_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_DECLARE_GLOBAL_TEMPORARY_TABLE_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_DROP_FUNCTION_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_DROP_INDEX_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_DROP_PROCEDURE_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_DROP_ROLE_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_DROP_SYNONYM_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_DROP_TRIGGER_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_FUNCTION_PARAMETER;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_GRANT_ON_FUNCTION_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_GRANT_ON_PROCEDURE_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_GRANT_ROLES_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_INDEX_COLUMN_REFERENCE;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_LOCK_TABLE_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_RENAME_INDEX_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.TYPE_RENAME_TABLE_STATEMENT;
import static org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlLexicon.UNIQUE_INDEX;
import java.util.ArrayList;
import java.util.List;
import org.modeshape.common.text.ParsingException;
import org.modeshape.common.text.TokenStream;
import org.modeshape.sequencer.ddl.DdlParserProblem;
import org.modeshape.sequencer.ddl.DdlSequencerI18n;
import org.modeshape.sequencer.ddl.DdlTokenStream;
import org.modeshape.sequencer.ddl.DdlTokenStream.DdlTokenizer;
import org.modeshape.sequencer.ddl.StandardDdlLexicon;
import org.modeshape.sequencer.ddl.StandardDdlParser;
import org.modeshape.sequencer.ddl.datatype.DataType;
import org.modeshape.sequencer.ddl.datatype.DataTypeParser;
import org.modeshape.sequencer.ddl.node.AstNode;

/**
 * Derby-specific DDL Parser. Includes custom data types as well as custom DDL statements.
 */
public class DerbyDdlParser extends StandardDdlParser implements DerbyDdlConstants, DerbyDdlConstants.DerbyStatementStartPhrases {

    /**
     * The Derby parser identifier.
     */
    public static final String ID = "DERBY";

    protected static final List<String[]> derbyDataTypeStrings = new ArrayList<String[]>(
                                                                                         DerbyDataTypes.CUSTOM_DATATYPE_START_PHRASES);

    private static final String TERMINATOR = DEFAULT_TERMINATOR;

    public DerbyDdlParser() {
        setDatatypeParser(new DerbyDataTypeParser());
        setDoUseTerminator(true);
        setTerminator(TERMINATOR);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.modeshape.sequencer.ddl.StandardDdlParser#initializeTokenStream(org.modeshape.sequencer.ddl.DdlTokenStream)
     */
    @Override
    protected void initializeTokenStream( DdlTokenStream tokens ) {
        super.initializeTokenStream(tokens);
        tokens.registerKeyWords(CUSTOM_KEYWORDS);
        tokens.registerStatementStartPhrase(ALTER_PHRASES);
        tokens.registerStatementStartPhrase(CREATE_PHRASES);
        tokens.registerStatementStartPhrase(DROP_PHRASES);
        tokens.registerStatementStartPhrase(SET_PHRASES);
        tokens.registerStatementStartPhrase(MISC_PHRASES);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.modeshape.sequencer.ddl.StandardDdlParser#getId()
     */
    @Override
    public String getId() {
        return ID;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.modeshape.sequencer.ddl.StandardDdlParser#getValidSchemaChildTypes()
     */
    @Override
    protected String[] getValidSchemaChildTypes() {
        return VALID_SCHEMA_CHILD_STMTS;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.modeshape.sequencer.ddl.StandardDdlParser#parseCustomStatement(org.modeshape.sequencer.ddl.DdlTokenStream,
     *      org.modeshape.sequencer.ddl.node.AstNode)
     */
    @Override
    protected AstNode parseCustomStatement( DdlTokenStream tokens,
                                            AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        AstNode result = super.parseCustomStatement(tokens, parentNode);
        if (result == null) {
            if (tokens.matches(STMT_LOCK_TABLE)) {
                result = parseLockTable(tokens, parentNode);
            } else if (tokens.matches(STMT_RENAME_TABLE)) {
                result = parseRenameTable(tokens, parentNode);
            } else if (tokens.matches(STMT_RENAME_INDEX)) {
                result = parseRenameIndex(tokens, parentNode);
            } else if (tokens.matches(STMT_DECLARE_GLOBAL_TEMP_TABLE)) {
                result = parseDeclareGlobalTempTable(tokens, parentNode);
            }
        }
        return result;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.modeshape.sequencer.ddl.StandardDdlParser#parseCreateStatement(org.modeshape.sequencer.ddl.DdlTokenStream,
     *      org.modeshape.sequencer.ddl.node.AstNode)
     */
    @Override
    protected AstNode parseCreateStatement( DdlTokenStream tokens,
                                            AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        if (tokens.matches(STMT_CREATE_INDEX) || tokens.matches(STMT_CREATE_UNIQUE_INDEX)) {
            return parseCreateIndex(tokens, parentNode);
        } else if (tokens.matches(STMT_CREATE_FUNCTION)) {
            return parseCreateFunction(tokens, parentNode);
        } else if (tokens.matches(STMT_CREATE_PROCEDURE)) {
            return parseCreateProcedure(tokens, parentNode);
        } else if (tokens.matches(STMT_CREATE_ROLE)) {
            return parseCreateRole(tokens, parentNode);
        } else if (tokens.matches(STMT_CREATE_SYNONYM)) {
            return parseCreateSynonym(tokens, parentNode);
        } else if (tokens.matches(STMT_CREATE_TRIGGER)) {
            return parseCreateTrigger(tokens, parentNode);
        }

        return super.parseCreateStatement(tokens, parentNode);

    }

    /**
     * Parses DDL CREATE INDEX
     * 
     * @param tokens the tokenized {@link DdlTokenStream} of the DDL input content; may not be null
     * @param parentNode the parent {@link AstNode} node; may not be null
     * @return the parsed CREATE INDEX
     * @throws ParsingException
     */
    protected AstNode parseCreateIndex( DdlTokenStream tokens,
                                        AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        markStartOfStatement(tokens);
        // CREATE [UNIQUE] INDEX index-Name
        // ON table-Name ( Simple-column-Name [ ASC | DESC ] [ , Simple-column-Name [ ASC | DESC ]] * )
        tokens.consume(CREATE); // CREATE

        boolean isUnique = tokens.canConsume("UNIQUE");

        tokens.consume("INDEX");
        String indexName = parseName(tokens);
        tokens.consume("ON");
        String tableName = parseName(tokens);

        AstNode indexNode = nodeFactory().node(indexName, parentNode, TYPE_CREATE_INDEX_STATEMENT);

        indexNode.setProperty(UNIQUE_INDEX, isUnique);
        indexNode.setProperty(TABLE_NAME, tableName);

        parseIndexTableColumns(tokens, indexNode);

        parseUntilTerminator(tokens);

        markEndOfStatement(tokens, indexNode);

        return indexNode;
    }

    private void parseIndexTableColumns( DdlTokenStream tokens,
                                         AstNode indexNode ) throws ParsingException {
        assert tokens != null;
        assert indexNode != null;

        // Assume we start with open parenthesis '(', then we parse comma separated list of column names followed by optional
        // ASC or DESC

        tokens.consume(L_PAREN); // EXPECTED

        while (!tokens.canConsume(R_PAREN)) {
            String colName = parseName(tokens);
            AstNode colRefNode = nodeFactory().node(colName, indexNode, TYPE_INDEX_COLUMN_REFERENCE);
            if (tokens.canConsume("ASC")) {
                colRefNode.setProperty(ORDER, "ASC");
            } else if (tokens.canConsume("DESC")) {
                colRefNode.setProperty(ORDER, "DESC");
            }
            tokens.canConsume(COMMA);
        }
    }

    /**
     * Parses DDL CREATE FUNCTION statement
     * 
     * @param tokens the tokenized {@link DdlTokenStream} of the DDL input content; may not be null
     * @param parentNode the parent {@link AstNode} node; may not be null
     * @return the parsed CREATE FUNCTION statement node
     * @throws ParsingException
     */
    protected AstNode parseCreateFunction( DdlTokenStream tokens,
                                           AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        markStartOfStatement(tokens);
        // CREATE FUNCTION function-name ( [ FunctionParameter [, FunctionParameter] ] * )
        // RETURNS ReturnDataType [ FunctionElement ] *

        // FunctionElement
        // {
        // | LANGUAGE { JAVA }
        // | {DETERMINISTIC | NOT DETERMINISTIC}
        // | EXTERNAL NAME string
        // | PARAMETER STYLE {JAVA | DERBY_JDBC_RESULT_SET}
        // | { NO SQL | CONTAINS SQL | READS SQL DATA }
        // | { RETURNS NULL ON NULL INPUT | CALLED ON NULL INPUT }
        // }
        tokens.consume(CREATE, "FUNCTION");

        String functionName = parseName(tokens);

        AstNode functionNode = nodeFactory().node(functionName, parentNode, TYPE_CREATE_FUNCTION_STATEMENT);

        parseFunctionParameters(tokens, functionNode);

        tokens.consume("RETURNS");

        if (tokens.canConsume("TABLE")) {
            AstNode tableNode = nodeFactory().node("TABLE", functionNode, TYPE_CREATE_TABLE_STATEMENT);
            tableNode.setProperty(DDL_START_LINE_NUMBER, getCurrentMarkedPosition().getLine());
            tableNode.setProperty(DDL_START_CHAR_INDEX, getCurrentMarkedPosition().getIndexInContent());
            tableNode.setProperty(DDL_START_COLUMN_NUMBER, getCurrentMarkedPosition().getColumn());
            parseColumnsAndConstraints(tokens, tableNode);
            String expressionSource = "TABLE " + tokens.getMarkedContent();
            tableNode.setProperty(DDL_EXPRESSION, expressionSource);
            tableNode.setProperty(DDL_LENGTH, expressionSource.length());

            functionNode.setProperty(IS_TABLE_TYPE, true);
        } else {
            // Assume DataType
            DataType datatype = getDatatypeParser().parse(tokens);
            if (datatype != null) {
                getDatatypeParser().setPropertiesOnNode(functionNode, datatype);
            } else {
                String msg = DdlSequencerI18n.missingReturnTypeForFunction.text(functionName);
                DdlParserProblem problem = new DdlParserProblem(Problems.WARNING, getCurrentMarkedPosition(), msg);
                addProblem(problem, functionNode);
            }
        }

        while (!isTerminator(tokens)) {
            if (tokens.matches("LANGUAGE")) {
                AstNode optionNode = nodeFactory().node("language", functionNode, TYPE_STATEMENT_OPTION);
                if (tokens.canConsume("LANGUAGE", "JAVA")) {
                    optionNode.setProperty(VALUE, "LANGUAGE JAVA");
                } else {
                    tokens.consume("LANGUAGE");
                    optionNode.setProperty(VALUE, "LANGUAGE");
                }
            } else if (tokens.canConsume("DETERMINISTIC")) {
                AstNode optionNode = nodeFactory().node("deterministic", functionNode, TYPE_STATEMENT_OPTION);
                optionNode.setProperty(VALUE, "DETERMINISTIC");
            } else if (tokens.canConsume("NOT", "DETERMINISTIC")) {
                AstNode optionNode = nodeFactory().node("deterministic", functionNode, TYPE_STATEMENT_OPTION);
                optionNode.setProperty(VALUE, "NOT DETERMINISTIC");
            } else if (tokens.canConsume("EXTERNAL", "NAME")) {
                String extName = parseName(tokens);
                AstNode optionNode = nodeFactory().node("externalName", functionNode, TYPE_STATEMENT_OPTION);
                optionNode.setProperty(VALUE, "EXTERNAL NAME" + SPACE + extName);
            } else if (tokens.canConsume("PARAMETER", "STYLE")) {
                if (tokens.canConsume("JAVA")) {
                    functionNode.setProperty(PARAMETER_STYLE, "PARAMETER STYLE" + SPACE + "JAVA");
                } else {
                    tokens.consume("DERBY_JDBC_RESULT_SET");
                    functionNode.setProperty(PARAMETER_STYLE, "PARAMETER STYLE" + SPACE + "DERBY_JDBC_RESULT_SET");
                }
            } else if (tokens.canConsume("NO", "SQL")) {
                AstNode optionNode = nodeFactory().node("sqlStatus", functionNode, TYPE_STATEMENT_OPTION);
                optionNode.setProperty(VALUE, "NO SQL");
            } else if (tokens.canConsume("CONTAINS", "SQL")) {
                AstNode optionNode = nodeFactory().node("sqlStatus", functionNode, TYPE_STATEMENT_OPTION);
                optionNode.setProperty(VALUE, "CONTAINS SQL");
            } else if (tokens.canConsume("READS", "SQL", "DATA")) {
                AstNode optionNode = nodeFactory().node("sqlStatus", functionNode, TYPE_STATEMENT_OPTION);
                optionNode.setProperty(VALUE, "READS SQL DATA");
            } else if (tokens.canConsume("RETURNS", "NULL", "ON", "NULL", "INPUT")) {
                AstNode optionNode = nodeFactory().node("nullInput", functionNode, TYPE_STATEMENT_OPTION);
                optionNode.setProperty(VALUE, "RETURNS NULL ON NULL INPUT");
            } else if (tokens.canConsume("CALLED", "ON", "NULL", "INPUT")) {
                AstNode optionNode = nodeFactory().node("nullInput", functionNode, TYPE_STATEMENT_OPTION);
                optionNode.setProperty(VALUE, "CALLED ON NULL INPUT");
            } else {
                String msg = DdlSequencerI18n.errorParsingDdlContent.text(functionName);
                DdlParserProblem problem = new DdlParserProblem(Problems.ERROR, getCurrentMarkedPosition(), msg);
                addProblem(problem, functionNode);
                break;
            }
        }

        markEndOfStatement(tokens, functionNode);

        return functionNode;
    }

    private void parseFunctionParameters( DdlTokenStream tokens,
                                          AstNode functionNode ) throws ParsingException {
        assert tokens != null;
        assert functionNode != null;

        // Assume we start with open parenthesis '(', then we parse comma separated list of function parameters
        // which have the form: [ parameter-Name ] DataType
        // So, try getting datatype, if datatype == NULL, then parseName() & parse datatype, then repeat as long as next token is
        // ","

        tokens.consume(L_PAREN); // EXPECTED

        while (!tokens.canConsume(R_PAREN)) {
            DataType datatype = getDatatypeParser().parse(tokens);
            if (datatype == null) {
                String paramName = parseName(tokens);
                datatype = getDatatypeParser().parse(tokens);
                AstNode paramNode = nodeFactory().node(paramName, functionNode, TYPE_FUNCTION_PARAMETER);
                getDatatypeParser().setPropertiesOnNode(paramNode, datatype);
            } else {
                AstNode paramNode = nodeFactory().node("functionParameter", functionNode, TYPE_FUNCTION_PARAMETER);
                getDatatypeParser().setPropertiesOnNode(paramNode, datatype);
            }
            tokens.canConsume(COMMA);
        }
    }

    /**
     * Parses DDL CREATE PROCEDURE statement
     * 
     * @param tokens the tokenized {@link DdlTokenStream} of the DDL input content; may not be null
     * @param parentNode the parent {@link AstNode} node; may not be null
     * @return the parsed CREATE PROCEDURE statement node
     * @throws ParsingException
     */
    protected AstNode parseCreateProcedure( DdlTokenStream tokens,
                                            AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        markStartOfStatement(tokens);

        tokens.consume(CREATE, "PROCEDURE");

        String functionName = parseName(tokens);

        AstNode functionNode = nodeFactory().node(functionName, parentNode, TYPE_CREATE_PROCEDURE_STATEMENT);

        parseUntilTerminator(tokens);
        markEndOfStatement(tokens, functionNode);

        return functionNode;
    }

    /**
     * Parses DDL CREATE ROLE statement
     * 
     * @param tokens the tokenized {@link DdlTokenStream} of the DDL input content; may not be null
     * @param parentNode the parent {@link AstNode} node; may not be null
     * @return the parsed CREATE ROLE statement node
     * @throws ParsingException
     */
    protected AstNode parseCreateRole( DdlTokenStream tokens,
                                       AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        markStartOfStatement(tokens);

        tokens.consume(CREATE, "ROLE");

        String functionName = parseName(tokens);

        AstNode functionNode = nodeFactory().node(functionName, parentNode, TYPE_CREATE_ROLE_STATEMENT);

        markEndOfStatement(tokens, functionNode);

        return functionNode;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.modeshape.sequencer.ddl.StandardDdlParser#parseDropStatement(org.modeshape.sequencer.ddl.DdlTokenStream,
     *      org.modeshape.sequencer.ddl.node.AstNode)
     */
    @Override
    protected AstNode parseDropStatement( DdlTokenStream tokens,
                                          AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        AstNode dropNode = null;

        String name = null;

        if (tokens.matches(STMT_DROP_FUNCTION)) {
            markStartOfStatement(tokens);
            tokens.consume(STMT_DROP_FUNCTION);
            name = parseName(tokens);
            dropNode = nodeFactory().node(name, parentNode, TYPE_DROP_FUNCTION_STATEMENT);
        } else if (tokens.matches(STMT_DROP_INDEX)) {
            markStartOfStatement(tokens);
            tokens.consume(STMT_DROP_INDEX);
            name = parseName(tokens);
            dropNode = nodeFactory().node(name, parentNode, TYPE_DROP_INDEX_STATEMENT);
        } else if (tokens.matches(STMT_DROP_PROCEDURE)) {
            markStartOfStatement(tokens);
            tokens.consume(STMT_DROP_PROCEDURE);
            name = parseName(tokens);
            dropNode = nodeFactory().node(name, parentNode, TYPE_DROP_PROCEDURE_STATEMENT);
        } else if (tokens.matches(STMT_DROP_ROLE)) {
            markStartOfStatement(tokens);
            tokens.consume(STMT_DROP_ROLE);
            name = parseName(tokens);
            dropNode = nodeFactory().node(name, parentNode, TYPE_DROP_ROLE_STATEMENT);
        } else if (tokens.matches(STMT_DROP_SYNONYM)) {
            markStartOfStatement(tokens);
            tokens.consume(STMT_DROP_SYNONYM);
            name = parseName(tokens);
            dropNode = nodeFactory().node(name, parentNode, TYPE_DROP_SYNONYM_STATEMENT);
        } else if (tokens.matches(STMT_DROP_TRIGGER)) {
            markStartOfStatement(tokens);
            tokens.consume(STMT_DROP_TRIGGER);
            name = parseName(tokens);
            dropNode = nodeFactory().node(name, parentNode, TYPE_DROP_TRIGGER_STATEMENT);
        }

        if (dropNode != null) {
            markEndOfStatement(tokens, dropNode);
        }

        if (dropNode == null) {
            dropNode = super.parseDropStatement(tokens, parentNode);
        }

        return dropNode;
    }

    /**
     * {@inheritDoc} Syntax for tables GRANT privilege-type ON [TABLE] { table-Name | view-Name } TO grantees Syntax for routines
     * GRANT EXECUTE ON { FUNCTION | PROCEDURE } routine-designator TO grantees Syntax for roles GRANT roleName [ {, roleName }* ]
     * TO grantees privilege-types ALL PRIVILEGES | privilege-list privilege-list table-privilege {, table-privilege }*
     * table-privilege DELETE | INSERT | REFERENCES [column list] | SELECT [column list] | TRIGGER | UPDATE [column list] column
     * list ( column-identifier {, column-identifier}* ) GRANT
     * 
     * @see org.modeshape.sequencer.ddl.StandardDdlParser#parseGrantStatement(org.modeshape.sequencer.ddl.DdlTokenStream,
     *      org.modeshape.sequencer.ddl.node.AstNode)
     */
    @Override
    protected AstNode parseGrantStatement( DdlTokenStream tokens,
                                           AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;
        assert tokens.matches(GRANT);

        markStartOfStatement(tokens);

        // Syntax for tables
        //
        // GRANT privilege-type ON [TABLE] { table-Name | view-Name } TO grantees
        //
        // Syntax for routines
        //
        // GRANT EXECUTE ON { FUNCTION | PROCEDURE } {function-name | procedure-name} TO grantees
        //
        // Syntax for roles
        //
        // GRANT roleName [ {, roleName }* ] TO grantees

        // privilege-types
        //
        // ALL PRIVILEGES | privilege-list
        //
        AstNode grantNode = null;
        boolean allPrivileges = false;

        List<AstNode> privileges = new ArrayList<AstNode>();

        tokens.consume("GRANT");
        if (tokens.canConsume("EXECUTE", "ON")) {
            AstNode node = nodeFactory().node("privilege");
            nodeFactory().setType(node, GRANT_PRIVILEGE);
            node.setProperty(TYPE, "EXECUTE");
            privileges = new ArrayList<AstNode>();
            privileges.add(node);
            if (tokens.canConsume("FUNCTION")) {
                String name = parseName(tokens);
                grantNode = nodeFactory().node(name, parentNode, TYPE_GRANT_ON_FUNCTION_STATEMENT);
            } else {
                tokens.consume("PROCEDURE");
                String name = parseName(tokens);
                grantNode = nodeFactory().node(name, parentNode, TYPE_GRANT_ON_PROCEDURE_STATEMENT);
            }
        } else {

            if (tokens.canConsume("ALL", "PRIVILEGES")) {
                allPrivileges = true;
            } else {
                parseGrantPrivileges(tokens, privileges);

                if (privileges.isEmpty()) {
                    // ASSUME: GRANT roleName [ {, roleName }* ] TO grantees
                    grantNode = nodeFactory().node("grantRoles", parentNode, TYPE_GRANT_ROLES_STATEMENT);
                    do {
                        String roleName = parseName(tokens);
                        nodeFactory().node(roleName, grantNode, ROLE_NAME);
                    } while (tokens.canConsume(COMMA));
                }
            }
            if (grantNode == null) {
                tokens.consume("ON");
                tokens.canConsume(TABLE); // OPTIONAL
                String name = parseName(tokens);
                grantNode = nodeFactory().node(name, parentNode, TYPE_GRANT_ON_TABLE_STATEMENT);
                // Attach privileges to grant node
                for (AstNode node : privileges) {
                    node.setParent(grantNode);
                }
                if (allPrivileges) {
                    grantNode.setProperty(ALL_PRIVILEGES, allPrivileges);
                }
            }

        }

        tokens.consume("TO");

        do {
            String grantee = parseName(tokens);
            nodeFactory().node(grantee, grantNode, GRANTEE);
        } while (tokens.canConsume(COMMA));

        markEndOfStatement(tokens, grantNode);

        return grantNode;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.modeshape.sequencer.ddl.StandardDdlParser#parseGrantPrivileges(org.modeshape.sequencer.ddl.DdlTokenStream,
     *      java.util.List)
     */
    @Override
    protected void parseGrantPrivileges( DdlTokenStream tokens,
                                         List<AstNode> privileges ) throws ParsingException {
        // privilege-types
        //
        // ALL PRIVILEGES | privilege-list
        //
        // privilege-list
        //
        // table-privilege {, table-privilege }*
        //
        // table-privilege
        // DELETE |
        // INSERT |
        // REFERENCES [column list] |
        // SELECT [column list] |
        // TRIGGER |
        // UPDATE [column list]
        // column list
        // ( column-identifier {, column-identifier}* )

        do {
            AstNode node = null;

            if (tokens.canConsume(DELETE)) {
                node = nodeFactory().node("privilege");
                node.setProperty(TYPE, DELETE);
            } else if (tokens.canConsume(INSERT)) {
                node = nodeFactory().node("privilege");
                node.setProperty(TYPE, INSERT);
            } else if (tokens.canConsume("REFERENCES")) {
                node = nodeFactory().node("privilege");
                node.setProperty(TYPE, "REFERENCES");
                parseColumnNameList(tokens, node, TYPE_COLUMN_REFERENCE);
            } else if (tokens.canConsume(SELECT)) {
                node = nodeFactory().node("privilege");
                node.setProperty(TYPE, SELECT);
                parseColumnNameList(tokens, node, TYPE_COLUMN_REFERENCE);
            } else if (tokens.canConsume("TRIGGER")) {
                node = nodeFactory().node("privilege");
                node.setProperty(TYPE, "TRIGGER");
            } else if (tokens.canConsume(UPDATE)) {
                node = nodeFactory().node("privilege");
                node.setProperty(TYPE, UPDATE);
                parseColumnNameList(tokens, node, TYPE_COLUMN_REFERENCE);
            }
            if (node == null) {
                break;
            }
            nodeFactory().setType(node, GRANT_PRIVILEGE);
            privileges.add(node);

        } while (tokens.canConsume(COMMA));

    }

    /**
     * {@inheritDoc}
     * 
     * @see org.modeshape.sequencer.ddl.StandardDdlParser#parseAlterTableStatement(org.modeshape.sequencer.ddl.DdlTokenStream,
     *      org.modeshape.sequencer.ddl.node.AstNode)
     */
    @Override
    protected AstNode parseAlterTableStatement( DdlTokenStream tokens,
                                                AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        markStartOfStatement(tokens);

        // ALTER TABLE table-Name
        // {
        // ADD COLUMN column-definition |
        // ADD CONSTRAINT clause |
        // DROP [ COLUMN ] column-name [ CASCADE | RESTRICT ] |
        // DROP { PRIMARY KEY | FOREIGN KEY constraint-name | UNIQUE constraint-name | CHECK constraint-name | CONSTRAINT
        // constraint-name } |
        // ALTER [ COLUMN ] column-alteration |
        // LOCKSIZE { ROW | TABLE }
        // }

        tokens.consume(ALTER, TABLE); // consumes 'ALTER TABLE'
        String tableName = parseName(tokens);

        AstNode alterTableNode = nodeFactory().node(tableName, parentNode, TYPE_ALTER_TABLE_STATEMENT);

        // System.out.println("  >> PARSIN ALTER STATEMENT >>  TABLE Name = " + tableName);

        if (tokens.canConsume("ADD")) {
            if (isTableConstraint(tokens)) {
                parseTableConstraint(tokens, alterTableNode, true);
            } else {
                // This segment can also be enclosed in "()" brackets to handle multiple ColumnDefinition ADDs
                if (tokens.matches(L_PAREN)) {
                    parseColumns(tokens, alterTableNode, true);
                } else {
                    parseSingleTerminatedColumnDefinition(tokens, alterTableNode, true);
                }
            }

        } else if (tokens.canConsume("DROP")) {
            // DROP { PRIMARY KEY | FOREIGN KEY constraint-name | UNIQUE constraint-name | CHECK constraint-name | CONSTRAINT
            // constraint-name }
            if (tokens.canConsume("PRIMARY", "KEY")) {
                String name = parseName(tokens); // constraint name
                nodeFactory().node(name, alterTableNode, TYPE_DROP_TABLE_CONSTRAINT_DEFINITION);
            } else if (tokens.canConsume("FOREIGN", "KEY")) {
                String name = parseName(tokens); // constraint name
                nodeFactory().node(name, alterTableNode, TYPE_DROP_TABLE_CONSTRAINT_DEFINITION);
            } else if (tokens.canConsume("UNIQUE")) {
                String name = parseName(tokens); // constraint name
                nodeFactory().node(name, alterTableNode, TYPE_DROP_TABLE_CONSTRAINT_DEFINITION);
            } else if (tokens.canConsume("CHECK")) {
                String name = parseName(tokens); // constraint name
                nodeFactory().node(name, alterTableNode, TYPE_DROP_TABLE_CONSTRAINT_DEFINITION);
            } else if (tokens.canConsume("CONSTRAINT")) {
                String name = parseName(tokens); // constraint name
                nodeFactory().node(name, alterTableNode, TYPE_DROP_TABLE_CONSTRAINT_DEFINITION);
            } else {
                // DROP [ COLUMN ] column-name [ CASCADE | RESTRICT ]
                tokens.canConsume("COLUMN"); // "COLUMN" is optional

                String columnName = parseName(tokens);

                AstNode columnNode = nodeFactory().node(columnName, alterTableNode, TYPE_DROP_COLUMN_DEFINITION);

                if (tokens.canConsume(DropBehavior.CASCADE)) {
                    columnNode.setProperty(StandardDdlLexicon.DROP_BEHAVIOR, DropBehavior.CASCADE);
                } else if (tokens.canConsume(DropBehavior.RESTRICT)) {
                    columnNode.setProperty(StandardDdlLexicon.DROP_BEHAVIOR, DropBehavior.RESTRICT);
                }
            }
        } else if (tokens.canConsume("ALTER")) {
            // column-alteration
            //
            // ALTER [ COLUMN ] column-Name SET DATA TYPE VARCHAR(integer) |
            // ALTER [ COLUMN ] column-Name SET DATA TYPE VARCHAR FOR BIT DATA(integer) |
            // ALTER [ COLUMN ] column-name SET INCREMENT BY integer-constant |
            // ALTER [ COLUMN ] column-name RESTART WITH integer-constant |
            // ALTER [ COLUMN ] column-name [ NOT ] NULL |
            // ALTER [ COLUMN ] column-name [ WITH | SET ] DEFAULT default-value |
            // ALTER [ COLUMN ] column-name DROP DEFAULT

            tokens.canConsume("COLUMN");
            String alterColumnName = parseName(tokens);

            AstNode columnNode = nodeFactory().node(alterColumnName, alterTableNode, TYPE_ALTER_COLUMN_DEFINITION);

            if (tokens.matches("DEFAULT")) {
                parseDefaultClause(tokens, columnNode);
            } else if (tokens.canConsume("SET")) {
                if (tokens.canConsume("DATA", "TYPE")) {
                    DataType datatype = getDatatypeParser().parse(tokens);

                    columnNode.setProperty(StandardDdlLexicon.DATATYPE_NAME, datatype.getName());
                    if (datatype.getLength() >= 0) {
                        columnNode.setProperty(StandardDdlLexicon.DATATYPE_LENGTH, datatype.getLength());
                    }
                    if (datatype.getPrecision() >= 0) {
                        columnNode.setProperty(StandardDdlLexicon.DATATYPE_PRECISION, datatype.getPrecision());
                    }
                    if (datatype.getScale() >= 0) {
                        columnNode.setProperty(StandardDdlLexicon.DATATYPE_SCALE, datatype.getScale());
                    }

                } else if (tokens.canConsume("INCREMENT")) {
                    tokens.consume("BY", TokenStream.ANY_VALUE);
                }
                if (tokens.matches("DEFAULT")) {
                    parseDefaultClause(tokens, columnNode);
                }
            } else if (tokens.canConsume("WITH")) {
                parseDefaultClause(tokens, columnNode);
            } else {
                tokens.canConsume("RESTART", "WITH", TokenStream.ANY_VALUE);
                tokens.canConsume("DROP", "DEFAULT");

                if (tokens.canConsume("NOT", "NULL")) {
                    columnNode.setProperty(StandardDdlLexicon.NULLABLE, "NOT NULL");
                } else if (tokens.canConsume("NULL")) {
                    columnNode.setProperty(StandardDdlLexicon.NULLABLE, "NULL");
                }
            }

        } else if (tokens.canConsume("LOCKSIZE")) {
            tokens.canConsume("ROWS");
            tokens.canConsume("TABLE");
        }

        markEndOfStatement(tokens, alterTableNode);

        return alterTableNode;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.modeshape.sequencer.ddl.StandardDdlParser#parseColumnDefinition(org.modeshape.sequencer.ddl.DdlTokenStream,
     *      org.modeshape.sequencer.ddl.node.AstNode, boolean)
     */
    @Override
    protected void parseColumnDefinition( DdlTokenStream tokens,
                                          AstNode tableNode,
                                          boolean isAlterTable ) throws ParsingException {
        // column-definition
        //
        // Simple-column-Name DataType
        // [ ColumnDefinition-level-constraint ]*
        // [ [ WITH ] DEFAULT { ConstantExpression | NULL } |generated-column-spec ]
        // [ ColumnDefinition-level-constraint ]*

        // generated-column-spec
        //
        // [ GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( START WITH IntegerConstant [ ,INCREMENT BY IntegerConstant] ) ] ] ]

        // EXAMPLE COLUMNS
        // (i INT GENERATED BY DEFAULT AS IDENTITY (START WITH 2, INCREMENT BY 1),
        // ch CHAR(50));

        tokens.canConsume("COLUMN"); // FOR ALTER TABLE ADD [COLUMN] case
        String columnName = parseName(tokens);
        DataType datatype = getDatatypeParser().parse(tokens);

        AstNode columnNode = nodeFactory().node(columnName, tableNode, TYPE_COLUMN_DEFINITION);

        columnNode.setProperty(StandardDdlLexicon.DATATYPE_NAME, datatype.getName());
        if (datatype.getLength() >= 0) {
            columnNode.setProperty(StandardDdlLexicon.DATATYPE_LENGTH, datatype.getLength());
        }
        if (datatype.getPrecision() >= 0) {
            columnNode.setProperty(StandardDdlLexicon.DATATYPE_PRECISION, datatype.getPrecision());
        }
        if (datatype.getScale() >= 0) {
            columnNode.setProperty(StandardDdlLexicon.DATATYPE_SCALE, datatype.getScale());
        }

        // Now clauses and constraints can be defined in any order, so we need to keep parsing until we get to a comma
        // Now clauses and constraints can be defined in any order, so we need to keep parsing until we get to a comma
        StringBuilder unusedTokensSB = new StringBuilder();

        while (tokens.hasNext() && !tokens.matches(COMMA)) {
            boolean parsedDefaultClause = parseDefaultClause(tokens, columnNode);
            if (!parsedDefaultClause) {
                boolean parsedCollate = parseCollateClause(tokens, columnNode);
                boolean parsedConstraint = parseColumnConstraint(tokens, columnNode, isAlterTable);
                boolean parsedGeneratedColumn = parseGeneratedColumnSpecClause(tokens, columnNode);
                if (!parsedCollate && !parsedConstraint && !parsedGeneratedColumn) {
                    // THIS IS AN ERROR. NOTHING FOUND.
                    // NEED TO absorb tokens
                    unusedTokensSB.append(SPACE).append(tokens.consume());
                }
            }
            tokens.canConsume(DdlTokenizer.COMMENT);
        }

        if (unusedTokensSB.length() > 0) {
            String msg = DdlSequencerI18n.unusedTokensParsingColumnDefinition.text(tableNode.getName());
            DdlParserProblem problem = new DdlParserProblem(Problems.WARNING, getCurrentMarkedPosition(), msg);
            problem.setUnusedSource(unusedTokensSB.toString());
            addProblem(problem, tableNode);
        }

    }

    /**
     * Utility method designed to parse columns within an ALTER TABLE ADD statement.
     * 
     * @param tokens the tokenized {@link DdlTokenStream} of the DDL input content; may not be null
     * @param tableNode
     * @param isAlterTable
     * @throws ParsingException
     */
    protected void parseColumns( DdlTokenStream tokens,
                                 AstNode tableNode,
                                 boolean isAlterTable ) throws ParsingException {
        String tableElementString = getTableElementsString(tokens, false);

        DdlTokenStream localTokens = new DdlTokenStream(tableElementString, DdlTokenStream.ddlTokenizer(false), false);

        localTokens.start();

        StringBuilder unusedTokensSB = new StringBuilder();

        do {
            if (isColumnDefinitionStart(localTokens)) {
                parseColumnDefinition(localTokens, tableNode, isAlterTable);
            } else {
                // THIS IS AN ERROR. NOTHING FOUND.
                // NEED TO absorb tokens
                unusedTokensSB.append(SPACE).append(localTokens.consume());
            }
        } while (localTokens.canConsume(COMMA));

        if (unusedTokensSB.length() > 0) {
            String msg = DdlSequencerI18n.unusedTokensParsingColumnDefinition.text(tableNode.getName());
            DdlParserProblem problem = new DdlParserProblem(Problems.WARNING, getCurrentMarkedPosition(), msg);
            problem.setUnusedSource(unusedTokensSB.toString());
            addProblem(problem, tableNode);
        }
    }

    private boolean parseGeneratedColumnSpecClause( DdlTokenStream tokens,
                                                    AstNode columnNode ) throws ParsingException {
        assert tokens != null;
        assert columnNode != null;
        // generated-column-spec
        //
        // [ GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( START WITH IntegerConstant [ ,INCREMENT BY IntegerConstant] ) ] ] ]
        if (tokens.canConsume("GENERATED")) {
            StringBuilder sb = new StringBuilder("GENERATED");

            if (tokens.canConsume("ALWAYS")) {
                sb.append(SPACE).append("ALWAYS");
            } else {
                tokens.consume("BY", "DEFAULT");
                sb.append(SPACE).append("BY DEFAULT");
            }

            tokens.consume("AS", "IDENTITY");
            sb.append(SPACE).append("AS IDENTITY");

            if (tokens.canConsume(L_PAREN, "START", "WITH")) {
                String value = tokens.consume(); // integer constant
                sb.append(SPACE).append(L_PAREN).append(SPACE).append("START WITH").append(SPACE).append(value);
                if (tokens.canConsume(COMMA, "INCREMENT", "BY")) {
                    value = tokens.consume();// integer constant
                    sb.append(COMMA).append("INCREMENT BY").append(SPACE).append(value);
                }
                tokens.consume(R_PAREN);
                sb.append(SPACE).append(R_PAREN);
            }
            AstNode propNode = nodeFactory().node(COLUMN_ATTRIBUTE, columnNode, TYPE_SIMPLE_PROPERTY);
            propNode.setProperty(PROPERTY_VALUE, sb.toString());

            return true;
        }

        return false;
    }

    private AstNode parseDeclareGlobalTempTable( DdlTokenStream tokens,
                                                 AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        markStartOfStatement(tokens);

        // DECLARE GLOBAL TEMPORARY TABLE table-Name
        // { column-definition [ , column-definition ] * }
        // [ ON COMMIT {DELETE | PRESERVE} ROWS ]
        // NOT LOGGED [ON ROLLBACK DELETE ROWS]

        tokens.consume(STMT_DECLARE_GLOBAL_TEMP_TABLE);
        String name = parseName(tokens);

        AstNode node = nodeFactory().node(name, parentNode, TYPE_DECLARE_GLOBAL_TEMPORARY_TABLE_STATEMENT);

        parseColumnsAndConstraints(tokens, node);

        if (tokens.canConsume("ON", "COMMIT")) {
            AstNode optionNode = nodeFactory().node("onCommit", node, TYPE_STATEMENT_OPTION);
            if (tokens.canConsume("DELETE", "ROWS")) {
                optionNode.setProperty(VALUE, "ON COMMIT DELETE ROWS");
            } else {
                tokens.consume("PRESERVE", "ROWS");
                optionNode.setProperty(VALUE, "ON COMMIT PRESERVE ROWS");
            }
        }
        tokens.consume("NOT", "LOGGED");

        if (tokens.canConsume("ON", "ROLLBACK", "DELETE", "ROWS")) {
            AstNode optionNode = nodeFactory().node("onRollback", node, TYPE_STATEMENT_OPTION);
            optionNode.setProperty(VALUE, "ON ROLLBACK DELETE ROWS");
        }

        markEndOfStatement(tokens, node);

        return node;
    }

    private AstNode parseLockTable( DdlTokenStream tokens,
                                    AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        markStartOfStatement(tokens);

        // LOCK TABLE table-Name IN { SHARE | EXCLUSIVE } MODE;

        tokens.consume(STMT_LOCK_TABLE);

        String name = parseName(tokens);

        AstNode node = nodeFactory().node(name, parentNode, TYPE_LOCK_TABLE_STATEMENT);

        tokens.consume("IN");

        if (tokens.canConsume("SHARE")) {
            AstNode propNode = nodeFactory().node("lockMode", node, TYPE_STATEMENT_OPTION);
            propNode.setProperty(VALUE, "SHARE");
        } else {
            tokens.consume("EXCLUSIVE");
            AstNode propNode = nodeFactory().node("lockMode", node, TYPE_STATEMENT_OPTION);
            propNode.setProperty(VALUE, "EXCLUSIVE");
        }
        tokens.consume("MODE");

        markEndOfStatement(tokens, node);

        return node;
    }

    private AstNode parseRenameTable( DdlTokenStream tokens,
                                      AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        markStartOfStatement(tokens);

        // RENAME TABLE SAMP.EMP_ACT TO EMPLOYEE_ACT;

        tokens.consume(STMT_RENAME_TABLE);

        String oldName = parseName(tokens);

        AstNode node = nodeFactory().node(oldName, parentNode, TYPE_RENAME_TABLE_STATEMENT);

        tokens.consume("TO");

        String newName = parseName(tokens);

        node.setProperty(NEW_NAME, newName);

        markEndOfStatement(tokens, node);

        return node;
    }

    private AstNode parseRenameIndex( DdlTokenStream tokens,
                                      AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        markStartOfStatement(tokens);

        // RENAME TABLE SAMP.EMP_ACT TO EMPLOYEE_ACT;

        tokens.consume(STMT_RENAME_INDEX);

        String oldName = parseName(tokens);

        AstNode node = nodeFactory().node(oldName, parentNode, TYPE_RENAME_INDEX_STATEMENT);

        tokens.consume("TO");

        String newName = parseName(tokens);

        node.setProperty(NEW_NAME, newName);

        markEndOfStatement(tokens, node);

        return node;
    }

    private AstNode parseCreateSynonym( DdlTokenStream tokens,
                                        AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        markStartOfStatement(tokens);
        // CREATE SYNONYM synonym-Name FOR { view-Name | table-Name }

        tokens.consume(STMT_CREATE_SYNONYM);

        String name = parseName(tokens);

        AstNode node = nodeFactory().node(name, parentNode, TYPE_CREATE_SYNONYM_STATEMENT);

        tokens.consume("FOR");

        String tableOrViewName = parseName(tokens);

        node.setProperty(TABLE_NAME, tableOrViewName);

        markEndOfStatement(tokens, node);

        return node;
    }

    private AstNode parseCreateTrigger( DdlTokenStream tokens,
                                        AstNode parentNode ) throws ParsingException {
        assert tokens != null;
        assert parentNode != null;

        markStartOfStatement(tokens);
        // CREATE TRIGGER TriggerName
        // { AFTER | NO CASCADE BEFORE }
        // { INSERT | DELETE | UPDATE [ OF column-Name [, column-Name]* ] }
        // ON table-Name
        // [ ReferencingClause ]
        // [ FOR EACH { ROW | STATEMENT } ] [ MODE DB2SQL ]
        // Triggered-SQL-statement

        // ReferencingClause
        // REFERENCING
        // {
        // { OLD | NEW } [ ROW ] [ AS ] correlation-Name [ { OLD | NEW } [ ROW ] [ AS ] correlation-Name ] |
        // { OLD TABLE | NEW TABLE } [ AS ] Identifier [ { OLD TABLE | NEW TABLE } [AS] Identifier ] |
        // { OLD_TABLE | NEW_TABLE } [ AS ] Identifier [ { OLD_TABLE | NEW_TABLE } [AS] Identifier ]
        // }

        // EXAMPLE:
        // CREATE TRIGGER t1 NO CASCADE BEFORE UPDATE ON x
        // FOR EACH ROW MODE DB2SQL
        // values app.notifyEmail('Jerry', 'Table x is about to be updated');

        tokens.consume(STMT_CREATE_TRIGGER);

        String name = parseName(tokens);

        AstNode node = nodeFactory().node(name, parentNode, TYPE_CREATE_TRIGGER_STATEMENT);

        String type = null;

        if (tokens.canConsume("AFTER")) {
            AstNode optionNode = nodeFactory().node("beforeOrAfter", node, TYPE_STATEMENT_OPTION);
            optionNode.setProperty(VALUE, "AFTER");
        } else {
            tokens.consume("NO", "CASCADE", "BEFORE");
            AstNode optionNode = nodeFactory().node("beforeOrAfter", node, TYPE_STATEMENT_OPTION);
            optionNode.setProperty(VALUE, "NO CASCADE BEFORE");
        }

        if (tokens.canConsume(INSERT)) {
            AstNode optionNode = nodeFactory().node("eventType", node, TYPE_STATEMENT_OPTION);
            optionNode.setProperty(VALUE, INSERT);
            type = INSERT;
        } else if (tokens.canConsume(DELETE)) {
            AstNode optionNode = nodeFactory().node("eventType", node, TYPE_STATEMENT_OPTION);
            optionNode.setProperty(VALUE, DELETE);
            type = DELETE;
        } else {
            tokens.consume(UPDATE);
            AstNode optionNode = nodeFactory().node("eventType", node, TYPE_STATEMENT_OPTION);
            optionNode.setProperty(VALUE, UPDATE);
            type = UPDATE;
        }

        if (tokens.canConsume("OF")) {
            // Parse comma separated column names
            String colName = parseName(tokens);
            nodeFactory().node(colName, node, TYPE_COLUMN_REFERENCE);

            while (tokens.canConsume(COMMA)) {
                colName = parseName(tokens);
                nodeFactory().node(colName, node, TYPE_COLUMN_REFERENCE);
            }
        }
        tokens.consume("ON");

        String tableName = parseName(tokens);

        node.setProperty(TABLE_NAME, tableName);

        if (tokens.canConsume("REFERENCING")) {
            // ReferencingClause
            // REFERENCING
            // {
            // { OLD | NEW } [ ROW ] [ AS ] correlation-Name [ { OLD | NEW } [ ROW ] [ AS ] correlation-Name ] |
            // { OLD TABLE | NEW TABLE } [ AS ] Identifier [ { OLD TABLE | NEW TABLE } [AS] Identifier ] |
            // { OLD_TABLE | NEW_TABLE } [ AS ] Identifier [ { OLD_TABLE | NEW_TABLE } [AS] Identifier ]
            // }

            StringBuilder sb = new StringBuilder();
            if (tokens.matchesAnyOf("OLD", "NEW")) {
                if (tokens.canConsume("OLD")) {
                    sb.append("OLD");
                } else {
                    tokens.consume("NEW");
                    sb.append("NEW");
                }
                if (tokens.canConsume("ROW")) {
                    sb.append(SPACE).append("ROW");
                }
                if (tokens.canConsume("AS")) {
                    sb.append(SPACE).append("AS");
                }
                if (tokens.matchesAnyOf("OLD", "NEW")) {
                    if (tokens.canConsume("OLD")) {
                        sb.append(SPACE).append("OLD");
                    } else {
                        tokens.consume("NEW");
                        sb.append(SPACE).append("NEW");
                    }

                    if (tokens.canConsume("ROW")) {
                        sb.append(SPACE).append("ROW");
                    }
                    if (tokens.canConsume("AS")) {
                        sb.append(SPACE).append("AS");
                    }
                    if (!tokens.matchesAnyOf("FOR", "MODE", type)) {
                        String corrName = parseName(tokens);
                        sb.append(SPACE).append(corrName);
                    }
                } else {
                    String corrName = parseName(tokens);
                    sb.append(SPACE).append(corrName);

                    if (tokens.matchesAnyOf("OLD", "NEW")) {
                        if (tokens.canConsume("OLD")) {
                            sb.append(SPACE).append("OLD");
                        } else {
                            tokens.consume("NEW");
                            sb.append(SPACE).append("NEW");
                        }

                        if (tokens.canConsume("ROW")) {
                            sb.append(SPACE).append("ROW");
                        }
                        if (tokens.canConsume("AS")) {
                            sb.append(SPACE).append("AS");
                        }
                        if (!tokens.matchesAnyOf("FOR", "MODE", type)) {
                            corrName = parseName(tokens);
                            sb.append(SPACE).append(corrName);
                        }
                    }
                }
            }
        }
        // [ FOR EACH { ROW | STATEMENT } ] [ MODE DB2SQL ]
        if (tokens.canConsume("FOR", "EACH")) {
            if (tokens.canConsume("ROW")) {
                AstNode optionNode = nodeFactory().node("forEach", node, TYPE_STATEMENT_OPTION);
                optionNode.setProperty(VALUE, "FOR EACH ROW");
            } else {
                tokens.consume("STATEMENT");
                AstNode optionNode = nodeFactory().node("forEach", node, TYPE_STATEMENT_OPTION);
                optionNode.setProperty(VALUE, "FOR EACH STATEMENT");
            }
        }
        if (tokens.canConsume("MODE")) {
            tokens.consume("DB2SQL");
            AstNode optionNode = nodeFactory().node("mode", node, TYPE_STATEMENT_OPTION);
            optionNode.setProperty(VALUE, "MODE DB2SQL");
        }

        String sql = parseUntilTerminatorIgnoreEmbeddedStatements(tokens);
        node.setProperty(SQL, sql);

        markEndOfStatement(tokens, node);

        return node;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.modeshape.sequencer.ddl.StandardDdlParser#getDataTypeStartWords()
     */
    @Override
    protected List<String> getCustomDataTypeStartWords() {
        return DerbyDataTypes.CUSTOM_DATATYPE_START_WORDS;
    }

    class DerbyDataTypeParser extends DataTypeParser {

        /**
         * {@inheritDoc}
         * 
         * @see org.modeshape.sequencer.ddl.datatype.DataTypeParser#isCustomDataType(org.modeshape.sequencer.ddl.DdlTokenStream)
         */
        @Override
        protected boolean isCustomDataType( DdlTokenStream tokens ) throws ParsingException {
            // Loop through the registered statement start string arrays and look for exact matches.

            for (String[] stmts : derbyDataTypeStrings) {
                if (tokens.matches(stmts)) return true;
            }
            return super.isCustomDataType(tokens);
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.modeshape.sequencer.ddl.datatype.DataTypeParser#parseApproxNumericType(org.modeshape.sequencer.ddl.DdlTokenStream)
         */
        @Override
        protected DataType parseApproxNumericType( DdlTokenStream tokens ) throws ParsingException {
            return super.parseApproxNumericType(tokens);
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.modeshape.sequencer.ddl.datatype.DataTypeParser#parseBitStringType(org.modeshape.sequencer.ddl.DdlTokenStream)
         */
        @Override
        protected DataType parseBitStringType( DdlTokenStream tokens ) throws ParsingException {
            return super.parseBitStringType(tokens);
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.modeshape.sequencer.ddl.datatype.DataTypeParser#parseCharStringType(org.modeshape.sequencer.ddl.DdlTokenStream)
         */
        @Override
        protected DataType parseCharStringType( DdlTokenStream tokens ) throws ParsingException {
            DataType result = super.parseCharStringType(tokens);

            canConsume(tokens, result, true, "FOR", "BIT", "DATA");

            return result;
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.modeshape.sequencer.ddl.datatype.DataTypeParser#parseCustomType(org.modeshape.sequencer.ddl.DdlTokenStream)
         */
        @Override
        protected DataType parseCustomType( DdlTokenStream tokens ) throws ParsingException {
            DataType dataType = null;
            String typeName = null;
            long length = 0;

            if (tokens.matches(DerbyDataTypes.DTYPE_BINARY_LARGE_OBJECT)
                || tokens.matches(DerbyDataTypes.DTYPE_CHARACTER_LARGE_OBJECT)) {
                dataType = new DataType();
                typeName = consume(tokens, dataType, true) + SPACE + consume(tokens, dataType, true) + SPACE
                           + consume(tokens, dataType, true);
                if (canConsume(tokens, dataType, true, L_PAREN)) {
                    String lengthValue = consume(tokens, dataType, false);
                    length = parseLong(lengthValue);
                    consume(tokens, dataType, true, R_PAREN);
                }
                dataType.setName(typeName);
                dataType.setLength(length);
            } else if (tokens.matches(DerbyDataTypes.DTYPE_CLOB) || tokens.matches(DerbyDataTypes.DTYPE_BLOB)) {
                dataType = new DataType();
                typeName = consume(tokens, dataType, true);
                if (canConsume(tokens, dataType, true, L_PAREN)) {
                    String lengthValue = consume(tokens, dataType, false);
                    length = parseLong(lengthValue);
                    consume(tokens, dataType, true, R_PAREN);
                }
                dataType.setName(typeName);
                dataType.setLength(length);
            } else if (tokens.matches(DerbyDataTypes.DTYPE_BIGINT)) {
                dataType = new DataType();
                typeName = consume(tokens, dataType, true);
                dataType.setName(typeName);
            } else if (tokens.matches(DerbyDataTypes.DTYPE_LONG_VARCHAR_FBD)) {
                dataType = new DataType();
                typeName = consume(tokens, dataType, true) + SPACE + consume(tokens, dataType, true) + SPACE
                           + consume(tokens, dataType, true) + SPACE + consume(tokens, dataType, true) + SPACE
                           + consume(tokens, dataType, true);
                dataType.setName(typeName);
            } else if (tokens.matches(DerbyDataTypes.DTYPE_LONG_VARCHAR)) {
                dataType = new DataType();
                typeName = consume(tokens, dataType, true) + SPACE + consume(tokens, dataType, true);
                typeName = consume(tokens, dataType, true);
                dataType.setName(typeName);
            } else if (tokens.matches(DerbyDataTypes.DTYPE_DOUBLE)) {
                dataType = new DataType();
                typeName = consume(tokens, dataType, true);
                dataType.setName(typeName);
            } else if (tokens.matches(DerbyDataTypes.DTYPE_XML)) {
                dataType = new DataType();
                typeName = consume(tokens, dataType, true);
                dataType.setName(typeName);
            }

            if (dataType == null) {
                super.parseCustomType(tokens);
            }
            return dataType;
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.modeshape.sequencer.ddl.datatype.DataTypeParser#parseDateTimeType(org.modeshape.sequencer.ddl.DdlTokenStream)
         */
        @Override
        protected DataType parseDateTimeType( DdlTokenStream tokens ) throws ParsingException {
            return super.parseDateTimeType(tokens);
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.modeshape.sequencer.ddl.datatype.DataTypeParser#parseExactNumericType(org.modeshape.sequencer.ddl.DdlTokenStream)
         */
        @Override
        protected DataType parseExactNumericType( DdlTokenStream tokens ) throws ParsingException {
            return super.parseExactNumericType(tokens);
        }

    }

}
