/*
 * Copyright 2004-2023 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (https://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 *
 * Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
 * Support for the operator "&&" as an alias for SPATIAL_INTERSECTS
 */
package org.h2.command;

import static org.h2.command.Token.ASTERISK;
import static org.h2.command.Token.AT;
import static org.h2.command.Token.BIGGER;
import static org.h2.command.Token.BIGGER_EQUAL;
import static org.h2.command.Token.CLOSE_BRACE;
import static org.h2.command.Token.CLOSE_BRACKET;
import static org.h2.command.Token.CLOSE_PAREN;
import static org.h2.command.Token.COLON;
import static org.h2.command.Token.COLON_COLON;
import static org.h2.command.Token.COLON_EQ;
import static org.h2.command.Token.COMMA;
import static org.h2.command.Token.CONCATENATION;
import static org.h2.command.Token.DOT;
import static org.h2.command.Token.END_OF_INPUT;
import static org.h2.command.Token.EQUAL;
import static org.h2.command.Token.LITERAL;
import static org.h2.command.Token.MINUS_SIGN;
import static org.h2.command.Token.NOT_EQUAL;
import static org.h2.command.Token.NOT_TILDE;
import static org.h2.command.Token.OPEN_BRACE;
import static org.h2.command.Token.OPEN_BRACKET;
import static org.h2.command.Token.OPEN_PAREN;
import static org.h2.command.Token.PARAMETER;
import static org.h2.command.Token.PERCENT;
import static org.h2.command.Token.PLUS_SIGN;
import static org.h2.command.Token.SEMICOLON;
import static org.h2.command.Token.SLASH;
import static org.h2.command.Token.SMALLER;
import static org.h2.command.Token.SMALLER_EQUAL;
import static org.h2.command.Token.SPATIAL_INTERSECTS;
import static org.h2.command.Token.TILDE;
import static org.h2.command.Token.TOKENS;
import static org.h2.util.ParserUtil.ALL;
import static org.h2.util.ParserUtil.AND;
import static org.h2.util.ParserUtil.ANY;
import static org.h2.util.ParserUtil.ARRAY;
import static org.h2.util.ParserUtil.AS;
import static org.h2.util.ParserUtil.ASYMMETRIC;
import static org.h2.util.ParserUtil.AUTHORIZATION;
import static org.h2.util.ParserUtil.BETWEEN;
import static org.h2.util.ParserUtil.CASE;
import static org.h2.util.ParserUtil.CAST;
import static org.h2.util.ParserUtil.CHECK;
import static org.h2.util.ParserUtil.CONSTRAINT;
import static org.h2.util.ParserUtil.CROSS;
import static org.h2.util.ParserUtil.CURRENT_CATALOG;
import static org.h2.util.ParserUtil.CURRENT_DATE;
import static org.h2.util.ParserUtil.CURRENT_PATH;
import static org.h2.util.ParserUtil.CURRENT_ROLE;
import static org.h2.util.ParserUtil.CURRENT_SCHEMA;
import static org.h2.util.ParserUtil.CURRENT_TIME;
import static org.h2.util.ParserUtil.CURRENT_TIMESTAMP;
import static org.h2.util.ParserUtil.CURRENT_USER;
import static org.h2.util.ParserUtil.DAY;
import static org.h2.util.ParserUtil.DEFAULT;
import static org.h2.util.ParserUtil.DISTINCT;
import static org.h2.util.ParserUtil.ELSE;
import static org.h2.util.ParserUtil.END;
import static org.h2.util.ParserUtil.EXCEPT;
import static org.h2.util.ParserUtil.EXISTS;
import static org.h2.util.ParserUtil.FALSE;
import static org.h2.util.ParserUtil.FETCH;
import static org.h2.util.ParserUtil.FIRST_KEYWORD;
import static org.h2.util.ParserUtil.FOR;
import static org.h2.util.ParserUtil.FOREIGN;
import static org.h2.util.ParserUtil.FROM;
import static org.h2.util.ParserUtil.FULL;
import static org.h2.util.ParserUtil.GROUP;
import static org.h2.util.ParserUtil.HAVING;
import static org.h2.util.ParserUtil.HOUR;
import static org.h2.util.ParserUtil.IDENTIFIER;
import static org.h2.util.ParserUtil.IF;
import static org.h2.util.ParserUtil.IN;
import static org.h2.util.ParserUtil.INNER;
import static org.h2.util.ParserUtil.INTERSECT;
import static org.h2.util.ParserUtil.INTERVAL;
import static org.h2.util.ParserUtil.IS;
import static org.h2.util.ParserUtil.JOIN;
import static org.h2.util.ParserUtil.KEY;
import static org.h2.util.ParserUtil.LAST_KEYWORD;
import static org.h2.util.ParserUtil.LEFT;
import static org.h2.util.ParserUtil.LIKE;
import static org.h2.util.ParserUtil.LIMIT;
import static org.h2.util.ParserUtil.LOCALTIME;
import static org.h2.util.ParserUtil.LOCALTIMESTAMP;
import static org.h2.util.ParserUtil.MINUS;
import static org.h2.util.ParserUtil.MINUTE;
import static org.h2.util.ParserUtil.MONTH;
import static org.h2.util.ParserUtil.NATURAL;
import static org.h2.util.ParserUtil.NOT;
import static org.h2.util.ParserUtil.NULL;
import static org.h2.util.ParserUtil.OFFSET;
import static org.h2.util.ParserUtil.ON;
import static org.h2.util.ParserUtil.OR;
import static org.h2.util.ParserUtil.ORDER;
import static org.h2.util.ParserUtil.PRIMARY;
import static org.h2.util.ParserUtil.QUALIFY;
import static org.h2.util.ParserUtil.RIGHT;
import static org.h2.util.ParserUtil.ROW;
import static org.h2.util.ParserUtil.ROWNUM;
import static org.h2.util.ParserUtil.SECOND;
import static org.h2.util.ParserUtil.SELECT;
import static org.h2.util.ParserUtil.SESSION_USER;
import static org.h2.util.ParserUtil.SET;
import static org.h2.util.ParserUtil.SOME;
import static org.h2.util.ParserUtil.SYMMETRIC;
import static org.h2.util.ParserUtil.SYSTEM_USER;
import static org.h2.util.ParserUtil.TABLE;
import static org.h2.util.ParserUtil.TO;
import static org.h2.util.ParserUtil.TRUE;
import static org.h2.util.ParserUtil.UNION;
import static org.h2.util.ParserUtil.UNIQUE;
import static org.h2.util.ParserUtil.UNKNOWN;
import static org.h2.util.ParserUtil.USER;
import static org.h2.util.ParserUtil.USING;
import static org.h2.util.ParserUtil.VALUE;
import static org.h2.util.ParserUtil.VALUES;
import static org.h2.util.ParserUtil.WHEN;
import static org.h2.util.ParserUtil.WHERE;
import static org.h2.util.ParserUtil.WINDOW;
import static org.h2.util.ParserUtil.WITH;
import static org.h2.util.ParserUtil.YEAR;
import static org.h2.util.ParserUtil._ROWID_;

import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.StringJoiner;
import java.util.TreeSet;

import org.h2.api.ErrorCode;
import org.h2.api.IntervalQualifier;
import org.h2.api.Trigger;
import org.h2.command.ddl.AlterDomainAddConstraint;
import org.h2.command.ddl.AlterDomainDropConstraint;
import org.h2.command.ddl.AlterDomainExpressions;
import org.h2.command.ddl.AlterDomainRename;
import org.h2.command.ddl.AlterDomainRenameConstraint;
import org.h2.command.ddl.AlterIndexRename;
import org.h2.command.ddl.AlterSchemaRename;
import org.h2.command.ddl.AlterSequence;
import org.h2.command.ddl.AlterTableAddConstraint;
import org.h2.command.ddl.AlterTableAlterColumn;
import org.h2.command.ddl.AlterTableDropConstraint;
import org.h2.command.ddl.AlterTableRename;
import org.h2.command.ddl.AlterTableRenameColumn;
import org.h2.command.ddl.AlterTableRenameConstraint;
import org.h2.command.ddl.AlterUser;
import org.h2.command.ddl.AlterView;
import org.h2.command.ddl.Analyze;
import org.h2.command.ddl.CommandWithColumns;
import org.h2.command.ddl.CreateAggregate;
import org.h2.command.ddl.CreateConstant;
import org.h2.command.ddl.CreateDomain;
import org.h2.command.ddl.CreateFunctionAlias;
import org.h2.command.ddl.CreateIndex;
import org.h2.command.ddl.CreateLinkedTable;
import org.h2.command.ddl.CreateMaterializedView;
import org.h2.command.ddl.CreateRole;
import org.h2.command.ddl.CreateSchema;
import org.h2.command.ddl.CreateSequence;
import org.h2.command.ddl.CreateSynonym;
import org.h2.command.ddl.CreateTable;
import org.h2.command.ddl.CreateTrigger;
import org.h2.command.ddl.CreateUser;
import org.h2.command.ddl.CreateView;
import org.h2.command.ddl.DeallocateProcedure;
import org.h2.command.ddl.DefineCommand;
import org.h2.command.ddl.DropAggregate;
import org.h2.command.ddl.DropConstant;
import org.h2.command.ddl.DropDatabase;
import org.h2.command.ddl.DropDomain;
import org.h2.command.ddl.DropFunctionAlias;
import org.h2.command.ddl.DropIndex;
import org.h2.command.ddl.DropMaterializedView;
import org.h2.command.ddl.DropRole;
import org.h2.command.ddl.DropSchema;
import org.h2.command.ddl.DropSequence;
import org.h2.command.ddl.DropSynonym;
import org.h2.command.ddl.DropTable;
import org.h2.command.ddl.DropTrigger;
import org.h2.command.ddl.DropUser;
import org.h2.command.ddl.DropView;
import org.h2.command.ddl.GrantRevoke;
import org.h2.command.ddl.PrepareProcedure;
import org.h2.command.ddl.RefreshMaterializedView;
import org.h2.command.ddl.SequenceOptions;
import org.h2.command.ddl.SetComment;
import org.h2.command.ddl.TruncateTable;
import org.h2.command.dml.AlterTableSet;
import org.h2.command.dml.BackupCommand;
import org.h2.command.dml.Call;
import org.h2.command.dml.CommandWithValues;
import org.h2.command.dml.DataChangeStatement;
import org.h2.command.dml.Delete;
import org.h2.command.dml.ExecuteImmediate;
import org.h2.command.dml.ExecuteProcedure;
import org.h2.command.dml.Explain;
import org.h2.command.dml.Help;
import org.h2.command.dml.Insert;
import org.h2.command.dml.Merge;
import org.h2.command.dml.MergeUsing;
import org.h2.command.dml.NoOperation;
import org.h2.command.dml.RunScriptCommand;
import org.h2.command.dml.ScriptCommand;
import org.h2.command.dml.Set;
import org.h2.command.dml.SetClauseList;
import org.h2.command.dml.SetSessionCharacteristics;
import org.h2.command.dml.SetTypes;
import org.h2.command.dml.TransactionCommand;
import org.h2.command.dml.Update;
import org.h2.command.query.ForUpdate;
import org.h2.command.query.Query;
import org.h2.command.query.QueryOrderBy;
import org.h2.command.query.Select;
import org.h2.command.query.SelectUnion;
import org.h2.command.query.TableValueConstructor;
import org.h2.constraint.ConstraintActionType;
import org.h2.engine.ConnectionInfo;
import org.h2.engine.Constants;
import org.h2.engine.Database;
import org.h2.engine.DbObject;
import org.h2.engine.DbSettings;
import org.h2.engine.IsolationLevel;
import org.h2.engine.Mode;
import org.h2.engine.Mode.ModeEnum;
import org.h2.engine.NullsDistinct;
import org.h2.engine.Procedure;
import org.h2.engine.Right;
import org.h2.engine.SessionLocal;
import org.h2.engine.User;
import org.h2.expression.Alias;
import org.h2.expression.ArrayConstructorByQuery;
import org.h2.expression.ArrayElementReference;
import org.h2.expression.BinaryOperation;
import org.h2.expression.BinaryOperation.OpType;
import org.h2.expression.ConcatenationOperation;
import org.h2.expression.DomainValueExpression;
import org.h2.expression.Expression;
import org.h2.expression.ExpressionColumn;
import org.h2.expression.ExpressionList;
import org.h2.expression.ExpressionWithFlags;
import org.h2.expression.ExpressionWithVariableParameters;
import org.h2.expression.FieldReference;
import org.h2.expression.Format;
import org.h2.expression.Format.FormatEnum;
import org.h2.expression.Parameter;
import org.h2.expression.Rownum;
import org.h2.expression.SearchedCase;
import org.h2.expression.SequenceValue;
import org.h2.expression.SimpleCase;
import org.h2.expression.Subquery;
import org.h2.expression.TimeZoneOperation;
import org.h2.expression.TypedValueExpression;
import org.h2.expression.UnaryOperation;
import org.h2.expression.ValueExpression;
import org.h2.expression.Variable;
import org.h2.expression.Wildcard;
import org.h2.expression.aggregate.AbstractAggregate;
import org.h2.expression.aggregate.Aggregate;
import org.h2.expression.aggregate.AggregateType;
import org.h2.expression.aggregate.JavaAggregate;
import org.h2.expression.aggregate.ListaggArguments;
import org.h2.expression.analysis.DataAnalysisOperation;
import org.h2.expression.analysis.Window;
import org.h2.expression.analysis.WindowFrame;
import org.h2.expression.analysis.WindowFrameBound;
import org.h2.expression.analysis.WindowFrameBoundType;
import org.h2.expression.analysis.WindowFrameExclusion;
import org.h2.expression.analysis.WindowFrameUnits;
import org.h2.expression.analysis.WindowFunction;
import org.h2.expression.analysis.WindowFunctionType;
import org.h2.expression.condition.BetweenPredicate;
import org.h2.expression.condition.BooleanTest;
import org.h2.expression.condition.CompareLike;
import org.h2.expression.condition.CompareLike.LikeType;
import org.h2.expression.condition.Comparison;
import org.h2.expression.condition.ConditionAndOr;
import org.h2.expression.condition.ConditionAndOrN;
import org.h2.expression.condition.ConditionIn;
import org.h2.expression.condition.ConditionInArray;
import org.h2.expression.condition.ConditionInQuery;
import org.h2.expression.condition.ConditionLocalAndGlobal;
import org.h2.expression.condition.ConditionNot;
import org.h2.expression.condition.ExistsPredicate;
import org.h2.expression.condition.IsJsonPredicate;
import org.h2.expression.condition.NullPredicate;
import org.h2.expression.condition.TypePredicate;
import org.h2.expression.condition.UniquePredicate;
import org.h2.expression.function.ArrayFunction;
import org.h2.expression.function.BitFunction;
import org.h2.expression.function.BuiltinFunctions;
import org.h2.expression.function.CSVWriteFunction;
import org.h2.expression.function.CardinalityExpression;
import org.h2.expression.function.CastSpecification;
import org.h2.expression.function.CoalesceFunction;
import org.h2.expression.function.CompatibilitySequenceValueFunction;
import org.h2.expression.function.CompressFunction;
import org.h2.expression.function.ConcatFunction;
import org.h2.expression.function.CryptFunction;
import org.h2.expression.function.CurrentDateTimeValueFunction;
import org.h2.expression.function.CurrentGeneralValueSpecification;
import org.h2.expression.function.DBObjectFunction;
import org.h2.expression.function.DataTypeSQLFunction;
import org.h2.expression.function.DateTimeFormatFunction;
import org.h2.expression.function.DateTimeFunction;
import org.h2.expression.function.DayMonthNameFunction;
import org.h2.expression.function.FileFunction;
import org.h2.expression.function.HashFunction;
import org.h2.expression.function.JavaFunction;
import org.h2.expression.function.JsonConstructorFunction;
import org.h2.expression.function.LengthFunction;
import org.h2.expression.function.MathFunction;
import org.h2.expression.function.MathFunction1;
import org.h2.expression.function.MathFunction2;
import org.h2.expression.function.NullIfFunction;
import org.h2.expression.function.RandFunction;
import org.h2.expression.function.RegexpFunction;
import org.h2.expression.function.SessionControlFunction;
import org.h2.expression.function.SetFunction;
import org.h2.expression.function.SignalFunction;
import org.h2.expression.function.SoundexFunction;
import org.h2.expression.function.StringFunction;
import org.h2.expression.function.StringFunction1;
import org.h2.expression.function.StringFunction2;
import org.h2.expression.function.SubstringFunction;
import org.h2.expression.function.SysInfoFunction;
import org.h2.expression.function.TableInfoFunction;
import org.h2.expression.function.ToCharFunction;
import org.h2.expression.function.TrimFunction;
import org.h2.expression.function.TruncateValueFunction;
import org.h2.expression.function.XMLFunction;
import org.h2.expression.function.table.ArrayTableFunction;
import org.h2.expression.function.table.CSVReadFunction;
import org.h2.expression.function.table.JavaTableFunction;
import org.h2.expression.function.table.LinkSchemaFunction;
import org.h2.expression.function.table.TableFunction;
import org.h2.index.Index;
import org.h2.message.DbException;
import org.h2.mode.FunctionsPostgreSQL;
import org.h2.mode.ModeFunction;
import org.h2.mode.OnDuplicateKeyValues;
import org.h2.mode.Regclass;
import org.h2.result.SortOrder;
import org.h2.schema.Domain;
import org.h2.schema.FunctionAlias;
import org.h2.schema.Schema;
import org.h2.schema.Sequence;
import org.h2.schema.UserAggregate;
import org.h2.schema.UserDefinedFunction;
import org.h2.table.Column;
import org.h2.table.DataChangeDeltaTable;
import org.h2.table.DataChangeDeltaTable.ResultOption;
import org.h2.table.DualTable;
import org.h2.table.FunctionTable;
import org.h2.table.IndexColumn;
import org.h2.table.IndexHints;
import org.h2.table.MaterializedView;
import org.h2.table.QueryExpressionTable;
import org.h2.table.RangeTable;
import org.h2.table.Table;
import org.h2.table.TableFilter;
import org.h2.table.TableView;
import org.h2.util.HasSQL;
import org.h2.util.IntervalUtils;
import org.h2.util.ParserUtil;
import org.h2.util.StringUtils;
import org.h2.util.Utils;
import org.h2.util.geometry.EWKTUtils;
import org.h2.util.json.JSONItemType;
import org.h2.util.json.JsonConstructorUtils;
import org.h2.value.CompareMode;
import org.h2.value.DataType;
import org.h2.value.ExtTypeInfoEnum;
import org.h2.value.ExtTypeInfoGeometry;
import org.h2.value.ExtTypeInfoNumeric;
import org.h2.value.ExtTypeInfoRow;
import org.h2.value.TypeInfo;
import org.h2.value.Value;
import org.h2.value.ValueArray;
import org.h2.value.ValueBigint;
import org.h2.value.ValueDate;
import org.h2.value.ValueDouble;
import org.h2.value.ValueGeometry;
import org.h2.value.ValueInteger;
import org.h2.value.ValueInterval;
import org.h2.value.ValueJson;
import org.h2.value.ValueNull;
import org.h2.value.ValueNumeric;
import org.h2.value.ValueRow;
import org.h2.value.ValueTime;
import org.h2.value.ValueTimeTimeZone;
import org.h2.value.ValueTimestamp;
import org.h2.value.ValueTimestampTimeZone;
import org.h2.value.ValueUuid;
import org.h2.value.ValueVarchar;

/**
 * The parser is used to convert a SQL statement string to an command object.
 *
 * @author Thomas Mueller
 * @author Noel Grandin
 * @author Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
 */
public class Parser {

    private static final String WITH_STATEMENT_SUPPORTS_LIMITED_SUB_STATEMENTS =
            "WITH statement supports only SELECT, TABLE, VALUES, " +
            "CREATE TABLE, INSERT, UPDATE, MERGE or DELETE statements";

    private final Database database;
    private final SessionLocal session;

    /**
     * @see org.h2.engine.DbSettings#databaseToLower
     */
    private final boolean identifiersToLower;
    /**
     * @see org.h2.engine.DbSettings#databaseToUpper
     */
    private final boolean identifiersToUpper;

    /**
     * @see org.h2.engine.SessionLocal#isVariableBinary()
     */
    private final boolean variableBinary;

    private final BitSet nonKeywords;

    ArrayList<Token> tokens;
    int tokenIndex;
    Token token;
    private int currentTokenType;
    private String currentToken;
    private String sqlCommand;
    private CreateView createView;
    private Prepared currentPrepared;
    private Select currentSelect;
    private List<TableView> cteCleanups;
    private ArrayList<Parameter> parameters;
    private BitSet usedParameters = new BitSet();
    private String schemaName;
    private ArrayList<String> expectedList;
    private boolean rightsChecked;
    private boolean recompileAlways;
    private boolean literalsChecked;
    private int orderInFrom;
    private boolean parseDomainConstraint;

    /**
     * Parses the specified collection of non-keywords.
     *
     * @param nonKeywords array of non-keywords in upper case
     * @return bit set of non-keywords, or {@code null}
     */
    public static BitSet parseNonKeywords(String[] nonKeywords) {
        if (nonKeywords.length == 0) {
            return null;
        }
        BitSet set = new BitSet();
        for (String nonKeyword : nonKeywords) {
            int index = Arrays.binarySearch(TOKENS, FIRST_KEYWORD, LAST_KEYWORD + 1, nonKeyword);
            if (index >= 0) {
                set.set(index);
            }
        }
        return set.isEmpty() ? null : set;
    }

    /**
     * Formats a comma-separated list of keywords.
     *
     * @param nonKeywords bit set of non-keywords, or {@code null}
     * @return comma-separated list of non-keywords
     */
    public static String formatNonKeywords(BitSet nonKeywords) {
        if (nonKeywords == null || nonKeywords.isEmpty()) {
            return "";
        }
        StringBuilder builder = new StringBuilder();
        for (int i = -1; (i = nonKeywords.nextSetBit(i + 1)) >= 0;) {
            if (i >= FIRST_KEYWORD && i <= LAST_KEYWORD) {
                if (builder.length() > 0) {
                    builder.append(',');
                }
                builder.append(TOKENS[i]);
            }
        }
        return builder.toString();
    }

    /**
     * Creates a new instance of parser.
     *
     * @param session the session
     */
    public Parser(SessionLocal session) {
        this.database = session.getDatabase();
        DbSettings settings = database.getSettings();
        this.identifiersToLower = settings.databaseToLower;
        this.identifiersToUpper = settings.databaseToUpper;
        this.variableBinary = session.isVariableBinary();
        this.nonKeywords = session.getNonKeywords();
        this.session = session;
    }

    /**
     * Creates a new instance of parser for special use cases.
     */
    public Parser() {
        database = null;
        identifiersToLower = false;
        identifiersToUpper = false;
        variableBinary = false;
        nonKeywords = null;
        session = null;
    }

    /**
     * Parse the statement and prepare it for execution.
     *
     * @param sql the SQL statement to parse
     * @return the prepared object
     */
    public Prepared prepare(String sql) {
        Prepared p = parse(sql, null);
        p.prepare();
        if (currentTokenType != END_OF_INPUT) {
            throw getSyntaxError();
        }
        return p;
    }

    /**
     * Parse a query and prepare its expressions. Rights and literals must be
     * already checked.
     *
     * @param sql the SQL statement to parse
     * @return the prepared object
     */
    public Query prepareQueryExpression(String sql) {
        Query q = (Query) parse(sql, null);
        q.prepareExpressions();
        if (currentTokenType != END_OF_INPUT) {
            throw getSyntaxError();
        }
        return q;
    }

    /**
     * Parse a statement or a list of statements, and prepare it for execution.
     *
     * @param sql the SQL statement to parse
     * @return the command object
     */
    public Command prepareCommand(String sql) {
        try {
            Prepared p = parse(sql, null);
            if (currentTokenType != SEMICOLON && currentTokenType != END_OF_INPUT) {
                addExpected(SEMICOLON);
                throw getSyntaxError();
            }
            try {
                p.prepare();
            } catch (Throwable t) {
                CommandContainer.clearCTE(session, p);
                throw t;
            }
            int sqlIndex = token.start();
            if (sqlIndex < sql.length()) {
                sql = sql.substring(0, sqlIndex);
            }
            CommandContainer c = new CommandContainer(session, sql, p);
            while (currentTokenType == SEMICOLON) {
                read();
            }
            if (currentTokenType != END_OF_INPUT) {
                int offset = token.start();
                return prepareCommandList(c, p, sql, sqlCommand.substring(offset), getRemainingTokens(offset));
            }
            return c;
        } catch (DbException e) {
            throw e.addSQL(sqlCommand);
        }
    }

    private CommandList prepareCommandList(CommandContainer command, Prepared p, String sql, String remainingSql,
            ArrayList<Token> remainingTokens) {
        try {
            ArrayList<Prepared> list = Utils.newSmallArrayList();
            for (;;) {
                if (p instanceof DefineCommand) {
                    // Next commands may depend on results of this command.
                    return new CommandList(session, sql, command, list, parameters, remainingSql);
                }
                try {
                    p = parse(remainingSql, remainingTokens);
                } catch (DbException ex) {
                    // This command may depend on results of previous commands.
                    if (ex.getErrorCode() == ErrorCode.CANNOT_MIX_INDEXED_AND_UNINDEXED_PARAMS) {
                        throw ex;
                    }
                    return new CommandList(session, sql, command, list, parameters, remainingSql);
                }
                list.add(p);
                if (currentTokenType != SEMICOLON && currentTokenType != END_OF_INPUT) {
                    addExpected(SEMICOLON);
                    throw getSyntaxError();
                }
                while (currentTokenType == SEMICOLON) {
                    read();
                }
                if (currentTokenType == END_OF_INPUT) {
                    break;
                }
                int offset = token.start();
                remainingSql = sqlCommand.substring(offset);
                remainingTokens = getRemainingTokens(offset);
            }
            return new CommandList(session, sql, command, list, parameters, null);
        } catch (Throwable t) {
            command.clearCTE();
            throw t;
        }
    }

    private ArrayList<Token> getRemainingTokens(int offset) {
        List<Token> subList = tokens.subList(tokenIndex, tokens.size());
        ArrayList<Token> remainingTokens = new ArrayList<>(subList);
        subList.clear();
        tokens.add(new Token.EndOfInputToken(offset));
        for (Token token : remainingTokens) {
            token.subtractFromStart(offset);
        }
        return remainingTokens;
    }

    /**
     * Parse the statement, but don't prepare it for execution.
     *
     * @param sql the SQL statement to parse
     * @param tokens tokens, or null
     * @return the prepared object
     */
    Prepared parse(String sql, ArrayList<Token> tokens) {
        initialize(sql, tokens, false);
        Prepared p;
        try {
            // first, try the fast variant
            p = parse(false);
        } catch (DbException e) {
            if (e.getErrorCode() == ErrorCode.SYNTAX_ERROR_1) {
                // now, get the detailed exception
                resetTokenIndex();
                p = parse(true);
            } else {
                throw e.addSQL(sql);
            }
        }
        return p;
    }

    private Prepared parse(boolean withExpectedList) {
        if (withExpectedList) {
            expectedList = new ArrayList<>();
        } else {
            expectedList = null;
        }
        currentSelect = null;
        currentPrepared = null;
        createView = null;
        cteCleanups = null;
        recompileAlways = false;
        usedParameters.clear();
        read();
        Prepared p;
        try {
            p = parsePrepared();
            p.setCteCleanups(cteCleanups);
        } catch (Throwable t) {
            if (cteCleanups != null) {
                CommandContainer.clearCTE(session, cteCleanups);
            }
            throw t;
        }
        p.setPrepareAlways(recompileAlways);
        p.setParameterList(parameters);
        return p;
    }

    private Prepared parsePrepared() {
        int start = tokenIndex;
        Prepared c = null;
        switch (currentTokenType) {
        case END_OF_INPUT:
        case SEMICOLON:
            c = new NoOperation(session);
            setSQL(c, start);
            return c;
        case PARAMETER:
            // read the ? as a parameter
            // this is an 'out' parameter - set a dummy value
            readParameter().setValue(ValueNull.INSTANCE);
            read(EQUAL);
            start = tokenIndex;
            read("CALL");
            c = parseCall();
            break;
        case OPEN_PAREN:
        case SELECT:
        case TABLE:
        case VALUES:
            c = parseQuery();
            break;
        case WITH:
            read();
            c = parseWithStatementOrQuery(start);
            break;
        case SET:
            read();
            c = parseSet();
            break;
        case IDENTIFIER:
            if (token.isQuoted()) {
                break;
            }
            /*
             * Convert a-z to A-Z. This method is safe, because only A-Z
             * characters are considered below.
             *
             * Unquoted identifier is never empty.
             */
            switch (currentToken.charAt(0) & 0xffdf) {
            case 'A':
                if (readIf("ALTER")) {
                    c = parseAlter();
                } else if (readIf("ANALYZE")) {
                    c = parseAnalyze();
                }
                break;
            case 'B':
                if (readIf("BACKUP")) {
                    c = parseBackup();
                } else if (readIf("BEGIN")) {
                    c = parseBegin();
                }
                break;
            case 'C':
                if (readIf("COMMIT")) {
                    c = parseCommit();
                } else if (readIf("CREATE")) {
                    c = parseCreate();
                } else if (readIf("CALL")) {
                    c = parseCall();
                } else if (readIf("CHECKPOINT")) {
                    c = parseCheckpoint();
                } else if (readIf("COMMENT")) {
                    c = parseComment();
                }
                break;
            case 'D':
                if (readIf("DELETE")) {
                    c = parseDelete(start);
                } else if (readIf("DROP")) {
                    c = parseDrop();
                } else if (readIf("DECLARE")) {
                    // support for DECLARE GLOBAL TEMPORARY TABLE...
                    c = parseCreate();
                } else if (database.getMode().getEnum() != ModeEnum.MSSQLServer && readIf("DEALLOCATE")) {
                    /*
                     * PostgreSQL-style DEALLOCATE is disabled in MSSQLServer
                     * mode because PostgreSQL-style EXECUTE is redefined in
                     * this mode.
                     */
                    c = parseDeallocate();
                }
                break;
            case 'E':
                if (readIf("EXPLAIN")) {
                    c = parseExplain();
                } else if (database.getMode().getEnum() != ModeEnum.MSSQLServer) {
                    if (readIf("EXECUTE")) {
                        c = parseExecutePostgre();
                    }
                } else {
                    if (readIf("EXEC") || readIf("EXECUTE")) {
                        c = parseExecuteSQLServer();
                    }
                }
                break;
            case 'G':
                if (readIf("GRANT")) {
                    c = parseGrantRevoke(CommandInterface.GRANT);
                }
                break;
            case 'H':
                if (readIf("HELP")) {
                    c = parseHelp();
                }
                break;
            case 'I':
                if (readIf("INSERT")) {
                    c = parseInsert(start);
                }
                break;
            case 'M':
                if (readIf("MERGE")) {
                    c = parseMerge(start);
                }
                break;
            case 'P':
                if (readIf("PREPARE")) {
                    c = parsePrepare();
                }
                break;
            case 'R':
                if (readIf("ROLLBACK")) {
                    c = parseRollback();
                } else if (readIf("REVOKE")) {
                    c = parseGrantRevoke(CommandInterface.REVOKE);
                } else if (readIf("RUNSCRIPT")) {
                    c = parseRunScript();
                } else if (readIf("RELEASE")) {
                    c = parseReleaseSavepoint();
                } else if (database.getMode().replaceInto && readIf("REPLACE")) {
                    c = parseReplace(start);
                } else if (readIf("REFRESH")) {
                    c = parseRefresh(start);
                }
                break;
            case 'S':
                if (readIf("SAVEPOINT")) {
                    c = parseSavepoint();
                } else if (readIf("SCRIPT")) {
                    c = parseScript();
                } else if (readIf("SHUTDOWN")) {
                    c = parseShutdown();
                } else if (readIf("SHOW")) {
                    c = parseShow();
                }
                break;
            case 'T':
                if (readIf("TRUNCATE")) {
                    c = parseTruncate();
                }
                break;
            case 'U':
                if (readIf("UPDATE")) {
                    c = parseUpdate(start);
                } else if (readIf("USE")) {
                    c = parseUse();
                }
                break;
            }
        }
        if (c == null) {
            throw getSyntaxError();
        }
        boolean withParamValues = readIf(OPEN_BRACE);
        if (withParamValues) {
            do {
                int index = (int) readLong() - 1;
                if (index < 0 || index >= parameters.size()) {
                    throw getSyntaxError();
                }
                Parameter p = parameters.get(index);
                if (p == null) {
                    throw getSyntaxError();
                }
                read(COLON);
                Expression expr = readExpression();
                expr = expr.optimize(session);
                p.setValue(expr.getValue(session));
            } while (readIf(COMMA));
            read(CLOSE_BRACE);
            for (Parameter p : parameters) {
                p.checkSet();
            }
            c.setWithParamValues(true);
        }
        if (withParamValues || c.getSQL() == null) {
            setSQL(c, start);
        }
        return c;
    }

    private DbException getSyntaxError() {
        if (expectedList == null || expectedList.isEmpty()) {
            return DbException.getSyntaxError(sqlCommand, token.start());
        }
        return DbException.getSyntaxError(sqlCommand, token.start(), String.join(", ", expectedList));
    }

    private Prepared parseBackup() {
        BackupCommand command = new BackupCommand(session);
        read(TO);
        command.setFileName(readExpression());
        return command;
    }

    private Prepared parseAnalyze() {
        Analyze command = new Analyze(session);
        if (readIf(TABLE)) {
            Table table = readTableOrView();
            command.setTable(table);
        }
        if (readIf("SAMPLE_SIZE")) {
            command.setTop(readNonNegativeInt());
        }
        return command;
    }

    private TransactionCommand parseBegin() {
        TransactionCommand command;
        if (!readIf("WORK")) {
            readIf("TRANSACTION");
        }
        command = new TransactionCommand(session, CommandInterface.BEGIN);
        return command;
    }

    private TransactionCommand parseCommit() {
        TransactionCommand command;
        if (readIf("TRANSACTION")) {
            command = new TransactionCommand(session, CommandInterface.COMMIT_TRANSACTION);
            command.setTransactionName(readIdentifier());
            return command;
        }
        command = new TransactionCommand(session, CommandInterface.COMMIT);
        readIf("WORK");
        return command;
    }

    private TransactionCommand parseShutdown() {
        int type = CommandInterface.SHUTDOWN;
        if (readIf("IMMEDIATELY")) {
            type = CommandInterface.SHUTDOWN_IMMEDIATELY;
        } else if (readIf("COMPACT")) {
            type = CommandInterface.SHUTDOWN_COMPACT;
        } else if (readIf("DEFRAG")) {
            type = CommandInterface.SHUTDOWN_DEFRAG;
        } else {
            readIf("SCRIPT");
        }
        return new TransactionCommand(session, type);
    }

    private TransactionCommand parseRollback() {
        TransactionCommand command;
        if (readIf("TRANSACTION")) {
            command = new TransactionCommand(session, CommandInterface.ROLLBACK_TRANSACTION);
            command.setTransactionName(readIdentifier());
            return command;
        }
        readIf("WORK");
        if (readIf(TO, "SAVEPOINT")) {
            command = new TransactionCommand(session, CommandInterface.ROLLBACK_TO_SAVEPOINT);
            command.setSavepointName(readIdentifier());
        } else {
            command = new TransactionCommand(session, CommandInterface.ROLLBACK);
        }
        return command;
    }

    private Prepared parsePrepare() {
        if (readIf("COMMIT")) {
            TransactionCommand command = new TransactionCommand(session, CommandInterface.PREPARE_COMMIT);
            command.setTransactionName(readIdentifier());
            return command;
        }
        return parsePrepareProcedure();
    }

    private Prepared parsePrepareProcedure() {
        if (database.getMode().getEnum() == ModeEnum.MSSQLServer) {
            throw getSyntaxError();
            /*
             * PostgreSQL-style PREPARE is disabled in MSSQLServer mode
             * because PostgreSQL-style EXECUTE is redefined in this
             * mode.
             */
        }
        String procedureName = readIdentifier();
        if (readIf(OPEN_PAREN)) {
            ArrayList<Column> list = Utils.newSmallArrayList();
            for (int i = 0;; i++) {
                Column column = parseColumnForTable("C" + i, true);
                list.add(column);
                if (!readIfMore()) {
                    break;
                }
            }
        }
        read(AS);
        Prepared prep = parsePrepared();
        PrepareProcedure command = new PrepareProcedure(session);
        command.setProcedureName(procedureName);
        command.setPrepared(prep);
        return command;
    }

    private TransactionCommand parseSavepoint() {
        TransactionCommand command = new TransactionCommand(session, CommandInterface.SAVEPOINT);
        command.setSavepointName(readIdentifier());
        return command;
    }

    private Prepared parseReleaseSavepoint() {
        Prepared command = new NoOperation(session);
        readIf("SAVEPOINT");
        readIdentifier();
        return command;
    }

    private Schema findSchema(String schemaName) {
        if (schemaName == null) {
            return null;
        }
        Schema schema = database.findSchema(schemaName);
        if (schema == null) {
            if (equalsToken("SESSION", schemaName)) {
                // for local temporary tables
                schema = database.getSchema(session.getCurrentSchemaName());
            }
        }
        return schema;
    }

    private Schema getSchema(String schemaName) {
        if (schemaName == null) {
            return null;
        }
        Schema schema = findSchema(schemaName);
        if (schema == null) {
            throw DbException.get(ErrorCode.SCHEMA_NOT_FOUND_1, schemaName);
        }
        return schema;
    }

    private Schema getSchema() {
        return getSchema(schemaName);
    }
    /*
     * Gets the current schema for scenarios that need a guaranteed, non-null schema object.
     *
     * This routine is solely here
     * because of the function readIdentifierWithSchema(String defaultSchemaName) - which
     * is often called with a null parameter (defaultSchemaName) - then 6 lines into the function
     * that routine nullifies the state field schemaName - which I believe is a bug.
     *
     * There are about 7 places where "readIdentifierWithSchema(null)" is called in this file.
     *
     * In other words when is it legal to not have an active schema defined by schemaName ?
     * I don't think it's ever a valid case. I don't understand when that would be allowed.
     * I spent a long time trying to figure this out.
     * As another proof of this point, the command "SET SCHEMA=NULL" is not a valid command.
     *
     * I did try to fix this in readIdentifierWithSchema(String defaultSchemaName)
     * - but every fix I tried cascaded so many unit test errors - so
     * I gave up. I think this needs a bigger effort to fix his, as part of bigger, dedicated story.
     *
     */
    private Schema getSchemaWithDefault() {
        if (schemaName == null) {
            schemaName = session.getCurrentSchemaName();
        }
        return getSchema(schemaName);
    }

    private Column readTableColumn(TableFilter filter) {
        String columnName = readIdentifier();
        if (readIf(DOT)) {
            columnName = readTableColumn(filter, columnName);
        }
        return filter.getTable().getColumn(columnName);
    }

    private String readTableColumn(TableFilter filter, String tableAlias) {
        String columnName = readIdentifier();
        if (readIf(DOT)) {
            String schema = tableAlias;
            tableAlias = columnName;
            columnName = readIdentifier();
            if (readIf(DOT)) {
                checkDatabaseName(schema);
                schema = tableAlias;
                tableAlias = columnName;
                columnName = readIdentifier();
            }
            if (!equalsToken(schema, filter.getTable().getSchema().getName())) {
                throw DbException.get(ErrorCode.SCHEMA_NOT_FOUND_1, schema);
            }
        }
        if (!equalsToken(tableAlias, filter.getTableAlias())) {
            throw DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1, tableAlias);
        }
        return columnName;
    }

    private DataChangeStatement parseUpdate(int start) {
        Update command = new Update(session);
        currentPrepared = command;
        Expression fetch = null;
        if (database.getMode().topInDML && readIf("TOP")) {
            read(OPEN_PAREN);
            fetch = readTerm().optimize(session);
            read(CLOSE_PAREN);
        }
        TableFilter targetTableFilter = readSimpleTableFilter();
        command.setTableFilter(targetTableFilter);
        int backupIndex = tokenIndex;
        if (database.getMode().discardWithTableHints) {
            discardWithTableHints();
        }
        command.setSetClauseList(readUpdateSetClause(targetTableFilter));
        if (database.getMode().allowUsingFromClauseInUpdateStatement && readIf(FROM)) {
            setTokenIndex(backupIndex);
            return parseUpdateFrom(targetTableFilter, start);
        }
        if (readIf(WHERE)) {
            command.setCondition(readExpression());
        }
        if (fetch == null) {
            // for MySQL compatibility
            // (this syntax is supported, but ignored)
            readIfOrderBy();
            fetch = readFetchOrLimit();
        }
        command.setFetch(fetch);
        setSQL(command, start);
        return command;
    }

    private MergeUsing parseUpdateFrom(TableFilter targetTableFilter, int start) {
        MergeUsing command = new MergeUsing(session, targetTableFilter);
        currentPrepared = command;
        SetClauseList updateSetClause = readUpdateSetClause(targetTableFilter);
        read(FROM);
        command.setSourceTableFilter(readTableReference());
        command.setOnCondition(readIf(WHERE) ? readExpression() : ValueExpression.TRUE);
        MergeUsing.WhenMatchedThenUpdate update = command.new WhenMatchedThenUpdate();
        update.setSetClauseList(updateSetClause);
        command.addWhen(update);
        setSQL(command, start);
        return command;
    }

    private SetClauseList readUpdateSetClause(TableFilter filter) {
        read(SET);
        SetClauseList list = new SetClauseList(filter.getTable());
        do {
            if (readIf(OPEN_PAREN)) {
                ArrayList<Column> columns = Utils.newSmallArrayList();
                ArrayList<Expression[]> allIndexes = Utils.newSmallArrayList();
                do {
                    columns.add(readTableColumn(filter));
                    allIndexes.add(readUpdateSetClauseArrayIndexes());
                } while (readIfMore());
                read(EQUAL);
                list.addMultiple(columns, allIndexes, readExpression());
            } else {
                Column column = readTableColumn(filter);
                Expression[] arrayIndexes = readUpdateSetClauseArrayIndexes();
                read(EQUAL);
                list.addSingle(column, arrayIndexes,
                        arrayIndexes == null ? readExpressionOrDefault() : readExpression());
            }
        } while (readIf(COMMA));
        return list;
    }

    private Expression[] readUpdateSetClauseArrayIndexes() {
        if (readIf(OPEN_BRACKET)) {
            ArrayList<Expression> list = Utils.newSmallArrayList();
            do {
                list.add(readExpression());
                read(CLOSE_BRACKET);
            } while (readIf(OPEN_BRACKET));
            return list.toArray(new Expression[0]);
        }
        return null;
    }

    private TableFilter readSimpleTableFilter() {
        return new TableFilter(session, readTableOrView(), readFromAlias(null), rightsChecked, currentSelect, 0, null);
    }

    private Delete parseDelete(int start) {
        Delete command = new Delete(session);
        Expression fetch = null;
        if (database.getMode().topInDML && readIf("TOP")) {
            fetch = readTerm().optimize(session);
        }
        currentPrepared = command;
        if (!readIf(FROM) && database.getMode().deleteIdentifierFrom) {
            readIdentifierWithSchema();
            read(FROM);
        }
        command.setTableFilter(readSimpleTableFilter());
        if (readIf(WHERE)) {
            command.setCondition(readExpression());
        }
        if (fetch == null) {
            fetch = readFetchOrLimit();
        }
        command.setFetch(fetch);
        setSQL(command, start);
        return command;
    }

    private Expression readFetchOrLimit() {
        Expression fetch = null;
        if (readIf(FETCH)) {
            if (!readIf("FIRST")) {
                read("NEXT");
            }
            if (readIf(ROW) || readIf("ROWS")) {
                fetch = ValueExpression.get(ValueInteger.get(1));
            } else {
                fetch = readExpression().optimize(session);
                if (!readIf(ROW)) {
                    read("ROWS");
                }
            }
            read("ONLY");
        } else if (database.getMode().limit && readIf(LIMIT)) {
            fetch = readTerm().optimize(session);
        }
        return fetch;
    }

    private IndexColumn[] parseIndexColumnList() {
        ArrayList<IndexColumn> columns = Utils.newSmallArrayList();
        do {
            columns.add(new IndexColumn(readIdentifier(), parseSortType()));
        } while (readIfMore());
        return columns.toArray(new IndexColumn[0]);
    }

    private int parseSortType() {
        int sortType = !readIf("ASC") && readIf("DESC") ? SortOrder.DESCENDING : SortOrder.ASCENDING;
        if (readIf("NULLS")) {
            if (readIf("FIRST")) {
                sortType |= SortOrder.NULLS_FIRST;
            } else {
                read("LAST");
                sortType |= SortOrder.NULLS_LAST;
            }
        }
        return sortType;
    }

    private String[] parseColumnList() {
        ArrayList<String> columns = Utils.newSmallArrayList();
        do {
            columns.add(readIdentifier());
        } while (readIfMore());
        return columns.toArray(new String[0]);
    }

    private Column[] parseColumnList(Table table) {
        ArrayList<Column> columns = Utils.newSmallArrayList();
        HashSet<Column> set = new HashSet<>();
        if (!readIf(CLOSE_PAREN)) {
            do {
                Column column = parseColumn(table);
                if (!set.add(column)) {
                    throw DbException.get(ErrorCode.DUPLICATE_COLUMN_NAME_1, column.getTraceSQL());
                }
                columns.add(column);
            } while (readIfMore());
        }
        return columns.toArray(new Column[0]);
    }

    private Column parseColumn(Table table) {
        if (currentTokenType == _ROWID_) {
            read();
            return table.getRowIdColumn();
        }
        return table.getColumn(readIdentifier());
    }

    /**
     * Read comma or closing brace.
     *
     * @return {@code true} if comma is read, {@code false} if brace is read
     */
    private boolean readIfMore() {
        if (readIf(COMMA)) {
            return true;
        }
        read(CLOSE_PAREN);
        return false;
    }

    private Prepared parseHelp() {
        HashSet<String> conditions = new HashSet<>();
        while (currentTokenType != END_OF_INPUT) {
            conditions.add(StringUtils.toUpperEnglish(readIdentifierOrKeyword()));
        }
        return new Help(session, conditions.toArray(new String[0]));
    }

    private Prepared parseShow() {
        StringBuilder buff = new StringBuilder("SELECT ");
        if (readIf("CLIENT_ENCODING")) {
            // for PostgreSQL compatibility
            buff.append("'UNICODE' CLIENT_ENCODING");
        } else if (readIf("DEFAULT_TRANSACTION_ISOLATION")) {
            // for PostgreSQL compatibility
            buff.append("'read committed' DEFAULT_TRANSACTION_ISOLATION");
        } else if (readIf("TRANSACTION")) {
            // for PostgreSQL compatibility
            read("ISOLATION");
            read("LEVEL");
            buff.append("LOWER(ISOLATION_LEVEL) TRANSACTION_ISOLATION FROM INFORMATION_SCHEMA.SESSIONS "
                    + "WHERE SESSION_ID = SESSION_ID()");
        } else if (readIf("DATESTYLE")) {
            // for PostgreSQL compatibility
            buff.append("'ISO' DATESTYLE");
        } else if (readIf("SEARCH_PATH")) {
            // for PostgreSQL compatibility
            String[] searchPath = session.getSchemaSearchPath();
            StringBuilder searchPathBuff = new StringBuilder();
            if (searchPath != null) {
                for (int i = 0; i < searchPath.length; i++) {
                    if (i > 0) {
                        searchPathBuff.append(", ");
                    }
                    ParserUtil.quoteIdentifier(searchPathBuff, searchPath[i], HasSQL.QUOTE_ONLY_WHEN_REQUIRED);
                }
            }
            StringUtils.quoteStringSQL(buff, searchPathBuff.toString());
            buff.append(" SEARCH_PATH");
        } else if (readIf("SERVER_VERSION")) {
            // for PostgreSQL compatibility
            buff.append("'" + Constants.PG_VERSION + "' SERVER_VERSION");
        } else if (readIf("SERVER_ENCODING")) {
            // for PostgreSQL compatibility
            buff.append("'UTF8' SERVER_ENCODING");
        } else if (readIf("SSL")) {
            // for PostgreSQL compatibility
            buff.append("'off' SSL");
        } else if (readIf("TABLES")) {
            // for MySQL compatibility
            String schema = database.getMainSchema().getName();
            if (readIf(FROM)) {
                schema = readIdentifier();
            }
            buff.append("TABLE_NAME, TABLE_SCHEMA FROM "
                    + "INFORMATION_SCHEMA.TABLES "
                    + "WHERE TABLE_SCHEMA=");
            StringUtils.quoteStringSQL(buff, schema).append(" ORDER BY TABLE_NAME");
        } else if (readIf("COLUMNS")) {
            // for MySQL compatibility
            read(FROM);
            String tableName = readIdentifierWithSchema();
            String schemaName = getSchema().getName();
            if (readIf(FROM)) {
                schemaName = readIdentifier();
            }
            buff.append("C.COLUMN_NAME FIELD, ");
            boolean oldInformationSchema = session.isOldInformationSchema();
            if (oldInformationSchema) {
                buff.append("C.COLUMN_TYPE");
            } else {
                buff.append("DATA_TYPE_SQL(");
                StringUtils.quoteStringSQL(buff, schemaName).append(", ");
                StringUtils.quoteStringSQL(buff, tableName).append(", 'TABLE', C.DTD_IDENTIFIER)");
            }
            buff.append(" TYPE, "
                    + "C.IS_NULLABLE \"NULL\", "
                    + "CASE (SELECT MAX(I.INDEX_TYPE_NAME) FROM "
                    + "INFORMATION_SCHEMA.INDEXES I ");
            if (!oldInformationSchema) {
                buff.append("JOIN INFORMATION_SCHEMA.INDEX_COLUMNS IC ");
            }
            buff.append("WHERE I.TABLE_SCHEMA=C.TABLE_SCHEMA "
                    + "AND I.TABLE_NAME=C.TABLE_NAME ");
            if (oldInformationSchema) {
                buff.append("AND I.COLUMN_NAME=C.COLUMN_NAME");
            } else {
                buff.append("AND IC.TABLE_SCHEMA=C.TABLE_SCHEMA "
                        + "AND IC.TABLE_NAME=C.TABLE_NAME "
                        + "AND IC.INDEX_SCHEMA=I.INDEX_SCHEMA "
                        + "AND IC.INDEX_NAME=I.INDEX_NAME "
                        + "AND IC.COLUMN_NAME=C.COLUMN_NAME");
            }
            buff.append(')'
                    + "WHEN 'PRIMARY KEY' THEN 'PRI' "
                    + "WHEN 'UNIQUE INDEX' THEN 'UNI' ELSE '' END `KEY`, "
                    + "COALESCE(COLUMN_DEFAULT, 'NULL') `DEFAULT` "
                    + "FROM INFORMATION_SCHEMA.COLUMNS C "
                    + "WHERE C.TABLE_SCHEMA=");
            StringUtils.quoteStringSQL(buff, schemaName).append(" AND C.TABLE_NAME=");
            StringUtils.quoteStringSQL(buff, tableName).append(" ORDER BY C.ORDINAL_POSITION");
        } else if (readIf("DATABASES") || readIf("SCHEMAS")) {
            // for MySQL compatibility
            buff.append("SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA");
        } else if (database.getMode().getEnum() == ModeEnum.PostgreSQL && readIf(ALL)) {
            // for PostgreSQL compatibility
            buff.append("NAME, SETTING FROM PG_CATALOG.PG_SETTINGS");
        }
        boolean b = session.getAllowLiterals();
        try {
            // need to temporarily enable it
            session.setAllowLiterals(true);
            return session.prepare(buff.toString());
        } finally {
            session.setAllowLiterals(b);
        }
    }

    private boolean isDerivedTable() {
        int offset = tokenIndex;
        int level = 0;
        while (tokens.get(offset).tokenType() == OPEN_PAREN) {
            level++;
            offset++;
        }
        boolean query = isDirectQuery(offset);
        s: if (query && level > 0) {
            offset = scanToCloseParen(offset + 1);
            if (offset < 0) {
                query = false;
                break s;
            }
            for (;;) {
                switch (tokens.get(offset).tokenType()) {
                case SEMICOLON:
                case END_OF_INPUT:
                    query = false;
                    break s;
                case OPEN_PAREN:
                    offset = scanToCloseParen(offset + 1);
                    if (offset < 0) {
                        query = false;
                        break s;
                    }
                    break;
                case CLOSE_PAREN:
                    if (--level == 0) {
                        break s;
                    }
                    offset++;
                    break;
                case JOIN:
                    query = false;
                    break s;
                default:
                    offset++;
                }
            }
        }
        return query;
    }

    private boolean isQuery() {
        int offset = tokenIndex;
        int level = 0;
        while (tokens.get(offset).tokenType() == OPEN_PAREN) {
            level++;
            offset++;
        }
        boolean query = isDirectQuery(offset);
        s: if (query && level > 0) {
            offset++;
            do {
                offset = scanToCloseParen(offset);
                if (offset < 0) {
                    query = false;
                    break s;
                }
                switch (tokens.get(offset).tokenType()) {
                default:
                    query = false;
                    break s;
                case END_OF_INPUT:
                case SEMICOLON:
                case CLOSE_PAREN:
                case ORDER:
                case OFFSET:
                case FETCH:
                case LIMIT:
                case UNION:
                case EXCEPT:
                case MINUS:
                case INTERSECT:
                }
            } while (--level > 0);
        }
        return query;
    }

    private int scanToCloseParen(int offset) {
        for (int level = 0;;) {
            switch (tokens.get(offset).tokenType()) {
            case SEMICOLON:
            case END_OF_INPUT:
                return -1;
            case OPEN_PAREN:
                level++;
                break;
            case CLOSE_PAREN:
                if (--level < 0) {
                    return offset + 1;
                }
            }
            offset++;
        }
    }

    private boolean isQueryQuick() {
        int offset = tokenIndex;
        while (tokens.get(offset).tokenType() == OPEN_PAREN) {
            offset++;
        }
        return isDirectQuery(offset);
    }

    private boolean isDirectQuery(int offset) {
        boolean query;
        switch (tokens.get(offset).tokenType()) {
        case SELECT:
        case VALUES:
        case WITH:
            query = true;
            break;
        case TABLE:
            query = tokens.get(offset + 1).tokenType() != OPEN_PAREN;
            break;
        default:
            query = false;
        }
        return query;
    }

    private Prepared parseMerge(int start) {
        read("INTO");
        TableFilter targetTableFilter = readSimpleTableFilter();
        if (readIf(USING)) {
            return parseMergeUsing(targetTableFilter, start);
        }
        return parseMergeInto(targetTableFilter, start);
    }

    private Prepared parseMergeInto(TableFilter targetTableFilter, int start) {
        Merge command = new Merge(session, false);
        currentPrepared = command;
        command.setTable(targetTableFilter.getTable());
        Table table = command.getTable();
        if (readIf(OPEN_PAREN)) {
            if (isQueryQuick()) {
                command.setQuery(parseQuery());
                read(CLOSE_PAREN);
                return command;
            }
            command.setColumns(parseColumnList(table));
        }
        if (readIf(KEY, OPEN_PAREN)) {
            command.setKeys(parseColumnList(table));
        }
        if (readIf(VALUES)) {
            parseValuesForCommand(command);
        } else {
            command.setQuery(parseQuery());
        }
        setSQL(command, start);
        return command;
    }

    private MergeUsing parseMergeUsing(TableFilter targetTableFilter, int start) {
        MergeUsing command = new MergeUsing(session, targetTableFilter);
        currentPrepared = command;
        command.setSourceTableFilter(readTableReference());
        read(ON);
        Expression condition = readExpression();
        command.setOnCondition(condition);

        read(WHEN);
        do {
            boolean matched = readIf("MATCHED");
            if (matched) {
                parseWhenMatched(command);
            } else {
                parseWhenNotMatched(command);
            }
        } while (readIf(WHEN));

        setSQL(command, start);
        return command;
    }

    private void parseWhenMatched(MergeUsing command) {
        Expression and = readIf(AND) ? readExpression() : null;
        read("THEN");
        MergeUsing.When when;
        if (readIf("UPDATE")) {
            MergeUsing.WhenMatchedThenUpdate update = command.new WhenMatchedThenUpdate();
            update.setSetClauseList(readUpdateSetClause(command.getTargetTableFilter()));
            when = update;
        } else {
            read("DELETE");
            when = command.new WhenMatchedThenDelete();
        }
        if (and == null && database.getMode().mergeWhere && readIf(WHERE)) {
            and = readExpression();
        }
        when.setAndCondition(and);
        command.addWhen(when);
    }

    private void parseWhenNotMatched(MergeUsing command) {
        read(NOT);
        read("MATCHED");
        Expression and = readIf(AND) ? readExpression() : null;
        read("THEN");
        read("INSERT");
        Column[] columns = readIf(OPEN_PAREN) ? parseColumnList(command.getTargetTableFilter().getTable()) : null;
        Boolean overridingSystem = readIfOverriding();
        read(VALUES);
        read(OPEN_PAREN);
        ArrayList<Expression> values = Utils.newSmallArrayList();
        if (!readIf(CLOSE_PAREN)) {
            do {
                values.add(readExpressionOrDefault());
            } while (readIfMore());
        }
        MergeUsing.WhenNotMatched when = command.new WhenNotMatched(columns, overridingSystem,
                values.toArray(new Expression[0]));
        when.setAndCondition(and);
        command.addWhen(when);
    }

    private Insert parseInsert(int start) {
        Insert command = new Insert(session);
        currentPrepared = command;
        Mode mode = database.getMode();
        if (mode.onDuplicateKeyUpdate && readIf("IGNORE")) {
            command.setIgnore(true);
        }
        read("INTO");
        Table table = readTableOrView();
        command.setTable(table);
        Column[] columns = null;
        if (readIf(OPEN_PAREN)) {
            if (isQueryQuick()) {
                command.setQuery(parseQuery());
                read(CLOSE_PAREN);
                return command;
            }
            columns = parseColumnList(table);
            command.setColumns(columns);
        }
        Boolean overridingSystem = readIfOverriding();
        command.setOverridingSystem(overridingSystem);
        boolean requireQuery = false;
        if (readIf("DIRECT")) {
            requireQuery = true;
            command.setInsertFromSelect(true);
        }
        if (readIf("SORTED")) {
            requireQuery = true;
        }
        readValues: {
            if (!requireQuery) {
                if (overridingSystem == null && readIf(DEFAULT, VALUES)) {
                    command.addRow(new Expression[0]);
                    break readValues;
                }
                if (readIf(VALUES)) {
                    parseValuesForCommand(command);
                    break readValues;
                }
                if (readIf(SET)) {
                    parseInsertSet(command, table, columns);
                    break readValues;
                }
            }
            command.setQuery(parseQuery());
        }
        if (mode.onDuplicateKeyUpdate || mode.insertOnConflict || mode.isolationLevelInSelectOrInsertStatement) {
            parseInsertCompatibility(command, table, mode);
        }
        setSQL(command, start);
        return command;
    }

    private Boolean readIfOverriding() {
        Boolean overridingSystem = null;
        if (readIf("OVERRIDING", USER, VALUE)) {
            overridingSystem = Boolean.FALSE;
        } else if (readIf("OVERRIDING", "SYSTEM", VALUE)) {
            overridingSystem = Boolean.TRUE;
        }
        return overridingSystem;
    }

    private void parseInsertSet(Insert command, Table table, Column[] columns) {
        if (columns != null) {
            throw getSyntaxError();
        }
        ArrayList<Column> columnList = Utils.newSmallArrayList();
        ArrayList<Expression> values = Utils.newSmallArrayList();
        do {
            columnList.add(parseColumn(table));
            read(EQUAL);
            values.add(readExpressionOrDefault());
        } while (readIf(COMMA));
        command.setColumns(columnList.toArray(new Column[0]));
        command.addRow(values.toArray(new Expression[0]));
    }

    private void parseInsertCompatibility(Insert command, Table table, Mode mode) {
        if (mode.onDuplicateKeyUpdate) {
            if (readIf(ON, "DUPLICATE", KEY, "UPDATE")) {
                do {
                    String columnName = readIdentifier();
                    if (readIf(DOT)) {
                        String schemaOrTableName = columnName;
                        String tableOrColumnName = readIdentifier();
                        if (readIf(DOT)) {
                            if (!table.getSchema().getName().equals(schemaOrTableName)) {
                                throw DbException.get(ErrorCode.SCHEMA_NAME_MUST_MATCH);
                            }
                            columnName = readIdentifier();
                        } else {
                            columnName = tableOrColumnName;
                            tableOrColumnName = schemaOrTableName;
                        }
                        if (!table.getName().equals(tableOrColumnName)) {
                            throw DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1, tableOrColumnName);
                        }
                    }
                    Column column = table.getColumn(columnName);
                    read(EQUAL);
                    command.addAssignmentForDuplicate(column, readExpressionOrDefault());
                } while (readIf(COMMA));
            }
        }
        if (mode.insertOnConflict) {
            if (readIf(ON, "CONFLICT", "DO", "NOTHING")) {
                command.setIgnore(true);
            }
        }
        if (mode.isolationLevelInSelectOrInsertStatement) {
            parseIsolationClause();
        }
    }

    /**
     * MySQL compatibility. REPLACE is similar to MERGE.
     */
    private Merge parseReplace(int start) {
        Merge command = new Merge(session, true);
        currentPrepared = command;
        read("INTO");
        Table table = readTableOrView();
        command.setTable(table);
        if (readIf(OPEN_PAREN)) {
            if (isQueryQuick()) {
                command.setQuery(parseQuery());
                read(CLOSE_PAREN);
                return command;
            }
            command.setColumns(parseColumnList(table));
        }
        if (readIf(VALUES)) {
            parseValuesForCommand(command);
        } else {
            command.setQuery(parseQuery());
        }
        setSQL(command, start);
        return command;
    }

    /**
     * REFRESH MATERIALIZED VIEW
     */
    private RefreshMaterializedView parseRefresh(int start) {
        read("MATERIALIZED");
        read("VIEW");
        Table table = readTableOrView(/*resolveMaterializedView*/false);
        if (!(table instanceof MaterializedView)) {
            throw DbException.get(ErrorCode.VIEW_NOT_FOUND_1, table.getName());
        }
        RefreshMaterializedView command = new RefreshMaterializedView(session, getSchema());
        currentPrepared = command;
        command.setView((MaterializedView) table);
        setSQL(command, start);
        return command;
    }

    private void parseValuesForCommand(CommandWithValues command) {
        ArrayList<Expression> values = Utils.newSmallArrayList();
        do {
            values.clear();
            boolean multiColumn;
            if (readIf(ROW, OPEN_PAREN)) {
                multiColumn = true;
            } else {
                multiColumn = readIf(OPEN_PAREN);
            }
            if (multiColumn) {
                if (!readIf(CLOSE_PAREN)) {
                    do {
                        values.add(readExpressionOrDefault());
                    } while (readIfMore());
                }
            } else {
                values.add(readExpressionOrDefault());
            }
            command.addRow(values.toArray(new Expression[0]));
        } while (readIf(COMMA));
    }

    private TableFilter readTablePrimary() {
        Table table;
        String alias = null;
        label: if (readIf(OPEN_PAREN)) {
            if (isDerivedTable()) {
                // Derived table
                return readDerivedTableWithCorrelation();
            } else {
                // Parenthesized joined table
                TableFilter tableFilter = readTableReference();
                read(CLOSE_PAREN);
                return readCorrelation(tableFilter);
            }
        } else if (readIf(VALUES)) {
            BitSet outerUsedParameters = initParametersScope();
            TableValueConstructor query = parseValues();
            alias = session.getNextSystemIdentifier(sqlCommand);
            table = query.toTable(alias, null, getUsedParameters(outerUsedParameters), createView != null,
                    currentSelect);
        } else if (readIf(TABLE, OPEN_PAREN)) {
            // Table function derived table
            ArrayTableFunction function = readTableFunction(ArrayTableFunction.TABLE);
            table = new FunctionTable(database.getMainSchema(), session, function);
        } else {
            boolean quoted = token.isQuoted();
            String tableName = readIdentifier();
            int backupIndex = tokenIndex;
            schemaName = null;
            if (readIf(DOT)) {
                tableName = readIdentifierWithSchema2(tableName);
            } else if (!quoted && readIf(TABLE, OPEN_PAREN)) {
                table = readDataChangeDeltaTable(upperName(tableName), backupIndex);
                break label;
            }
            Schema schema;
            if (schemaName == null) {
                schema = null;
            } else {
                schema = findSchema(schemaName);
                if (schema == null) {
                    if (isDualTable(tableName)) {
                        table = new DualTable(database);
                        break label;
                    }
                    throw DbException.get(ErrorCode.SCHEMA_NOT_FOUND_1, schemaName);
                }
            }
            boolean foundLeftParen = readIf(OPEN_PAREN);
            if (foundLeftParen && readIf("INDEX")) {
                // Sybase compatibility with
                // "select * from test (index table1_index)"
                readIdentifierWithSchema(null);
                read(CLOSE_PAREN);
                foundLeftParen = false;
            }
            if (foundLeftParen) {
                Schema mainSchema = database.getMainSchema();
                if (equalsToken(tableName, RangeTable.NAME)
                        || equalsToken(tableName, RangeTable.ALIAS)) {
                    Expression min = readExpression();
                    read(COMMA);
                    Expression max = readExpression();
                    if (readIf(COMMA)) {
                        Expression step = readExpression();
                        read(CLOSE_PAREN);
                        table = new RangeTable(mainSchema, min, max, step);
                    } else {
                        read(CLOSE_PAREN);
                        table = new RangeTable(mainSchema, min, max);
                    }
                } else {
                    table = new FunctionTable(mainSchema, session, readTableFunction(tableName, schema));
                }
            } else {
                table = readTableOrView(tableName, /*resolveMaterializedView*/true);
            }
        }
        ArrayList<String> derivedColumnNames = null;
        IndexHints indexHints = null;
        if (readIfUseIndex()) {
            indexHints = parseIndexHints(table);
        } else {
            alias = readFromAlias(alias);
            if (alias != null) {
                derivedColumnNames = readDerivedColumnNames();
                if (readIfUseIndex()) {
                    indexHints = parseIndexHints(table);
                }
            }
        }
        return buildTableFilter(table, alias, derivedColumnNames, indexHints);
    }

    private TableFilter readCorrelation(TableFilter tableFilter) {
        String alias = readFromAlias(null);
        if (alias != null) {
            tableFilter.setAlias(alias);
            ArrayList<String> derivedColumnNames = readDerivedColumnNames();
            if (derivedColumnNames != null) {
                tableFilter.setDerivedColumns(derivedColumnNames);
            }
        }
        return tableFilter;
    }

    private TableFilter readDerivedTableWithCorrelation() {
        BitSet outerUsedParameters = initParametersScope();
        Query query = parseQueryExpression();
        ArrayList<Parameter> queryParameters = getUsedParameters(outerUsedParameters);
        read(CLOSE_PAREN);
        Table table;
        String alias;
        ArrayList<String> derivedColumnNames = null;
        IndexHints indexHints = null;
        if (readIfUseIndex()) {
            alias = session.getNextSystemIdentifier(sqlCommand);
            table = query.toTable(alias, null, queryParameters, createView != null, currentSelect);
            indexHints = parseIndexHints(table);
        } else {
            alias = readFromAlias(null);
            if (alias != null) {
                derivedColumnNames = readDerivedColumnNames();
                Column[] columnTemplates = null;
                if (derivedColumnNames != null) {
                    query.init();
                    columnTemplates = QueryExpressionTable.createQueryColumnTemplateList(
                            derivedColumnNames.toArray(new String[0]), query, new String[1])
                            .toArray(new Column[0]);
                }
                table = query.toTable(alias, columnTemplates, queryParameters, createView != null, currentSelect);
                if (readIfUseIndex()) {
                    indexHints = parseIndexHints(table);
                }
            } else {
                alias = session.getNextSystemIdentifier(sqlCommand);
                table = query.toTable(alias, null, queryParameters, createView != null, currentSelect);
            }
        }
        return buildTableFilter(table, alias, derivedColumnNames, indexHints);
    }

    private TableFilter buildTableFilter(Table table, String alias, ArrayList<String> derivedColumnNames,
            IndexHints indexHints) {
        if (database.getMode().discardWithTableHints) {
            discardWithTableHints();
        }
        // inherit alias for CTE as views from table name
        if (alias == null && table.isView() && table.isTableExpression()) {
            alias = table.getName();
        }
        TableFilter filter = new TableFilter(session, table, alias, rightsChecked,
                currentSelect, orderInFrom++, indexHints);
        if (derivedColumnNames != null) {
            filter.setDerivedColumns(derivedColumnNames);
        }
        return filter;
    }

    private Table readDataChangeDeltaTable(String resultOptionName, int backupIndex) {
        int start = tokenIndex;
        DataChangeStatement statement;
        ResultOption resultOption = ResultOption.FINAL;
        switch (resultOptionName) {
        case "OLD":
            resultOption = ResultOption.OLD;
            if (readIf("UPDATE")) {
                statement = parseUpdate(start);
            } else if (readIf("DELETE")) {
                statement = parseDelete(start);
            } else if (readIf("MERGE")) {
                statement = (DataChangeStatement) parseMerge(start);
            } else if (database.getMode().replaceInto && readIf("REPLACE")) {
                statement = parseReplace(start);
            } else {
                throw getSyntaxError();
            }
            break;
        case "NEW":
            resultOption = ResultOption.NEW;
            //$FALL-THROUGH$
        case "FINAL":
            if (readIf("INSERT")) {
                statement = parseInsert(start);
            } else if (readIf("UPDATE")) {
                statement = parseUpdate(start);
            } else if (readIf("MERGE")) {
                statement = (DataChangeStatement) parseMerge(start);
            } else if (database.getMode().replaceInto && readIf("REPLACE")) {
                statement = parseReplace(start);
            } else {
                throw getSyntaxError();
            }
            break;
        default:
            setTokenIndex(backupIndex);
            addExpected("OLD TABLE");
            addExpected("NEW TABLE");
            addExpected("FINAL TABLE");
            throw getSyntaxError();
        }
        read(CLOSE_PAREN);
        if (currentSelect != null) {
            // Lobs aren't copied, so use it for more safety
            currentSelect.setNeverLazy(true);
        }
        return new DataChangeDeltaTable(getSchemaWithDefault(), session, statement, resultOption);
    }

    private TableFunction readTableFunction(String name, Schema schema) {
        if (schema == null) {
            switch (upperName(name)) {
            case "UNNEST":
                return readUnnestFunction();
            case "TABLE_DISTINCT":
                return readTableFunction(ArrayTableFunction.TABLE_DISTINCT);
            case "CSVREAD":
                recompileAlways = true;
                return readParameters(new CSVReadFunction());
            case "LINK_SCHEMA":
                recompileAlways = true;
                return readParameters(new LinkSchemaFunction());
            }
        }
        FunctionAlias functionAlias = getFunctionAliasWithinPath(name, schema);
        if (!functionAlias.isDeterministic()) {
            recompileAlways = true;
        }
        ArrayList<Expression> argList = Utils.newSmallArrayList();
        if (!readIf(CLOSE_PAREN)) {
            do {
                argList.add(readExpression());
            } while (readIfMore());
        }
        return new JavaTableFunction(functionAlias, argList.toArray(new Expression[0]));
    }

    private boolean readIfUseIndex() {
        int start = tokenIndex;
        if (!readIf("USE")) {
            return false;
        }
        if (!readIf("INDEX")) {
            setTokenIndex(start);
            return false;
        }
        return true;
    }

    private IndexHints parseIndexHints(Table table) {
        read(OPEN_PAREN);
        LinkedHashSet<String> indexNames = new LinkedHashSet<>();
        if (!readIf(CLOSE_PAREN)) {
            do {
                String indexName = readIdentifierWithSchema();
                Index index = table.getIndex(indexName);
                indexNames.add(index.getName());
            } while (readIfMore());
        }
        return IndexHints.createUseIndexHints(indexNames);
    }

    private String readFromAlias(String alias) {
        if (readIf(AS) || isIdentifier()) {
            alias = readIdentifier();
        }
        return alias;
    }

    private ArrayList<String> readDerivedColumnNames() {
        if (readIf(OPEN_PAREN)) {
            ArrayList<String> derivedColumnNames = new ArrayList<>();
            do {
                derivedColumnNames.add(readIdentifier());
            } while (readIfMore());
            return derivedColumnNames;
        }
        return null;
    }

    private void discardWithTableHints() {
        if (readIf(WITH, OPEN_PAREN)) {
            do {
                discardTableHint();
            } while (readIfMore());
        }
    }

    private void discardTableHint() {
        if (readIf("INDEX")) {
            if (readIf(OPEN_PAREN)) {
                do {
                    readExpression();
                } while (readIfMore());
            } else {
                read(EQUAL);
                readExpression();
            }
        } else {
            readExpression();
        }
    }

    private Prepared parseTruncate() {
        read(TABLE);
        Table table = readTableOrView();
        boolean restart = database.getMode().truncateTableRestartIdentity;
        if (readIf("CONTINUE", "IDENTITY")) {
            restart = false;
        } else if (readIf("RESTART", "IDENTITY")) {
            restart = true;
        }
        TruncateTable command = new TruncateTable(session);
        command.setTable(table);
        command.setRestart(restart);
        return command;
    }

    private boolean readIfExists(boolean ifExists) {
        if (readIf(IF, EXISTS)) {
            ifExists = true;
        }
        return ifExists;
    }

    private Prepared parseComment() {
        int type = 0;
        read(ON);
        boolean column = false;
        if (readIf(TABLE) || readIf("VIEW")) {
            type = DbObject.TABLE_OR_VIEW;
        } else if (readIf("COLUMN")) {
            column = true;
            type = DbObject.TABLE_OR_VIEW;
        } else if (readIf("CONSTANT")) {
            type = DbObject.CONSTANT;
        } else if (readIf(CONSTRAINT)) {
            type = DbObject.CONSTRAINT;
        } else if (readIf("ALIAS")) {
            type = DbObject.FUNCTION_ALIAS;
        } else if (readIf("INDEX")) {
            type = DbObject.INDEX;
        } else if (readIf("ROLE")) {
            type = DbObject.ROLE;
        } else if (readIf("SCHEMA")) {
            type = DbObject.SCHEMA;
        } else if (readIf("SEQUENCE")) {
            type = DbObject.SEQUENCE;
        } else if (readIf("TRIGGER")) {
            type = DbObject.TRIGGER;
        } else if (readIf(USER)) {
            type = DbObject.USER;
        } else if (readIf("DOMAIN")) {
            type = DbObject.DOMAIN;
        } else {
            throw getSyntaxError();
        }
        SetComment command = new SetComment(session);
        String objectName;
        if (column) {
            // can't use readIdentifierWithSchema() because
            // it would not read [catalog.]schema.table.column correctly
            objectName = readIdentifier();
            String tmpSchemaName = null;
            read(DOT);
            boolean allowEmpty = database.getMode().allowEmptySchemaValuesAsDefaultSchema;
            String columnName = allowEmpty && currentTokenType == DOT ? null : readIdentifier();
            if (readIf(DOT)) {
                tmpSchemaName = objectName;
                objectName = columnName;
                columnName = allowEmpty && currentTokenType == DOT ? null : readIdentifier();
                if (readIf(DOT)) {
                    checkDatabaseName(tmpSchemaName);
                    tmpSchemaName = objectName;
                    objectName = columnName;
                    columnName = readIdentifier();
                }
            }
            if (columnName == null || objectName == null) {
                throw DbException.getSyntaxError(sqlCommand, token.start(), "table.column");
            }
            schemaName = tmpSchemaName != null ? tmpSchemaName : session.getCurrentSchemaName();
            command.setColumn(true);
            command.setColumnName(columnName);
        } else {
            objectName = readIdentifierWithSchema();
        }
        command.setSchemaName(schemaName);
        command.setObjectName(objectName);
        command.setObjectType(type);
        read(IS);
        command.setCommentExpression(readExpression());
        return command;
    }

    private Prepared parseDrop() {
        if (readIf(TABLE)) {
            boolean ifExists = readIfExists(false);
            DropTable command = new DropTable(session);
            do {
                String tableName = readIdentifierWithSchema();
                command.addTable(getSchema(), tableName);
            } while (readIf(COMMA));
            ifExists = readIfExists(ifExists);
            command.setIfExists(ifExists);
            if (readIf("CASCADE")) {
                command.setDropAction(ConstraintActionType.CASCADE);
                readIf("CONSTRAINTS");
            } else if (readIf("RESTRICT")) {
                command.setDropAction(ConstraintActionType.RESTRICT);
            } else if (readIf("IGNORE")) {
                // TODO SET_DEFAULT works in the same way as CASCADE
                command.setDropAction(ConstraintActionType.SET_DEFAULT);
            }
            return command;
        } else if (readIf("INDEX")) {
            boolean ifExists = readIfExists(false);
            String indexName = readIdentifierWithSchema();
            DropIndex command = new DropIndex(session, getSchema());
            command.setIndexName(indexName);
            ifExists = readIfExists(ifExists);
            command.setIfExists(ifExists);
            //Support for MySQL: DROP INDEX index_name ON tbl_name
            if (readIf(ON)) {
                readIdentifierWithSchema();
            }
            return command;
        } else if (readIf(USER)) {
            boolean ifExists = readIfExists(false);
            DropUser command = new DropUser(session);
            command.setUserName(readIdentifier());
            ifExists = readIfExists(ifExists);
            readIf("CASCADE");
            command.setIfExists(ifExists);
            return command;
        } else if (readIf("SEQUENCE")) {
            boolean ifExists = readIfExists(false);
            String sequenceName = readIdentifierWithSchema();
            DropSequence command = new DropSequence(session, getSchema());
            command.setSequenceName(sequenceName);
            ifExists = readIfExists(ifExists);
            command.setIfExists(ifExists);
            return command;
        } else if (readIf("CONSTANT")) {
            boolean ifExists = readIfExists(false);
            String constantName = readIdentifierWithSchema();
            DropConstant command = new DropConstant(session, getSchema());
            command.setConstantName(constantName);
            ifExists = readIfExists(ifExists);
            command.setIfExists(ifExists);
            return command;
        } else if (readIf("TRIGGER")) {
            boolean ifExists = readIfExists(false);
            String triggerName = readIdentifierWithSchema();
            DropTrigger command = new DropTrigger(session, getSchema());
            command.setTriggerName(triggerName);
            ifExists = readIfExists(ifExists);
            command.setIfExists(ifExists);
            return command;
        } else if (readIf("MATERIALIZED")) {
            read("VIEW");
            boolean ifExists = readIfExists(false);
            String viewName = readIdentifierWithSchema();
            DropMaterializedView command = new DropMaterializedView(session, getSchema());
            command.setViewName(viewName);
            ifExists = readIfExists(ifExists);
            command.setIfExists(ifExists);
            return command;
        } else if (readIf("VIEW")) {
            boolean ifExists = readIfExists(false);
            String viewName = readIdentifierWithSchema();
            DropView command = new DropView(session, getSchema());
            command.setViewName(viewName);
            ifExists = readIfExists(ifExists);
            command.setIfExists(ifExists);
            ConstraintActionType dropAction = parseCascadeOrRestrict();
            if (dropAction != null) {
                command.setDropAction(dropAction);
            }
            return command;
        } else if (readIf("ROLE")) {
            boolean ifExists = readIfExists(false);
            DropRole command = new DropRole(session);
            command.setRoleName(readIdentifier());
            ifExists = readIfExists(ifExists);
            command.setIfExists(ifExists);
            return command;
        } else if (readIf("ALIAS")) {
            boolean ifExists = readIfExists(false);
            String aliasName = readIdentifierWithSchema();
            DropFunctionAlias command = new DropFunctionAlias(session,
                    getSchema());
            command.setAliasName(aliasName);
            ifExists = readIfExists(ifExists);
            command.setIfExists(ifExists);
            return command;
        } else if (readIf("SCHEMA")) {
            boolean ifExists = readIfExists(false);
            DropSchema command = new DropSchema(session);
            command.setSchemaName(readIdentifier());
            ifExists = readIfExists(ifExists);
            command.setIfExists(ifExists);
            ConstraintActionType dropAction = parseCascadeOrRestrict();
            if (dropAction != null) {
                command.setDropAction(dropAction);
            }
            return command;
        } else if (readIf(ALL, "OBJECTS")) {
            DropDatabase command = new DropDatabase(session);
            command.setDropAllObjects(true);
            if (readIf("DELETE", "FILES")) {
                command.setDeleteFiles(true);
            }
            return command;
        } else if (readIf("DOMAIN") || readIf("TYPE") || readIf("DATATYPE")) {
            return parseDropDomain();
        } else if (readIf("AGGREGATE")) {
            return parseDropAggregate();
        } else if (readIf("SYNONYM")) {
            boolean ifExists = readIfExists(false);
            String synonymName = readIdentifierWithSchema();
            DropSynonym command = new DropSynonym(session, getSchema());
            command.setSynonymName(synonymName);
            ifExists = readIfExists(ifExists);
            command.setIfExists(ifExists);
            return command;
        }
        throw getSyntaxError();
    }

    private DropDomain parseDropDomain() {
        boolean ifExists = readIfExists(false);
        String domainName = readIdentifierWithSchema();
        DropDomain command = new DropDomain(session, getSchema());
        command.setDomainName(domainName);
        ifExists = readIfExists(ifExists);
        command.setIfDomainExists(ifExists);
        ConstraintActionType dropAction = parseCascadeOrRestrict();
        if (dropAction != null) {
            command.setDropAction(dropAction);
        }
        return command;
    }

    private DropAggregate parseDropAggregate() {
        boolean ifExists = readIfExists(false);
        String name = readIdentifierWithSchema();
        DropAggregate command = new DropAggregate(session, getSchema());
        command.setName(name);
        ifExists = readIfExists(ifExists);
        command.setIfExists(ifExists);
        return command;
    }

    private TableFilter readTableReference() {
        for (TableFilter top, last = top = readTablePrimary(), join;; last = join) {
            switch (currentTokenType) {
            case RIGHT: {
                read();
                readIf("OUTER");
                read(JOIN);
                // the right hand side is the 'inner' table usually
                join = readTableReference();
                Expression on = readJoinSpecification(top, join, true);
                addJoin(join, top, true, on);
                top = join;
                break;
            }
            case LEFT: {
                read();
                readIf("OUTER");
                read(JOIN);
                join = readTableReference();
                Expression on = readJoinSpecification(top, join, false);
                addJoin(top, join, true, on);
                break;
            }
            case FULL:
                read();
                throw getSyntaxError();
            case INNER: {
                read();
                read(JOIN);
                join = readTableReference();
                Expression on = readJoinSpecification(top, join, false);
                addJoin(top, join, false, on);
                break;
            }
            case JOIN: {
                read();
                join = readTableReference();
                Expression on = readJoinSpecification(top, join, false);
                addJoin(top, join, false, on);
                break;
            }
            case CROSS: {
                read();
                read(JOIN);
                join = readTablePrimary();
                addJoin(top, join, false, null);
                break;
            }
            case NATURAL: {
                read();
                read(JOIN);
                join = readTablePrimary();
                Expression on = null;
                for (Column column1 : last.getTable().getColumns()) {
                    Column column2 = join.getColumn(last.getColumnName(column1), true);
                    if (column2 != null) {
                        on = addJoinColumn(on, last, join, column1, column2, false);
                    }
                }
                addJoin(top, join, false, on);
                break;
            }
            default:
                if (expectedList != null) {
                    // FULL is intentionally excluded
                    addMultipleExpected(RIGHT, LEFT, INNER, JOIN, CROSS, NATURAL);
                }
                return top;
            }
        }
    }

    private Expression readJoinSpecification(TableFilter filter1, TableFilter filter2, boolean rightJoin) {
        Expression on = null;
        if (readIf(ON)) {
            on = readExpression();
        } else if (readIf(USING, OPEN_PAREN)) {
            do {
                String columnName = readIdentifier();
                on = addJoinColumn(on, filter1, filter2, filter1.getColumn(columnName, false),
                        filter2.getColumn(columnName, false), rightJoin);
            } while (readIfMore());
        }
        return on;
    }

    private Expression addJoinColumn(Expression on, TableFilter filter1, TableFilter filter2, Column column1,
            Column column2, boolean rightJoin) {
        if (rightJoin) {
            filter1.addCommonJoinColumns(column1, column2, filter2);
            filter2.addCommonJoinColumnToExclude(column2);
        } else {
            filter1.addCommonJoinColumns(column1, column1, filter1);
            filter2.addCommonJoinColumnToExclude(column2);
        }
        Expression tableExpr = new ExpressionColumn(database, filter1.getSchemaName(), filter1.getTableAlias(),
                filter1.getColumnName(column1));
        Expression joinExpr = new ExpressionColumn(database, filter2.getSchemaName(), filter2.getTableAlias(),
                filter2.getColumnName(column2));
        Expression equal = new Comparison(Comparison.EQUAL, tableExpr, joinExpr, false);
        if (on == null) {
            on = equal;
        } else {
            on = new ConditionAndOr(ConditionAndOr.AND, on, equal);
        }
        return on;
    }

    /**
     * Add one join to another. This method creates nested join between them if
     * required.
     *
     * @param top parent join
     * @param join child join
     * @param outer if child join is an outer join
     * @param on the join condition
     * @see TableFilter#addJoin(TableFilter, boolean, Expression)
     */
    private void addJoin(TableFilter top, TableFilter join, boolean outer, Expression on) {
        if (join.getJoin() != null) {
            String joinTable = Constants.PREFIX_JOIN + token.start();
            TableFilter n = new TableFilter(session, new DualTable(database),
                    joinTable, rightsChecked, currentSelect, join.getOrderInFrom(),
                    null);
            n.setNestedJoin(join);
            join = n;
        }
        top.addJoin(join, outer, on);
    }

    private Prepared parseExecutePostgre() {
        if (readIf("IMMEDIATE")) {
            return new ExecuteImmediate(session, readExpression());
        }
        ExecuteProcedure command = new ExecuteProcedure(session);
        String procedureName = readIdentifier();
        Procedure p = session.getProcedure(procedureName);
        if (p == null) {
            throw DbException.get(ErrorCode.FUNCTION_ALIAS_NOT_FOUND_1,
                    procedureName);
        }
        command.setProcedure(p);
        if (readIf(OPEN_PAREN)) {
            for (int i = 0;; i++) {
                command.setExpression(i, readExpression());
                if (!readIfMore()) {
                    break;
                }
            }
        }
        return command;
    }

    private Prepared parseExecuteSQLServer() {
        Call command = new Call(session);
        currentPrepared = command;
        String schemaName = null;
        String name = readIdentifier();
        if (readIf(DOT)) {
            schemaName = name;
            name = readIdentifier();
            if (readIf(DOT)) {
                checkDatabaseName(schemaName);
                schemaName = name;
                name = readIdentifier();
            }
        }
        FunctionAlias functionAlias = getFunctionAliasWithinPath(name,
                schemaName != null ? database.getSchema(schemaName) : null);
        Expression[] args;
        ArrayList<Expression> argList = Utils.newSmallArrayList();
        if (currentTokenType != SEMICOLON && currentTokenType != END_OF_INPUT) {
            do {
                argList.add(readExpression());
            } while (readIf(COMMA));
        }
        args = argList.toArray(new Expression[0]);
        command.setExpression(new JavaFunction(functionAlias, args));
        return command;
    }

    private FunctionAlias getFunctionAliasWithinPath(String name, Schema schema) {
        UserDefinedFunction userDefinedFunction = findUserDefinedFunctionWithinPath(schema, name);
        if (userDefinedFunction instanceof FunctionAlias) {
            return (FunctionAlias) userDefinedFunction;
        }
        throw DbException.get(ErrorCode.FUNCTION_NOT_FOUND_1, name);
    }

    private DeallocateProcedure parseDeallocate() {
        readIf("PLAN");
        DeallocateProcedure command = new DeallocateProcedure(session);
        command.setProcedureName(readIdentifier());
        return command;
    }

    private Explain parseExplain() {
        Explain command = new Explain(session);
        if (readIf("ANALYZE")) {
            command.setExecuteCommand(true);
        } else {
            if (readIf("PLAN")) {
                readIf(FOR);
            }
        }
        switch (currentTokenType) {
        case SELECT:
        case TABLE:
        case VALUES:
        case WITH:
        case OPEN_PAREN:
            Query query = parseQuery();
            query.setNeverLazy(true);
            command.setCommand(query);
            break;
        default:
            int start = tokenIndex;
            if (readIf("DELETE")) {
                command.setCommand(parseDelete(start));
            } else if (readIf("UPDATE")) {
                command.setCommand(parseUpdate(start));
            } else if (readIf("INSERT")) {
                command.setCommand(parseInsert(start));
            } else if (readIf("MERGE")) {
                command.setCommand(parseMerge(start));
            } else {
                throw getSyntaxError();
            }
        }
        return command;
    }

    private Query parseQuery() {
        BitSet outerUsedParameters = initParametersScope();
        Query command = parseQueryExpression();
        ArrayList<Parameter> params = getUsedParameters(outerUsedParameters);
        command.setParameterList(params);
        command.init();
        return command;
    }

    private Prepared parseWithStatementOrQuery(int start) {
        BitSet outerUsedParameters = initParametersScope();
        Prepared command = parseWith();
        ArrayList<Parameter> params = getUsedParameters(outerUsedParameters);
        command.setParameterList(params);
        if (command instanceof Query) {
            Query query = (Query) command;
            query.init();
        }
        setSQL(command, start);
        return command;
    }

    private Query parseQueryExpression() {
        Query query;
        if (readIf(WITH)) {
            try {
                query = (Query) parseWith();
            } catch (ClassCastException e) {
                throw DbException.get(ErrorCode.SYNTAX_ERROR_1, "WITH statement supports only query in this context");
            }
            // recursive can not be lazy
            query.setNeverLazy(true);
        } else {
            query = parseQueryExpressionBodyAndEndOfQuery();
        }
        return query;
    }

    private Query parseQueryExpressionBodyAndEndOfQuery() {
        int start = tokenIndex;
        Query command = parseQueryExpressionBody();
        parseEndOfQuery(command);
        setSQL(command, start);
        return command;
    }

    private Query parseQueryExpressionBody() {
        Query command = parseQueryTerm();
        for (;;) {
            SelectUnion.UnionType type;
            if (readIf(UNION)) {
                if (readIf(ALL)) {
                    type = SelectUnion.UnionType.UNION_ALL;
                } else {
                    readIf(DISTINCT);
                    type = SelectUnion.UnionType.UNION;
                }
            } else if (readIf(EXCEPT) || readIf(MINUS)) {
                type = SelectUnion.UnionType.EXCEPT;
            } else {
                break;
            }
            command = new SelectUnion(session, type, command, parseQueryTerm());
        }
        return command;
    }

    private Query parseQueryTerm() {
        Query command = parseQueryPrimary();
        while (readIf(INTERSECT)) {
            command = new SelectUnion(session, SelectUnion.UnionType.INTERSECT, command, parseQueryPrimary());
        }
        return command;
    }

    private void parseEndOfQuery(Query command) {
        if (readIf(ORDER, "BY")) {
            Select oldSelect = currentSelect;
            if (command instanceof Select) {
                currentSelect = (Select) command;
            }
            ArrayList<QueryOrderBy> orderList = Utils.newSmallArrayList();
            do {
                boolean canBeNumber = currentTokenType == LITERAL;
                QueryOrderBy order = new QueryOrderBy();
                Expression expr = readExpression();
                if (canBeNumber && expr instanceof ValueExpression && expr.getType().getValueType() == Value.INTEGER) {
                    order.columnIndexExpr = expr;
                } else if (expr instanceof Parameter) {
                    recompileAlways = true;
                    order.columnIndexExpr = expr;
                } else {
                    order.expression = expr;
                }
                order.sortType = parseSortType();
                orderList.add(order);
            } while (readIf(COMMA));
            command.setOrder(orderList);
            currentSelect = oldSelect;
        }
        if (command.getFetch() == null) {
            // make sure aggregate functions will not work here
            Select temp = currentSelect;
            currentSelect = null;
            boolean hasOffsetOrFetch = false;
            // Standard SQL OFFSET / FETCH
            if (readIf(OFFSET)) {
                hasOffsetOrFetch = true;
                command.setOffset(readExpression().optimize(session));
                if (!readIf(ROW)) {
                    readIf("ROWS");
                }
            }
            if (readIf(FETCH)) {
                hasOffsetOrFetch = true;
                if (!readIf("FIRST")) {
                    read("NEXT");
                }
                if (readIf(ROW) || readIf("ROWS")) {
                    command.setFetch(ValueExpression.get(ValueInteger.get(1)));
                } else {
                    command.setFetch(readExpression().optimize(session));
                    if (readIf("PERCENT")) {
                        command.setFetchPercent(true);
                    }
                    if (!readIf(ROW)) {
                        read("ROWS");
                    }
                }
                if (readIf(WITH, "TIES")) {
                    command.setWithTies(true);
                } else {
                    read("ONLY");
                }
            }
            // MySQL-style LIMIT / OFFSET
            if (!hasOffsetOrFetch && database.getMode().limit && readIf(LIMIT)) {
                Expression limit = readExpression().optimize(session);
                if (readIf(OFFSET)) {
                    command.setOffset(readExpression().optimize(session));
                } else if (readIf(COMMA)) {
                    // MySQL: [offset, ] rowcount
                    Expression offset = limit;
                    limit = readExpression().optimize(session);
                    command.setOffset(offset);
                }
                command.setFetch(limit);
            }
            currentSelect = temp;
        }
        if (readIf(FOR)) {
            if (readIf("UPDATE")) {
                if (readIf("OF")) {
                    do {
                        readIdentifierWithSchema();
                    } while (readIf(COMMA));
                }
                ForUpdate forUpdate;
                if (readIf("NOWAIT")) {
                    forUpdate = ForUpdate.NOWAIT;
                } else if (readIf("WAIT")) {
                    BigDecimal timeout;
                    if (currentTokenType != LITERAL || (timeout = token.value(session).getBigDecimal()) == null
                            || timeout.signum() < 0
                            || timeout.compareTo(BigDecimal.valueOf(Integer.MAX_VALUE, 3)) > 0) {
                        throw DbException.getSyntaxError(sqlCommand, token.start(), "timeout (0..2147483.647)");
                    }
                    read();
                    forUpdate = ForUpdate.wait(timeout.movePointRight(3).intValue());
                } else if (readIf("SKIP", "LOCKED")) {
                    forUpdate = ForUpdate.SKIP_LOCKED;
                } else {
                    forUpdate = ForUpdate.DEFAULT;
                }
                command.setForUpdate(forUpdate);
            } else if (readIf("READ") || readIf(FETCH)) {
                read("ONLY");
            }
        }
        if (database.getMode().isolationLevelInSelectOrInsertStatement) {
            parseIsolationClause();
        }
    }

    /**
     * DB2 isolation clause
     */
    private void parseIsolationClause() {
        if (readIf(WITH)) {
            if (readIf("RR") || readIf("RS")) {
                // concurrent-access-resolution clause
                if (readIf("USE", AND, "KEEP")) {
                    if (readIf("SHARE") || readIf("UPDATE") ||
                            readIf("EXCLUSIVE")) {
                        // ignore
                    }
                    read("LOCKS");
                }
            } else if (readIf("CS") || readIf("UR")) {
                // ignore
            }
        }
    }

    private Query parseQueryPrimary() {
        if (readIf(OPEN_PAREN)) {
            Query command = parseQueryExpressionBodyAndEndOfQuery();
            read(CLOSE_PAREN);
            return command;
        }
        int start = tokenIndex;
        if (readIf(SELECT)) {
            return parseSelect(start);
        } else if (readIf(TABLE)) {
            return parseExplicitTable(start);
        }
        read(VALUES);
        return parseValues();
    }

    private void parseSelectFromPart(Select command) {
        do {
            TableFilter top = readTableReference();
            command.addTableFilter(top, true);
            boolean isOuter = false;
            for (;;) {
                TableFilter n = top.getNestedJoin();
                if (n != null) {
                    n.visit(f -> command.addTableFilter(f, false));
                }
                TableFilter join = top.getJoin();
                if (join == null) {
                    break;
                }
                isOuter = isOuter | join.isJoinOuter();
                if (isOuter) {
                    command.addTableFilter(join, false);
                } else {
                    // make flat so the optimizer can work better
                    Expression on = join.getJoinCondition();
                    if (on != null) {
                        command.addCondition(on);
                    }
                    join.removeJoinCondition();
                    top.removeJoin();
                    command.addTableFilter(join, true);
                }
                top = join;
            }
        } while (readIf(COMMA));
    }

    private void parseSelectExpressions(Select command) {
        if (database.getMode().topInSelect && readIf("TOP")) {
            Select temp = currentSelect;
            // make sure aggregate functions will not work in TOP and LIMIT
            currentSelect = null;
            // can't read more complex expressions here because
            // SELECT TOP 1 +? A FROM TEST could mean
            // SELECT TOP (1+?) A FROM TEST or
            // SELECT TOP 1 (+?) AS A FROM TEST
            command.setFetch(readTerm().optimize(session));
            if (readIf("PERCENT")) {
                command.setFetchPercent(true);
            }
            if (readIf(WITH, "TIES")) {
                command.setWithTies(true);
            }
            currentSelect = temp;
        }
        if (readIf(DISTINCT)) {
            if (readIf(ON, OPEN_PAREN)) {
                ArrayList<Expression> distinctExpressions = Utils.newSmallArrayList();
                do {
                    distinctExpressions.add(readExpression());
                } while (readIfMore());
                command.setDistinct(distinctExpressions.toArray(new Expression[0]));
            } else {
                command.setDistinct();
            }
        } else {
            readIf(ALL);
        }
        ArrayList<Expression> expressions = Utils.newSmallArrayList();
        do {
            if (readIf(ASTERISK)) {
                expressions.add(parseWildcard(null, null));
            } else {
                switch (currentTokenType) {
                case FROM:
                case WHERE:
                case GROUP:
                case HAVING:
                case WINDOW:
                case QUALIFY:
                case ORDER:
                case OFFSET:
                case FETCH:
                case CLOSE_PAREN:
                case SEMICOLON:
                case END_OF_INPUT:
                    break;
                default:
                    Expression expr = readExpression();
                    if (readIf(AS) || isIdentifier()) {
                        expr = new Alias(expr, readIdentifier(), database.getMode().aliasColumnName);
                    }
                    expressions.add(expr);
                }
            }
        } while (readIf(COMMA));
        command.setExpressions(expressions);
    }

    private Select parseSelect(int start) {
        Select command = new Select(session, currentSelect);
        Select oldSelect = currentSelect;
        Prepared oldPrepared = currentPrepared;
        BitSet outerUsedParameters = initParametersScope();
        currentSelect = command;
        currentPrepared = command;
        parseSelectExpressions(command);
        if (!readIf(FROM)) {
            // select without FROM
            TableFilter filter = new TableFilter(session, new DualTable(database), null, rightsChecked,
                    currentSelect, 0, null);
            command.addTableFilter(filter, true);
        } else {
            parseSelectFromPart(command);
        }
        if (readIf(WHERE)) {
            command.addCondition(readExpressionWithGlobalConditions());
        }
        // the group by is read for the outer select (or not a select)
        // so that columns that are not grouped can be used
        currentSelect = oldSelect;
        if (readIf(GROUP, "BY")) {
            command.setGroupQuery();
            ArrayList<Expression> list = Utils.newSmallArrayList();
            do {
                if (isToken(OPEN_PAREN) && isOrdinaryGroupingSet()) {
                    if (!readIf(CLOSE_PAREN)) {
                        do {
                            list.add(readExpression());
                        } while (readIfMore());
                    }
                } else {
                    Expression expr = readExpression();
                    if (database.getMode().groupByColumnIndex && expr instanceof ValueExpression &&
                            expr.getType().getValueType() == Value.INTEGER) {
                        ArrayList<Expression> expressions = command.getExpressions();
                        for (Expression e : expressions) {
                            if (e instanceof Wildcard) {
                                throw getSyntaxError();
                            }
                        }
                        int idx = expr.getValue(session).getInt();
                        if (idx < 1 || idx > expressions.size()) {
                            throw DbException.get(ErrorCode.GROUP_BY_NOT_IN_THE_RESULT, Integer.toString(idx),
                                    Integer.toString(expressions.size()));
                        }
                        list.add(expressions.get(idx - 1));
                    } else {
                        list.add(expr);
                    }
                }
            } while (readIf(COMMA));
            if (!list.isEmpty()) {
                command.setGroupBy(list);
            }
        }
        currentSelect = command;
        if (readIf(HAVING)) {
            command.setGroupQuery();
            command.setHaving(readExpressionWithGlobalConditions());
        }
        if (readIf(WINDOW)) {
            do {
                int sqlIndex = token.start();
                String name = readIdentifier();
                read(AS);
                Window w = readWindowSpecification();
                if (!currentSelect.addWindow(name, w)) {
                    throw DbException.getSyntaxError(sqlCommand, sqlIndex, "unique identifier");
                }
            } while (readIf(COMMA));
        }
        if (readIf(QUALIFY)) {
            command.setWindowQuery();
            command.setQualify(readExpressionWithGlobalConditions());
        }
        command.setParameterList(getUsedParameters(outerUsedParameters));
        currentSelect = oldSelect;
        currentPrepared = oldPrepared;
        setSQL(command, start);
        return command;
    }

    /**
     * Checks whether current opening parenthesis can be a start of ordinary
     * grouping set. This method reads this parenthesis if it is.
     *
     * @return whether current opening parenthesis can be a start of ordinary
     *         grouping set
     */
    private boolean isOrdinaryGroupingSet() {
        int offset = scanToCloseParen(tokenIndex + 1);
        if (offset < 0) {
            // Try to parse as expression to get better syntax error
            return false;
        }
        switch (tokens.get(offset).tokenType()) {
        // End of query
        case CLOSE_PAREN:
        case SEMICOLON:
        case END_OF_INPUT:
        // Next grouping element
        case COMMA:
        // Next select clause
        case HAVING:
        case WINDOW:
        case QUALIFY:
        // Next query expression body clause
        case UNION:
        case EXCEPT:
        case MINUS:
        case INTERSECT:
        // Next query expression clause
        case ORDER:
        case OFFSET:
        case FETCH:
        case LIMIT:
        case FOR:
            setTokenIndex(tokenIndex + 1);
            return true;
        default:
            return false;
        }
    }

    private Query parseExplicitTable(int start) {
        Table table = readTableOrView();
        Select command = new Select(session, currentSelect);
        TableFilter filter = new TableFilter(session, table, null, rightsChecked,
                command, orderInFrom++, null);
        command.addTableFilter(filter, true);
        command.setExplicitTable();
        setSQL(command, start);
        return command;
    }

    private void setSQL(Prepared command, int start) {
        String s = sqlCommand;
        int beginIndex = tokens.get(start).start();
        int endIndex = token.start();
        while (beginIndex < endIndex && s.charAt(beginIndex) <= ' ') {
            beginIndex++;
        }
        while (beginIndex < endIndex && s.charAt(endIndex - 1) <= ' ') {
            endIndex--;
        }
        s = s.substring(beginIndex, endIndex);
        ArrayList<Token> commandTokens;
        if (start == 0 && currentTokenType == END_OF_INPUT) {
            commandTokens = tokens;
            if (beginIndex != 0) {
                for (int i = 0, l = commandTokens.size() - 1; i < l; i++) {
                    commandTokens.get(i).subtractFromStart(beginIndex);
                }
            }
            token.setStart(s.length());
            sqlCommand = s;
        } else {
            List<Token> subList = tokens.subList(start, tokenIndex);
            commandTokens = new ArrayList<>(subList.size() + 1);
            for (int i = start; i < tokenIndex; i++) {
                Token t = tokens.get(i).clone();
                t.subtractFromStart(beginIndex);
                commandTokens.add(t);
            }
            commandTokens.add(new Token.EndOfInputToken(s.length()));
        }
        command.setSQL(s, commandTokens);
    }

    private Expression readExpressionOrDefault() {
        if (readIf(DEFAULT)) {
            return ValueExpression.DEFAULT;
        }
        return readExpression();
    }

    private Expression readExpressionWithGlobalConditions() {
        Expression r = readCondition();
        if (readIf(AND)) {
            r = readAnd(new ConditionAndOr(ConditionAndOr.AND, r, readCondition()));
        } else if (readIf("_LOCAL_AND_GLOBAL_")) {
            r = readAnd(new ConditionLocalAndGlobal(r, readCondition()));
        }
        return readExpressionPart2(r);
    }

    private Expression readExpression() {
        return readExpressionPart2(readAnd(readCondition()));
    }

    private Expression readExpressionPart2(Expression r1) {
        if (!readIf(OR)) {
            return r1;
        }
        Expression r2 = readAnd(readCondition());
        if (!readIf(OR)) {
            return new ConditionAndOr(ConditionAndOr.OR, r1, r2);
        }
        // Above logic to avoid allocating an ArrayList for the common case.
        // We combine into ConditionAndOrN here rather than letting the optimisation
        // pass do it, to avoid StackOverflowError during stuff like mapColumns.
        final ArrayList<Expression> expressions = new ArrayList<>();
        expressions.add(r1);
        expressions.add(r2);
        do {
            expressions.add(readAnd(readCondition()));
        }
        while (readIf(OR));
        return new ConditionAndOrN(ConditionAndOr.OR, expressions);
    }

    private Expression readAnd(Expression r) {
        if (!readIf(AND)) {
            return r;
        }
        Expression expr2 = readCondition();
        if (!readIf(AND)) {
            return new ConditionAndOr(ConditionAndOr.AND, r, expr2);
        }
        // Above logic to avoid allocating an ArrayList for the common case.
        // We combine into ConditionAndOrN here rather than letting the optimisation
        // pass do it, to avoid StackOverflowError during stuff like mapColumns.
        final ArrayList<Expression> expressions = new ArrayList<>();
        expressions.add(r);
        expressions.add(expr2);
        do {
            expressions.add(readCondition());
        }
        while (readIf(AND));
        return new ConditionAndOrN(ConditionAndOr.AND, expressions);
    }

    private Expression readCondition() {
        switch (currentTokenType) {
        case NOT:
            read();
            return new ConditionNot(readCondition());
        case EXISTS: {
            read();
            read(OPEN_PAREN);
            Query query = parseQuery();
            // can not reduce expression because it might be a union except
            // query with distinct
            read(CLOSE_PAREN);
            return new ExistsPredicate(query);
        }
        case UNIQUE: {
            read();
            NullsDistinct nullsDistinct = readNullsDistinct(NullsDistinct.DISTINCT);
            read(OPEN_PAREN);
            Query query = parseQuery();
            read(CLOSE_PAREN);
            return new UniquePredicate(query, nullsDistinct);
        }
        default:
            if (readIf("INTERSECTS", OPEN_PAREN)) {
                Expression r1 = readConcat();
                read(COMMA);
                Expression r2 = readConcat();
                read(CLOSE_PAREN);
                return new Comparison(Comparison.SPATIAL_INTERSECTS, r1, r2, false);
            }
            if (expectedList != null) {
                addMultipleExpected(NOT, EXISTS, UNIQUE);
                addExpected("INTERSECTS");
            }
        }
        Expression l, c = readConcat();
        do {
            l = c;
            // special case: NOT NULL is not part of an expression (as in CREATE
            // TABLE TEST(ID INT DEFAULT 0 NOT NULL))
            int backup = tokenIndex;
            boolean not = readIf(NOT);
            if (not && isToken(NULL)) {
                // this really only works for NOT NULL!
                setTokenIndex(backup);
                break;
            }
            c = readConditionRightHandSide(l, not, false);
        } while (c != null);
        return l;
    }

    private Expression readConditionRightHandSide(Expression r, boolean not, boolean whenOperand) {
        if (!not && readIf(IS)) {
            r = readConditionIs(r, whenOperand);
        } else {
            switch (currentTokenType) {
            case BETWEEN: {
                read();
                boolean symmetric = readIf(SYMMETRIC);
                if (!symmetric) {
                    readIf(ASYMMETRIC);
                }
                Expression a = readConcat();
                read(AND);
                r = new BetweenPredicate(r, not, whenOperand, symmetric, a, readConcat());
                break;
            }
            case IN:
                read();
                r = readInPredicate(r, not, whenOperand);
                break;
            case LIKE: {
                read();
                r = readLikePredicate(r, LikeType.LIKE, not, whenOperand);
                break;
            }
            default:
                if (readIf("ILIKE")) {
                    r = readLikePredicate(r, LikeType.ILIKE, not, whenOperand);
                } else if (readIf("REGEXP")) {
                    Expression b = readConcat();
                    recompileAlways = true;
                    r = new CompareLike(database, r, not, whenOperand, b, null, LikeType.REGEXP);
                } else if (not) {
                    if (whenOperand) {
                        return null;
                    }
                    if (expectedList != null) {
                        addMultipleExpected(BETWEEN, IN, LIKE);
                    }
                    throw getSyntaxError();
                } else {
                    int compareType = getCompareType(currentTokenType);
                    if (compareType < 0) {
                        return null;
                    }
                    read();
                    r = readComparison(r, compareType, whenOperand);
                }
            }
        }
        return r;
    }

    private Expression readConditionIs(Expression left, boolean whenOperand) {
        boolean isNot = readIf(NOT);
        switch (currentTokenType) {
        case NULL:
            read();
            left = new NullPredicate(left, isNot, whenOperand);
            break;
        case DISTINCT:
            read();
            read(FROM);
            left = readComparison(left, isNot ? Comparison.EQUAL_NULL_SAFE : Comparison.NOT_EQUAL_NULL_SAFE,
                    whenOperand);
            break;
        case TRUE:
            read();
            left = new BooleanTest(left, isNot, whenOperand, true);
            break;
        case FALSE:
            read();
            left = new BooleanTest(left, isNot, whenOperand, false);
            break;
        case UNKNOWN:
            read();
            left = new BooleanTest(left, isNot, whenOperand, null);
            break;
        default:
            if (readIf("OF")) {
                left = readTypePredicate(left, isNot, whenOperand);
            } else if (readIf("JSON")) {
                left = readJsonPredicate(left, isNot, whenOperand);
            } else {
                if (expectedList != null) {
                    addMultipleExpected(NULL, DISTINCT, TRUE, FALSE, UNKNOWN);
                }
                /*
                 * Databases that were created in 1.4.199 and older
                 * versions can contain invalid generated IS [ NOT ]
                 * expressions.
                 */
                if (whenOperand || !session.isQuirksMode()) {
                    throw getSyntaxError();
                }
                left = new Comparison(isNot ? Comparison.NOT_EQUAL_NULL_SAFE : Comparison.EQUAL_NULL_SAFE, left,
                        readConcat(), false);
            }
        }
        return left;
    }

    private TypePredicate readTypePredicate(Expression left, boolean not, boolean whenOperand) {
        read(OPEN_PAREN);
        ArrayList<TypeInfo> typeList = Utils.newSmallArrayList();
        do {
            typeList.add(parseDataType());
        } while (readIfMore());
        return new TypePredicate(left, not, whenOperand, typeList.toArray(new TypeInfo[0]));
    }

    private Expression readInPredicate(Expression left, boolean not, boolean whenOperand) {
        read(OPEN_PAREN);
        if (!whenOperand && database.getMode().allowEmptyInPredicate && readIf(CLOSE_PAREN)) {
            return ValueExpression.getBoolean(not);
        }
        ArrayList<Expression> v;
        if (isQuery()) {
            Query query = parseQuery();
            if (!readIfMore()) {
                return new ConditionInQuery(left, not, whenOperand, query, false, Comparison.EQUAL);
            }
            v = Utils.newSmallArrayList();
            v.add(new Subquery(query));
        } else {
            v = Utils.newSmallArrayList();
        }
        do {
            v.add(readExpression());
        } while (readIfMore());
        return new ConditionIn(left, not, whenOperand, v);
    }

    private IsJsonPredicate readJsonPredicate(Expression left, boolean not, boolean whenOperand) {
        JSONItemType itemType;
        if (readIf(VALUE)) {
            itemType = JSONItemType.VALUE;
        } else if (readIf(ARRAY)) {
            itemType = JSONItemType.ARRAY;
        } else if (readIf("OBJECT")) {
            itemType = JSONItemType.OBJECT;
        } else if (readIf("SCALAR")) {
            itemType = JSONItemType.SCALAR;
        } else {
            itemType = JSONItemType.VALUE;
        }
        boolean unique = false;
        if (readIf(WITH, UNIQUE)) {
            readIf("KEYS");
            unique = true;
        } else if (readIf("WITHOUT", UNIQUE)) {
            readIf("KEYS");
        }
        return new IsJsonPredicate(left, not, whenOperand, unique, itemType);
    }

    private Expression readLikePredicate(Expression left, LikeType likeType, boolean not, boolean whenOperand) {
        Expression right = readConcat();
        Expression esc = readIf("ESCAPE") ? readConcat() : null;
        recompileAlways = true;
        return new CompareLike(database, left, not, whenOperand, right, esc, likeType);
    }

    private Expression readComparison(Expression left, int compareType, boolean whenOperand) {
        int start = tokenIndex;
        if (readIf(ALL, OPEN_PAREN)) {
            if (isQuery()) {
                left = new ConditionInQuery(left, false, whenOperand, parseQuery(), true, compareType);
            } else {
                left = new ConditionInArray(left, whenOperand, readExpression(), true, compareType);
            }
            read(CLOSE_PAREN);
        } else if (readIf(ANY, OPEN_PAREN)) {
            left = readAnyComparison(left, compareType, whenOperand, start);
        } else if (readIf(SOME, OPEN_PAREN)) {
            left = readAnyComparison(left, compareType, whenOperand, start);
        } else {
            left = new Comparison(compareType, left, readConcat(), whenOperand);
        }
        return left;
    }

    private Expression readAnyComparison(Expression left, int compareType, boolean whenOperand, int start) {
        if (isQuery()) {
            left = new ConditionInQuery(left, false, whenOperand, parseQuery(), false, compareType);
        } else {
            left = new ConditionInArray(left, whenOperand, readExpression(), false, compareType);
        }
        read(CLOSE_PAREN);
        return left;
    }

    private Expression readConcat() {
        Expression op1 = readSum();
        for (;;) {
            switch (currentTokenType) {
            case CONCATENATION: {
                read();
                Expression op2 = readSum();
                if (readIf(CONCATENATION)) {
                    ConcatenationOperation c = new ConcatenationOperation();
                    c.addParameter(op1);
                    c.addParameter(op2);
                    do {
                        c.addParameter(readSum());
                    } while (readIf(CONCATENATION));
                    c.doneWithParameters();
                    op1 = c;
                } else {
                    op1 = new ConcatenationOperation(op1, op2);
                }
                break;
            }
            case TILDE: // PostgreSQL compatibility
                op1 = readTildeCondition(op1, false);
                break;
            case NOT_TILDE: // PostgreSQL compatibility
                op1 = readTildeCondition(op1, true);
                break;
            default:
                // Don't add compatibility operators
                addExpected(CONCATENATION);
                return op1;
            }
        }
    }

    private Expression readSum() {
        Expression r = readFactor();
        while (true) {
            if (readIf(PLUS_SIGN)) {
                r = new BinaryOperation(OpType.PLUS, r, readFactor());
            } else if (readIf(MINUS_SIGN)) {
                r = new BinaryOperation(OpType.MINUS, r, readFactor());
            } else {
                return r;
            }
        }
    }

    private Expression readFactor() {
        Expression r = readTerm();
        while (true) {
            if (readIf(ASTERISK)) {
                r = new BinaryOperation(OpType.MULTIPLY, r, readTerm());
            } else if (readIf(SLASH)) {
                r = new BinaryOperation(OpType.DIVIDE, r, readTerm());
            } else if (readIf(PERCENT)) {
                r = new MathFunction(r, readTerm(), MathFunction.MOD);
            } else {
                return r;
            }
        }
    }

    private Expression readTildeCondition(Expression r, boolean not) {
        read();
        if (readIf(ASTERISK)) {
            r = new CastSpecification(r, TypeInfo.TYPE_VARCHAR_IGNORECASE);
        }
        return new CompareLike(database, r, not, false, readSum(), null, LikeType.REGEXP);
    }

    private Expression readAggregate(AggregateType aggregateType, String aggregateName) {
        if (currentSelect == null) {
            expectedList = null;
            throw getSyntaxError();
        }
        Aggregate r;
        switch (aggregateType) {
        case COUNT:
            if (readIf(ASTERISK)) {
                r = new Aggregate(AggregateType.COUNT_ALL, new Expression[0], currentSelect, false);
            } else {
                boolean distinct = readDistinctAgg();
                Expression on = readExpression();
                if (on instanceof Wildcard && !distinct) {
                    // PostgreSQL compatibility: count(t.*)
                    r = new Aggregate(AggregateType.COUNT_ALL, new Expression[0], currentSelect, false);
                } else {
                    r = new Aggregate(AggregateType.COUNT, new Expression[] { on }, currentSelect, distinct);
                }
            }
            break;
        case COVAR_POP:
        case COVAR_SAMP:
        case CORR:
        case REGR_SLOPE:
        case REGR_INTERCEPT:
        case REGR_COUNT:
        case REGR_R2:
        case REGR_AVGX:
        case REGR_AVGY:
        case REGR_SXX:
        case REGR_SYY:
        case REGR_SXY:
            r = new Aggregate(aggregateType, new Expression[] { readExpression(), readNextArgument() },
                    currentSelect, false);
            break;
        case HISTOGRAM:
            r = new Aggregate(aggregateType, new Expression[] { readExpression() }, currentSelect, false);
            break;
        case LISTAGG: {
            boolean distinct = readDistinctAgg();
            Expression arg = readExpression();
            ListaggArguments extraArguments = new ListaggArguments();
            ArrayList<QueryOrderBy> orderByList;
            if ("STRING_AGG".equals(aggregateName)) {
                // PostgreSQL compatibility: string_agg(expression, delimiter)
                read(COMMA);
                extraArguments.setSeparator(readStringOrParameter());
                orderByList = readIfOrderBy();
            } else if ("GROUP_CONCAT".equals(aggregateName)) {
                orderByList = readIfOrderBy();
                if (readIf("SEPARATOR")) {
                    extraArguments.setSeparator(readStringOrParameter());
                }
            } else {
                if (readIf(COMMA)) {
                    extraArguments.setSeparator(readStringOrParameter());
                }
                if (readIf(ON)) {
                    read("OVERFLOW");
                    if (readIf("TRUNCATE")) {
                        extraArguments.setOnOverflowTruncate(true);
                        if (currentTokenType == LITERAL) {
                            extraArguments.setFilter(readStringOrParameter());
                        }
                        if (!readIf(WITH)) {
                            read("WITHOUT");
                            extraArguments.setWithoutCount(true);
                        }
                        read("COUNT");
                    } else {
                        read("ERROR");
                    }
                }
                orderByList = null;
            }
            Expression[] args = new Expression[] { arg };
            int index = tokenIndex;
            read(CLOSE_PAREN);
            if (orderByList == null && isToken("WITHIN")) {
                r = readWithinGroup(aggregateType, args, distinct, extraArguments, false, false);
            } else {
                setTokenIndex(index);
                r = new Aggregate(AggregateType.LISTAGG, args, currentSelect, distinct);
                r.setExtraArguments(extraArguments);
                if (orderByList != null) {
                    r.setOrderByList(orderByList);
                }
            }
            break;
        }
        case ARRAY_AGG: {
            boolean distinct = readDistinctAgg();
            r = new Aggregate(AggregateType.ARRAY_AGG, new Expression[] { readExpression() }, currentSelect, distinct);
            r.setOrderByList(readIfOrderBy());
            break;
        }
        case RANK:
        case DENSE_RANK:
        case PERCENT_RANK:
        case CUME_DIST: {
            if (isToken(CLOSE_PAREN)) {
                return readWindowFunction(aggregateName);
            }
            ArrayList<Expression> expressions = Utils.newSmallArrayList();
            do {
                expressions.add(readExpression());
            } while (readIfMore());
            r = readWithinGroup(aggregateType, expressions.toArray(new Expression[0]), false, null, true, false);
            break;
        }
        case PERCENTILE_CONT:
        case PERCENTILE_DISC: {
            Expression num = readExpression();
            read(CLOSE_PAREN);
            r = readWithinGroup(aggregateType, new Expression[] { num }, false, null, false, true);
            break;
        }
        case MODE: {
            if (readIf(CLOSE_PAREN)) {
                r = readWithinGroup(AggregateType.MODE, new Expression[0], false, null, false, true);
            } else {
                Expression expr = readExpression();
                r = new Aggregate(AggregateType.MODE, new Expression[0], currentSelect, false);
                if (readIf(ORDER)) {
                    read("BY");
                    Expression expr2 = readExpression();
                    String sql = expr.getSQL(HasSQL.DEFAULT_SQL_FLAGS), sql2 = expr2.getSQL(HasSQL.DEFAULT_SQL_FLAGS);
                    if (!sql.equals(sql2)) {
                        throw DbException.getSyntaxError(ErrorCode.IDENTICAL_EXPRESSIONS_SHOULD_BE_USED, sqlCommand,
                                token.start(), sql, sql2);
                    }
                    readAggregateOrder(r, expr, true);
                } else {
                    readAggregateOrder(r, expr, false);
                }
            }
            break;
        }
        case JSON_OBJECTAGG: {
            boolean withKey = readIf(KEY);
            Expression key = readExpression();
            if (withKey) {
                read(VALUE);
            } else if (!(readIf(VALUE) || (database.getMode().acceptsCommaAsJsonKeyValueSeparator && readIf(COMMA)))) {
                read(COLON);
            }
            Expression value = readExpression();
            r = new Aggregate(AggregateType.JSON_OBJECTAGG, new Expression[] { key, value }, currentSelect, false);
            readJsonObjectFunctionFlags(r, false);
            break;
        }
        case JSON_ARRAYAGG: {
            boolean distinct = readDistinctAgg();
            r = new Aggregate(AggregateType.JSON_ARRAYAGG, new Expression[] { readExpression() }, currentSelect,
                    distinct);
            r.setOrderByList(readIfOrderBy());
            r.setFlags(JsonConstructorUtils.JSON_ABSENT_ON_NULL);
            readJsonObjectFunctionFlags(r, true);
            break;
        }
        default:
            boolean distinct = readDistinctAgg();
            r = new Aggregate(aggregateType, new Expression[] { readExpression() }, currentSelect, distinct);
            break;
        }
        read(CLOSE_PAREN);
        readFilterAndOver(r);
        return r;
    }

    private Aggregate readWithinGroup(AggregateType aggregateType, Expression[] args, boolean distinct,
            Object extraArguments, boolean forHypotheticalSet, boolean simple) {
        read("WITHIN");
        read(GROUP);
        read(OPEN_PAREN);
        read(ORDER);
        read("BY");
        Aggregate r = new Aggregate(aggregateType, args, currentSelect, distinct);
        r.setExtraArguments(extraArguments);
        if (forHypotheticalSet) {
            int count = args.length;
            ArrayList<QueryOrderBy> orderList = new ArrayList<>(count);
            for (int i = 0; i < count; i++) {
                if (i > 0) {
                    read(COMMA);
                }
                orderList.add(parseSortSpecification());
            }
            r.setOrderByList(orderList);
        } else if (simple) {
            readAggregateOrder(r, readExpression(), true);
        } else {
            r.setOrderByList(parseSortSpecificationList());
        }
        return r;
    }

    private void readAggregateOrder(Aggregate r, Expression expr, boolean parseSortType) {
        ArrayList<QueryOrderBy> orderList = new ArrayList<>(1);
        QueryOrderBy order = new QueryOrderBy();
        order.expression = expr;
        if (parseSortType) {
            order.sortType = parseSortType();
        }
        orderList.add(order);
        r.setOrderByList(orderList);
    }

    private ArrayList<QueryOrderBy> readIfOrderBy() {
        if (readIf(ORDER, "BY")) {
            return parseSortSpecificationList();
        }
        return null;
    }

    private ArrayList<QueryOrderBy> parseSortSpecificationList() {
        ArrayList<QueryOrderBy> orderList = Utils.newSmallArrayList();
        do {
            orderList.add(parseSortSpecification());
        } while (readIf(COMMA));
        return orderList;
    }

    private QueryOrderBy parseSortSpecification() {
        QueryOrderBy order = new QueryOrderBy();
        order.expression = readExpression();
        order.sortType = parseSortType();
        return order;
    }

    private Expression readUserDefinedFunctionIf(Schema schema, String functionName) {
        UserDefinedFunction userDefinedFunction = findUserDefinedFunctionWithinPath(schema, functionName);
        if (userDefinedFunction == null) {
            return null;
        } else if (userDefinedFunction instanceof FunctionAlias) {
            FunctionAlias functionAlias = (FunctionAlias) userDefinedFunction;
            ArrayList<Expression> argList = Utils.newSmallArrayList();
            if (!readIf(CLOSE_PAREN)) {
                do {
                    argList.add(readExpression());
                } while (readIfMore());
            }
            return new JavaFunction(functionAlias, argList.toArray(new Expression[0]));
        } else {
            UserAggregate aggregate = (UserAggregate) userDefinedFunction;
            boolean distinct = readDistinctAgg();
            ArrayList<Expression> params = Utils.newSmallArrayList();
            do {
                params.add(readExpression());
            } while (readIfMore());
            Expression[] list = params.toArray(new Expression[0]);
            JavaAggregate agg = new JavaAggregate(aggregate, list, currentSelect, distinct);
            readFilterAndOver(agg);
            return agg;
        }
    }

    private boolean readDistinctAgg() {
        if (readIf(DISTINCT)) {
            return true;
        }
        readIf(ALL);
        return false;
    }

    private void readFilterAndOver(AbstractAggregate aggregate) {
        if (readIf("FILTER", OPEN_PAREN, WHERE)) {
            Expression filterCondition = readExpression();
            read(CLOSE_PAREN);
            aggregate.setFilterCondition(filterCondition);
        }
        readOver(aggregate);
    }

    private void readOver(DataAnalysisOperation operation) {
        if (readIf("OVER")) {
            operation.setOverCondition(readWindowNameOrSpecification());
            currentSelect.setWindowQuery();
        } else if (operation.isAggregate()) {
            currentSelect.setGroupQuery();
        } else {
            throw getSyntaxError();
        }
    }

    private Window readWindowNameOrSpecification() {
        return isToken(OPEN_PAREN) ? readWindowSpecification() : new Window(readIdentifier(), null, null, null);
    }

    private Window readWindowSpecification() {
        read(OPEN_PAREN);
        String parent = null;
        if (currentTokenType == IDENTIFIER) {
            String current = currentToken;
            if (token.isQuoted() || ( //
                    !equalsToken(current, "PARTITION") //
                    && !equalsToken(current, "ROWS") //
                    && !equalsToken(current, "RANGE") //
                    && !equalsToken(current, "GROUPS"))) {
                parent = current;
                read();
            }
        }
        ArrayList<Expression> partitionBy = null;
        if (readIf("PARTITION", "BY")) {
            partitionBy = Utils.newSmallArrayList();
            do {
                Expression expr = readExpression();
                partitionBy.add(expr);
            } while (readIf(COMMA));
        }
        ArrayList<QueryOrderBy> orderBy = readIfOrderBy();
        WindowFrame frame = readWindowFrame();
        read(CLOSE_PAREN);
        return new Window(parent, partitionBy, orderBy, frame);
    }

    private WindowFrame readWindowFrame() {
        WindowFrameUnits units;
        if (readIf("ROWS")) {
            units = WindowFrameUnits.ROWS;
        } else if (readIf("RANGE")) {
            units = WindowFrameUnits.RANGE;
        } else if (readIf("GROUPS")) {
            units = WindowFrameUnits.GROUPS;
        } else {
            return null;
        }
        WindowFrameBound starting, following;
        if (readIf(BETWEEN)) {
            starting = readWindowFrameRange();
            read(AND);
            following = readWindowFrameRange();
        } else {
            starting = readWindowFrameStarting();
            following = null;
        }
        int sqlIndex = token.start();
        WindowFrameExclusion exclusion = WindowFrameExclusion.EXCLUDE_NO_OTHERS;
        if (readIf("EXCLUDE")) {
            if (readIf("CURRENT", ROW)) {
                exclusion = WindowFrameExclusion.EXCLUDE_CURRENT_ROW;
            } else if (readIf(GROUP)) {
                exclusion = WindowFrameExclusion.EXCLUDE_GROUP;
            } else if (readIf("TIES")) {
                exclusion = WindowFrameExclusion.EXCLUDE_TIES;
            } else {
                read("NO");
                read("OTHERS");
            }
        }
        WindowFrame frame = new WindowFrame(units, starting, following, exclusion);
        if (!frame.isValid()) {
            throw DbException.getSyntaxError(sqlCommand, sqlIndex);
        }
        return frame;
    }

    private WindowFrameBound readWindowFrameStarting() {
        if (readIf("UNBOUNDED")) {
            read("PRECEDING");
            return new WindowFrameBound(WindowFrameBoundType.UNBOUNDED_PRECEDING, null);
        }
        if (readIf("CURRENT")) {
            read(ROW);
            return new WindowFrameBound(WindowFrameBoundType.CURRENT_ROW, null);
        }
        Expression value = readExpression();
        read("PRECEDING");
        return new WindowFrameBound(WindowFrameBoundType.PRECEDING, value);
    }

    private WindowFrameBound readWindowFrameRange() {
        if (readIf("UNBOUNDED")) {
            if (readIf("PRECEDING")) {
                return new WindowFrameBound(WindowFrameBoundType.UNBOUNDED_PRECEDING, null);
            }
            read("FOLLOWING");
            return new WindowFrameBound(WindowFrameBoundType.UNBOUNDED_FOLLOWING, null);
        }
        if (readIf("CURRENT")) {
            read(ROW);
            return new WindowFrameBound(WindowFrameBoundType.CURRENT_ROW, null);
        }
        Expression value = readExpression();
        if (readIf("PRECEDING")) {
            return new WindowFrameBound(WindowFrameBoundType.PRECEDING, value);
        }
        read("FOLLOWING");
        return new WindowFrameBound(WindowFrameBoundType.FOLLOWING, value);
    }

    private Expression readFunction(Schema schema, String name) {
        String upperName = upperName(name);
        if (schema != null) {
            return readFunctionWithSchema(schema, name, upperName);
        }
        boolean allowOverride = database.isAllowBuiltinAliasOverride();
        if (allowOverride) {
            Expression e = readUserDefinedFunctionIf(null, name);
            if (e != null) {
                return e;
            }
        }
        AggregateType agg = Aggregate.getAggregateType(upperName);
        if (agg != null) {
            return readAggregate(agg, upperName);
        }
        Expression e = readBuiltinFunctionIf(upperName);
        if (e != null) {
            return e;
        }
        e = readWindowFunction(upperName);
        if (e != null) {
            return e;
        }
        e = readCompatibilityFunction(upperName);
        if (e != null) {
            return e;
        }
        if (!allowOverride) {
            e = readUserDefinedFunctionIf(null, name);
            if (e != null) {
                return e;
            }
        }
        throw DbException.get(ErrorCode.FUNCTION_NOT_FOUND_1, name);
    }

    private Expression readFunctionWithSchema(Schema schema, String name, String upperName) {
        if (database.getMode().getEnum() == ModeEnum.PostgreSQL
                && schema.getName().equals(database.sysIdentifier("PG_CATALOG"))) {
            FunctionsPostgreSQL function = FunctionsPostgreSQL.getFunction(upperName);
            if (function != null) {
                return readParameters(function);
            }
        }
        Expression function = readUserDefinedFunctionIf(schema, name);
        if (function != null) {
            return function;
        }
        throw DbException.get(ErrorCode.FUNCTION_NOT_FOUND_1, name);
    }

    private Expression readCompatibilityFunction(String name) {
        switch (name) {
        // ||
        case "ARRAY_APPEND":
        case "ARRAY_CAT":
            return new ConcatenationOperation(readExpression(), readLastArgument());
        // []
        case "ARRAY_GET":
            return new ArrayElementReference(readExpression(), readLastArgument());
        // CARDINALITY
        case "ARRAY_LENGTH":
            return new CardinalityExpression(readSingleArgument(), false);
        // Simple case
        case "DECODE": {
            Expression caseOperand = readExpression();
            boolean canOptimize = caseOperand.isConstant() && !caseOperand.getValue(session).containsNull();
            Expression a = readNextArgument(), b = readNextArgument();
            SimpleCase.SimpleWhen when = decodeToWhen(caseOperand, canOptimize, a, b), current = when;
            Expression elseResult = null;
            while (readIf(COMMA)) {
                a = readExpression();
                if (readIf(COMMA)) {
                    b = readExpression();
                    SimpleCase.SimpleWhen next = decodeToWhen(caseOperand, canOptimize, a, b);
                    current.setWhen(next);
                    current = next;
                } else {
                    elseResult = a;
                    break;
                }
            }
            read(CLOSE_PAREN);
            return new SimpleCase(caseOperand, when, elseResult);
        }
        // Searched case
        case "CASEWHEN":
            return readCompatibilityCase(readExpression());
        case "NVL2":
            return readCompatibilityCase(new NullPredicate(readExpression(), true, false));
        // Cast specification
        case "CONVERT": {
            Expression arg;
            Column column;
            if (database.getMode().swapConvertFunctionParameters) {
                column = parseColumnWithType(null);
                arg = readNextArgument();
            } else {
                arg = readExpression();
                read(COMMA);
                column = parseColumnWithType(null);
            }
            read(CLOSE_PAREN);
            return new CastSpecification(arg, column);
        }
        // COALESCE
        case "IFNULL":
            return new CoalesceFunction(CoalesceFunction.COALESCE, readExpression(), readLastArgument());
        case "NVL":
            return readCoalesceFunction(CoalesceFunction.COALESCE);
        // CURRENT_CATALOG
        case "DATABASE":
            read(CLOSE_PAREN);
            return new CurrentGeneralValueSpecification(CurrentGeneralValueSpecification.CURRENT_CATALOG);
        // CURRENT_DATE
        case "CURDATE":
            return readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.CURRENT_DATE, true, name);
        case "TODAY":
            read(CLOSE_PAREN);
            return ModeFunction.getCompatibilityDateTimeValueFunction(database, "TODAY", -1);
        // CURRENT_SCHEMA
        case "SCHEMA":
            read(CLOSE_PAREN);
            return new CurrentGeneralValueSpecification(CurrentGeneralValueSpecification.CURRENT_SCHEMA);
        // CURRENT_TIMESTAMP
        case "SYSTIMESTAMP": {
            int scale = -1;
            if (!readIf(CLOSE_PAREN)) {
                scale = readInt();
                if (scale < 0 || scale > ValueTime.MAXIMUM_SCALE) {
                    throw DbException.get(ErrorCode.INVALID_VALUE_SCALE, Integer.toString(scale), "0",
                            /* compile-time constant */ "" + ValueTime.MAXIMUM_SCALE);
                }
                read(CLOSE_PAREN);
            }
            return ModeFunction.getCompatibilityDateTimeValueFunction(database, "SYSTIMESTAMP", scale);
        }
        // EXTRACT
        case "DAY":
        case "DAY_OF_MONTH":
        case "DAYOFMONTH":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.DAY, readSingleArgument(), null);
        case "DAY_OF_WEEK":
        case "DAYOFWEEK":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.DAY_OF_WEEK, readSingleArgument(),
                    null);
        case "DAY_OF_YEAR":
        case "DAYOFYEAR":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.DAY_OF_YEAR, readSingleArgument(),
                    null);
        case "HOUR":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.HOUR, readSingleArgument(), null);
        case "ISO_DAY_OF_WEEK":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.ISO_DAY_OF_WEEK,
                    readSingleArgument(), null);
        case "ISO_WEEK":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.ISO_WEEK, readSingleArgument(),
                    null);
        case "ISO_YEAR":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.ISO_WEEK_YEAR, readSingleArgument(),
                    null);
        case "MINUTE":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.MINUTE, readSingleArgument(), null);
        case "MONTH":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.MONTH, readSingleArgument(), null);
        case "QUARTER":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.QUARTER, readSingleArgument(), //
                    null);
        case "SECOND":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.SECOND, readSingleArgument(), null);
        case "WEEK":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.WEEK, readSingleArgument(), null);
        case "YEAR":
            return new DateTimeFunction(DateTimeFunction.EXTRACT, DateTimeFunction.YEAR, readSingleArgument(), null);
        // LOCALTIME
        case "CURTIME":
            return readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.LOCALTIME, true, "CURTIME");
        // LOCALTIMESTAMP
        case "NOW":
            return readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.LOCALTIMESTAMP, true, "NOW");
        case "SYSDATE":
            read(CLOSE_PAREN);
            return ModeFunction.getCompatibilityDateTimeValueFunction(database, "SYSDATE", -1);
        // LOCATE
        case "INSTR": {
            Expression arg1 = readExpression();
            return new StringFunction(readNextArgument(), arg1, readIfArgument(), StringFunction.LOCATE);
        }
        case "POSITION": {
            // can't read expression because IN would be read too early
            Expression arg1 = readConcat();
            if (!readIf(COMMA)) {
                read(IN);
            }
            return new StringFunction(arg1, readSingleArgument(), null, StringFunction.LOCATE);
        }
        // LOWER
        case "LCASE":
            return new StringFunction1(readSingleArgument(), StringFunction1.LOWER);
        // SUBSTRING
        case "SUBSTR":
            return readSubstringFunction();
        // UPPER
        case "UCASE":
            return new StringFunction1(readSingleArgument(), StringFunction1.UPPER);
        // Sequence value
        case "CURRVAL":
            return readCompatibilitySequenceValueFunction(true);
        case "NEXTVAL":
            return readCompatibilitySequenceValueFunction(false);
        default:
            return null;
        }
    }

    private <T extends ExpressionWithVariableParameters> T readParameters(T expression) {
        if (!readIf(CLOSE_PAREN)) {
            do {
                expression.addParameter(readExpression());
            } while (readIfMore());
        }
        expression.doneWithParameters();
        return expression;
    }

    private SimpleCase.SimpleWhen decodeToWhen(Expression caseOperand, boolean canOptimize, Expression whenOperand,
            Expression result) {
        if (!canOptimize && (!whenOperand.isConstant() || whenOperand.getValue(session).containsNull())) {
            whenOperand = new Comparison(Comparison.EQUAL_NULL_SAFE, caseOperand, whenOperand, true);
        }
        return new SimpleCase.SimpleWhen(whenOperand, result);
    }

    private Expression readCompatibilityCase(Expression when) {
        return new SearchedCase(new Expression[] { when, readNextArgument(), readLastArgument() });
    }

    private Expression readCompatibilitySequenceValueFunction(boolean current) {
        Expression arg1 = readExpression(), arg2 = readIf(COMMA) ? readExpression() : null;
        read(CLOSE_PAREN);
        return new CompatibilitySequenceValueFunction(arg1, arg2, current);
    }

    private Expression readBuiltinFunctionIf(String upperName) {
        switch (upperName) {
        case "ABS":
            return new MathFunction(readSingleArgument(), null, MathFunction.ABS);
        case "MOD":
            return new MathFunction(readExpression(), readLastArgument(), MathFunction.MOD);
        case "SIN":
            return new MathFunction1(readSingleArgument(), MathFunction1.SIN);
        case "COS":
            return new MathFunction1(readSingleArgument(), MathFunction1.COS);
        case "TAN":
            return new MathFunction1(readSingleArgument(), MathFunction1.TAN);
        case "COT":
            return new MathFunction1(readSingleArgument(), MathFunction1.COT);
        case "SINH":
            return new MathFunction1(readSingleArgument(), MathFunction1.SINH);
        case "COSH":
            return new MathFunction1(readSingleArgument(), MathFunction1.COSH);
        case "TANH":
            return new MathFunction1(readSingleArgument(), MathFunction1.TANH);
        case "ASIN":
            return new MathFunction1(readSingleArgument(), MathFunction1.ASIN);
        case "ACOS":
            return new MathFunction1(readSingleArgument(), MathFunction1.ACOS);
        case "ATAN":
            return new MathFunction1(readSingleArgument(), MathFunction1.ATAN);
        case "ATAN2":
            return new MathFunction2(readExpression(), readLastArgument(), MathFunction2.ATAN2);
        case "LOG": {
            Expression arg1 = readExpression();
            if (readIf(COMMA)) {
                return new MathFunction2(arg1, readSingleArgument(), MathFunction2.LOG);
            } else {
                read(CLOSE_PAREN);
                return new MathFunction1(arg1,
                        database.getMode().logIsLogBase10 ? MathFunction1.LOG10 : MathFunction1.LN);
            }
        }
        case "LOG10":
            return new MathFunction1(readSingleArgument(), MathFunction1.LOG10);
        case "LN":
            return new MathFunction1(readSingleArgument(), MathFunction1.LN);
        case "EXP":
            return new MathFunction1(readSingleArgument(), MathFunction1.EXP);
        case "POWER":
            return new MathFunction2(readExpression(), readLastArgument(), MathFunction2.POWER);
        case "SQRT":
            return new MathFunction1(readSingleArgument(), MathFunction1.SQRT);
        case "FLOOR":
            return new MathFunction(readSingleArgument(), null, MathFunction.FLOOR);
        case "CEIL":
        case "CEILING":
            return new MathFunction(readSingleArgument(), null, MathFunction.CEIL);
        case "ROUND":
            return new MathFunction(readExpression(), readIfArgument(), MathFunction.ROUND);
        case "ROUNDMAGIC":
            return new MathFunction(readSingleArgument(), null, MathFunction.ROUNDMAGIC);
        case "SIGN":
            return new MathFunction(readSingleArgument(), null, MathFunction.SIGN);
        case "TRUNC":
        case "TRUNCATE":
            return new MathFunction(readExpression(), readIfArgument(), MathFunction.TRUNC);
        case "DEGREES":
            return new MathFunction1(readSingleArgument(), MathFunction1.DEGREES);
        case "RADIANS":
            return new MathFunction1(readSingleArgument(), MathFunction1.RADIANS);
        case "BITAND":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.BITAND);
        case "BITOR":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.BITOR);
        case "BITXOR":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.BITXOR);
        case "BITNOT":
            return new BitFunction(readSingleArgument(), null, BitFunction.BITNOT);
        case "BITNAND":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.BITNAND);
        case "BITNOR":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.BITNOR);
        case "BITXNOR":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.BITXNOR);
        case "BITGET":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.BITGET);
        case "BITCOUNT":
            return new BitFunction(readSingleArgument(), null, BitFunction.BITCOUNT);
        case "LSHIFT":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.LSHIFT);
        case "RSHIFT":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.RSHIFT);
        case "ULSHIFT":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.ULSHIFT);
        case "URSHIFT":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.URSHIFT);
        case "ROTATELEFT":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.ROTATELEFT);
        case "ROTATERIGHT":
            return new BitFunction(readExpression(), readLastArgument(), BitFunction.ROTATERIGHT);
        case "EXTRACT": {
            int field = readDateTimeField();
            read(FROM);
            return new DateTimeFunction(DateTimeFunction.EXTRACT, field, readSingleArgument(), null);
        }
        case "DATE_TRUNC":
            return new DateTimeFunction(DateTimeFunction.DATE_TRUNC, readDateTimeField(), readLastArgument(), null);
        case "DATEADD":
        case "TIMESTAMPADD":
            return new DateTimeFunction(DateTimeFunction.DATEADD, readDateTimeField(), readNextArgument(),
                    readLastArgument());
        case "DATEDIFF":
        case "TIMESTAMPDIFF":
            return new DateTimeFunction(DateTimeFunction.DATEDIFF, readDateTimeField(), readNextArgument(),
                    readLastArgument());
        case "LAST_DAY":
            return new DateTimeFunction(DateTimeFunction.LAST_DAY, -1, readSingleArgument(), null);
        case "FORMATDATETIME":
            return readDateTimeFormatFunction(DateTimeFormatFunction.FORMATDATETIME);
        case "PARSEDATETIME":
            return readDateTimeFormatFunction(DateTimeFormatFunction.PARSEDATETIME);
        case "DAYNAME":
            return new DayMonthNameFunction(readSingleArgument(), DayMonthNameFunction.DAYNAME);
        case "MONTHNAME":
            return new DayMonthNameFunction(readSingleArgument(), DayMonthNameFunction.MONTHNAME);
        case "CARDINALITY":
            return new CardinalityExpression(readSingleArgument(), false);
        case "ARRAY_MAX_CARDINALITY":
            return new CardinalityExpression(readSingleArgument(), true);
        case "LOCATE":
            return new StringFunction(readExpression(), readNextArgument(), readIfArgument(), StringFunction.LOCATE);
        case "INSERT":
            return new StringFunction(readExpression(), readNextArgument(), readNextArgument(), readLastArgument(),
                    StringFunction.INSERT);
        case "REPLACE":
            return new StringFunction(readExpression(), readNextArgument(), readIfArgument(), StringFunction.REPLACE);
        case "LPAD":
            return new StringFunction(readExpression(), readNextArgument(), readIfArgument(), StringFunction.LPAD);
        case "RPAD":
            return new StringFunction(readExpression(), readNextArgument(), readIfArgument(), StringFunction.RPAD);
        case "TRANSLATE":
            return new StringFunction(readExpression(), readNextArgument(), readLastArgument(),
                    StringFunction.TRANSLATE);
        case "UPPER":
            return new StringFunction1(readSingleArgument(), StringFunction1.UPPER);
        case "LOWER":
            return new StringFunction1(readSingleArgument(), StringFunction1.LOWER);
        case "ASCII":
            return new StringFunction1(readSingleArgument(), StringFunction1.ASCII);
        case "CHAR":
        case "CHR":
            return new StringFunction1(readSingleArgument(), StringFunction1.CHAR);
        case "STRINGENCODE":
            return new StringFunction1(readSingleArgument(), StringFunction1.STRINGENCODE);
        case "STRINGDECODE":
            return new StringFunction1(readSingleArgument(), StringFunction1.STRINGDECODE);
        case "STRINGTOUTF8":
            return new StringFunction1(readSingleArgument(), StringFunction1.STRINGTOUTF8);
        case "UTF8TOSTRING":
            return new StringFunction1(readSingleArgument(), StringFunction1.UTF8TOSTRING);
        case "HEXTORAW":
            return new StringFunction1(readSingleArgument(), StringFunction1.HEXTORAW);
        case "RAWTOHEX":
            return new StringFunction1(readSingleArgument(), StringFunction1.RAWTOHEX);
        case "SPACE":
            return new StringFunction1(readSingleArgument(), StringFunction1.SPACE);
        case "QUOTE_IDENT":
            return new StringFunction1(readSingleArgument(), StringFunction1.QUOTE_IDENT);
        case "SUBSTRING":
            return readSubstringFunction();
        case "TO_CHAR": {
            Expression arg1 = readExpression(), arg2, arg3;
            if (readIf(COMMA)) {
                arg2 = readExpression();
                arg3 = readIf(COMMA) ? readExpression() : null;
            } else {
                arg3 = arg2 = null;
            }
            read(CLOSE_PAREN);
            return new ToCharFunction(arg1, arg2, arg3);
        }
        case "REPEAT":
            return new StringFunction2(readExpression(), readLastArgument(), StringFunction2.REPEAT);
        case "CHAR_LENGTH":
        case "CHARACTER_LENGTH":
        case "LENGTH":
            return new LengthFunction(readIfSingleArgument(), LengthFunction.CHAR_LENGTH);
        case "OCTET_LENGTH":
            return new LengthFunction(readIfSingleArgument(), LengthFunction.OCTET_LENGTH);
        case "BIT_LENGTH":
            return new LengthFunction(readIfSingleArgument(), LengthFunction.BIT_LENGTH);
        case "TRIM":
            return readTrimFunction();
        case "LTRIM":
            return new TrimFunction(readExpression(), readIfArgument(),
                    TrimFunction.LEADING | TrimFunction.MULTI_CHARACTER);
        case "RTRIM":
            return new TrimFunction(readExpression(), readIfArgument(),
                    TrimFunction.TRAILING | TrimFunction.MULTI_CHARACTER);
        case "BTRIM":
            return new TrimFunction(readExpression(), readIfArgument(),
                    TrimFunction.LEADING | TrimFunction.TRAILING | TrimFunction.MULTI_CHARACTER);
        case "REGEXP_LIKE":
            return readParameters(new RegexpFunction(RegexpFunction.REGEXP_LIKE));
        case "REGEXP_REPLACE":
            return readParameters(new RegexpFunction(RegexpFunction.REGEXP_REPLACE));
        case "REGEXP_SUBSTR":
            return readParameters(new RegexpFunction(RegexpFunction.REGEXP_SUBSTR));
        case "XMLATTR":
            return readParameters(new XMLFunction(XMLFunction.XMLATTR));
        case "XMLCDATA":
            return readParameters(new XMLFunction(XMLFunction.XMLCDATA));
        case "XMLCOMMENT":
            return readParameters(new XMLFunction(XMLFunction.XMLCOMMENT));
        case "XMLNODE":
            return readParameters(new XMLFunction(XMLFunction.XMLNODE));
        case "XMLSTARTDOC":
            return readParameters(new XMLFunction(XMLFunction.XMLSTARTDOC));
        case "XMLTEXT":
            return readParameters(new XMLFunction(XMLFunction.XMLTEXT));
        case "TRIM_ARRAY":
            return new ArrayFunction(readExpression(), readLastArgument(), null, ArrayFunction.TRIM_ARRAY);
        case "ARRAY_CONTAINS":
            return new ArrayFunction(readExpression(), readLastArgument(), null, ArrayFunction.ARRAY_CONTAINS);
        case "ARRAY_SLICE":
            return new ArrayFunction(readExpression(), readNextArgument(), readLastArgument(),
                    ArrayFunction.ARRAY_SLICE);
        case "COMPRESS":
            return new CompressFunction(readExpression(), readIfArgument(), CompressFunction.COMPRESS);
        case "EXPAND":
            return new CompressFunction(readSingleArgument(), null, CompressFunction.EXPAND);
        case "SOUNDEX":
            return new SoundexFunction(readSingleArgument(), null, SoundexFunction.SOUNDEX);
        case "DIFFERENCE":
            return new SoundexFunction(readExpression(), readLastArgument(), SoundexFunction.DIFFERENCE);
        case "JSON_OBJECT": {
            JsonConstructorFunction function = new JsonConstructorFunction(false);
            if (currentTokenType != CLOSE_PAREN && !readJsonObjectFunctionFlags(function, false)) {
                do {
                    boolean withKey = readIf(KEY);
                    function.addParameter(readExpression());
                    if (withKey) {
                        read(VALUE);
                    } else {
                        if (!(readIf(VALUE) ||
                                (database.getMode().acceptsCommaAsJsonKeyValueSeparator && readIf(COMMA)))) {
                            read(COLON);
                        }
                    }
                    function.addParameter(readExpression());
                } while (readIf(COMMA));
                readJsonObjectFunctionFlags(function, false);
            }
            read(CLOSE_PAREN);
            function.doneWithParameters();
            return function;
        }
        case "JSON_ARRAY": {
            JsonConstructorFunction function = new JsonConstructorFunction(true);
            function.setFlags(JsonConstructorUtils.JSON_ABSENT_ON_NULL);
            if (currentTokenType != CLOSE_PAREN && !readJsonObjectFunctionFlags(function, true)) {
                do {
                    function.addParameter(readExpression());
                } while (readIf(COMMA));
                readJsonObjectFunctionFlags(function, true);
            }
            read(CLOSE_PAREN);
            function.doneWithParameters();
            return function;
        }
        case "ENCRYPT":
            return new CryptFunction(readExpression(), readNextArgument(), readLastArgument(), CryptFunction.ENCRYPT);
        case "DECRYPT":
            return new CryptFunction(readExpression(), readNextArgument(), readLastArgument(), CryptFunction.DECRYPT);
        case "COALESCE":
            return readCoalesceFunction(CoalesceFunction.COALESCE);
        case "GREATEST":
            return readCoalesceFunction(CoalesceFunction.GREATEST);
        case "LEAST":
            return readCoalesceFunction(CoalesceFunction.LEAST);
        case "NULLIF":
            return new NullIfFunction(readExpression(), readLastArgument());
        case "CONCAT":
            return readConcatFunction(ConcatFunction.CONCAT);
        case "CONCAT_WS":
            return readConcatFunction(ConcatFunction.CONCAT_WS);
        case "HASH":
            return new HashFunction(readExpression(), readNextArgument(), readIfArgument(), HashFunction.HASH);
        case "ORA_HASH": {
            Expression arg1 = readExpression();
            if (readIfMore()) {
                return new HashFunction(arg1, readExpression(), readIfArgument(), HashFunction.ORA_HASH);
            }
            return new HashFunction(arg1, HashFunction.ORA_HASH);
        }
        case "RAND":
        case "RANDOM":
            return new RandFunction(readIfSingleArgument(), RandFunction.RAND);
        case "SECURE_RAND":
            return new RandFunction(readSingleArgument(), RandFunction.SECURE_RAND);
        case "RANDOM_UUID":
        case "UUID":
            read(CLOSE_PAREN);
            return new RandFunction(null, RandFunction.RANDOM_UUID);
        case "ABORT_SESSION":
            return new SessionControlFunction(readIfSingleArgument(), SessionControlFunction.ABORT_SESSION);
        case "CANCEL_SESSION":
            return new SessionControlFunction(readIfSingleArgument(), SessionControlFunction.CANCEL_SESSION);
        case "AUTOCOMMIT":
            read(CLOSE_PAREN);
            return new SysInfoFunction(SysInfoFunction.AUTOCOMMIT);
        case "DATABASE_PATH":
            read(CLOSE_PAREN);
            return new SysInfoFunction(SysInfoFunction.DATABASE_PATH);
        case "H2VERSION":
            read(CLOSE_PAREN);
            return new SysInfoFunction(SysInfoFunction.H2VERSION);
        case "LOCK_MODE":
            read(CLOSE_PAREN);
            return new SysInfoFunction(SysInfoFunction.LOCK_MODE);
        case "LOCK_TIMEOUT":
            read(CLOSE_PAREN);
            return new SysInfoFunction(SysInfoFunction.LOCK_TIMEOUT);
        case "MEMORY_FREE":
            read(CLOSE_PAREN);
            return new SysInfoFunction(SysInfoFunction.MEMORY_FREE);
        case "MEMORY_USED":
            read(CLOSE_PAREN);
            return new SysInfoFunction(SysInfoFunction.MEMORY_USED);
        case "READONLY":
            read(CLOSE_PAREN);
            return new SysInfoFunction(SysInfoFunction.READONLY);
        case "SESSION_ID":
            read(CLOSE_PAREN);
            return new SysInfoFunction(SysInfoFunction.SESSION_ID);
        case "TRANSACTION_ID":
            read(CLOSE_PAREN);
            return new SysInfoFunction(SysInfoFunction.TRANSACTION_ID);
        case "DISK_SPACE_USED":
            return new TableInfoFunction(readIfSingleArgument(), null, TableInfoFunction.DISK_SPACE_USED);
        case "ESTIMATED_ENVELOPE":
            return new TableInfoFunction(readExpression(), readLastArgument(), TableInfoFunction.ESTIMATED_ENVELOPE);
        case "FILE_READ":
            return new FileFunction(readExpression(), readIfArgument(), FileFunction.FILE_READ);
        case "FILE_WRITE":
            return new FileFunction(readExpression(), readLastArgument(), FileFunction.FILE_WRITE);
        case "DATA_TYPE_SQL":
            return new DataTypeSQLFunction(readExpression(), readNextArgument(), readNextArgument(),
                    readLastArgument());
        case "DB_OBJECT_ID":
            return new DBObjectFunction(readExpression(), readNextArgument(), readIfArgument(),
                    DBObjectFunction.DB_OBJECT_ID);
        case "DB_OBJECT_SQL":
            return new DBObjectFunction(readExpression(), readNextArgument(), readIfArgument(),
                    DBObjectFunction.DB_OBJECT_SQL);
        case "CSVWRITE":
            return readParameters(new CSVWriteFunction());
        case "SIGNAL":
            return new SignalFunction(readExpression(), readLastArgument());
        case "TRUNCATE_VALUE":
            return new TruncateValueFunction(readExpression(), readNextArgument(), readLastArgument());
        case "ZERO":
            read(CLOSE_PAREN);
            return ValueExpression.get(ValueInteger.get(0));
        case "PI":
            read(CLOSE_PAREN);
            return ValueExpression.get(ValueDouble.get(Math.PI));
        }
        ModeFunction function = ModeFunction.getFunction(database, upperName);
        return function != null ? readParameters(function) : null;
    }

    private Expression readDateTimeFormatFunction(int function) {
        DateTimeFormatFunction f = new DateTimeFormatFunction(function);
        f.addParameter(readExpression());
        read(COMMA);
        f.addParameter(readExpression());
        if (readIf(COMMA)) {
            f.addParameter(readExpression());
            if (readIf(COMMA)) {
                f.addParameter(readExpression());
            }
        }
        read(CLOSE_PAREN);
        f.doneWithParameters();
        return f;
    }

    private Expression readTrimFunction() {
        int flags;
        boolean needFrom = false;
        if (readIf("LEADING")) {
            flags = TrimFunction.LEADING;
            needFrom = true;
        } else if (readIf("TRAILING")) {
            flags = TrimFunction.TRAILING;
            needFrom = true;
        } else {
            needFrom = readIf("BOTH");
            flags = TrimFunction.LEADING | TrimFunction.TRAILING;
        }
        Expression from, space = null;
        if (needFrom) {
            if (!readIf(FROM)) {
                space = readExpression();
                read(FROM);
            }
            from = readExpression();
        } else {
            if (readIf(FROM)) {
                from = readExpression();
            } else {
                from = readExpression();
                if (readIf(FROM)) {
                    space = from;
                    from = readExpression();
                } else if (readIf(COMMA)) {
                    space = readExpression();
                }
            }
        }
        read(CLOSE_PAREN);
        return new TrimFunction(from, space, flags);
    }

    private ArrayTableFunction readUnnestFunction() {
        ArrayTableFunction f = new ArrayTableFunction(ArrayTableFunction.UNNEST);
        ArrayList<Column> columns = Utils.newSmallArrayList();
        if (!readIf(CLOSE_PAREN)) {
            int i = 0;
            do {
                Expression expr = readExpression();
                TypeInfo columnType = TypeInfo.TYPE_NULL;
                boolean constant = expr.isConstant();
                if (constant || expr instanceof CastSpecification) {
                    if (constant) {
                        expr = expr.optimize(session);
                    }
                    TypeInfo exprType = expr.getType();
                    switch (exprType.getValueType()) {
                    case Value.JSON:
                        columnType = TypeInfo.TYPE_JSON;
                        break;
                    case Value.ARRAY:
                        columnType = (TypeInfo) exprType.getExtTypeInfo();
                        break;
                    }
                }
                f.addParameter(expr);
                columns.add(new Column("C" + ++i, columnType));
            } while (readIfMore());
        }
        if (readIf(WITH, "ORDINALITY")) {
            columns.add(new Column("NORD", TypeInfo.TYPE_INTEGER));
        }
        f.setColumns(columns);
        f.doneWithParameters();
        return f;
    }

    private ArrayTableFunction readTableFunction(int functionType) {
        ArrayTableFunction f = new ArrayTableFunction(functionType);
        ArrayList<Column> columns = Utils.newSmallArrayList();
        do {
            columns.add(parseColumnWithType(readIdentifier()));
            read(EQUAL);
            f.addParameter(readExpression());
        } while (readIfMore());
        f.setColumns(columns);
        f.doneWithParameters();
        return f;
    }

    private Expression readSingleArgument() {
        Expression arg = readExpression();
        read(CLOSE_PAREN);
        return arg;
    }

    private Expression readNextArgument() {
        read(COMMA);
        return readExpression();
    }

    private Expression readLastArgument() {
        read(COMMA);
        Expression arg = readExpression();
        read(CLOSE_PAREN);
        return arg;
    }

    private Expression readIfSingleArgument() {
        Expression arg;
        if (readIf(CLOSE_PAREN)) {
            arg = null;
        } else {
            arg = readExpression();
            read(CLOSE_PAREN);
        }
        return arg;
    }

    private Expression readIfArgument() {
        Expression arg = readIf(COMMA) ? readExpression() : null;
        read(CLOSE_PAREN);
        return arg;
    }

    private Expression readCoalesceFunction(int function) {
        CoalesceFunction f = new CoalesceFunction(function);
        f.addParameter(readExpression());
        while (readIfMore()) {
            f.addParameter(readExpression());
        }
        f.doneWithParameters();
        return f;
    }

    private Expression readConcatFunction(int function) {
        ConcatFunction f = new ConcatFunction(function);
        f.addParameter(readExpression());
        f.addParameter(readNextArgument());
        if (function == ConcatFunction.CONCAT_WS) {
            f.addParameter(readNextArgument());
        }
        while (readIfMore()) {
            f.addParameter(readExpression());
        }
        f.doneWithParameters();
        return f;
    }

    private Expression readSubstringFunction() {
        // Standard variants are:
        // SUBSTRING(X FROM 1)
        // SUBSTRING(X FROM 1 FOR 1)
        // Different non-standard variants include:
        // SUBSTRING(X,1)
        // SUBSTRING(X,1,1)
        // SUBSTRING(X FOR 1) -- Postgres
        SubstringFunction function = new SubstringFunction();
        function.addParameter(readExpression());
        if (readIf(FROM)) {
            function.addParameter(readExpression());
            if (readIf(FOR)) {
                function.addParameter(readExpression());
            }
        } else if (readIf(FOR)) {
            function.addParameter(ValueExpression.get(ValueInteger.get(1)));
            function.addParameter(readExpression());
        } else {
            read(COMMA);
            function.addParameter(readExpression());
            if (readIf(COMMA)) {
                function.addParameter(readExpression());
            }
        }
        read(CLOSE_PAREN);
        function.doneWithParameters();
        return function;
    }

    private int readDateTimeField() {
        int field = -1;
        switch (currentTokenType) {
        case IDENTIFIER:
            if (!token.isQuoted()) {
                field = DateTimeFunction.getField(currentToken);
            }
            break;
        case LITERAL:
            if (token.value(session).getValueType() == Value.VARCHAR) {
                field = DateTimeFunction.getField(token.value(session).getString());
            }
            break;
        case YEAR:
            field = DateTimeFunction.YEAR;
            break;
        case MONTH:
            field = DateTimeFunction.MONTH;
            break;
        case DAY:
            field = DateTimeFunction.DAY;
            break;
        case HOUR:
            field = DateTimeFunction.HOUR;
            break;
        case MINUTE:
            field = DateTimeFunction.MINUTE;
            break;
        case SECOND:
            field = DateTimeFunction.SECOND;
        }
        if (field < 0) {
            addExpected("date-time field");
            throw getSyntaxError();
        }
        read();
        return field;
    }

    private WindowFunction readWindowFunction(String name) {
        WindowFunctionType type = WindowFunctionType.get(name);
        if (type == null) {
            return null;
        }
        if (currentSelect == null) {
            throw getSyntaxError();
        }
        int numArgs = WindowFunction.getMinArgumentCount(type);
        Expression[] args = null;
        if (numArgs > 0) {
            // There is no functions with numArgs == 0 && numArgsMax > 0
            int numArgsMax = WindowFunction.getMaxArgumentCount(type);
            args = new Expression[numArgsMax];
            if (numArgs == numArgsMax) {
                for (int i = 0; i < numArgs; i++) {
                    if (i > 0) {
                        read(COMMA);
                    }
                    args[i] = readExpression();
                }
            } else {
                int i = 0;
                while (i < numArgsMax) {
                    if (i > 0 && !readIf(COMMA)) {
                        break;
                    }
                    args[i] = readExpression();
                    i++;
                }
                if (i < numArgs) {
                    throw getSyntaxError();
                }
                if (i != numArgsMax) {
                    args = Arrays.copyOf(args, i);
                }
            }
        }
        read(CLOSE_PAREN);
        WindowFunction function = new WindowFunction(type, currentSelect, args);
        switch (type) {
        case NTH_VALUE:
            readFromFirstOrLast(function);
            //$FALL-THROUGH$
        case LEAD:
        case LAG:
        case FIRST_VALUE:
        case LAST_VALUE:
            readRespectOrIgnoreNulls(function);
            //$FALL-THROUGH$
        default:
            // Avoid warning
        }
        readOver(function);
        return function;
    }

    private void readFromFirstOrLast(WindowFunction function) {
        if (readIf(FROM, "LAST")) {
            function.setFromLast(true);
        } else {
            readIf(FROM, "FIRST");
        }
    }

    private void readRespectOrIgnoreNulls(WindowFunction function) {
        if (readIf("IGNORE", "NULLS")) {
            function.setIgnoreNulls(true);
        } else {
            readIf("RESPECT", "NULLS");
        }
    }

    private boolean readJsonObjectFunctionFlags(ExpressionWithFlags function, boolean forArray) {
        boolean result = false;
        int flags = function.getFlags();
        if (readIf(NULL, ON, NULL)) {
            flags &= ~JsonConstructorUtils.JSON_ABSENT_ON_NULL;
            result = true;
        } else if (readIf("ABSENT", ON, NULL)) {
            flags |= JsonConstructorUtils.JSON_ABSENT_ON_NULL;
            result = true;
        }
        if (!forArray) {
            if (readIf(WITH, UNIQUE, "KEYS")) {
                flags |= JsonConstructorUtils.JSON_WITH_UNIQUE_KEYS;
                result = true;
            } else if (readIf("WITHOUT", UNIQUE, "KEYS")) {
                flags &= ~JsonConstructorUtils.JSON_WITH_UNIQUE_KEYS;
                result = true;
            }
        }
        if (result) {
            function.setFlags(flags);
        }
        return result;
    }

    private Expression readKeywordCompatibilityFunctionOrColumn() {
        boolean nonKeyword = nonKeywords != null && nonKeywords.get(currentTokenType);
        String name = currentToken;
        read();
        if (readIf(OPEN_PAREN)) {
            return readCompatibilityFunction(upperName(name));
        } else if (nonKeyword) {
            return readIf(DOT) ? readTermObjectDot(name) : new ExpressionColumn(database, null, null, name);
        }
        throw getSyntaxError();
    }

    private Expression readCurrentDateTimeValueFunction(int function, boolean hasParen, String name) {
        int scale = -1;
        if (hasParen) {
            if (function != CurrentDateTimeValueFunction.CURRENT_DATE && currentTokenType != CLOSE_PAREN) {
                scale = readInt();
                if (scale < 0 || scale > ValueTime.MAXIMUM_SCALE) {
                    throw DbException.get(ErrorCode.INVALID_VALUE_SCALE, Integer.toString(scale), "0",
                            /* compile-time constant */ "" + ValueTime.MAXIMUM_SCALE);
                }
            }
            read(CLOSE_PAREN);
        }
        if (database.isAllowBuiltinAliasOverride()) {
            FunctionAlias functionAlias = database.getSchema(session.getCurrentSchemaName())
                    .findFunction(name != null ? name : CurrentDateTimeValueFunction.getName(function));
            if (functionAlias != null) {
                return new JavaFunction(functionAlias,
                        scale >= 0 ? new Expression[] { ValueExpression.get(ValueInteger.get(scale)) }
                                : new Expression[0]);
            }
        }
        return new CurrentDateTimeValueFunction(function, scale);
    }

    private Expression readIfWildcardRowidOrSequencePseudoColumn(String schema, String objectName) {
        if (readIf(ASTERISK)) {
            return parseWildcard(schema, objectName);
        }
        if (readIf(_ROWID_)) {
            return new ExpressionColumn(database, schema, objectName);
        }
        if (database.getMode().nextvalAndCurrvalPseudoColumns) {
            return readIfSequencePseudoColumn(schema, objectName);
        }
        return null;
    }

    private Wildcard parseWildcard(String schema, String objectName) {
        Wildcard wildcard = new Wildcard(schema, objectName);
        if (readIf(EXCEPT, OPEN_PAREN)) {
            ArrayList<ExpressionColumn> exceptColumns = Utils.newSmallArrayList();
            do {
                String s = null, t = null;
                String name = readIdentifier();
                if (readIf(DOT)) {
                    t = name;
                    name = readIdentifier();
                    if (readIf(DOT)) {
                        s = t;
                        t = name;
                        name = readIdentifier();
                        if (readIf(DOT)) {
                            checkDatabaseName(s);
                            s = t;
                            t = name;
                            name = readIdentifier();
                        }
                    }
                }
                exceptColumns.add(new ExpressionColumn(database, s, t, name));
            } while (readIfMore());
            wildcard.setExceptColumns(exceptColumns);
        }
        return wildcard;
    }

    private SequenceValue readIfSequencePseudoColumn(String schema, String objectName) {
        if (schema == null) {
            schema = session.getCurrentSchemaName();
        }
        if (isToken("NEXTVAL")) {
            Sequence sequence = findSequence(schema, objectName);
            if (sequence != null) {
                read();
                return new SequenceValue(sequence, getCurrentPreparedOrSelect());
            }
        } else if (isToken("CURRVAL")) {
            Sequence sequence = findSequence(schema, objectName);
            if (sequence != null) {
                read();
                return new SequenceValue(sequence);
            }
        }
        return null;
    }

    private Expression readTermObjectDot(String objectName) {
        Expression expr = readIfWildcardRowidOrSequencePseudoColumn(null, objectName);
        if (expr != null) {
            return expr;
        }
        String name = readIdentifier();
        if (readIf(OPEN_PAREN)) {
            return readFunction(database.getSchema(objectName), name);
        } else if (readIf(DOT)) {
            String schema = objectName;
            objectName = name;
            expr = readIfWildcardRowidOrSequencePseudoColumn(schema, objectName);
            if (expr != null) {
                return expr;
            }
            name = readIdentifier();
            if (readIf(OPEN_PAREN)) {
                checkDatabaseName(schema);
                return readFunction(database.getSchema(objectName), name);
            } else if (readIf(DOT)) {
                checkDatabaseName(schema);
                schema = objectName;
                objectName = name;
                expr = readIfWildcardRowidOrSequencePseudoColumn(schema, objectName);
                if (expr != null) {
                    return expr;
                }
                name = readIdentifier();
            }
            return new ExpressionColumn(database, schema, objectName, name);
        }
        return new ExpressionColumn(database, null, objectName, name);
    }

    private void checkDatabaseName(String databaseName) {
        if (!database.getIgnoreCatalogs() && !equalsToken(database.getShortName(), databaseName)) {
            throw DbException.get(ErrorCode.DATABASE_NOT_FOUND_1, databaseName);
        }
    }

    private Parameter readParameter() {
        int index = ((Token.ParameterToken) token).index() - 1;
        read();
        usedParameters.set(index);
        return parameters.get(index);
    }

    private BitSet initParametersScope() {
        BitSet outerUsedParameters = usedParameters;
        usedParameters = new BitSet();
        return outerUsedParameters;
    }

    private ArrayList<Parameter> getUsedParameters(BitSet outerUsedParameters) {
        BitSet innerUsedParameters = usedParameters;
        int size = innerUsedParameters.cardinality();
        ArrayList<Parameter> params = new ArrayList<>(size);
        if (size > 0) {
            for (int i = -1; (i = innerUsedParameters.nextSetBit(i + 1)) >= 0;) {
                params.add(parameters.get(i));
            }
        }
        outerUsedParameters.or(innerUsedParameters);
        usedParameters = outerUsedParameters;
        return params;
    }

    private Expression readTerm() {
        Expression r = currentTokenType == IDENTIFIER ? readTermWithIdentifier() : readTermWithoutIdentifier();
        for (;;) {
            if (readIf(OPEN_BRACKET)) {
                r = new ArrayElementReference(r, readExpression());
                read(CLOSE_BRACKET);
                continue;
            }
            if (readIf(DOT)) {
                r = new FieldReference(r, readIdentifier());
                continue;
            }
            if (readIf(COLON_COLON)) {
                r = readColonColonAfterTerm(r);
                continue;
            }
            TypeInfo ti = readIntervalQualifier();
            if (ti != null) {
                r = new CastSpecification(r, ti);
                continue;
            }
            int index = tokenIndex;
            if (readIf("AT")) {
                if (readIf("TIME", "ZONE")) {
                    r = new TimeZoneOperation(r, readExpression());
                    continue;
                } else if (readIf("LOCAL")) {
                    r = new TimeZoneOperation(r, null);
                    continue;
                } else {
                    setTokenIndex(index);
                }
            } else if (readIf("FORMAT")) {
                if (readIf("JSON")) {
                    r = new Format(r, FormatEnum.JSON);
                    continue;
                } else {
                    setTokenIndex(index);
                }
            }
            break;
        }
        return r;
    }

    private Expression readTermWithoutIdentifier() {
        Expression r;
        switch (currentTokenType) {
        case AT:
            read();
            r = new Variable(session, readIdentifier());
            if (readIf(COLON_EQ)) {
                r = new SetFunction(r, readExpression());
            }
            break;
        case PARAMETER:
            r = readParameter();
            break;
        case TABLE:
        case SELECT:
        case WITH:
            r = new Subquery(parseQuery());
            break;
        case MINUS_SIGN:
            read();
            if (currentTokenType == LITERAL) {
                r = ValueExpression.get(token.value(session).negate());
                int rType = r.getType().getValueType();
                if (rType == Value.BIGINT &&
                        r.getValue(session).getLong() == Integer.MIN_VALUE) {
                    // convert Integer.MIN_VALUE to type 'int'
                    // (Integer.MAX_VALUE+1 is of type 'long')
                    r = ValueExpression.get(ValueInteger.get(Integer.MIN_VALUE));
                } else if (rType == Value.NUMERIC &&
                        r.getValue(session).getBigDecimal().compareTo(Value.MIN_LONG_DECIMAL) == 0) {
                    // convert Long.MIN_VALUE to type 'long'
                    // (Long.MAX_VALUE+1 is of type 'decimal')
                    r = ValueExpression.get(ValueBigint.MIN);
                }
                read();
            } else {
                r = new UnaryOperation(readTerm());
            }
            break;
        case PLUS_SIGN:
            read();
            r = readTerm();
            break;
        case OPEN_PAREN:
            read();
            if (readIf(CLOSE_PAREN)) {
                r = ValueExpression.get(ValueRow.EMPTY);
            } else if (isQuery()) {
                r = new Subquery(parseQuery());
                read(CLOSE_PAREN);
            } else {
                r = readExpression();
                if (readIfMore()) {
                    ArrayList<Expression> list = Utils.newSmallArrayList();
                    list.add(r);
                    do {
                        list.add(readExpression());
                    } while (readIfMore());
                    r = new ExpressionList(list.toArray(new Expression[0]), false);
                } else if (r instanceof BinaryOperation) {
                    BinaryOperation binaryOperation = (BinaryOperation) r;
                    if (binaryOperation.getOperationType() == OpType.MINUS) {
                        TypeInfo ti = readIntervalQualifier();
                        if (ti != null) {
                            binaryOperation.setForcedType(ti);
                        }
                    }
                }
            }
            break;
        case ARRAY:
            read();
            if (readIf(OPEN_BRACKET)) {
                if (readIf(CLOSE_BRACKET)) {
                    r = ValueExpression.get(ValueArray.EMPTY);
                } else {
                    ArrayList<Expression> list = Utils.newSmallArrayList();
                    do {
                        list.add(readExpression());
                    } while (readIf(COMMA));
                    read(CLOSE_BRACKET);
                    r = new ExpressionList(list.toArray(new Expression[0]), true);
                }
            } else {
                read(OPEN_PAREN);
                Query q = parseQuery();
                read(CLOSE_PAREN);
                r = new ArrayConstructorByQuery(q);
            }
            break;
        case INTERVAL:
            read();
            r = readInterval();
            break;
        case ROW:
            if (readIf(ROW, OPEN_PAREN)) {
                if (readIf(CLOSE_PAREN)) {
                    r = ValueExpression.get(ValueRow.EMPTY);
                } else {
                    ArrayList<Expression> list = Utils.newSmallArrayList();
                    do {
                        list.add(readExpression());
                    } while (readIfMore());
                    r = new ExpressionList(list.toArray(new Expression[0]), false);
                }
            } else {
                r = readTermWithIdentifier();
            }
            break;
        case TRUE:
            read();
            r = ValueExpression.TRUE;
            break;
        case FALSE:
            read();
            r = ValueExpression.FALSE;
            break;
        case UNKNOWN:
            read();
            r = TypedValueExpression.UNKNOWN;
            break;
        case ROWNUM:
            read();
            if (readIf(OPEN_PAREN)) {
                read(CLOSE_PAREN);
            }
            if (currentSelect == null && currentPrepared == null) {
                throw getSyntaxError();
            }
            r = new Rownum(getCurrentPreparedOrSelect());
            break;
        case NULL:
            read();
            r = ValueExpression.NULL;
            break;
        case _ROWID_:
            read();
            r = new ExpressionColumn(database, null, null);
            break;
        case LITERAL:
            r = ValueExpression.get(token.value(session));
            read();
            break;
        case VALUES:
            if (database.getMode().onDuplicateKeyUpdate) {
                if (currentPrepared instanceof Insert) {
                    r = readOnDuplicateKeyValues(((Insert) currentPrepared).getTable(), null);
                    break;
                } else if (currentPrepared instanceof Update) {
                    Update update = (Update) currentPrepared;
                    r = readOnDuplicateKeyValues(update.getTable(), update);
                    break;
                }
            }
            r = new Subquery(parseQuery());
            break;
        case CASE:
            read();
            r = readCase();
            break;
        case CAST: {
            read();
            read(OPEN_PAREN);
            Expression arg = readExpression();
            read(AS);
            Column column = parseColumnWithType(null);
            Expression template = readIf("FORMAT") ? readExpression() : null;
            read(CLOSE_PAREN);
            r = new CastSpecification(arg, column, template);
            break;
        }
        case CURRENT_CATALOG:
            r = readCurrentGeneralValueSpecification(CurrentGeneralValueSpecification.CURRENT_CATALOG);
            break;
        case CURRENT_DATE:
            read();
            r = readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.CURRENT_DATE, readIf(OPEN_PAREN), null);
            break;
        case CURRENT_PATH:
            r = readCurrentGeneralValueSpecification(CurrentGeneralValueSpecification.CURRENT_PATH);
            break;
        case CURRENT_ROLE:
            r = readCurrentGeneralValueSpecification(CurrentGeneralValueSpecification.CURRENT_ROLE);
            break;
        case CURRENT_SCHEMA:
            r = readCurrentGeneralValueSpecification(CurrentGeneralValueSpecification.CURRENT_SCHEMA);
            break;
        case CURRENT_TIME:
            read();
            r = readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.CURRENT_TIME, readIf(OPEN_PAREN), null);
            break;
        case CURRENT_TIMESTAMP:
            read();
            r = readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.CURRENT_TIMESTAMP, readIf(OPEN_PAREN),
                    null);
            break;
        case CURRENT_USER:
        case USER:
            r = readCurrentGeneralValueSpecification(CurrentGeneralValueSpecification.CURRENT_USER);
            break;
        case SESSION_USER:
            r = readCurrentGeneralValueSpecification(CurrentGeneralValueSpecification.SESSION_USER);
            break;
        case SYSTEM_USER:
            r = readCurrentGeneralValueSpecification(CurrentGeneralValueSpecification.SYSTEM_USER);
            break;
        case ANY:
        case SOME:
            read();
            read(OPEN_PAREN);
            r = readAggregate(AggregateType.ANY, "ANY");
            break;
        case DAY:
        case HOUR:
        case MINUTE:
        case MONTH:
        case SECOND:
        case YEAR:
            r = readKeywordCompatibilityFunctionOrColumn();
            break;
        case LEFT:
            r = readColumnIfNotFunction();
            if (r == null) {
                r = new StringFunction2(readExpression(), readLastArgument(), StringFunction2.LEFT);
            }
            break;
        case LOCALTIME:
            read();
            r = readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.LOCALTIME, readIf(OPEN_PAREN), null);
            break;
        case LOCALTIMESTAMP:
            read();
            r = readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.LOCALTIMESTAMP, readIf(OPEN_PAREN), //
                    null);
            break;
        case RIGHT:
            r = readColumnIfNotFunction();
            if (r == null) {
                r = new StringFunction2(readExpression(), readLastArgument(), StringFunction2.RIGHT);
            }
            break;
        case SET:
            r = readColumnIfNotFunction();
            if (r == null) {
                r = readSetFunction();
            }
            break;
        case VALUE:
            if (parseDomainConstraint) {
                read();
                r = new DomainValueExpression();
                break;
            }
            //$FALL-THROUGH$
        default:
            if (!isIdentifier()) {
                throw getSyntaxError();
            }
            r = readTermWithIdentifier();
            break;
        }
        return r;
    }

    private Expression readTermWithIdentifier() {
        Expression r;
        String name = currentToken;
        boolean quoted = token.isQuoted();
        read();
        if (readIf(OPEN_PAREN)) {
            r = readFunction(null, name);
        } else if (readIf(DOT)) {
            r = readTermObjectDot(name);
        } else if (quoted) {
            r = new ExpressionColumn(database, null, null, name);
        } else {
            r = readTermWithIdentifier(name, quoted);
        }
        return r;
    }

    private Expression readColonColonAfterTerm(Expression r) {
        if (database.getMode().getEnum() == ModeEnum.PostgreSQL) {
            // PostgreSQL compatibility
            if (readIf("PG_CATALOG")) {
                read(DOT);
            }
            if (readIf("REGCLASS")) {
                return new Regclass(r);
            }
        }
        return new CastSpecification(r, parseColumnWithType(null));
    }

    private Expression readCurrentGeneralValueSpecification(int specification) {
        read();
        if (readIf(OPEN_PAREN)) {
            read(CLOSE_PAREN);
        }
        return new CurrentGeneralValueSpecification(specification);
    }

    private Expression readColumnIfNotFunction() {
        boolean nonKeyword = nonKeywords != null && nonKeywords.get(currentTokenType);
        String name = currentToken;
        read();
        if (readIf(OPEN_PAREN)) {
            return null;
        } else if (nonKeyword) {
            return readIf(DOT) ? readTermObjectDot(name) : new ExpressionColumn(database, null, null, name);
        }
        throw getSyntaxError();
    }

    private Expression readSetFunction() {
        SetFunction function = new SetFunction(readExpression(), readLastArgument());
        if (database.isAllowBuiltinAliasOverride()) {
            FunctionAlias functionAlias = database.getSchema(session.getCurrentSchemaName()).findFunction(
                    function.getName());
            if (functionAlias != null) {
                return new JavaFunction(functionAlias,
                        new Expression[] { function.getSubexpression(0), function.getSubexpression(1) });
            }
        }
        return function;
    }

    private Expression readOnDuplicateKeyValues(Table table, Update update) {
        read();
        read(OPEN_PAREN);
        Column c = readTableColumn(new TableFilter(session, table, null, rightsChecked, null, 0, null));
        read(CLOSE_PAREN);
        return new OnDuplicateKeyValues(c, update);
    }

    private Expression readTermWithIdentifier(String name, boolean quoted) {
        /*
         * Convert a-z to A-Z. This method is safe, because only A-Z
         * characters are considered below.
         *
         * Unquoted identifier is never empty.
         */
        switch (name.charAt(0) & 0xffdf) {
        case 'C':
            if (equalsToken("CURRENT", name)) {
                if (readIf(VALUE, FOR)) {
                    return new SequenceValue(readSequence());
                }
                if (database.getMode().getEnum() == ModeEnum.DB2) {
                    return parseDB2SpecialRegisters(name);
                }
            }
            break;
        case 'D':
            if (currentTokenType == LITERAL && token.value(session).getValueType() == Value.VARCHAR &&
                    (equalsToken("DATE", name) || equalsToken("D", name))) {
                String date = token.value(session).getString();
                read();
                return ValueExpression.get(ValueDate.parse(date));
            }
            break;
        case 'E':
            if (currentTokenType == LITERAL && token.value(session).getValueType() == Value.VARCHAR //
                    && equalsToken("E", name)) {
                String text = token.value(session).getString();
                // the PostgreSQL ODBC driver uses
                // LIKE E'PROJECT\\_DATA' instead of LIKE
                // 'PROJECT\_DATA'
                // N: SQL-92 "National Language" strings
                text = StringUtils.replaceAll(text, "\\\\", "\\");
                read();
                return ValueExpression.get(ValueVarchar.get(text));
            }
            break;
        case 'G':
            if (currentTokenType == LITERAL) {
                int t = token.value(session).getValueType();
                if (t == Value.VARCHAR && equalsToken("GEOMETRY", name)) {
                    ValueExpression v = ValueExpression.get(ValueGeometry.get(token.value(session).getString()));
                    read();
                    return v;
                } else if (t == Value.VARBINARY && equalsToken("GEOMETRY", name)) {
                    ValueExpression v = ValueExpression
                            .get(ValueGeometry.getFromEWKB(token.value(session).getBytesNoCopy()));
                    read();
                    return v;
                }
            }
            break;
        case 'J':
            if (currentTokenType == LITERAL) {
                int t = token.value(session).getValueType();
                if (t == Value.VARCHAR && equalsToken("JSON", name)) {
                    ValueExpression v = ValueExpression.get(ValueJson.fromJson(token.value(session).getString()));
                    read();
                    return v;
                } else if (t == Value.VARBINARY && equalsToken("JSON", name)) {
                    ValueExpression v = ValueExpression.get(ValueJson.fromJson(token.value(session).getBytesNoCopy()));
                    read();
                    return v;
                }
            }
            break;
        case 'N':
            if (equalsToken("NEXT", name)) {
                if (readIf(VALUE, FOR)) {
                    return new SequenceValue(readSequence(), getCurrentPreparedOrSelect());
                }
            }
            break;
        case 'T':
            if (equalsToken("TIME", name)) {
                if (readIf(WITH, "TIME", "ZONE")) {
                    if (currentTokenType != LITERAL || token.value(session).getValueType() != Value.VARCHAR) {
                        throw getSyntaxError();
                    }
                    String time = token.value(session).getString();
                    read();
                    return ValueExpression.get(ValueTimeTimeZone.parse(time, session));
                } else {
                    boolean without = readIf("WITHOUT", "TIME", "ZONE");
                    if (currentTokenType == LITERAL && token.value(session).getValueType() == Value.VARCHAR) {
                        String time = token.value(session).getString();
                        read();
                        return ValueExpression.get(ValueTime.parse(time, session));
                    } else if (without) {
                        throw getSyntaxError();
                    }
                }
            } else if (equalsToken("TIMESTAMP", name)) {
                if (readIf(WITH, "TIME", "ZONE")) {
                    if (currentTokenType != LITERAL || token.value(session).getValueType() != Value.VARCHAR) {
                        throw getSyntaxError();
                    }
                    String timestamp = token.value(session).getString();
                    read();
                    return ValueExpression.get(ValueTimestampTimeZone.parse(timestamp, session));
                } else {
                    boolean without = readIf("WITHOUT", "TIME", "ZONE");
                    if (currentTokenType == LITERAL && token.value(session).getValueType() == Value.VARCHAR) {
                        String timestamp = token.value(session).getString();
                        read();
                        return ValueExpression.get(ValueTimestamp.parse(timestamp, session));
                    } else if (without) {
                        throw getSyntaxError();
                    }
                }
            } else if (currentTokenType == LITERAL && token.value(session).getValueType() == Value.VARCHAR) {
                if (equalsToken("T", name)) {
                    String time = token.value(session).getString();
                    read();
                    return ValueExpression.get(ValueTime.parse(time, session));
                } else if (equalsToken("TS", name)) {
                    String timestamp = token.value(session).getString();
                    read();
                    return ValueExpression.get(ValueTimestamp.parse(timestamp, session));
                }
            }
            break;
        case 'U':
            if (currentTokenType == LITERAL && token.value(session).getValueType() == Value.VARCHAR
                    && equalsToken("UUID", name)) {
                String uuid = token.value(session).getString();
                read();
                return ValueExpression.get(ValueUuid.get(uuid));
            }
            break;
        }
        return new ExpressionColumn(database, null, null, name, quoted);
    }

    private Prepared getCurrentPreparedOrSelect() {
        Prepared p = currentPrepared;
        return p != null ? p : currentSelect;
    }

    private Expression readInterval() {
        boolean negative = readIf(MINUS_SIGN);
        if (!negative) {
            readIf(PLUS_SIGN);
        }
        if (currentTokenType != LITERAL || token.value(session).getValueType() != Value.VARCHAR) {
            addExpected("string");
            throw getSyntaxError();
        }
        String s = token.value(session).getString();
        read();
        TypeInfo typeInfo = readIntervalQualifier();
        try {
            ValueInterval interval = IntervalUtils.parseInterval(
                    IntervalQualifier.valueOf(typeInfo.getValueType() - Value.INTERVAL_YEAR), negative, s);
            if (typeInfo.getDeclaredPrecision() != -1L || typeInfo.getDeclaredScale() != -1) {
                return TypedValueExpression.get(interval.castTo(typeInfo, session), typeInfo);
            }
            return ValueExpression.get(interval);
        } catch (Exception e) {
            throw DbException.get(ErrorCode.INVALID_DATETIME_CONSTANT_2, e, "INTERVAL", s);
        }
    }

    private Expression parseDB2SpecialRegisters(String name) {
        // Only "CURRENT" name is supported
        if (readIf("TIMESTAMP")) {
            if (readIf(WITH, "TIME", "ZONE")) {
                return readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.CURRENT_TIMESTAMP,
                        readIf(OPEN_PAREN), null);
            }
            return readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.LOCALTIMESTAMP, readIf(OPEN_PAREN),
                    null);
        } else if (readIf("TIME")) {
            // Time with fractional seconds is not supported by DB2
            return readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.LOCALTIME, false, null);
        } else if (readIf("DATE")) {
            return readCurrentDateTimeValueFunction(CurrentDateTimeValueFunction.CURRENT_DATE, false, null);
        }
        // No match, parse CURRENT as a column
        return new ExpressionColumn(database, null, null, name);
    }

    private Expression readCase() {
        Expression c;
        if (readIf(WHEN)) {
            SearchedCase searched = new SearchedCase();
            do {
                Expression condition = readExpression();
                read("THEN");
                searched.addParameter(condition);
                searched.addParameter(readExpression());
            } while (readIf(WHEN));
            if (readIf(ELSE)) {
                searched.addParameter(readExpression());
            }
            searched.doneWithParameters();
            c = searched;
        } else {
            Expression caseOperand = readExpression();
            read(WHEN);
            SimpleCase.SimpleWhen when = readSimpleWhenClause(caseOperand), current = when;
            while (readIf(WHEN)) {
                SimpleCase.SimpleWhen next = readSimpleWhenClause(caseOperand);
                current.setWhen(next);
                current = next;
            }
            c = new SimpleCase(caseOperand, when, readIf(ELSE) ? readExpression() : null);
        }
        read(END);
        return c;
    }

    private SimpleCase.SimpleWhen readSimpleWhenClause(Expression caseOperand) {
        Expression whenOperand = readWhenOperand(caseOperand);
        if (readIf(COMMA)) {
            ArrayList<Expression> operands = Utils.newSmallArrayList();
            operands.add(whenOperand);
            do {
                operands.add(readWhenOperand(caseOperand));
            } while (readIf(COMMA));
            read("THEN");
            return new SimpleCase.SimpleWhen(operands.toArray(new Expression[0]), readExpression());
        }
        read("THEN");
        return new SimpleCase.SimpleWhen(whenOperand, readExpression());
    }

    private Expression readWhenOperand(Expression caseOperand) {
        int backup = tokenIndex;
        boolean not = readIf(NOT);
        Expression whenOperand = readConditionRightHandSide(caseOperand, not, true);
        if (whenOperand == null) {
            if (not) {
                setTokenIndex(backup);
            }
            whenOperand = readExpression();
        }
        return whenOperand;
    }

    private int readNonNegativeInt() {
        int v = readInt();
        if (v < 0) {
            throw DbException.getInvalidValueException("non-negative integer", v);
        }
        return v;
    }

    private int readInt() {
        boolean minus = false;
        if (currentTokenType == MINUS_SIGN) {
            minus = true;
            read();
        } else if (currentTokenType == PLUS_SIGN) {
            read();
        }
        if (currentTokenType != LITERAL) {
            throw DbException.getSyntaxError(sqlCommand, token.start(), "integer");
        }
        Value value = token.value(session);
        if (minus) {
            // must do that now, otherwise Integer.MIN_VALUE would not work
            value = value.negate();
        }
        int i = value.getInt();
        read();
        return i;
    }

    private long readPositiveLong() {
        long v = readLong();
        if (v <= 0) {
            throw DbException.getInvalidValueException("positive long", v);
        }
        return v;
    }

    private long readLong() {
        boolean minus = false;
        if (currentTokenType == MINUS_SIGN) {
            minus = true;
            read();
        } else if (currentTokenType == PLUS_SIGN) {
            read();
        }
        if (currentTokenType != LITERAL) {
            throw DbException.getSyntaxError(sqlCommand, token.start(), "long");
        }
        Value value = token.value(session);
        if (minus) {
            // must do that now, otherwise Long.MIN_VALUE would not work
            value = value.negate();
        }
        long i = value.getLong();
        read();
        return i;
    }

    private boolean readBooleanSetting() {
        switch (currentTokenType) {
        case ON:
        case TRUE:
            read();
            return true;
        case FALSE:
            read();
            return false;
        case LITERAL:
            boolean result = token.value(session).getBoolean();
            read();
            return result;
        }
        if (readIf("OFF")) {
            return false;
        } else {
            if (expectedList != null) {
                addMultipleExpected(ON, TRUE, FALSE);
            }
            throw getSyntaxError();
        }
    }

    private String readString() {
        int sqlIndex = token.start();
        Expression expr = readExpression();
        try {
            String s = expr.optimize(session).getValue(session).getString();
            if (s == null || s.length() <= Constants.MAX_STRING_LENGTH) {
                return s;
            }
        } catch (DbException e) {
        }
        throw DbException.getSyntaxError(sqlCommand, sqlIndex, "character string");
    }

    private Expression readStringOrParameter() {
        int sqlIndex = token.start();
        Expression expr = readExpression();
        try {
            expr = expr.optimize(session);
            if (expr instanceof Parameter) {
                return expr;
            }
            Value v = expr.getValue(session);
            int valueType = v.getValueType();
            if ((valueType == NULL || valueType == Value.VARCHAR) && expr instanceof ValueExpression) {
                return expr;
            }
            String s = v.getString();
            if (s == null || s.length() <= Constants.MAX_STRING_LENGTH) {
                return s == null ? ValueExpression.NULL : ValueExpression.get(ValueVarchar.get(s, database));
            }
        } catch (DbException e) {
        }
        throw DbException.getSyntaxError(sqlCommand, sqlIndex, "character string");
    }

    // TODO: why does this function allow defaultSchemaName=null - which resets
    // the parser schemaName for everyone ?
    private String readIdentifierWithSchema(String defaultSchemaName) {
        String s = readIdentifier();
        schemaName = defaultSchemaName;
        if (readIf(DOT)) {
            s = readIdentifierWithSchema2(s);
        }
        return s;
    }

    private String readIdentifierWithSchema2(String s) {
        schemaName = s;
        if (database.getMode().allowEmptySchemaValuesAsDefaultSchema && readIf(DOT)) {
            if (equalsToken(schemaName, database.getShortName()) || database.getIgnoreCatalogs()) {
                schemaName = session.getCurrentSchemaName();
                s = readIdentifier();
            }
        } else {
            s = readIdentifier();
            if (currentTokenType == DOT) {
                if (equalsToken(schemaName, database.getShortName()) || database.getIgnoreCatalogs()) {
                    read();
                    schemaName = s;
                    s = readIdentifier();
                }
            }
        }
        return s;
    }

    private String readIdentifierWithSchema() {
        return readIdentifierWithSchema(session.getCurrentSchemaName());
    }

    private String readIdentifier() {
        if (!isIdentifier()) {
            /*
             * Sometimes a new keywords are introduced. During metadata
             * initialization phase keywords are accepted as identifiers to
             * allow migration from older versions.
             */
            if (!session.isQuirksMode() || !isKeyword(currentTokenType)) {
                throw DbException.getSyntaxError(sqlCommand, token.start(), "identifier");
            }
        }
        String s = currentToken;
        read();
        return s;
    }

    private String readIdentifierOrKeyword() {
        if (currentTokenType < IDENTIFIER || currentTokenType > LAST_KEYWORD) {
            addExpected("identifier or keyword");
            throw getSyntaxError();
        }
        String s = currentToken;
        read();
        return s;
    }

    private void read(String expected) {
        if (!testToken(expected, token)) {
            addExpected(expected);
            throw getSyntaxError();
        }
        read();
    }

    private void read(int tokenType) {
        if (tokenType != currentTokenType) {
            addExpected(tokenType);
            throw getSyntaxError();
        }
        read();
    }

    private boolean readIf(String tokenName) {
        if (testToken(tokenName, token)) {
            read();
            return true;
        }
        addExpected(tokenName);
        return false;
    }

    private boolean readIf(String tokenName1, String tokenName2) {
        int i = tokenIndex + 1;
        if (i + 1 < tokens.size() && testToken(tokenName1, token) && testToken(tokenName2, tokens.get(i))) {
            setTokenIndex(i + 1);
            return true;
        }
        addExpected(tokenName1, tokenName2);
        return false;
    }

    private boolean readIf(String tokenName1, int tokenType2) {
        int i = tokenIndex + 1;
        if (i + 1 < tokens.size() && tokens.get(i).tokenType() == tokenType2 && testToken(tokenName1, token)) {
            setTokenIndex(i + 1);
            return true;
        }
        addExpected(tokenName1, TOKENS[tokenType2]);
        return false;
    }

    private boolean readIf(int tokenType) {
        if (tokenType == currentTokenType) {
            read();
            return true;
        }
        addExpected(tokenType);
        return false;
    }

    private boolean readIf(int tokenType1, int tokenType2) {
        if (tokenType1 == currentTokenType) {
            int i = tokenIndex + 1;
            if (tokens.get(i).tokenType() == tokenType2) {
                setTokenIndex(i + 1);
                return true;
            }
        }
        addExpected(tokenType1, tokenType2);
        return false;
    }

    private boolean readIf(int tokenType1, String tokenName2) {
        if (tokenType1 == currentTokenType) {
            int i = tokenIndex + 1;
            if (testToken(tokenName2, tokens.get(i))) {
                setTokenIndex(i + 1);
                return true;
            }
        }
        addExpected(TOKENS[tokenType1], tokenName2);
        return false;
    }

    private boolean readIf(Object... tokensTypesOrNames) {
        int count = tokensTypesOrNames.length;
        int size = tokens.size();
        int i = tokenIndex;
        check: if (i + count < size) {
            for (Object tokenTypeOrName : tokensTypesOrNames) {
                if (!testToken(tokenTypeOrName, tokens.get(i++))) {
                    break check;
                }
            }
            setTokenIndex(i);
            return true;
        }
        addExpected(tokensTypesOrNames);
        return false;
    }

    private boolean isToken(String tokenName) {
        if (testToken(tokenName, token)) {
            return true;
        }
        addExpected(tokenName);
        return false;
    }

    private boolean testToken(Object expected, Token token) {
        return expected instanceof Integer ? (int) expected == token.tokenType() : testToken((String) expected, token);
    }

    private boolean testToken(String tokenName, Token token) {
        if (!token.isQuoted()) {
            String s = token.asIdentifier();
            return identifiersToUpper ? tokenName.equals(s) : tokenName.equalsIgnoreCase(s);
        }
        return false;
    }

    private boolean isToken(int tokenType) {
        if (tokenType == currentTokenType) {
            return true;
        }
        addExpected(tokenType);
        return false;
    }

    private boolean equalsToken(String a, String b) {
        if (a == null) {
            return b == null;
        } else
            return a.equals(b) || !identifiersToUpper && a.equalsIgnoreCase(b);
    }

    private boolean isIdentifier() {
        return currentTokenType == IDENTIFIER || nonKeywords != null && nonKeywords.get(currentTokenType);
    }

    private void addExpected(String token) {
        if (expectedList != null) {
            expectedList.add(token);
        }
    }

    private void addExpected(int tokenType) {
        if (expectedList != null) {
            expectedList.add(TOKENS[tokenType]);
        }
    }

    private void addExpected(int tokenType1, int tokenType2) {
        if (expectedList != null) {
            expectedList.add(TOKENS[tokenType1] + ' ' + TOKENS[tokenType2]);
        }
    }

    private void addExpected(String tokenType1, String tokenType2) {
        if (expectedList != null) {
            expectedList.add(tokenType1 + ' ' + tokenType2);
        }
    }

    private void addExpected(Object... tokens) {
        if (expectedList != null) {
            StringJoiner j = new StringJoiner(" ");
            for (Object token : tokens) {
                j.add(token instanceof Integer ? TOKENS[(int) token] : (String) token);
            }
            expectedList.add(j.toString());
        }
    }

    private void addMultipleExpected(int ... tokenTypes) {
        for (int tokenType : tokenTypes) {
            expectedList.add(TOKENS[tokenType]);
        }
    }

    private void read() {
        if (expectedList != null) {
            expectedList.clear();
        }
        int size = tokens.size();
        if (tokenIndex + 1 < size) {
            token = tokens.get(++tokenIndex);
            currentTokenType = token.tokenType();
            currentToken = token.asIdentifier();
            if (currentToken != null && currentToken.length() > Constants.MAX_IDENTIFIER_LENGTH) {
                throw DbException.get(ErrorCode.NAME_TOO_LONG_2, currentToken.substring(0, 32),
                        "" + Constants.MAX_IDENTIFIER_LENGTH);
            } else if (currentTokenType == LITERAL) {
                checkLiterals();
            }
        } else {
            throw getSyntaxError();
        }
    }

    private void checkLiterals() {
        if (!literalsChecked && session != null && !session.getAllowLiterals()) {
            int allowed = database.getAllowLiterals();
            if (allowed == Constants.ALLOW_LITERALS_NONE
                    || ((token instanceof Token.CharacterStringToken || token instanceof Token.BinaryStringToken)
                            && allowed != Constants.ALLOW_LITERALS_ALL)) {
                throw DbException.get(ErrorCode.LITERALS_ARE_NOT_ALLOWED);
            }
        }
    }

    private void initialize(String sql, ArrayList<Token> tokens, boolean stopOnCloseParen) {
        if (sql == null) {
            sql = "";
        }
        sqlCommand = sql;
        if (tokens == null) {
            BitSet usedParameters = new BitSet();
            this.tokens = new Tokenizer(database, identifiersToUpper, identifiersToLower, nonKeywords)
                    .tokenize(sql, stopOnCloseParen, usedParameters);
            if (parameters == null) {
                int l = usedParameters.length();
                if (l > Constants.MAX_PARAMETER_INDEX) {
                    throw DbException.getInvalidValueException("parameter index", l);
                }
                if (l > 0) {
                    parameters = new ArrayList<>(l);
                    for (int i = 0; i < l; i++) {
                        /*
                         * We need to create parameters even when they aren't
                         * actually used, for example, VALUES ?1, ?3 needs
                         * parameters ?1, ?2, and ?3.
                         */
                        parameters.add(new Parameter(i));
                    }
                } else {
                    parameters = new ArrayList<>();
                }
            }
        } else {
            this.tokens = tokens;
        }
        resetTokenIndex();
    }

    private void resetTokenIndex() {
        tokenIndex = -1;
        token = null;
        currentTokenType = -1;
        currentToken = null;
    }

    void setTokenIndex(int index) {
        if (index != tokenIndex) {
            if (expectedList != null) {
                expectedList.clear();
            }
            token = tokens.get(index);
            tokenIndex = index;
            currentTokenType = token.tokenType();
            currentToken = token.asIdentifier();
        }
    }

    private static boolean isKeyword(int tokenType) {
        return tokenType >= FIRST_KEYWORD && tokenType <= LAST_KEYWORD;
    }

    private boolean isKeyword(String s) {
        return ParserUtil.isKeyword(s, !identifiersToUpper);
    }

    private String upperName(String name) {
        return identifiersToUpper ? name : StringUtils.toUpperEnglish(name);
    }

    private Column parseColumnForTable(String columnName, boolean defaultNullable) {
        Column column;
        Mode mode = database.getMode();
        if (mode.identityDataType && readIf("IDENTITY")) {
            column = new Column(columnName, TypeInfo.TYPE_BIGINT);
            parseCompatibilityIdentityOptions(column);
            column.setPrimaryKey(true);
        } else if (mode.serialDataTypes && readIf("BIGSERIAL")) {
            column = new Column(columnName, TypeInfo.TYPE_BIGINT);
            column.setIdentityOptions(new SequenceOptions(), false);
        } else if (mode.serialDataTypes && readIf("SERIAL")) {
            column = new Column(columnName, TypeInfo.TYPE_INTEGER);
            column.setIdentityOptions(new SequenceOptions(), false);
        } else {
            column = parseColumnWithType(columnName);
        }
        if (readIf("INVISIBLE")) {
            column.setVisible(false);
        } else if (readIf("VISIBLE")) {
            column.setVisible(true);
        }
        boolean defaultOnNull = false;
        NullConstraintType nullConstraint = parseNotNullConstraint();
        defaultIdentityGeneration: if (!column.isIdentity()) {
            if (readIf(AS)) {
                column.setGeneratedExpression(readExpression());
            } else if (readIf(DEFAULT)) {
                if (readIf(ON, NULL)) {
                    defaultOnNull = true;
                    break defaultIdentityGeneration;
                }
                column.setDefaultExpression(session, readExpression());
            } else if (readIf("GENERATED")) {
                boolean always = readIf("ALWAYS");
                if (!always) {
                    read("BY");
                    read(DEFAULT);
                }
                read(AS);
                if (readIf("IDENTITY")) {
                    SequenceOptions options = new SequenceOptions();
                    if (readIf(OPEN_PAREN)) {
                        parseSequenceOptions(options, null, false, false);
                        read(CLOSE_PAREN);
                    }
                    column.setIdentityOptions(options, always);
                    break defaultIdentityGeneration;
                } else if (!always) {
                    throw getSyntaxError();
                } else {
                    column.setGeneratedExpression(readExpression());
                }
            }
            if (!column.isGenerated() && readIf(ON, "UPDATE")) {
                column.setOnUpdateExpression(session, readExpression());
            }
            nullConstraint = parseNotNullConstraint(nullConstraint);
            if (parseCompatibilityIdentity(column, mode)) {
                nullConstraint = parseNotNullConstraint(nullConstraint);
            }
        }
        switch (nullConstraint) {
        case NULL_IS_ALLOWED:
            if (column.isIdentity()) {
                throw DbException.get(ErrorCode.COLUMN_MUST_NOT_BE_NULLABLE_1, column.getName());
            }
            column.setNullable(true);
            break;
        case NULL_IS_NOT_ALLOWED:
            column.setNullable(false);
            break;
        case NO_NULL_CONSTRAINT_FOUND:
            if (!column.isIdentity()) {
                column.setNullable(defaultNullable);
            }
            break;
        default:
            throw DbException.get(ErrorCode.UNKNOWN_MODE_1,
                    "Internal Error - unhandled case: " + nullConstraint.name());
        }
        if (!defaultOnNull) {
            if (readIf(DEFAULT, ON, NULL)) {
                defaultOnNull = true;
            } else if (readIf("NULL_TO_DEFAULT")) {
                defaultOnNull = true;
            }
        }
        if (defaultOnNull) {
            column.setDefaultOnNull(true);
        }
        if (!column.isGenerated()) {
            if (readIf("SEQUENCE")) {
                column.setSequence(readSequence(), column.isGeneratedAlways());
            }
        }
        if (readIf("SELECTIVITY")) {
            column.setSelectivity(readNonNegativeInt());
        }
        if (mode.mySqlTableOptions) {
            if (readIf("CHARACTER")) {
                readIf(SET);
                readMySQLCharset();
            }
            if (readIf("COLLATE")) {
                readMySQLCharset();
            }
        }
        String comment = readCommentIf();
        if (comment != null) {
            column.setComment(comment);
        }
        return column;
    }

    private void parseCompatibilityIdentityOptions(Column column) {
        SequenceOptions options = new SequenceOptions();
        if (readIf(OPEN_PAREN)) {
            options.setStartValue(ValueExpression.get(ValueBigint.get(readLong())));
            if (readIf(COMMA)) {
                options.setIncrement(ValueExpression.get(ValueBigint.get(readLong())));
            }
            read(CLOSE_PAREN);
        }
        column.setIdentityOptions(options, false);
    }

    private String readCommentIf() {
        if (readIf("COMMENT")) {
            readIf(IS);
            return readString();
        }
        return null;
    }

    private Column parseColumnWithType(String columnName) {
        TypeInfo typeInfo = readIfDataType();
        if (typeInfo == null) {
            String domainName = readIdentifierWithSchema();
            return getColumnWithDomain(columnName, getSchema().getDomain(domainName));
        }
        return new Column(columnName, typeInfo);
    }

    private TypeInfo parseDataType() {
        TypeInfo typeInfo = readIfDataType();
        if (typeInfo == null) {
            addExpected("data type");
            throw getSyntaxError();
        }
        return typeInfo;
    }

    private TypeInfo readIfDataType() {
        TypeInfo typeInfo = readIfDataType1();
        if (typeInfo != null) {
            while (readIf(ARRAY)) {
                typeInfo = parseArrayType(typeInfo);
            }
        }
        return typeInfo;
    }

    private TypeInfo readIfDataType1() {
        switch (currentTokenType) {
        case IDENTIFIER:
            if (token.isQuoted()) {
                return null;
            }
            break;
        case INTERVAL: {
            read();
            TypeInfo typeInfo = readIntervalQualifier();
            if (typeInfo == null) {
                throw intervalQualifierError();
            }
            return typeInfo;
        }
        case NULL:
            read();
            return TypeInfo.TYPE_NULL;
        case ROW:
            read();
            return parseRowType();
        case ARRAY:
            // Partial compatibility with 1.4.200 and older versions
            if (session.isQuirksMode()) {
                read();
                return parseArrayType(TypeInfo.TYPE_VARCHAR);
            }
            addExpected("data type");
            throw getSyntaxError();
        default:
            if (isKeyword(currentTokenType)) {
                break;
            }
            addExpected("data type");
            throw getSyntaxError();
        }
        int index = tokenIndex;
        String originalCase = currentToken;
        read();
        if (currentTokenType == DOT) {
            setTokenIndex(index);
            return null;
        }
        String original = upperName(originalCase);
        switch (original) {
        case "BINARY":
            if (readIf("VARYING")) {
                original = "BINARY VARYING";
            } else if (readIf("LARGE")) {
                read("OBJECT");
                original = "BINARY LARGE OBJECT";
            } else if (variableBinary) {
                original = "VARBINARY";
            }
            break;
        case "CHAR":
            if (readIf("VARYING")) {
                original = "CHAR VARYING";
            } else if (readIf("LARGE")) {
                read("OBJECT");
                original = "CHAR LARGE OBJECT";
            }
            break;
        case "CHARACTER":
            if (readIf("VARYING")) {
                original = "CHARACTER VARYING";
            } else if (readIf("LARGE")) {
                read("OBJECT");
                original = "CHARACTER LARGE OBJECT";
            }
            break;
        case "DATE":
            return database.getMode().dateIsTimestamp0 ? TypeInfo.getTypeInfo(Value.TIMESTAMP, -1L, 0, null)
                    : TypeInfo.TYPE_DATE;
        case "DATETIME":
        case "DATETIME2":
            return parseDateTimeType(false);
        case "DEC":
        case "DECIMAL":
            return parseNumericType(true);
        case "DECFLOAT":
            return parseDecfloatType();
        case "DOUBLE":
            if (readIf("PRECISION")) {
                original = "DOUBLE PRECISION";
            }
            break;
        case "ENUM":
            return parseEnumType();
        case "FLOAT":
            return parseFloatType();
        case "GEOMETRY":
            return parseGeometryType();
        case "LONG":
            if (readIf("RAW")) {
                original = "LONG RAW";
            }
            break;
        case "NATIONAL":
            if (readIf("CHARACTER")) {
                if (readIf("VARYING")) {
                    original = "NATIONAL CHARACTER VARYING";
                } else if (readIf("LARGE")) {
                    read("OBJECT");
                    original = "NATIONAL CHARACTER LARGE OBJECT";
                } else {
                    original = "NATIONAL CHARACTER";
                }
            } else {
                read("CHAR");
                if (readIf("VARYING")) {
                    original = "NATIONAL CHAR VARYING";
                } else {
                    original = "NATIONAL CHAR";
                }
            }
            break;
        case "NCHAR":
            if (readIf("VARYING")) {
                original = "NCHAR VARYING";
            } else if (readIf("LARGE")) {
                read("OBJECT");
                original = "NCHAR LARGE OBJECT";
            }
            break;
        case "NUMBER":
            if (database.getMode().disallowedTypes.contains("NUMBER")) {
                throw DbException.get(ErrorCode.UNKNOWN_DATA_TYPE_1, "NUMBER");
            }
            if (!isToken(OPEN_PAREN)) {
                return TypeInfo.getTypeInfo(Value.DECFLOAT, 40, -1, null);
            }
            //$FALL-THROUGH$
        case "NUMERIC":
            return parseNumericType(false);
        case "SMALLDATETIME":
            return parseDateTimeType(true);
        case "TIME":
            return parseTimeType();
        case "TIMESTAMP":
            return parseTimestampType();
        }
        // Domain names can't have multiple words without quotes
        if (originalCase.length() == original.length()) {
            Domain domain = database.getSchema(session.getCurrentSchemaName()).findDomain(originalCase);
            if (domain != null) {
                setTokenIndex(index);
                return null;
            }
        }
        Mode mode = database.getMode();
        DataType dataType = DataType.getTypeByName(original, mode);
        if (dataType == null || mode.disallowedTypes.contains(original)) {
            throw DbException.get(ErrorCode.UNKNOWN_DATA_TYPE_1, original);
        }
        long precision;
        int scale;
        if (dataType.specialPrecisionScale) {
            precision = dataType.defaultPrecision;
            scale = dataType.defaultScale;
        } else {
            precision = -1L;
            scale = -1;
        }
        int t = dataType.type;
        if (database.getIgnoreCase() && t == Value.VARCHAR && !equalsToken("VARCHAR_CASESENSITIVE", original)) {
            dataType = DataType.getDataType(t = Value.VARCHAR_IGNORECASE);
        }
        if ((dataType.supportsPrecision || dataType.supportsScale) && readIf(OPEN_PAREN)) {
            if (!readIf("MAX")) {
                if (dataType.supportsPrecision) {
                    precision = readPrecision(t);
                    if (precision < dataType.minPrecision) {
                        throw getInvalidPrecisionException(dataType, precision);
                    } else if (precision > dataType.maxPrecision)
                    badPrecision: {
                        if (session.isQuirksMode() || session.isTruncateLargeLength()) {
                            switch (dataType.type) {
                            case Value.CHAR:
                            case Value.VARCHAR:
                            case Value.VARCHAR_IGNORECASE:
                            case Value.BINARY:
                            case Value.VARBINARY:
                            case Value.JAVA_OBJECT:
                            case Value.JSON:
                                precision = dataType.maxPrecision;
                                break badPrecision;
                            }
                        }
                        throw getInvalidPrecisionException(dataType, precision);
                    }
                    if (dataType.supportsScale) {
                        if (readIf(COMMA)) {
                            scale = readInt();
                            if (scale < dataType.minScale || scale > dataType.maxScale) {
                                throw DbException.get(ErrorCode.INVALID_VALUE_SCALE, Integer.toString(scale),
                                        Integer.toString(dataType.minScale), Integer.toString(dataType.maxScale));
                            }
                        }
                    }
                } else {
                    scale = readInt();
                    if (scale < dataType.minScale || scale > dataType.maxScale) {
                        throw DbException.get(ErrorCode.INVALID_VALUE_SCALE, Integer.toString(scale),
                                Integer.toString(dataType.minScale), Integer.toString(dataType.maxScale));
                    }
                }
            }
            read(CLOSE_PAREN);
        }
        if (mode.allNumericTypesHavePrecision
                && (DataType.isNumericType(dataType.type) || dataType.type == Value.BOOLEAN)) {
            if (readIf(OPEN_PAREN)) {
                // Support for MySQL: INT(11), MEDIUMINT(8) and so on.
                // Just ignore the precision.
                readNonNegativeInt();
                read(CLOSE_PAREN);
            }
            readIf("UNSIGNED");
        }
        if (mode.forBitData && DataType.isStringType(t)) {
            if (readIf(FOR, "BIT", "DATA")) {
                dataType = DataType.getDataType(t = Value.VARBINARY);
            }
        }
        return TypeInfo.getTypeInfo(t, precision, scale, null);
    }

    private static DbException getInvalidPrecisionException(DataType dataType, long precision) {
        return DbException.get(ErrorCode.INVALID_VALUE_PRECISION, Long.toString(precision),
                Long.toString(dataType.minPrecision), Long.toString(dataType.maxPrecision));
    }

    private static Column getColumnWithDomain(String columnName, Domain domain) {
        Column column = new Column(columnName, domain.getDataType());
        column.setComment(domain.getComment());
        column.setDomain(domain);
        return column;
    }

    private TypeInfo parseFloatType() {
        int type = Value.DOUBLE;
        int precision;
        if (readIf(OPEN_PAREN)) {
            precision = readNonNegativeInt();
            read(CLOSE_PAREN);
            if (precision < 1 || precision > 53) {
                throw DbException.get(ErrorCode.INVALID_VALUE_PRECISION, Integer.toString(precision), "1", "53");
            }
            if (precision <= 24) {
                type = Value.REAL;
            }
        } else {
            precision = 0;
        }
        return TypeInfo.getTypeInfo(type, precision, -1, null);
    }

    private TypeInfo parseNumericType(boolean decimal) {
        long precision = -1L;
        int scale = -1;
        if (readIf(OPEN_PAREN)) {
            precision = readPrecision(Value.NUMERIC);
            if (precision < 1) {
                throw getInvalidNumericPrecisionException(precision);
            } else if (precision > Constants.MAX_NUMERIC_PRECISION) {
                if (session.isQuirksMode() || session.isTruncateLargeLength()) {
                    precision = Constants.MAX_NUMERIC_PRECISION;
                } else {
                    throw getInvalidNumericPrecisionException(precision);
                }
            }
            if (readIf(COMMA)) {
                scale = readInt();
                if (scale < 0 || scale > ValueNumeric.MAXIMUM_SCALE) {
                    throw DbException.get(ErrorCode.INVALID_VALUE_SCALE, Integer.toString(scale),
                            "0", "" + ValueNumeric.MAXIMUM_SCALE);
                }
            }
            read(CLOSE_PAREN);
        } else if (database.getMode().numericIsDecfloat) {
            return TypeInfo.TYPE_DECFLOAT;
        }
        return TypeInfo.getTypeInfo(Value.NUMERIC, precision, scale, decimal ? ExtTypeInfoNumeric.DECIMAL : null);
    }

    private TypeInfo parseDecfloatType() {
        long precision = -1L;
        if (readIf(OPEN_PAREN)) {
            precision = readPrecision(Value.DECFLOAT);
            if (precision < 1 || precision > Constants.MAX_NUMERIC_PRECISION) {
                throw getInvalidNumericPrecisionException(precision);
            }
            read(CLOSE_PAREN);
        }
        return TypeInfo.getTypeInfo(Value.DECFLOAT, precision, -1, null);
    }

    private static DbException getInvalidNumericPrecisionException(long precision) {
        return DbException.get(ErrorCode.INVALID_VALUE_PRECISION, Long.toString(precision), "1",
                "" + Constants.MAX_NUMERIC_PRECISION);
    }

    private TypeInfo parseTimeType() {
        int scale = -1;
        if (readIf(OPEN_PAREN)) {
            scale = readNonNegativeInt();
            if (scale > ValueTime.MAXIMUM_SCALE) {
                throw DbException.get(ErrorCode.INVALID_VALUE_SCALE, Integer.toString(scale), "0",
                        /* Folds to a constant */ "" + ValueTime.MAXIMUM_SCALE);
            }
            read(CLOSE_PAREN);
        }
        int type = Value.TIME;
        if (readIf(WITH, "TIME", "ZONE")) {
            type = Value.TIME_TZ;
        } else {
            readIf("WITHOUT", "TIME", "ZONE");
        }
        return TypeInfo.getTypeInfo(type, -1L, scale, null);
    }

    private TypeInfo parseTimestampType() {
        int scale = -1;
        if (readIf(OPEN_PAREN)) {
            scale = readNonNegativeInt();
            // Allow non-standard TIMESTAMP(..., ...) syntax
            if (readIf(COMMA)) {
                scale = readNonNegativeInt();
            }
            if (scale > ValueTimestamp.MAXIMUM_SCALE) {
                throw DbException.get(ErrorCode.INVALID_VALUE_SCALE, Integer.toString(scale), "0",
                        /* Folds to a constant */ "" + ValueTimestamp.MAXIMUM_SCALE);
            }
            read(CLOSE_PAREN);
        }
        int type = Value.TIMESTAMP;
        if (readIf(WITH, "TIME", "ZONE")) {
            type = Value.TIMESTAMP_TZ;
        } else {
            readIf("WITHOUT", "TIME", "ZONE");
        }
        return TypeInfo.getTypeInfo(type, -1L, scale, null);
    }

    private TypeInfo parseDateTimeType(boolean smallDateTime) {
        int scale;
        if (smallDateTime) {
            scale = 0;
        } else {
            scale = -1;
            if (readIf(OPEN_PAREN)) {
                scale = readNonNegativeInt();
                if (scale > ValueTimestamp.MAXIMUM_SCALE) {
                    throw DbException.get(ErrorCode.INVALID_VALUE_SCALE, Integer.toString(scale), "0",
                            /* folds to a constant */ "" + ValueTimestamp.MAXIMUM_SCALE);
                }
                read(CLOSE_PAREN);
            }
        }
        return TypeInfo.getTypeInfo(Value.TIMESTAMP, -1L, scale, null);
    }

    private TypeInfo readIntervalQualifier() {
        IntervalQualifier qualifier;
        int precision = -1, scale = -1;
        switch (currentTokenType) {
        case YEAR:
            read();
            if (readIf(OPEN_PAREN)) {
                precision = readNonNegativeInt();
                read(CLOSE_PAREN);
            }
            if (readIf(TO, MONTH)) {
                qualifier = IntervalQualifier.YEAR_TO_MONTH;
            } else {
                qualifier = IntervalQualifier.YEAR;
            }
            break;
        case MONTH:
            read();
            if (readIf(OPEN_PAREN)) {
                precision = readNonNegativeInt();
                read(CLOSE_PAREN);
            }
            qualifier = IntervalQualifier.MONTH;
            break;
        case DAY:
            read();
            if (readIf(OPEN_PAREN)) {
                precision = readNonNegativeInt();
                read(CLOSE_PAREN);
            }
            if (readIf(TO)) {
                switch (currentTokenType) {
                case HOUR:
                    read();
                    qualifier = IntervalQualifier.DAY_TO_HOUR;
                    break;
                case MINUTE:
                    read();
                    qualifier = IntervalQualifier.DAY_TO_MINUTE;
                    break;
                case SECOND:
                    read();
                    if (readIf(OPEN_PAREN)) {
                        scale = readNonNegativeInt();
                        read(CLOSE_PAREN);
                    }
                    qualifier = IntervalQualifier.DAY_TO_SECOND;
                    break;
                default:
                    throw intervalDayError();
                }
            } else {
                qualifier = IntervalQualifier.DAY;
            }
            break;
        case HOUR:
            read();
            if (readIf(OPEN_PAREN)) {
                precision = readNonNegativeInt();
                read(CLOSE_PAREN);
            }
            if (readIf(TO)) {
                switch (currentTokenType) {
                case MINUTE:
                    read();
                    qualifier = IntervalQualifier.HOUR_TO_MINUTE;
                    break;
                case SECOND:
                    read();
                    if (readIf(OPEN_PAREN)) {
                        scale = readNonNegativeInt();
                        read(CLOSE_PAREN);
                    }
                    qualifier = IntervalQualifier.HOUR_TO_SECOND;
                    break;
                default:
                    throw intervalHourError();
                }
            } else {
                qualifier = IntervalQualifier.HOUR;
            }
            break;
        case MINUTE:
            read();
            if (readIf(OPEN_PAREN)) {
                precision = readNonNegativeInt();
                read(CLOSE_PAREN);
            }
            if (readIf(TO, SECOND)) {
                if (readIf(OPEN_PAREN)) {
                    scale = readNonNegativeInt();
                    read(CLOSE_PAREN);
                }
                qualifier = IntervalQualifier.MINUTE_TO_SECOND;
            } else {
                qualifier = IntervalQualifier.MINUTE;
            }
            break;
        case SECOND:
            read();
            if (readIf(OPEN_PAREN)) {
                precision = readNonNegativeInt();
                if (readIf(COMMA)) {
                    scale = readNonNegativeInt();
                }
                read(CLOSE_PAREN);
            }
            qualifier = IntervalQualifier.SECOND;
            break;
        default:
            return null;
        }
        if (precision >= 0) {
            if (precision == 0 || precision > ValueInterval.MAXIMUM_PRECISION) {
                throw DbException.get(ErrorCode.INVALID_VALUE_PRECISION, Integer.toString(precision), "1",
                        /* Folds to a constant */ "" + ValueInterval.MAXIMUM_PRECISION);
            }
        }
        if (scale >= 0) {
            if (scale > ValueInterval.MAXIMUM_SCALE) {
                throw DbException.get(ErrorCode.INVALID_VALUE_SCALE, Integer.toString(scale), "0",
                        /* Folds to a constant */ "" + ValueInterval.MAXIMUM_SCALE);
            }
        }
        return TypeInfo.getTypeInfo(qualifier.ordinal() + Value.INTERVAL_YEAR, precision, scale, null);
    }

    private DbException intervalQualifierError() {
        if (expectedList != null) {
            addMultipleExpected(YEAR, MONTH, DAY, HOUR, MINUTE, SECOND);
        }
        return getSyntaxError();
    }

    private DbException intervalDayError() {
        if (expectedList != null) {
            addMultipleExpected(HOUR, MINUTE, SECOND);
        }
        return getSyntaxError();
    }

    private DbException intervalHourError() {
        if (expectedList != null) {
            addMultipleExpected(MINUTE, SECOND);
        }
        return getSyntaxError();
    }

    private TypeInfo parseArrayType(TypeInfo componentType) {
        int precision = -1;
        if (readIf(OPEN_BRACKET)) {
            // Maximum cardinality may be zero
            precision = readNonNegativeInt();
            if (precision > Constants.MAX_ARRAY_CARDINALITY) {
                throw DbException.get(ErrorCode.INVALID_VALUE_PRECISION, Integer.toString(precision), "0",
                        /* Folds to a constant */ "" + Constants.MAX_ARRAY_CARDINALITY);
            }
            read(CLOSE_BRACKET);
        }
        return TypeInfo.getTypeInfo(Value.ARRAY, precision, -1, componentType);
    }

    private TypeInfo parseEnumType() {
        read(OPEN_PAREN);
        ArrayList<String> enumeratorList = new ArrayList<>();
        do {
            enumeratorList.add(readString());
        } while (readIfMore());
        return TypeInfo.getTypeInfo(Value.ENUM, -1L, -1, new ExtTypeInfoEnum(enumeratorList.toArray(new String[0])));
    }

    private TypeInfo parseGeometryType() {
        ExtTypeInfoGeometry extTypeInfo;
        if (readIf(OPEN_PAREN)) {
            int type = 0;
            if (currentTokenType != IDENTIFIER || token.isQuoted()) {
                throw getSyntaxError();
            }
            if (!readIf("GEOMETRY")) {
                try {
                    type = EWKTUtils.parseGeometryType(currentToken);
                    read();
                    if (type / 1_000 == 0 && currentTokenType == IDENTIFIER && !token.isQuoted()) {
                        type += EWKTUtils.parseDimensionSystem(currentToken) * 1_000;
                        read();
                    }
                } catch (IllegalArgumentException ex) {
                    throw getSyntaxError();
                }
            }
            Integer srid = null;
            if (readIf(COMMA)) {
                srid = readInt();
            }
            read(CLOSE_PAREN);
            extTypeInfo = new ExtTypeInfoGeometry(type, srid);
        } else {
            extTypeInfo = null;
        }
        return TypeInfo.getTypeInfo(Value.GEOMETRY, -1L, -1, extTypeInfo);
    }

    private TypeInfo parseRowType() {
        read(OPEN_PAREN);
        LinkedHashMap<String, TypeInfo> fields = new LinkedHashMap<>();
        do {
            String name = readIdentifier();
            if (fields.putIfAbsent(name, parseDataType()) != null) {
                throw DbException.get(ErrorCode.DUPLICATE_COLUMN_NAME_1, name);
            }
        } while (readIfMore());
        return TypeInfo.getTypeInfo(Value.ROW, -1L, -1, new ExtTypeInfoRow(fields));
    }

    private long readPrecision(int valueType) {
        long p = readPositiveLong();
        if (currentTokenType != IDENTIFIER || token.isQuoted()) {
            return p;
        }
        if ((valueType == Value.BLOB || valueType == Value.CLOB) && currentToken.length() == 1) {
            long mul;
            /*
             * Convert a-z to A-Z. This method is safe, because only A-Z
             * characters are considered below.
             */
            switch (currentToken.charAt(0) & 0xffdf) {
            case 'K':
                mul = 1L << 10;
                break;
            case 'M':
                mul = 1L << 20;
                break;
            case 'G':
                mul = 1L << 30;
                break;
            case 'T':
                mul = 1L << 40;
                break;
            case 'P':
                mul = 1L << 50;
                break;
            default:
                throw getSyntaxError();
            }
            if (p > Long.MAX_VALUE / mul) {
                throw DbException.getInvalidValueException("precision", p + currentToken);
            }
            p *= mul;
            read();
            if (currentTokenType != IDENTIFIER || token.isQuoted()) {
                return p;
            }
        }
        switch (valueType) {
        case Value.VARCHAR:
        case Value.VARCHAR_IGNORECASE:
        case Value.CLOB:
        case Value.CHAR:
            if (!readIf("CHARACTERS") && !readIf("OCTETS")) {
                if (database.getMode().charAndByteLengthUnits && !readIf("CHAR")) {
                    readIf("BYTE");
                }
            }
        }
        return p;
    }

    private Prepared parseCreate() {
        boolean orReplace = false;
        if (readIf(OR, "REPLACE")) {
            orReplace = true;
        }
        boolean force = readIf("FORCE");
        if (readIf("VIEW")) {
            return parseCreateView(force, orReplace);
        } else if (readIf("MATERIALIZED")) {
            read("VIEW");
            return parseCreateMaterializedView(force, orReplace);
        } else if (readIf("ALIAS")) {
            return parseCreateFunctionAlias(force);
        } else if (readIf("SEQUENCE")) {
            return parseCreateSequence();
        } else if (readIf(USER)) {
            return parseCreateUser();
        } else if (readIf("TRIGGER")) {
            return parseCreateTrigger(force);
        } else if (readIf("ROLE")) {
            return parseCreateRole();
        } else if (readIf("SCHEMA")) {
            return parseCreateSchema();
        } else if (readIf("CONSTANT")) {
            return parseCreateConstant();
        } else if (readIf("DOMAIN") || readIf("TYPE") || readIf("DATATYPE")) {
            return parseCreateDomain();
        } else if (readIf("AGGREGATE")) {
            return parseCreateAggregate(force);
        } else if (readIf("LINKED")) {
            return parseCreateLinkedTable(false, false, force);
        }
        // tables or linked tables
        boolean memory = false, cached = false;
        if (readIf("MEMORY")) {
            memory = true;
        } else if (readIf("CACHED")) {
            cached = true;
        }
        if (readIf("LOCAL", "TEMPORARY")) {
            if (readIf("LINKED")) {
                return parseCreateLinkedTable(true, false, force);
            }
            read(TABLE);
            return parseCreateTable(true, false, cached);
        } else if (readIf("GLOBAL", "TEMPORARY")) {
            if (readIf("LINKED")) {
                return parseCreateLinkedTable(true, true, force);
            }
            read(TABLE);
            return parseCreateTable(true, true, cached);
        } else if (readIf("TEMP") || readIf("TEMPORARY")) {
            if (readIf("LINKED")) {
                return parseCreateLinkedTable(true, true, force);
            }
            read(TABLE);
            return parseCreateTable(true, true, cached);
        } else if (readIf(TABLE)) {
            if (!cached && !memory) {
                cached = database.getDefaultTableType() == Table.TYPE_CACHED;
            }
            return parseCreateTable(false, false, cached);
        } else if (readIf("SYNONYM")) {
            return parseCreateSynonym(orReplace);
        } else {
            boolean hash = false, primaryKey = false;
            NullsDistinct nullsDistinct = null;
            boolean spatial = false;
            String indexName = null;
            Schema oldSchema = null;
            boolean ifNotExists = false;
            if (session.isQuirksMode() && readIf(PRIMARY, KEY)) {
                if (readIf("HASH")) {
                    hash = true;
                }
                primaryKey = true;
                if (!isToken(ON)) {
                    ifNotExists = readIfNotExists();
                    indexName = readIdentifierWithSchema(null);
                    oldSchema = getSchema();
                }
            } else {
                if (readIf(UNIQUE)) {
                    nullsDistinct = readNullsDistinct(database.getMode().nullsDistinct);
                }
                if (readIf("HASH")) {
                    hash = true;
                } else if (nullsDistinct == null && readIf("SPATIAL")) {
                    spatial = true;
                }
                read("INDEX");
                if (!isToken(ON)) {
                    ifNotExists = readIfNotExists();
                    indexName = readIdentifierWithSchema(null);
                    oldSchema = getSchema();
                }
            }
            read(ON);
            String tableName = readIdentifierWithSchema();
            checkSchema(oldSchema);
            String comment = readCommentIf();
            if (!readIf(OPEN_PAREN)) {
                // PostgreSQL compatibility
                if (hash || spatial) {
                    throw getSyntaxError();
                }
                read(USING);
                if (readIf("BTREE")) {
                    // default
                } else if (readIf("HASH")) {
                    hash = true;
                } else {
                    read("RTREE");
                    spatial = true;
                }
                read(OPEN_PAREN);
            }
            CreateIndex command = new CreateIndex(session, getSchema());
            command.setIfNotExists(ifNotExists);
            command.setPrimaryKey(primaryKey);
            command.setTableName(tableName);
            command.setHash(hash);
            command.setSpatial(spatial);
            command.setIndexName(indexName);
            command.setComment(comment);
            IndexColumn[] columns;
            int uniqueColumnCount = 0;
            if (spatial) {
                columns = new IndexColumn[] { new IndexColumn(readIdentifier()) };
                if (nullsDistinct != null) {
                    uniqueColumnCount = 1;
                }
                read(CLOSE_PAREN);
            } else {
                columns = parseIndexColumnList();
                if (nullsDistinct != null) {
                    uniqueColumnCount = columns.length;
                    if (readIf("INCLUDE")) {
                        read(OPEN_PAREN);
                        IndexColumn[] columnsToInclude = parseIndexColumnList();
                        int nonUniqueCount = columnsToInclude.length;
                        columns = Arrays.copyOf(columns, uniqueColumnCount + nonUniqueCount);
                        System.arraycopy(columnsToInclude, 0, columns, uniqueColumnCount, nonUniqueCount);
                    }
                } else if (primaryKey) {
                    uniqueColumnCount = columns.length;
                }
            }
            command.setIndexColumns(columns);
            command.setUnique(nullsDistinct, uniqueColumnCount);
            return command;
        }
    }

    private NullsDistinct readNullsDistinct(NullsDistinct defaultDistinct) {
        if (readIf("NULLS")) {
            if (readIf(DISTINCT)) {
                return NullsDistinct.DISTINCT;
            }
            if (readIf(NOT, DISTINCT)) {
                return NullsDistinct.NOT_DISTINCT;
            }
            if (readIf(ALL, DISTINCT)) {
                return NullsDistinct.ALL_DISTINCT;
            }
            throw getSyntaxError();
        }
        return defaultDistinct;
    }

    /**
     * @return true if we expect to see a TABLE clause
     */
    private boolean addRoleOrRight(GrantRevoke command) {
        if (readIf(SELECT)) {
            command.addRight(Right.SELECT);
            return true;
        } else if (readIf("DELETE")) {
            command.addRight(Right.DELETE);
            return true;
        } else if (readIf("INSERT")) {
            command.addRight(Right.INSERT);
            return true;
        } else if (readIf("UPDATE")) {
            command.addRight(Right.UPDATE);
            return true;
        } else if (readIf("CONNECT")) {
            // ignore this right
            return true;
        } else if (readIf("RESOURCE")) {
            // ignore this right
            return true;
        } else {
            command.addRoleName(readIdentifier());
            return false;
        }
    }

    private GrantRevoke parseGrantRevoke(int operationType) {
        GrantRevoke command = new GrantRevoke(session);
        command.setOperationType(operationType);
        boolean tableClauseExpected;
        if (readIf(ALL)) {
            readIf("PRIVILEGES");
            command.addRight(Right.ALL);
            tableClauseExpected = true;
        } else if (readIf("ALTER")) {
            read(ANY);
            read("SCHEMA");
            command.addRight(Right.ALTER_ANY_SCHEMA);
            command.addTable(null);
            tableClauseExpected = false;
        } else {
            tableClauseExpected = addRoleOrRight(command);
            while (readIf(COMMA)) {
                if (addRoleOrRight(command) != tableClauseExpected) {
                    throw DbException.get(ErrorCode.ROLES_AND_RIGHT_CANNOT_BE_MIXED);
                }
            }
        }
        if (tableClauseExpected) {
            if (readIf(ON)) {
                if (readIf("SCHEMA")) {
                    command.setSchema(database.getSchema(readIdentifier()));
                } else {
                    readIf(TABLE);
                    do {
                        Table table = readTableOrView();
                        command.addTable(table);
                    } while (readIf(COMMA));
                }
            }
        }
        read(operationType == CommandInterface.GRANT ? TO : FROM);
        command.setGranteeName(readIdentifier());
        return command;
    }

    private TableValueConstructor parseValues() {
        ArrayList<ArrayList<Expression>> rows = Utils.newSmallArrayList();
        ArrayList<Expression> row = parseValuesRow(Utils.newSmallArrayList());
        rows.add(row);
        int columnCount = row.size();
        while (readIf(COMMA)) {
            row = parseValuesRow(new ArrayList<>(columnCount));
            if (row.size() != columnCount) {
                throw DbException.get(ErrorCode.COLUMN_COUNT_DOES_NOT_MATCH);
            }
            rows.add(row);
        }
        return new TableValueConstructor(session, rows);
    }

    private ArrayList<Expression> parseValuesRow(ArrayList<Expression> row) {
        if (!readIf(ROW, OPEN_PAREN) && !readIf(OPEN_PAREN)) {
            row.add(readExpression());
            return row;
        }
        do {
            row.add(readExpression());
        } while (readIfMore());
        return row;
    }

    private Call parseCall() {
        Call command = new Call(session);
        currentPrepared = command;
        if (readIf(TABLE, OPEN_PAREN)) {
            command.setTableFunction(readTableFunction(ArrayTableFunction.TABLE));
            return command;
        }
        int index = tokenIndex;
        boolean canBeFunction = isIdentifier();
        try {
            command.setExpression(readExpression());
        } catch (DbException e) {
            if (canBeFunction && e.getErrorCode() == ErrorCode.FUNCTION_NOT_FOUND_1) {
                setTokenIndex(index);
                String schemaName = null, name = readIdentifier();
                if (readIf(DOT)) {
                    schemaName = name;
                    name = readIdentifier();
                    if (readIf(DOT)) {
                        checkDatabaseName(schemaName);
                        schemaName = name;
                        name = readIdentifier();
                    }
                }
                read(OPEN_PAREN);
                Schema schema = schemaName != null ? database.getSchema(schemaName) : null;
                command.setTableFunction(readTableFunction(name, schema));
                return command;
            }
            throw e;
        }
        return command;
    }

    private CreateRole parseCreateRole() {
        CreateRole command = new CreateRole(session);
        command.setIfNotExists(readIfNotExists());
        command.setRoleName(readIdentifier());
        return command;
    }

    private CreateSchema parseCreateSchema() {
        CreateSchema command = new CreateSchema(session);
        command.setIfNotExists(readIfNotExists());
        String authorization;
        if (readIf(AUTHORIZATION)) {
            authorization = readIdentifier();
            command.setSchemaName(authorization);
            command.setAuthorization(authorization);
        } else {
            command.setSchemaName(readIdentifier());
            if (readIf(AUTHORIZATION)) {
                authorization = readIdentifier();
            } else {
                authorization = session.getUser().getName();
            }
        }
        command.setAuthorization(authorization);
        if (readIf(WITH)) {
            command.setTableEngineParams(readTableEngineParams());
        }
        return command;
    }

    private ArrayList<String> readTableEngineParams() {
        ArrayList<String> tableEngineParams = Utils.newSmallArrayList();
        do {
            tableEngineParams.add(readIdentifier());
        } while (readIf(COMMA));
        return tableEngineParams;
    }

    private CreateSequence parseCreateSequence() {
        boolean ifNotExists = readIfNotExists();
        String sequenceName = readIdentifierWithSchema();
        CreateSequence command = new CreateSequence(session, getSchema());
        command.setIfNotExists(ifNotExists);
        command.setSequenceName(sequenceName);
        SequenceOptions options = new SequenceOptions();
        parseSequenceOptions(options, command, true, false);
        command.setOptions(options);
        return command;
    }

    private boolean readIfNotExists() {
        if (readIf(IF, NOT, EXISTS)) {
            return true;
        }
        return false;
    }

    private CreateConstant parseCreateConstant() {
        boolean ifNotExists = readIfNotExists();
        String constantName = readIdentifierWithSchema();
        Schema schema = getSchema();
        if (isKeyword(constantName)) {
            throw DbException.get(ErrorCode.CONSTANT_ALREADY_EXISTS_1,
                    constantName);
        }
        read(VALUE);
        Expression expr = readExpression();
        CreateConstant command = new CreateConstant(session, schema);
        command.setConstantName(constantName);
        command.setExpression(expr);
        command.setIfNotExists(ifNotExists);
        return command;
    }

    private CreateAggregate parseCreateAggregate(boolean force) {
        boolean ifNotExists = readIfNotExists();
        String name = readIdentifierWithSchema(), upperName;
        if (isKeyword(name) || BuiltinFunctions.isBuiltinFunction(database, upperName = upperName(name))
                || Aggregate.getAggregateType(upperName) != null) {
            throw DbException.get(ErrorCode.FUNCTION_ALIAS_ALREADY_EXISTS_1, name);
        }
        CreateAggregate command = new CreateAggregate(session, getSchema());
        command.setForce(force);
        command.setName(name);
        command.setIfNotExists(ifNotExists);
        read(FOR);
        command.setJavaClassMethod(readStringOrIdentifier());
        return command;
    }

    private CreateDomain parseCreateDomain() {
        boolean ifNotExists = readIfNotExists();
        String domainName = readIdentifierWithSchema();
        Schema schema = getSchema();
        CreateDomain command = new CreateDomain(session, schema);
        command.setIfNotExists(ifNotExists);
        command.setTypeName(domainName);
        readIf(AS);
        TypeInfo dataType = readIfDataType();
        if (dataType != null) {
            command.setDataType(dataType);
        } else {
            String parentDomainName = readIdentifierWithSchema();
            command.setParentDomain(getSchema().getDomain(parentDomainName));
        }
        if (readIf(DEFAULT)) {
            command.setDefaultExpression(readExpression());
        }
        if (readIf(ON, "UPDATE")) {
            command.setOnUpdateExpression(readExpression());
        }
        // Compatibility with 1.4.200 and older versions
        if (readIf("SELECTIVITY")) {
            readNonNegativeInt();
        }
        String comment = readCommentIf();
        if (comment != null) {
            command.setComment(comment);
        }
        for (;;) {
            String constraintName;
            if (readIf(CONSTRAINT)) {
                constraintName = readIdentifier();
                read(CHECK);
            } else if (readIf(CHECK)) {
                constraintName = null;
            } else {
                break;
            }
            AlterDomainAddConstraint constraint = new AlterDomainAddConstraint(session, schema, ifNotExists);
            constraint.setConstraintName(constraintName);
            constraint.setDomainName(domainName);
            parseDomainConstraint = true;
            try {
                constraint.setCheckExpression(readExpression());
            } finally {
                parseDomainConstraint = false;
            }
            command.addConstraintCommand(constraint);
        }
        return command;
    }

    private CreateTrigger parseCreateTrigger(boolean force) {
        boolean ifNotExists = readIfNotExists();
        String triggerName = readIdentifierWithSchema(null);
        Schema schema = getSchema();
        boolean insteadOf, isBefore;
        if (readIf("INSTEAD", "OF")) {
            isBefore = true;
            insteadOf = true;
        } else if (readIf("BEFORE")) {
            insteadOf = false;
            isBefore = true;
        } else {
            read("AFTER");
            insteadOf = false;
            isBefore = false;
        }
        int typeMask = 0;
        boolean onRollback = false;
        boolean allowOr = database.getMode().getEnum() == ModeEnum.PostgreSQL;
        do {
            if (readIf("INSERT")) {
                typeMask |= Trigger.INSERT;
            } else if (readIf("UPDATE")) {
                typeMask |= Trigger.UPDATE;
            } else if (readIf("DELETE")) {
                typeMask |= Trigger.DELETE;
            } else if (readIf(SELECT)) {
                typeMask |= Trigger.SELECT;
            } else if (readIf("ROLLBACK")) {
                onRollback = true;
            } else {
                throw getSyntaxError();
            }
        } while (readIf(COMMA) || allowOr && readIf(OR));
        read(ON);
        String tableName = readIdentifierWithSchema();
        checkSchema(schema);
        CreateTrigger command = new CreateTrigger(session, getSchema());
        command.setForce(force);
        command.setTriggerName(triggerName);
        command.setIfNotExists(ifNotExists);
        command.setInsteadOf(insteadOf);
        command.setBefore(isBefore);
        command.setOnRollback(onRollback);
        command.setTypeMask(typeMask);
        command.setTableName(tableName);
        if (readIf(FOR, "EACH")) {
            if (readIf(ROW)) {
                command.setRowBased(true);
            } else {
                read("STATEMENT");
            }
        }
        if (readIf("QUEUE")) {
            command.setQueueSize(readNonNegativeInt());
        }
        command.setNoWait(readIf("NOWAIT"));
        if (readIf(AS)) {
            command.setTriggerSource(readString());
        } else {
            read("CALL");
            command.setTriggerClassName(readStringOrIdentifier());
        }
        return command;
    }

    private CreateUser parseCreateUser() {
        CreateUser command = new CreateUser(session);
        command.setIfNotExists(readIfNotExists());
        command.setUserName(readIdentifier());
        command.setComment(readCommentIf());
        if (readIf("PASSWORD")) {
            command.setPassword(readExpression());
        } else if (readIf("SALT")) {
            command.setSalt(readExpression());
            read("HASH");
            command.setHash(readExpression());
        } else if (readIf("IDENTIFIED")) {
            read("BY");
            // uppercase if not quoted
            command.setPassword(ValueExpression.get(ValueVarchar.get(readIdentifier())));
        } else {
            throw getSyntaxError();
        }
        if (readIf("ADMIN")) {
            command.setAdmin(true);
        }
        return command;
    }

    private CreateFunctionAlias parseCreateFunctionAlias(boolean force) {
        boolean ifNotExists = readIfNotExists();
        String aliasName;
        if (currentTokenType == IDENTIFIER) {
            aliasName = readIdentifierWithSchema();
        } else if (isKeyword(currentTokenType)) {
            aliasName = currentToken;
            read();
            schemaName = session.getCurrentSchemaName();
        } else {
            addExpected("identifier");
            throw getSyntaxError();
        }
        String upperName = upperName(aliasName);
        if (isReservedFunctionName(upperName)) {
            throw DbException.get(ErrorCode.FUNCTION_ALIAS_ALREADY_EXISTS_1, aliasName);
        }
        CreateFunctionAlias command = new CreateFunctionAlias(session, getSchema());
        command.setForce(force);
        command.setAliasName(aliasName);
        command.setIfNotExists(ifNotExists);
        command.setDeterministic(readIf("DETERMINISTIC"));
        // Compatibility with old versions of H2
        readIf("NOBUFFER");
        if (readIf(AS)) {
            command.setSource(readString());
        } else {
            read(FOR);
            command.setJavaClassMethod(readStringOrIdentifier());
        }
        return command;
    }

    private String readStringOrIdentifier() {
        return currentTokenType != IDENTIFIER ? readString() : readIdentifier();
    }

    private boolean isReservedFunctionName(String name) {
        int tokenType = ParserUtil.getTokenType(name, false, false);
        if (tokenType != ParserUtil.IDENTIFIER) {
            if (database.isAllowBuiltinAliasOverride()) {
                switch (tokenType) {
                case CURRENT_DATE:
                case CURRENT_TIME:
                case CURRENT_TIMESTAMP:
                case DAY:
                case HOUR:
                case LOCALTIME:
                case LOCALTIMESTAMP:
                case MINUTE:
                case MONTH:
                case SECOND:
                case YEAR:
                    return false;
                }
            }
            return true;
        }
        return Aggregate.getAggregateType(name) != null
                || BuiltinFunctions.isBuiltinFunction(database, name) && !database.isAllowBuiltinAliasOverride();
    }

    private Prepared parseWith() {
        List<TableView> viewsCreated = new ArrayList<>();
        try {
            return parseWith1(viewsCreated);
        } catch (Throwable t) {
            CommandContainer.clearCTE(session, viewsCreated);
            throw t;
        }
    }

    private Prepared parseWith1(List<TableView> viewsCreated) {
        readIf("RECURSIVE");

        // This WITH statement is not a temporary view - it is part of a persistent view
        // as in CREATE VIEW abc AS WITH my_cte - this auto detects that condition.
        final boolean isTemporary = !session.isParsingCreateView();

        do {
            viewsCreated.add(parseSingleCommonTableExpression(isTemporary));
        } while (readIf(COMMA));

        Prepared p;
        // Reverse the order of constructed CTE views - as the destruction order
        // (since later created view may depend on previously created views -
        //  we preserve that dependency order in the destruction sequence )
        // used in setCteCleanups.
        Collections.reverse(viewsCreated);

        int start = tokenIndex;
        if (isQueryQuick()) {
            p = parseWithQuery();
        } else if (readIf("INSERT")) {
            p = parseInsert(start);
            p.setPrepareAlways(true);
        } else if (readIf("UPDATE")) {
            p = parseUpdate(start);
            p.setPrepareAlways(true);
        } else if (readIf("MERGE")) {
            p = parseMerge(start);
            p.setPrepareAlways(true);
        } else if (readIf("DELETE")) {
            p = parseDelete(start);
            p.setPrepareAlways(true);
        } else if (readIf("CREATE")) {
            if (!isToken(TABLE)) {
                throw DbException.get(ErrorCode.SYNTAX_ERROR_1,
                        WITH_STATEMENT_SUPPORTS_LIMITED_SUB_STATEMENTS);
            }
            p = parseCreate();
            p.setPrepareAlways(true);
        } else {
            throw DbException.get(ErrorCode.SYNTAX_ERROR_1,
                    WITH_STATEMENT_SUPPORTS_LIMITED_SUB_STATEMENTS);
        }

        // Clean up temporary views starting with last to first (in case of
        // dependencies) - but only if they are not persistent.
        if (isTemporary) {
            if (cteCleanups == null) {
                cteCleanups = new ArrayList<>(viewsCreated.size());
            }
            cteCleanups.addAll(viewsCreated);
        }
        return p;
    }

    private Prepared parseWithQuery() {
        Query query = parseQueryExpressionBodyAndEndOfQuery();
        query.setPrepareAlways(true);
        query.setNeverLazy(true);
        return query;
    }

    private TableView parseSingleCommonTableExpression(boolean isTemporary) {
        String cteViewName = readIdentifierWithSchema();
        Schema schema = getSchema();
        ArrayList<Column> columns = Utils.newSmallArrayList();
        String[] cols = null;

        // column names are now optional - they can be inferred from the named
        // query, if not supplied by user
        if (readIf(OPEN_PAREN)) {
            cols = parseColumnList();
            for (String c : cols) {
                // we don't really know the type of the column, so STRING will
                // have to do, UNKNOWN does not work here
                columns.add(new Column(c, TypeInfo.TYPE_VARCHAR));
            }
        }

        Table oldViewFound;
        if (!isTemporary) {
            oldViewFound = getSchema().findTableOrView(session, cteViewName);
        } else {
            oldViewFound = session.findLocalTempTable(cteViewName);
        }
        // this persistent check conflicts with check 10 lines down
        if (oldViewFound != null) {
            if (!(oldViewFound instanceof TableView)) {
                throw DbException.get(ErrorCode.TABLE_OR_VIEW_ALREADY_EXISTS_1,
                        cteViewName);
            }
            TableView tv = (TableView) oldViewFound;
            if (!tv.isTableExpression()) {
                throw DbException.get(ErrorCode.TABLE_OR_VIEW_ALREADY_EXISTS_1,
                        cteViewName);
            }
            if (!isTemporary) {
                oldViewFound.lock(session, Table.EXCLUSIVE_LOCK);
                database.removeSchemaObject(session, oldViewFound);

            } else {
                session.removeLocalTempTable(oldViewFound);
            }
        }
        /*
         * This table is created as a workaround because recursive table
         * expressions need to reference something that look like themselves to
         * work (its removed after creation in this method). Only create table
         * data and table if we don't have a working CTE already.
         */
        Table recursiveTable = TableView.createShadowTableForRecursiveTableExpression(
                isTemporary, session, cteViewName, schema, columns, database);
        List<Column> columnTemplateList;
        String[] querySQLOutput = new String[1];
        BitSet outerUsedParameters = initParametersScope();
        ArrayList<Parameter> queryParameters;
        try {
            read(AS);
            read(OPEN_PAREN);
            Query withQuery = parseQuery();
            if (!isTemporary) {
                withQuery.session = session;
            }
            read(CLOSE_PAREN);
            columnTemplateList = QueryExpressionTable.createQueryColumnTemplateList(cols, withQuery, querySQLOutput);

        } finally {
            queryParameters = getUsedParameters(outerUsedParameters);
            TableView.destroyShadowTableForRecursiveExpression(isTemporary, session, recursiveTable);
        }

        return createCTEView(cteViewName, querySQLOutput[0], queryParameters, columnTemplateList, isTemporary);
    }

    private TableView createCTEView(String cteViewName, String querySQL, ArrayList<Parameter> queryParameters,
                                    List<Column> columnTemplateList, boolean isTemporary) {
        Schema schema = getSchemaWithDefault();
        int id = database.allocateObjectId();
        Column[] columnTemplateArray = columnTemplateList.toArray(new Column[0]);

        // No easy way to determine if this is a recursive query up front, so we just compile
        // it twice - once without the flag set, and if we didn't see a recursive term,
        // then we just compile it again.
        TableView view;
        synchronized (session) {
            view = new TableView(schema, id, cteViewName, querySQL,
                    queryParameters, columnTemplateArray, session,
                    true, false, true,
                    isTemporary);
            if (!view.isRecursiveQueryDetected()) {
                if (!isTemporary) {
                    database.addSchemaObject(session, view);
                    view.lock(session, Table.EXCLUSIVE_LOCK);
                    database.removeSchemaObject(session, view);
                } else {
                    session.addLocalTempTable(view);
                    session.removeLocalTempTable(view);
                }
                id = database.allocateObjectId();
                view = new TableView(schema, id, cteViewName, querySQL, queryParameters,
                        columnTemplateArray, session,
                        false/* assume recursive */, false, true,
                        isTemporary);
            }
            // both removeSchemaObject and removeLocalTempTable hold meta locks
            database.unlockMeta(session);
        }
        view.setTableExpression(true);
        view.setTemporary(isTemporary);
        view.setHidden(true);
        view.setOnCommitDrop(false);
        if (!isTemporary) {
            database.addSchemaObject(session, view);
            view.unlock(session);
            database.unlockMeta(session);
        } else {
            session.addLocalTempTable(view);
        }
        return view;
    }

    private CreateView parseCreateView(boolean force, boolean orReplace) {
        boolean ifNotExists = readIfNotExists();
        boolean isTableExpression = readIf("TABLE_EXPRESSION");
        String viewName = readIdentifierWithSchema();
        CreateView command = new CreateView(session, getSchema());
        this.createView = command;
        command.setViewName(viewName);
        command.setIfNotExists(ifNotExists);
        command.setComment(readCommentIf());
        command.setOrReplace(orReplace);
        command.setForce(force);
        command.setTableExpression(isTableExpression);
        if (readIf(OPEN_PAREN)) {
            String[] cols = parseColumnList();
            command.setColumnNames(cols);
        }
        read(AS);
        String select = StringUtils.cache(sqlCommand.substring(token.start()));
        try {
            Query query;
            session.setParsingCreateView(true);
            try {
                query = parseQuery();
                query.prepare();
            } finally {
                session.setParsingCreateView(false);
            }
            command.setSelect(query);
        } catch (DbException e) {
            if (force) {
                command.setSelectSQL(select);
                while (currentTokenType != END_OF_INPUT) {
                    read();
                }
            } else {
                throw e;
            }
        }
        return command;
    }

    private CreateMaterializedView parseCreateMaterializedView(boolean force, boolean orReplace) {
        boolean ifNotExists = readIfNotExists();
        String viewName = readIdentifierWithSchema();
        read(AS);
        CreateMaterializedView command = new CreateMaterializedView(session, getSchema());
        command.setViewName(viewName);
        command.setIfNotExists(ifNotExists);
        command.setComment(readCommentIf());
        command.setOrReplace(orReplace);
        if (force) {
            throw new UnsupportedOperationException("not yet implemented");
        }
        String select = StringUtils.cache(sqlCommand.substring(token.start()));
        Query query;
        session.setParsingCreateView(true);
        try {
            query = parseQuery();
        } finally {
            session.setParsingCreateView(false);
        }
        command.setSelect(query);
        command.setSelectSQL(select);
        return command;
    }

    private TransactionCommand parseCheckpoint() {
        TransactionCommand command;
        if (readIf("SYNC")) {
            command = new TransactionCommand(session,
                    CommandInterface.CHECKPOINT_SYNC);
        } else {
            command = new TransactionCommand(session,
                    CommandInterface.CHECKPOINT);
        }
        return command;
    }

    private Prepared parseAlter() {
        if (readIf(TABLE)) {
            return parseAlterTable();
        } else if (readIf(USER)) {
            return parseAlterUser();
        } else if (readIf("INDEX")) {
            return parseAlterIndex();
        } else if (readIf("SCHEMA")) {
            return parseAlterSchema();
        } else if (readIf("SEQUENCE")) {
            return parseAlterSequence();
        } else if (readIf("VIEW")) {
            return parseAlterView();
        } else if (readIf("DOMAIN")) {
            return parseAlterDomain();
        }
        throw getSyntaxError();
    }

    private void checkSchema(Schema old) {
        if (old != null && getSchema() != old) {
            throw DbException.get(ErrorCode.SCHEMA_NAME_MUST_MATCH);
        }
    }

    private AlterIndexRename parseAlterIndex() {
        boolean ifExists = readIfExists(false);
        String indexName = readIdentifierWithSchema();
        Schema old = getSchema();
        AlterIndexRename command = new AlterIndexRename(session);
        command.setOldSchema(old);
        command.setOldName(indexName);
        command.setIfExists(ifExists);
        read("RENAME");
        read(TO);
        String newName = readIdentifierWithSchema(old.getName());
        checkSchema(old);
        command.setNewName(newName);
        return command;
    }

    private DefineCommand parseAlterDomain() {
        boolean ifDomainExists = readIfExists(false);
        String domainName = readIdentifierWithSchema();
        Schema schema = getSchema();
        if (readIf("ADD")) {
            boolean ifNotExists = false;
            String constraintName = null;
            String comment = null;
            if (readIf(CONSTRAINT)) {
                ifNotExists = readIfNotExists();
                constraintName = readIdentifierWithSchema(schema.getName());
                checkSchema(schema);
                comment = readCommentIf();
            }
            read(CHECK);
            AlterDomainAddConstraint command = new AlterDomainAddConstraint(session, schema, ifNotExists);
            command.setDomainName(domainName);
            command.setConstraintName(constraintName);
            parseDomainConstraint = true;
            try {
                command.setCheckExpression(readExpression());
            } finally {
                parseDomainConstraint = false;
            }
            command.setIfDomainExists(ifDomainExists);
            command.setComment(comment);
            if (readIf("NOCHECK")) {
                command.setCheckExisting(false);
            } else {
                readIf(CHECK);
                command.setCheckExisting(true);
            }
            return command;
        } else if (readIf("DROP")) {
            if (readIf(CONSTRAINT)) {
                boolean ifConstraintExists = readIfExists(false);
                String constraintName = readIdentifierWithSchema(schema.getName());
                checkSchema(schema);
                AlterDomainDropConstraint command = new AlterDomainDropConstraint(session, getSchema(),
                        ifConstraintExists);
                command.setConstraintName(constraintName);
                command.setDomainName(domainName);
                command.setIfDomainExists(ifDomainExists);
                return command;
            } else if (readIf(DEFAULT)) {
                AlterDomainExpressions command = new AlterDomainExpressions(session, schema,
                        CommandInterface.ALTER_DOMAIN_DEFAULT);
                command.setDomainName(domainName);
                command.setIfDomainExists(ifDomainExists);
                command.setExpression(null);
                return command;
            } else if (readIf(ON, "UPDATE")) {
                AlterDomainExpressions command = new AlterDomainExpressions(session, schema,
                        CommandInterface.ALTER_DOMAIN_ON_UPDATE);
                command.setDomainName(domainName);
                command.setIfDomainExists(ifDomainExists);
                command.setExpression(null);
                return command;
            }
        } else if (readIf("RENAME")) {
            if (readIf(CONSTRAINT)) {
                String constraintName = readIdentifierWithSchema(schema.getName());
                checkSchema(schema);
                read(TO);
                AlterDomainRenameConstraint command = new AlterDomainRenameConstraint(session, schema);
                command.setDomainName(domainName);
                command.setIfDomainExists(ifDomainExists);
                command.setConstraintName(constraintName);
                command.setNewConstraintName(readIdentifier());
                return command;
            }
            read(TO);
            String newName = readIdentifierWithSchema(schema.getName());
            checkSchema(schema);
            AlterDomainRename command = new AlterDomainRename(session, getSchema());
            command.setDomainName(domainName);
            command.setIfDomainExists(ifDomainExists);
            command.setNewDomainName(newName);
            return command;
        } else {
            read(SET);
            if (readIf(DEFAULT)) {
                AlterDomainExpressions command = new AlterDomainExpressions(session, schema,
                        CommandInterface.ALTER_DOMAIN_DEFAULT);
                command.setDomainName(domainName);
                command.setIfDomainExists(ifDomainExists);
                command.setExpression(readExpression());
                return command;
            } else if (readIf(ON, "UPDATE")) {
                AlterDomainExpressions command = new AlterDomainExpressions(session, schema,
                        CommandInterface.ALTER_DOMAIN_ON_UPDATE);
                command.setDomainName(domainName);
                command.setIfDomainExists(ifDomainExists);
                command.setExpression(readExpression());
                return command;
            }
        }
        throw getSyntaxError();
    }

    private DefineCommand parseAlterView() {
        boolean ifExists = readIfExists(false);
        String viewName = readIdentifierWithSchema();
        Schema schema = getSchema();
        Table tableView = schema.findTableOrView(session, viewName);
        if (!(tableView instanceof TableView) && !ifExists) {
            throw DbException.get(ErrorCode.VIEW_NOT_FOUND_1, viewName);
        }
        if (readIf("RENAME", TO)) {
            String newName = readIdentifierWithSchema(schema.getName());
            checkSchema(schema);
            AlterTableRename command = new AlterTableRename(session, getSchema());
            command.setTableName(viewName);
            command.setNewTableName(newName);
            command.setIfTableExists(ifExists);
            return command;
        } else {
            read("RECOMPILE");
            TableView view = (TableView) tableView;
            AlterView command = new AlterView(session);
            command.setIfExists(ifExists);
            command.setView(view);
            return command;
        }
    }

    private Prepared parseAlterSchema() {
        boolean ifExists = readIfExists(false);
        String schemaName = readIdentifierWithSchema();
        Schema old = getSchema();
        read("RENAME");
        read(TO);
        String newName = readIdentifierWithSchema(old.getName());
        Schema schema = findSchema(schemaName);
        if (schema == null) {
            if (ifExists) {
                return new NoOperation(session);
            }
            throw DbException.get(ErrorCode.SCHEMA_NOT_FOUND_1, schemaName);
        }
        AlterSchemaRename command = new AlterSchemaRename(session);
        command.setOldSchema(schema);
        checkSchema(old);
        command.setNewName(newName);
        return command;
    }

    private AlterSequence parseAlterSequence() {
        boolean ifExists = readIfExists(false);
        String sequenceName = readIdentifierWithSchema();
        AlterSequence command = new AlterSequence(session, getSchema());
        command.setSequenceName(sequenceName);
        command.setIfExists(ifExists);
        SequenceOptions options = new SequenceOptions();
        parseSequenceOptions(options, null, false, false);
        command.setOptions(options);
        return command;
    }

    private boolean parseSequenceOptions(SequenceOptions options, CreateSequence command, boolean allowDataType,
            boolean forAlterColumn) {
        boolean result = false;
        for (;;) {
            if (allowDataType && readIf(AS)) {
                TypeInfo dataType = parseDataType();
                if (!DataType.isNumericType(dataType.getValueType())) {
                    throw DbException.getUnsupportedException(dataType
                            .getSQL(new StringBuilder("CREATE SEQUENCE AS "), HasSQL.TRACE_SQL_FLAGS).toString());
                }
                options.setDataType(dataType);
            } else if (readIf("START", WITH)
                    || (database.getMode().getEnum() == ModeEnum.PostgreSQL && readIf("START"))) {
                options.setStartValue(readExpression());
            } else if (readIf("RESTART")) {
                options.setRestartValue(readIf(WITH) ? readExpression() : ValueExpression.DEFAULT);
            } else if (command != null && parseCreateSequenceOption(command)) {
                //
            } else if (forAlterColumn) {
                int index = tokenIndex;
                if (readIf(SET)) {
                    if (!parseBasicSequenceOption(options)) {
                        setTokenIndex(index);
                        break;
                    }
                } else {
                    break;
                }
            } else if (!parseBasicSequenceOption(options)) {
                break;
            }
            result = true;
        }
        return result;
    }

    private boolean parseCreateSequenceOption(CreateSequence command) {
        if (readIf("BELONGS_TO_TABLE")) {
            command.setBelongsToTable(true);
        } else if (readIf(ORDER)) {
            // Oracle compatibility
        } else {
            return false;
        }
        return true;
    }

    private boolean parseBasicSequenceOption(SequenceOptions options) {
        if (readIf("INCREMENT")) {
            // TODO Why BY is optional?
            readIf("BY");
            options.setIncrement(readExpression());
        } else if (readIf("MINVALUE")) {
            options.setMinValue(readExpression());
        } else if (readIf("MAXVALUE")) {
            options.setMaxValue(readExpression());
        } else if (readIf("CYCLE")) {
            options.setCycle(Sequence.Cycle.CYCLE);
        } else if (readIf("NO")) {
            if (readIf("MINVALUE")) {
                options.setMinValue(ValueExpression.NULL);
            } else if (readIf("MAXVALUE")) {
                options.setMaxValue(ValueExpression.NULL);
            } else if (readIf("CYCLE")) {
                options.setCycle(Sequence.Cycle.NO_CYCLE);
            } else if (readIf("CACHE")) {
                options.setCacheSize(ValueExpression.get(ValueBigint.get(1)));
            } else {
                throw getSyntaxError();
            }
        } else if (readIf("EXHAUSTED")) {
            options.setCycle(Sequence.Cycle.EXHAUSTED);
        } else if (readIf("CACHE")) {
            options.setCacheSize(readExpression());
            // Various compatibility options
        } else if (readIf("NOMINVALUE")) {
            options.setMinValue(ValueExpression.NULL);
        } else if (readIf("NOMAXVALUE")) {
            options.setMaxValue(ValueExpression.NULL);
        } else if (readIf("NOCYCLE")) {
            options.setCycle(Sequence.Cycle.NO_CYCLE);
        } else if (readIf("NOCACHE")) {
            options.setCacheSize(ValueExpression.get(ValueBigint.get(1)));
        } else {
            return false;
        }
        return true;
    }

    private AlterUser parseAlterUser() {
        String userName = readIdentifier();
        if (readIf(SET)) {
            AlterUser command = new AlterUser(session);
            command.setType(CommandInterface.ALTER_USER_SET_PASSWORD);
            command.setUser(database.getUser(userName));
            if (readIf("PASSWORD")) {
                command.setPassword(readExpression());
            } else if (readIf("SALT")) {
                command.setSalt(readExpression());
                read("HASH");
                command.setHash(readExpression());
            } else {
                throw getSyntaxError();
            }
            return command;
        } else if (readIf("RENAME", TO)) {
            AlterUser command = new AlterUser(session);
            command.setType(CommandInterface.ALTER_USER_RENAME);
            command.setUser(database.getUser(userName));
            command.setNewName(readIdentifier());
            return command;
        } else if (readIf("ADMIN")) {
            AlterUser command = new AlterUser(session);
            command.setType(CommandInterface.ALTER_USER_ADMIN);
            User user = database.getUser(userName);
            command.setUser(user);
            if (readIf(TRUE)) {
                command.setAdmin(true);
            } else if (readIf(FALSE)) {
                command.setAdmin(false);
            } else {
                throw getSyntaxError();
            }
            return command;
        }
        throw getSyntaxError();
    }

    private void readIfEqualOrTo() {
        if (!readIf(EQUAL)) {
            readIf(TO);
        }
    }

    private Prepared parseSet() {
        if (readIf(AT)) {
            Set command = new Set(session, SetTypes.VARIABLE);
            command.setString(readIdentifier());
            readIfEqualOrTo();
            command.setExpression(readExpression());
            return command;
        } else if (readIf("AUTOCOMMIT")) {
            readIfEqualOrTo();
            return new TransactionCommand(session, readBooleanSetting() ? CommandInterface.SET_AUTOCOMMIT_TRUE
                    : CommandInterface.SET_AUTOCOMMIT_FALSE);
        } else if (readIf("EXCLUSIVE")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.EXCLUSIVE);
            command.setExpression(readExpression());
            return command;
        } else if (readIf("IGNORECASE")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.IGNORECASE);
            command.setInt(readBooleanSetting() ? 1 : 0);
            return command;
        } else if (readIf("PASSWORD")) {
            readIfEqualOrTo();
            AlterUser command = new AlterUser(session);
            command.setType(CommandInterface.ALTER_USER_SET_PASSWORD);
            command.setUser(session.getUser());
            command.setPassword(readExpression());
            return command;
        } else if (readIf("SALT")) {
            readIfEqualOrTo();
            AlterUser command = new AlterUser(session);
            command.setType(CommandInterface.ALTER_USER_SET_PASSWORD);
            command.setUser(session.getUser());
            command.setSalt(readExpression());
            read("HASH");
            command.setHash(readExpression());
            return command;
        } else if (readIf("MODE")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.MODE);
            command.setString(readIdentifier());
            return command;
        } else if (readIf("DATABASE")) {
            readIfEqualOrTo();
            read("COLLATION");
            return parseSetCollation();
        } else if (readIf("COLLATION")) {
            readIfEqualOrTo();
            return parseSetCollation();
        } else if (readIf("CLUSTER")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.CLUSTER);
            command.setString(readString());
            return command;
        } else if (readIf("DATABASE_EVENT_LISTENER")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.DATABASE_EVENT_LISTENER);
            command.setString(readString());
            return command;
        } else if (readIf("ALLOW_LITERALS")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.ALLOW_LITERALS);
            int v;
            if (readIf(ALL)) {
                v = Constants.ALLOW_LITERALS_ALL;
            } else if (readIf("NONE")) {
                v = Constants.ALLOW_LITERALS_NONE;
            } else if (readIf("NUMBERS")) {
                v = Constants.ALLOW_LITERALS_NUMBERS;
            } else {
                v = readNonNegativeInt();
            }
            command.setInt(v);
            return command;
        } else if (readIf("DEFAULT_TABLE_TYPE")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.DEFAULT_TABLE_TYPE);
            int v;
            if (readIf("MEMORY")) {
                v = Table.TYPE_MEMORY;
            } else if (readIf("CACHED")) {
                v = Table.TYPE_CACHED;
            } else {
                v = readNonNegativeInt();
            }
            command.setInt(v);
            return command;
        } else if (readIf("SCHEMA")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.SCHEMA);
            command.setExpression(readExpressionOrIdentifier());
            return command;
        } else if (readIf("CATALOG")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.CATALOG);
            command.setExpression(readExpressionOrIdentifier());
            return command;
        } else if (readIf(SetTypes.getTypeName(SetTypes.SCHEMA_SEARCH_PATH))) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.SCHEMA_SEARCH_PATH);
            ArrayList<String> list = Utils.newSmallArrayList();
            do {
                list.add(readIdentifier());
            } while (readIf(COMMA));
            command.setStringArray(list.toArray(new String[0]));
            return command;
        } else if (readIf("JAVA_OBJECT_SERIALIZER")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.JAVA_OBJECT_SERIALIZER);
            command.setString(readString());
            return command;
        } else if (readIf("IGNORE_CATALOGS")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.IGNORE_CATALOGS);
            command.setInt(readBooleanSetting() ? 1 : 0);
            return command;
        } else if (readIf("SESSION")) {
            read("CHARACTERISTICS");
            read(AS);
            read("TRANSACTION");
            return parseSetTransactionMode();
        } else if (readIf("TRANSACTION")) {
            // TODO should affect only the current transaction
            return parseSetTransactionMode();
        } else if (readIf("TIME")) {
            read("ZONE");
            Set command = new Set(session, SetTypes.TIME_ZONE);
            if (!readIf("LOCAL")) {
                command.setExpression(readExpression());
            }
            return command;
        } else if (readIf("NON_KEYWORDS")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.NON_KEYWORDS);
            ArrayList<String> list = Utils.newSmallArrayList();
            if (currentTokenType != END_OF_INPUT && currentTokenType != SEMICOLON) {
                do {
                    list.add(StringUtils.toUpperEnglish(readIdentifierOrKeyword()));
                } while (readIf(COMMA));
            }
            command.setStringArray(list.toArray(new String[0]));
            return command;
        } else if (readIf("DEFAULT_NULL_ORDERING")) {
            readIfEqualOrTo();
            Set command = new Set(session, SetTypes.DEFAULT_NULL_ORDERING);
            command.setString(readIdentifier());
            return command;
        } else if (readIf("LOG")) {
            throw DbException.getUnsupportedException("LOG");
        } else {
            String upperName = upperName(currentToken);
            if (ConnectionInfo.isIgnoredByParser(upperName)) {
                read();
                readIfEqualOrTo();
                read();
                return new NoOperation(session);
            }
            int type = SetTypes.getType(upperName);
            if (type >= 0) {
                read();
                readIfEqualOrTo();
                Set command = new Set(session, type);
                command.setExpression(readExpression());
                return command;
            }
            ModeEnum modeEnum = database.getMode().getEnum();
            if (modeEnum != ModeEnum.REGULAR) {
                Prepared command = readSetCompatibility(modeEnum);
                if (command != null) {
                    return command;
                }
            }
            if (session.isQuirksMode()) {
                switch (upperName) {
                case "BINARY_COLLATION":
                case "UUID_COLLATION":
                    read();
                    readIfEqualOrTo();
                    readIdentifier();
                    return new NoOperation(session);
                }
            }
            throw getSyntaxError();
        }
    }

    private Prepared parseSetTransactionMode() {
        IsolationLevel isolationLevel;
        read("ISOLATION");
        read("LEVEL");
        if (readIf("READ")) {
            if (readIf("UNCOMMITTED")) {
                isolationLevel = IsolationLevel.READ_UNCOMMITTED;
            } else {
                read("COMMITTED");
                isolationLevel = IsolationLevel.READ_COMMITTED;
            }
        } else if (readIf("REPEATABLE")) {
            read("READ");
            isolationLevel = IsolationLevel.REPEATABLE_READ;
        } else if (readIf("SNAPSHOT")) {
            isolationLevel = IsolationLevel.SNAPSHOT;
        } else {
            read("SERIALIZABLE");
            isolationLevel = IsolationLevel.SERIALIZABLE;
        }
        return new SetSessionCharacteristics(session, isolationLevel);
    }

    private Expression readExpressionOrIdentifier() {
        if (isIdentifier()) {
            return ValueExpression.get(ValueVarchar.get(readIdentifier()));
        }
        return readExpression();
    }

    private Prepared parseUse() {
        readIfEqualOrTo();
        Set command = new Set(session, SetTypes.SCHEMA);
        command.setExpression(ValueExpression.get(ValueVarchar.get(readIdentifier())));
        return command;
    }

    private Set parseSetCollation() {
        Set command = new Set(session, SetTypes.COLLATION);
        String name = readIdentifier();
        command.setString(name);
        if (equalsToken(name, CompareMode.OFF)) {
            return command;
        }
        Collator coll = CompareMode.getCollator(name);
        if (coll == null) {
            throw DbException.getInvalidValueException("collation", name);
        }
        if (readIf("STRENGTH")) {
            if (readIf(PRIMARY)) {
                command.setInt(Collator.PRIMARY);
            } else if (readIf("SECONDARY")) {
                command.setInt(Collator.SECONDARY);
            } else if (readIf("TERTIARY")) {
                command.setInt(Collator.TERTIARY);
            } else if (readIf("IDENTICAL")) {
                command.setInt(Collator.IDENTICAL);
            }
        } else {
            command.setInt(coll.getStrength());
        }
        return command;
    }

    private Prepared readSetCompatibility(ModeEnum modeEnum) {
        switch (modeEnum) {
        case Derby:
            if (readIf("CREATE")) {
                readIfEqualOrTo();
                // (CREATE=TRUE in the database URL)
                read();
                return new NoOperation(session);
            }
            break;
        case HSQLDB:
            if (readIf("LOGSIZE")) {
                readIfEqualOrTo();
                Set command = new Set(session, SetTypes.MAX_LOG_SIZE);
                command.setExpression(readExpression());
                return command;
            }
            break;
        case MariaDB:
        case MySQL:
            if (readIf("FOREIGN_KEY_CHECKS")) {
                readIfEqualOrTo();
                Set command = new Set(session, SetTypes.REFERENTIAL_INTEGRITY);
                command.setExpression(readExpression());
                return command;
            } else if (readIf("NAMES")) {
                // Quercus PHP MySQL driver compatibility
                readIfEqualOrTo();
                read();
                return new NoOperation(session);
            }
            break;
        case PostgreSQL:
            if (readIf("STATEMENT_TIMEOUT")) {
                readIfEqualOrTo();
                Set command = new Set(session, SetTypes.QUERY_TIMEOUT);
                command.setInt(readNonNegativeInt());
                return command;
            } else if (readIf("CLIENT_ENCODING") || readIf("CLIENT_MIN_MESSAGES") || readIf("JOIN_COLLAPSE_LIMIT")) {
                readIfEqualOrTo();
                read();
                return new NoOperation(session);
            } else if (readIf("DATESTYLE")) {
                readIfEqualOrTo();
                if (!readIf("ISO")) {
                    String s = readString();
                    if (!equalsToken(s, "ISO")) {
                        throw getSyntaxError();
                    }
                }
                return new NoOperation(session);
            } else if (readIf("SEARCH_PATH")) {
                readIfEqualOrTo();
                Set command = new Set(session, SetTypes.SCHEMA_SEARCH_PATH);
                ArrayList<String> list = Utils.newSmallArrayList();
                String pgCatalog = database.sysIdentifier("PG_CATALOG");
                boolean hasPgCatalog = false;
                do {
                    // some PG clients will send single-quoted alias
                    String s = currentTokenType == LITERAL ? readString() : readIdentifier();
                    if ("$user".equals(s)) {
                        continue;
                    }
                    if (pgCatalog.equals(s)) {
                        hasPgCatalog = true;
                    }
                    list.add(s);
                } while (readIf(COMMA));
                // If "pg_catalog" is not in the path then it will be searched before
                // searching any of the path items. See
                // https://www.postgresql.org/docs/8.2/runtime-config-client.html
                if (!hasPgCatalog) {
                    if (database.findSchema(pgCatalog) != null) {
                        list.add(0, pgCatalog);
                    }
                }
                command.setStringArray(list.toArray(new String[0]));
                return command;
            }
            break;
        default:
        }
        return null;
    }

    private RunScriptCommand parseRunScript() {
        RunScriptCommand command = new RunScriptCommand(session);
        read(FROM);
        command.setFileNameExpr(readExpression());
        if (readIf("COMPRESSION")) {
            command.setCompressionAlgorithm(readIdentifier());
        }
        if (readIf("CIPHER")) {
            command.setCipher(readIdentifier());
            if (readIf("PASSWORD")) {
                command.setPassword(readExpression());
            }
        }
        if (readIf("CHARSET")) {
            command.setCharset(Charset.forName(readString()));
        }
        if (readIf("FROM_1X")) {
            command.setFrom1X();
        } else {
            if (readIf("QUIRKS_MODE")) {
                command.setQuirksMode(true);
            }
            if (readIf("VARIABLE_BINARY")) {
                command.setVariableBinary(true);
            }
        }
        return command;
    }

    private ScriptCommand parseScript() {
        ScriptCommand command = new ScriptCommand(session);
        boolean data = true, passwords = true, settings = true, version = true;
        boolean dropTables = false, simple = false, withColumns = false;
        if (readIf("NODATA")) {
            data = false;
        } else {
            if (readIf("SIMPLE")) {
                simple = true;
            }
            if (readIf("COLUMNS")) {
                withColumns = true;
            }
        }
        if (readIf("NOPASSWORDS")) {
            passwords = false;
        }
        if (readIf("NOSETTINGS")) {
            settings = false;
        }
        if (readIf("NOVERSION")) {
            version = false;
        }
        if (readIf("DROP")) {
            dropTables = true;
        }
        if (readIf("BLOCKSIZE")) {
            long blockSize = readLong();
            command.setLobBlockSize(blockSize);
        }
        command.setData(data);
        command.setPasswords(passwords);
        command.setSettings(settings);
        command.setVersion(version);
        command.setDrop(dropTables);
        command.setSimple(simple);
        command.setWithColumns(withColumns);
        if (readIf(TO)) {
            command.setFileNameExpr(readExpression());
            if (readIf("COMPRESSION")) {
                command.setCompressionAlgorithm(readIdentifier());
            }
            if (readIf("CIPHER")) {
                command.setCipher(readIdentifier());
                if (readIf("PASSWORD")) {
                    command.setPassword(readExpression());
                }
            }
            if (readIf("CHARSET")) {
                command.setCharset(Charset.forName(readString()));
            }
        }
        if (readIf("SCHEMA")) {
            HashSet<String> schemaNames = new HashSet<>();
            do {
                schemaNames.add(readIdentifier());
            } while (readIf(COMMA));
            command.setSchemaNames(schemaNames);
        } else if (readIf(TABLE)) {
            ArrayList<Table> tables = Utils.newSmallArrayList();
            do {
                tables.add(readTableOrView());
            } while (readIf(COMMA));
            command.setTables(tables);
        }
        return command;
    }

    /**
     * Is this the Oracle DUAL table or the IBM/DB2 SYSIBM table?
     *
     * @param tableName table name.
     * @return {@code true} if the table is DUAL special table. Otherwise returns {@code false}.
     * @see <a href="https://en.wikipedia.org/wiki/DUAL_table">Wikipedia: DUAL table</a>
     */
    private boolean isDualTable(String tableName) {
        return ((schemaName == null || equalsToken(schemaName, "SYS")) && equalsToken("DUAL", tableName))
                || (database.getMode().sysDummy1 && (schemaName == null || equalsToken(schemaName, "SYSIBM"))
                        && equalsToken("SYSDUMMY1", tableName));
    }

    private Table readTableOrView() {
        return readTableOrView(readIdentifierWithSchema(null), /*resolveMaterializedView*/true);
    }

    private Table readTableOrView(boolean resolveMaterializedView) {
        return readTableOrView(readIdentifierWithSchema(null), resolveMaterializedView);
    }

    private Table readTableOrView(String tableName, boolean resolveMaterializedView) {
        if (schemaName != null) {
            Table table = getSchema().resolveTableOrView(session, tableName, resolveMaterializedView);
            if (table != null) {
                return table;
            }
        } else {
            Table table = database.getSchema(session.getCurrentSchemaName())
                    .resolveTableOrView(session, tableName, resolveMaterializedView);
            if (table != null) {
                return table;
            }
            String[] schemaNames = session.getSchemaSearchPath();
            if (schemaNames != null) {
                for (String name : schemaNames) {
                    Schema s = database.getSchema(name);
                    table = s.resolveTableOrView(session, tableName, resolveMaterializedView);
                    if (table != null) {
                        return table;
                    }
                }
            }
        }
        if (isDualTable(tableName)) {
            return new DualTable(database);
        }

        throw getTableOrViewNotFoundDbException(tableName);
    }

    private DbException getTableOrViewNotFoundDbException(String tableName) {
        if (schemaName != null) {
            return getTableOrViewNotFoundDbException(schemaName, tableName);
        }

        String currentSchemaName = session.getCurrentSchemaName();
        String[] schemaSearchPath = session.getSchemaSearchPath();
        if (schemaSearchPath == null) {
            return getTableOrViewNotFoundDbException(Collections.singleton(currentSchemaName), tableName);
        }

        LinkedHashSet<String> schemaNames = new LinkedHashSet<>();
        schemaNames.add(currentSchemaName);
        schemaNames.addAll(Arrays.asList(schemaSearchPath));
        return getTableOrViewNotFoundDbException(schemaNames, tableName);
    }

    private DbException getTableOrViewNotFoundDbException(String schemaName, String tableName) {
        return getTableOrViewNotFoundDbException(Collections.singleton(schemaName), tableName);
    }

    private DbException getTableOrViewNotFoundDbException(
            java.util.Set<String> schemaNames, String tableName) {
        if (database == null || database.getFirstUserTable() == null) {
            return DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_DATABASE_EMPTY_1, tableName);
        }

        if (database.getSettings().caseInsensitiveIdentifiers) {
            return DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1, tableName);
        }

        java.util.Set<String> candidates = new TreeSet<>();
        for (String schemaName : schemaNames) {
            findTableNameCandidates(schemaName, tableName, candidates);
        }

        if (candidates.isEmpty()) {
            return DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1, tableName);
        }

        return DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_WITH_CANDIDATES_2,
                tableName,
                String.join(", ", candidates));
    }

    private void findTableNameCandidates(String schemaName, String tableName, java.util.Set<String> candidates) {
        Schema schema = database.getSchema(schemaName);
        String ucTableName = StringUtils.toUpperEnglish(tableName);
        Collection<Table> allTablesAndViews = schema.getAllTablesAndViews(session);
        for (Table candidate : allTablesAndViews) {
            String candidateName = candidate.getName();
            if (ucTableName.equals(StringUtils.toUpperEnglish(candidateName))) {
                candidates.add(candidateName);
            }
        }
    }

    private UserDefinedFunction findUserDefinedFunctionWithinPath(Schema schema, String name) {
        if (schema != null) {
            return schema.findFunctionOrAggregate(name);
        }
        schema = database.getSchema(session.getCurrentSchemaName());
        UserDefinedFunction userDefinedFunction = schema.findFunctionOrAggregate(name);
        if (userDefinedFunction != null) {
            return userDefinedFunction;
        }
        String[] schemaNames = session.getSchemaSearchPath();
        if (schemaNames != null) {
            for (String schemaName : schemaNames) {
                Schema schemaFromPath = database.getSchema(schemaName);
                if (schemaFromPath != schema) {
                    userDefinedFunction = schemaFromPath.findFunctionOrAggregate(name);
                    if (userDefinedFunction != null) {
                        return userDefinedFunction;
                    }
                }
            }
        }
        return null;
    }

    private Sequence findSequence(String schema, String sequenceName) {
        Sequence sequence = database.getSchema(schema).findSequence(
                sequenceName);
        if (sequence != null) {
            return sequence;
        }
        String[] schemaNames = session.getSchemaSearchPath();
        if (schemaNames != null) {
            for (String n : schemaNames) {
                sequence = database.getSchema(n).findSequence(sequenceName);
                if (sequence != null) {
                    return sequence;
                }
            }
        }
        return null;
    }

    private Sequence readSequence() {
        // same algorithm as readTableOrView
        String sequenceName = readIdentifierWithSchema(null);
        if (schemaName != null) {
            return getSchema().getSequence(sequenceName);
        }
        Sequence sequence = findSequence(session.getCurrentSchemaName(),
                sequenceName);
        if (sequence != null) {
            return sequence;
        }
        throw DbException.get(ErrorCode.SEQUENCE_NOT_FOUND_1, sequenceName);
    }

    private Prepared parseAlterTable() {
        boolean ifTableExists = readIfExists(false);
        String tableName = readIdentifierWithSchema();
        Schema schema = getSchema();
        if (readIf("ADD")) {
            Prepared command = parseTableConstraintIf(tableName, schema, ifTableExists);
            if (command != null) {
                return command;
            }
            return parseAlterTableAddColumn(tableName, schema, ifTableExists);
        } else if (readIf(SET)) {
            return parseAlterTableSet(schema, tableName, ifTableExists);
        } else if (readIf("RENAME")) {
            return parseAlterTableRename(schema, tableName, ifTableExists);
        } else if (readIf("DROP")) {
            return parseAlterTableDrop(schema, tableName, ifTableExists);
        } else if (readIf("ALTER")) {
            return parseAlterTableAlter(schema, tableName, ifTableExists);
        } else {
            Mode mode = database.getMode();
            if (mode.alterTableExtensionsMySQL || mode.alterTableModifyColumn) {
                return parseAlterTableCompatibility(schema, tableName, ifTableExists, mode);
            }
        }
        throw getSyntaxError();
    }

    private Prepared parseAlterTableAlter(Schema schema, String tableName, boolean ifTableExists) {
        readIf("COLUMN");
        boolean ifExists = readIfExists(false);
        String columnName = readIdentifier();
        Column column = columnIfTableExists(schema, tableName, columnName, ifTableExists, ifExists);
        if (readIf("RENAME")) {
            read(TO);
            AlterTableRenameColumn command = new AlterTableRenameColumn(
                    session, schema);
            command.setTableName(tableName);
            command.setIfTableExists(ifTableExists);
            command.setIfExists(ifExists);
            command.setOldColumnName(columnName);
            String newName = readIdentifier();
            command.setNewColumnName(newName);
            return command;
        } else if (readIf("DROP")) {
            if (readIf(DEFAULT)) {
                if (readIf(ON, NULL)) {
                    AlterTableAlterColumn command = new AlterTableAlterColumn(session, schema);
                    command.setTableName(tableName);
                    command.setIfTableExists(ifTableExists);
                    command.setOldColumn(column);
                    command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_DEFAULT_ON_NULL);
                    command.setBooleanFlag(false);
                    return command;
                }
                return getAlterTableAlterColumnDropDefaultExpression(schema, tableName, ifTableExists, column,
                        CommandInterface.ALTER_TABLE_ALTER_COLUMN_DEFAULT);
            } else if (readIf("EXPRESSION")) {
                return getAlterTableAlterColumnDropDefaultExpression(schema, tableName, ifTableExists, column,
                        CommandInterface.ALTER_TABLE_ALTER_COLUMN_DROP_EXPRESSION);
            } else if (readIf("IDENTITY")) {
                return getAlterTableAlterColumnDropDefaultExpression(schema, tableName, ifTableExists, column,
                        CommandInterface.ALTER_TABLE_ALTER_COLUMN_DROP_IDENTITY);
            }
            if (readIf(ON, "UPDATE")) {
                AlterTableAlterColumn command = new AlterTableAlterColumn(session, schema);
                command.setTableName(tableName);
                command.setIfTableExists(ifTableExists);
                command.setOldColumn(column);
                command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_ON_UPDATE);
                command.setDefaultExpression(null);
                return command;
            }
            read(NOT);
            read(NULL);
            AlterTableAlterColumn command = new AlterTableAlterColumn(
                    session, schema);
            command.setTableName(tableName);
            command.setIfTableExists(ifTableExists);
            command.setOldColumn(column);
            command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_DROP_NOT_NULL);
            return command;
        } else if (readIf("TYPE")) {
            // PostgreSQL compatibility
            return parseAlterTableAlterColumnDataType(schema, tableName, columnName, ifTableExists, ifExists);
        } else if (readIf("SELECTIVITY")) {
            AlterTableAlterColumn command = new AlterTableAlterColumn(
                    session, schema);
            command.setTableName(tableName);
            command.setIfTableExists(ifTableExists);
            command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_SELECTIVITY);
            command.setOldColumn(column);
            command.setSelectivity(readExpression());
            return command;
        }
        Prepared command = parseAlterTableAlterColumnIdentity(schema, tableName, ifTableExists, column);
        if (command != null) {
            return command;
        }
        if (readIf(SET)) {
            return parseAlterTableAlterColumnSet(schema, tableName, ifTableExists, ifExists, columnName, column);
        }
        return parseAlterTableAlterColumnType(schema, tableName, columnName, ifTableExists, ifExists, true);
    }

    private Prepared getAlterTableAlterColumnDropDefaultExpression(Schema schema, String tableName,
            boolean ifTableExists, Column column, int type) {
        AlterTableAlterColumn command = new AlterTableAlterColumn(session, schema);
        command.setTableName(tableName);
        command.setIfTableExists(ifTableExists);
        command.setOldColumn(column);
        command.setType(type);
        command.setDefaultExpression(null);
        return command;
    }

    private Prepared parseAlterTableAlterColumnIdentity(Schema schema, String tableName, boolean ifTableExists,
            Column column) {
        Boolean always = null;
        if (readIf(SET, "GENERATED")) {
            if (readIf("ALWAYS")) {
                always = true;
            } else {
                read("BY");
                read(DEFAULT);
                always = false;
            }
        }
        SequenceOptions options = new SequenceOptions();
        if (!parseSequenceOptions(options, null, false, true) && always == null) {
            return null;
        }
        if (column == null) {
            return new NoOperation(session);
        }
        if (!column.isIdentity()) {
            AlterTableAlterColumn command = new AlterTableAlterColumn(session, schema);
            parseAlterColumnUsingIf(command);
            command.setTableName(tableName);
            command.setIfTableExists(ifTableExists);
            command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_CHANGE_TYPE);
            command.setOldColumn(column);
            Column newColumn = column.getClone();
            newColumn.setIdentityOptions(options, always != null && always);
            command.setNewColumn(newColumn);
            return command;
        }
        AlterSequence command = new AlterSequence(session, schema);
        command.setColumn(column, always);
        command.setOptions(options);
        return commandIfTableExists(schema, tableName, ifTableExists, command);
    }

    private Prepared parseAlterTableAlterColumnSet(Schema schema, String tableName, boolean ifTableExists,
            boolean ifExists, String columnName, Column column) {
        if (readIf("DATA", "TYPE")) {
            return parseAlterTableAlterColumnDataType(schema, tableName, columnName, ifTableExists, ifExists);
        }
        AlterTableAlterColumn command = new AlterTableAlterColumn(
                session, schema);
        command.setTableName(tableName);
        command.setIfTableExists(ifTableExists);
        command.setOldColumn(column);
        NullConstraintType nullConstraint = parseNotNullConstraint();
        switch (nullConstraint) {
        case NULL_IS_ALLOWED:
            command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_DROP_NOT_NULL);
            break;
        case NULL_IS_NOT_ALLOWED:
            command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_NOT_NULL);
            break;
        case NO_NULL_CONSTRAINT_FOUND:
            if (readIf(DEFAULT)) {
                if (readIf(ON, NULL)) {
                    command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_DEFAULT_ON_NULL);
                    command.setBooleanFlag(true);
                    break;
                }
                Expression defaultExpression = readExpression();
                command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_DEFAULT);
                command.setDefaultExpression(defaultExpression);
            } else if (readIf(ON, "UPDATE")) {
                Expression onUpdateExpression = readExpression();
                command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_ON_UPDATE);
                command.setDefaultExpression(onUpdateExpression);
            } else if (readIf("INVISIBLE")) {
                command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_VISIBILITY);
                command.setBooleanFlag(false);
            } else if (readIf("VISIBLE")) {
                command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_VISIBILITY);
                command.setBooleanFlag(true);
            }
            break;
        default:
            throw DbException.get(ErrorCode.UNKNOWN_MODE_1,
                    "Internal Error - unhandled case: " + nullConstraint.name());
        }
        return command;
    }

    private Prepared parseAlterTableDrop(Schema schema, String tableName, boolean ifTableExists) {
        if (readIf(CONSTRAINT)) {
            boolean ifExists = readIfExists(false);
            String constraintName = readIdentifierWithSchema(schema.getName());
            ifExists = readIfExists(ifExists);
            checkSchema(schema);
            AlterTableDropConstraint command = new AlterTableDropConstraint(session, getSchema(), ifExists);
            command.setTableName(tableName);
            command.setIfTableExists(ifTableExists);
            command.setConstraintName(constraintName);
            ConstraintActionType dropAction = parseCascadeOrRestrict();
            if (dropAction != null) {
                command.setDropAction(dropAction);
            }
            return command;
        } else if (readIf(PRIMARY, KEY)) {
            Table table = tableIfTableExists(schema, tableName, ifTableExists);
            if (table == null) {
                return new NoOperation(session);
            }
            Index idx = table.getPrimaryKey();
            DropIndex command = new DropIndex(session, schema);
            command.setIndexName(idx.getName());
            return command;
        } else if (database.getMode().alterTableExtensionsMySQL) {
            Prepared command = parseAlterTableDropCompatibility(schema, tableName, ifTableExists);
            if (command != null) {
                return command;
            }
        }
        readIf("COLUMN");
        boolean ifExists = readIfExists(false);
        ArrayList<Column> columnsToRemove = new ArrayList<>();
        Table table = tableIfTableExists(schema, tableName, ifTableExists);
        // For Oracle compatibility - open bracket required
        boolean openingBracketDetected = readIf(OPEN_PAREN);
        do {
            String columnName = readIdentifier();
            if (table != null) {
                Column column = table.getColumn(columnName, ifExists);
                if (column != null) {
                    columnsToRemove.add(column);
                }
            }
        } while (readIf(COMMA));
        if (openingBracketDetected) {
            // For Oracle compatibility - close bracket
            read(CLOSE_PAREN);
        }
        if (table == null || columnsToRemove.isEmpty()) {
            return new NoOperation(session);
        }
        AlterTableAlterColumn command = new AlterTableAlterColumn(session, schema);
        command.setType(CommandInterface.ALTER_TABLE_DROP_COLUMN);
        command.setTableName(tableName);
        command.setIfTableExists(ifTableExists);
        command.setColumnsToRemove(columnsToRemove);
        return command;
    }

    private Prepared parseAlterTableDropCompatibility(Schema schema, String tableName, boolean ifTableExists) {
        if (readIf(FOREIGN, KEY)) {
            // For MariaDB
            boolean ifExists = readIfExists(false);
            String constraintName = readIdentifierWithSchema(schema.getName());
            checkSchema(schema);
            AlterTableDropConstraint command = new AlterTableDropConstraint(session, getSchema(), ifExists);
            command.setTableName(tableName);
            command.setIfTableExists(ifTableExists);
            command.setConstraintName(constraintName);
            return command;
        } else if (readIf("INDEX")) {
            // For MariaDB
            boolean ifExists = readIfExists(false);
            String indexOrConstraintName = readIdentifierWithSchema(schema.getName());
            if (schema.findIndex(session, indexOrConstraintName) != null) {
                DropIndex dropIndexCommand = new DropIndex(session, getSchema());
                dropIndexCommand.setIndexName(indexOrConstraintName);
                return commandIfTableExists(schema, tableName, ifTableExists, dropIndexCommand);
            } else {
                AlterTableDropConstraint dropCommand = new AlterTableDropConstraint(session, getSchema(), ifExists);
                dropCommand.setTableName(tableName);
                dropCommand.setIfTableExists(ifTableExists);
                dropCommand.setConstraintName(indexOrConstraintName);
                return dropCommand;
            }
        }
        return null;
    }

    private Prepared parseAlterTableRename(Schema schema, String tableName, boolean ifTableExists) {
        if (readIf("COLUMN")) {
            // PostgreSQL syntax
            String columnName = readIdentifier();
            read(TO);
            AlterTableRenameColumn command = new AlterTableRenameColumn(
                    session, schema);
            command.setTableName(tableName);
            command.setIfTableExists(ifTableExists);
            command.setOldColumnName(columnName);
            command.setNewColumnName(readIdentifier());
            return command;
        } else if (readIf(CONSTRAINT)) {
            String constraintName = readIdentifierWithSchema(schema.getName());
            checkSchema(schema);
            read(TO);
            AlterTableRenameConstraint command = new AlterTableRenameConstraint(session, schema);
            command.setTableName(tableName);
            command.setIfTableExists(ifTableExists);
            command.setConstraintName(constraintName);
            command.setNewConstraintName(readIdentifier());
            return command;
        } else {
            read(TO);
            String newName = readIdentifierWithSchema(schema.getName());
            checkSchema(schema);
            AlterTableRename command = new AlterTableRename(session,
                    getSchema());
            command.setTableName(tableName);
            command.setNewTableName(newName);
            command.setIfTableExists(ifTableExists);
            command.setHidden(readIf("HIDDEN"));
            return command;
        }
    }

    private Prepared parseAlterTableSet(Schema schema, String tableName, boolean ifTableExists) {
        read("REFERENTIAL_INTEGRITY");
        int type = CommandInterface.ALTER_TABLE_SET_REFERENTIAL_INTEGRITY;
        boolean value = readBooleanSetting();
        AlterTableSet command = new AlterTableSet(session,
                schema, type, value);
        command.setTableName(tableName);
        command.setIfTableExists(ifTableExists);
        if (readIf(CHECK)) {
            command.setCheckExisting(true);
        } else if (readIf("NOCHECK")) {
            command.setCheckExisting(false);
        }
        return command;
    }

    private Prepared parseAlterTableCompatibility(Schema schema, String tableName, boolean ifTableExists, Mode mode) {
        if (mode.alterTableExtensionsMySQL) {
            if (readIf("AUTO_INCREMENT")) {
                readIf(EQUAL);
                Expression restart = readExpression();
                Table table = tableIfTableExists(schema, tableName, ifTableExists);
                if (table == null) {
                    return new NoOperation(session);
                }
                Index idx = table.findPrimaryKey();
                if (idx != null) {
                    for (IndexColumn ic : idx.getIndexColumns()) {
                        Column column = ic.column;
                        if (column.isIdentity()) {
                            AlterSequence command = new AlterSequence(session, schema);
                            command.setColumn(column, null);
                            SequenceOptions options = new SequenceOptions();
                            options.setRestartValue(restart);
                            command.setOptions(options);
                            return command;
                        }
                    }
                }
                throw DbException.get(ErrorCode.COLUMN_NOT_FOUND_1, "AUTO_INCREMENT PRIMARY KEY");
            } else if (readIf("CHANGE")) {
                readIf("COLUMN");
                String columnName = readIdentifier();
                String newColumnName = readIdentifier();
                Column column = columnIfTableExists(schema, tableName, columnName, ifTableExists, false);
                boolean nullable = column == null ? true : column.isNullable();
                // new column type ignored. RENAME and MODIFY are
                // a single command in MySQL but two different commands in H2.
                parseColumnForTable(newColumnName, nullable);
                AlterTableRenameColumn command = new AlterTableRenameColumn(session, schema);
                command.setTableName(tableName);
                command.setIfTableExists(ifTableExists);
                command.setOldColumnName(columnName);
                command.setNewColumnName(newColumnName);
                return command;
            } else if (readIf("CONVERT")) {
                readIf(TO);
                readIf("CHARACTER");
                readIf(SET);
                readMySQLCharset();

                if (readIf("COLLATE")) {
                    readMySQLCharset();
                }

                return new NoOperation(session);
            }
        }
        if (mode.alterTableModifyColumn && readIf("MODIFY")) {
            // MySQL compatibility (optional)
            readIf("COLUMN");
            // Oracle specifies (but will not require) an opening parenthesis
            boolean hasOpeningBracket = readIf(OPEN_PAREN);
            String columnName = readIdentifier();
            AlterTableAlterColumn command;
            NullConstraintType nullConstraint = parseNotNullConstraint();
            switch (nullConstraint) {
            case NULL_IS_ALLOWED:
            case NULL_IS_NOT_ALLOWED:
                command = new AlterTableAlterColumn(session, schema);
                command.setTableName(tableName);
                command.setIfTableExists(ifTableExists);
                Column column = columnIfTableExists(schema, tableName, columnName, ifTableExists, false);
                command.setOldColumn(column);
                if (nullConstraint == NullConstraintType.NULL_IS_ALLOWED) {
                    command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_DROP_NOT_NULL);
                } else {
                    command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_NOT_NULL);
                }
                break;
            case NO_NULL_CONSTRAINT_FOUND:
                command = parseAlterTableAlterColumnType(schema, tableName, columnName, ifTableExists, false,
                        mode.alterTableModifyColumnPreserveNullability);
                break;
            default:
                throw DbException.get(ErrorCode.UNKNOWN_MODE_1,
                        "Internal Error - unhandled case: " + nullConstraint.name());
            }
            if (hasOpeningBracket) {
                read(CLOSE_PAREN);
            }
            return command;
        }
        throw getSyntaxError();
    }

    private Table tableIfTableExists(Schema schema, String tableName, boolean ifTableExists) {
        Table table = schema.resolveTableOrView(session, tableName);
        if (table == null && !ifTableExists) {
            throw getTableOrViewNotFoundDbException(schema.getName(), tableName);
        }
        return table;
    }

    private Column columnIfTableExists(Schema schema, String tableName,
            String columnName, boolean ifTableExists, boolean ifExists) {
        Table table = tableIfTableExists(schema, tableName, ifTableExists);
        if (table == null) {
            return null;
        }
        return table.getColumn(columnName, ifExists);
    }

    private Prepared commandIfTableExists(Schema schema, String tableName,
            boolean ifTableExists, Prepared commandIfTableExists) {
        return tableIfTableExists(schema, tableName, ifTableExists) == null
            ? new NoOperation(session)
            : commandIfTableExists;
    }

    private AlterTableAlterColumn parseAlterTableAlterColumnType(Schema schema,
            String tableName, String columnName, boolean ifTableExists, boolean ifExists, boolean preserveNotNull) {
        Column oldColumn = columnIfTableExists(schema, tableName, columnName, ifTableExists, ifExists);
        Column newColumn = parseColumnForTable(columnName,
                !preserveNotNull || oldColumn == null || oldColumn.isNullable());
        AlterTableAlterColumn command = new AlterTableAlterColumn(session, schema);
        parseAlterColumnUsingIf(command);
        command.setTableName(tableName);
        command.setIfTableExists(ifTableExists);
        command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_CHANGE_TYPE);
        command.setOldColumn(oldColumn);
        command.setNewColumn(newColumn);
        return command;
    }

    private AlterTableAlterColumn parseAlterTableAlterColumnDataType(Schema schema,
            String tableName, String columnName, boolean ifTableExists, boolean ifExists) {
        Column oldColumn = columnIfTableExists(schema, tableName, columnName, ifTableExists, ifExists);
        Column newColumn = parseColumnWithType(columnName);
        if (oldColumn != null) {
            if (!oldColumn.isNullable()) {
                newColumn.setNullable(false);
            }
            if (!oldColumn.getVisible()) {
                newColumn.setVisible(false);
            }
            Expression e = oldColumn.getDefaultExpression();
            if (e != null) {
                if (oldColumn.isGenerated()) {
                    newColumn.setGeneratedExpression(e);
                } else {
                    newColumn.setDefaultExpression(session, e);
                }
            }
            e = oldColumn.getOnUpdateExpression();
            if (e != null) {
                newColumn.setOnUpdateExpression(session, e);
            }
            Sequence s = oldColumn.getSequence();
            if (s != null) {
                newColumn.setIdentityOptions(new SequenceOptions(s, newColumn.getType()),
                        oldColumn.isGeneratedAlways());
            }
            String c = oldColumn.getComment();
            if (c != null) {
                newColumn.setComment(c);
            }
        }
        AlterTableAlterColumn command = new AlterTableAlterColumn(session, schema);
        parseAlterColumnUsingIf(command);
        command.setTableName(tableName);
        command.setIfTableExists(ifTableExists);
        command.setType(CommandInterface.ALTER_TABLE_ALTER_COLUMN_CHANGE_TYPE);
        command.setOldColumn(oldColumn);
        command.setNewColumn(newColumn);
        return command;
    }

    private AlterTableAlterColumn parseAlterTableAddColumn(String tableName,
            Schema schema, boolean ifTableExists) {
        readIf("COLUMN");
        AlterTableAlterColumn command = new AlterTableAlterColumn(session,
                schema);
        command.setType(CommandInterface.ALTER_TABLE_ADD_COLUMN);
        command.setTableName(tableName);
        command.setIfTableExists(ifTableExists);
        if (readIf(OPEN_PAREN)) {
            command.setIfNotExists(false);
            do {
                parseTableColumnDefinition(command, schema, tableName, false);
            } while (readIfMore());
        } else {
            boolean ifNotExists = readIfNotExists();
            command.setIfNotExists(ifNotExists);
            parseTableColumnDefinition(command, schema, tableName, false);
            parseAlterColumnUsingIf(command);
        }
        if (readIf("BEFORE")) {
            command.setAddBefore(readIdentifier());
        } else if (readIf("AFTER")) {
            command.setAddAfter(readIdentifier());
        } else if (readIf("FIRST")) {
            command.setAddFirst();
        }
        return command;
    }

    private void parseAlterColumnUsingIf(AlterTableAlterColumn command) {
        if (readIf(USING)) {
            command.setUsingExpression(readExpression());
        }
    }

    private ConstraintActionType parseAction() {
        ConstraintActionType result = parseCascadeOrRestrict();
        if (result != null) {
            return result;
        }
        if (readIf("NO", "ACTION")) {
            return ConstraintActionType.RESTRICT;
        }
        read(SET);
        if (readIf(NULL)) {
            return ConstraintActionType.SET_NULL;
        }
        read(DEFAULT);
        return ConstraintActionType.SET_DEFAULT;
    }

    private ConstraintActionType parseCascadeOrRestrict() {
        if (readIf("CASCADE")) {
            return ConstraintActionType.CASCADE;
        } else if (readIf("RESTRICT")) {
            return ConstraintActionType.RESTRICT;
        } else {
            return null;
        }
    }

    private DefineCommand parseTableConstraintIf(String tableName, Schema schema, boolean ifTableExists) {
        String constraintName = null, comment = null;
        boolean ifNotExists = false;
        if (readIf(CONSTRAINT)) {
            ifNotExists = readIfNotExists();
            constraintName = readIdentifierWithSchema(schema.getName());
            checkSchema(schema);
            comment = readCommentIf();
        }
        AlterTableAddConstraint command;
        switch (currentTokenType) {
        case PRIMARY:
            read();
            read(KEY);
            command = new AlterTableAddConstraint(session, schema,
                    CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_PRIMARY_KEY, ifNotExists);
            if (readIf("HASH")) {
                command.setPrimaryKeyHash(true);
            }
            read(OPEN_PAREN);
            command.setIndexColumns(parseIndexColumnList());
            if (readIf("INDEX")) {
                String indexName = readIdentifierWithSchema();
                command.setIndex(getSchema().findIndex(session, indexName));
            }
            break;
        case UNIQUE: {
            read();
            NullsDistinct nullsDistinct = readNullsDistinct(database.getMode().nullsDistinct);
            // MySQL compatibility
            boolean compatibility = database.getMode().indexDefinitionInCreateTable;
            if (compatibility) {
                if (!readIf(KEY)) {
                    readIf("INDEX");
                }
                if (!isToken(OPEN_PAREN)) {
                    constraintName = readIdentifier();
                }
            }
            read(OPEN_PAREN);
            command = new AlterTableAddConstraint(session, schema, CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_UNIQUE,
                    ifNotExists);
            command.setNullsDistinct(nullsDistinct);
            if (readIf(VALUE, CLOSE_PAREN)) {
                command.setIndexColumns(null);
            } else {
                command.setIndexColumns(parseIndexColumnList());
            }
            if (readIf("INDEX")) {
                String indexName = readIdentifierWithSchema();
                command.setIndex(getSchema().findIndex(session, indexName));
            }
            if (compatibility) {
                readIf(USING, "BTREE");
            }
            break;
        }
        case FOREIGN:
            read();
            read(KEY);
            read(OPEN_PAREN);
            command = new AlterTableAddConstraint(session, schema,
                    CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_REFERENTIAL, ifNotExists);
            command.setIndexColumns(parseIndexColumnList());
            if (readIf("INDEX")) {
                String indexName = readIdentifierWithSchema();
                command.setIndex(schema.findIndex(session, indexName));
            }
            read("REFERENCES");
            parseReferences(command, schema, tableName);
            break;
        case CHECK:
            read();
            command = new AlterTableAddConstraint(session, schema, CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_CHECK,
                    ifNotExists);
            command.setCheckExpression(readExpression());
            break;
        default:
            if (constraintName == null) {
                Mode mode = database.getMode();
                if (mode.indexDefinitionInCreateTable) {
                    int start = tokenIndex;
                    if (readIf(KEY) || readIf("INDEX")) {
                        // MySQL
                        // need to read ahead, as it could be a column name
                        if (DataType.getTypeByName(currentToken, mode) == null) {
                            CreateIndex createIndex = new CreateIndex(session, schema);
                            createIndex.setComment(comment);
                            createIndex.setTableName(tableName);
                            createIndex.setIfTableExists(ifTableExists);
                            if (!readIf(OPEN_PAREN)) {
                                createIndex.setIndexName(readIdentifier());
                                read(OPEN_PAREN);
                            }
                            createIndex.setIndexColumns(parseIndexColumnList());
                            // MySQL compatibility
                            if (readIf(USING)) {
                                read("BTREE");
                            }
                            return createIndex;
                        } else {
                            // known data type
                            setTokenIndex(start);
                        }
                    }
                }
                return null;
            } else {
                if (expectedList != null) {
                    addMultipleExpected(PRIMARY, UNIQUE, FOREIGN, CHECK);
                }
                throw getSyntaxError();
            }
        }
        if (command.getType() != CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_PRIMARY_KEY) {
            if (readIf("NOCHECK")) {
                command.setCheckExisting(false);
            } else {
                readIf(CHECK);
                command.setCheckExisting(true);
            }
        }
        command.setTableName(tableName);
        command.setIfTableExists(ifTableExists);
        command.setConstraintName(constraintName);
        command.setComment(comment);
        return command;
    }

    private void parseReferences(AlterTableAddConstraint command,
            Schema schema, String tableName) {
        if (readIf(OPEN_PAREN)) {
            command.setRefTableName(schema, tableName);
            command.setRefIndexColumns(parseIndexColumnList());
        } else {
            String refTableName = readIdentifierWithSchema(schema.getName());
            command.setRefTableName(getSchema(), refTableName);
            if (readIf(OPEN_PAREN)) {
                command.setRefIndexColumns(parseIndexColumnList());
            }
        }
        if (readIf("INDEX")) {
            String indexName = readIdentifierWithSchema();
            command.setRefIndex(getSchema().findIndex(session, indexName));
        }
        while (readIf(ON)) {
            if (readIf("DELETE")) {
                command.setDeleteAction(parseAction());
            } else {
                read("UPDATE");
                command.setUpdateAction(parseAction());
            }
        }
        if (!readIf(NOT, "DEFERRABLE")) {
            readIf("DEFERRABLE");
        }
    }

    private CreateLinkedTable parseCreateLinkedTable(boolean temp,
            boolean globalTemp, boolean force) {
        read(TABLE);
        boolean ifNotExists = readIfNotExists();
        String tableName = readIdentifierWithSchema();
        CreateLinkedTable command = new CreateLinkedTable(session, getSchema());
        command.setTemporary(temp);
        command.setGlobalTemporary(globalTemp);
        command.setForce(force);
        command.setIfNotExists(ifNotExists);
        command.setTableName(tableName);
        command.setComment(readCommentIf());
        read(OPEN_PAREN);
        command.setDriver(readString());
        read(COMMA);
        command.setUrl(readString());
        read(COMMA);
        command.setUser(readString());
        read(COMMA);
        command.setPassword(readString());
        read(COMMA);
        String originalTable = readString();
        if (readIf(COMMA)) {
            command.setOriginalSchema(originalTable);
            originalTable = readString();
        }
        command.setOriginalTable(originalTable);
        read(CLOSE_PAREN);
        if (readIf("EMIT", "UPDATES")) {
            command.setEmitUpdates(true);
        } else if (readIf("READONLY")) {
            command.setReadOnly(true);
        }
        if (readIf("FETCH_SIZE")) {
            command.setFetchSize(readNonNegativeInt());
        }
        if (readIf("AUTOCOMMIT")) {
            if (readIf("ON")) {
                command.setAutoCommit(true);
            } else if (readIf("OFF")) {
                command.setAutoCommit(false);
            }
        }
        return command;
    }

    private CreateTable parseCreateTable(boolean temp, boolean globalTemp,
            boolean persistIndexes) {
        boolean ifNotExists = readIfNotExists();
        String tableName = readIdentifierWithSchema();
        if (temp && globalTemp && equalsToken("SESSION", schemaName)) {
            // support weird syntax: declare global temporary table session.xy
            // (...) not logged
            schemaName = session.getCurrentSchemaName();
            globalTemp = false;
        }
        Schema schema = getSchema();
        CreateTable command = new CreateTable(session, schema);
        command.setPersistIndexes(persistIndexes);
        command.setTemporary(temp);
        command.setGlobalTemporary(globalTemp);
        command.setIfNotExists(ifNotExists);
        command.setTableName(tableName);
        command.setComment(readCommentIf());
        if (readIf(OPEN_PAREN)) {
            if (!readIf(CLOSE_PAREN)) {
                do {
                    parseTableColumnDefinition(command, schema, tableName, true);
                } while (readIfMore());
            }
        }
        if (database.getMode().mySqlTableOptions) {
            parseCreateTableMySQLTableOptions(command);
        }
        if (readIf("ENGINE")) {
            command.setTableEngine(readIdentifier());
        }
        if (readIf(WITH)) {
            command.setTableEngineParams(readTableEngineParams());
        }
        if (temp) {
            if (readIf(ON, "COMMIT")) {
                if (readIf("DROP")) {
                    command.setOnCommitDrop();
                } else if (readIf("DELETE")) {
                    read("ROWS");
                    command.setOnCommitTruncate();
                }
            } else if (readIf(NOT)) {
                if (readIf("PERSISTENT")) {
                    command.setPersistData(false);
                } else {
                    read("LOGGED");
                }
            }
            if (readIf("TRANSACTIONAL")) {
                command.setTransactional(true);
            }
        } else if (!persistIndexes && readIf(NOT, "PERSISTENT")) {
            command.setPersistData(false);
        }
        if (readIf("HIDDEN")) {
            command.setHidden(true);
        }
        if (readIf(AS)) {
            readIf("SORTED");
            command.setQuery(parseQuery());
            if (readIf(WITH)) {
                command.setWithNoData(readIf("NO"));
                read("DATA");
            }
        }
        return command;
    }

    private void parseTableColumnDefinition(CommandWithColumns command, Schema schema, String tableName,
            boolean forCreateTable) {
        DefineCommand c = parseTableConstraintIf(tableName, schema, false);
        if (c != null) {
            command.addConstraintCommand(c);
            return;
        }
        String columnName = readIdentifier();
        if (forCreateTable && (currentTokenType == COMMA || currentTokenType == CLOSE_PAREN)) {
            command.addColumn(new Column(columnName, TypeInfo.TYPE_UNKNOWN));
            return;
        }
        Column column = parseColumnForTable(columnName, true);
        if (column.hasIdentityOptions() && column.isPrimaryKey()) {
            command.addConstraintCommand(newPrimaryKeyConstraintCommand(session, schema, tableName, column));
        }
        command.addColumn(column);
        readColumnConstraints(command, schema, tableName, column);
    }

    /**
     * Create a new alter table command.
     *
     * @param session the session
     * @param schema the schema
     * @param tableName the table
     * @param column the column
     * @return the command
     */
    public static AlterTableAddConstraint newPrimaryKeyConstraintCommand(SessionLocal session, Schema schema,
            String tableName, Column column) {
        column.setPrimaryKey(false);
        AlterTableAddConstraint pk = new AlterTableAddConstraint(session, schema,
                CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_PRIMARY_KEY, false);
        pk.setTableName(tableName);
        pk.setIndexColumns(new IndexColumn[] { new IndexColumn(column.getName()) });
        return pk;
    }

    private void readColumnConstraints(CommandWithColumns command, Schema schema, String tableName, Column column) {
        String comment = column.getComment();
        boolean hasPrimaryKey = false, hasNotNull = false;
        NullConstraintType nullType;
        Mode mode = database.getMode();
        for (;;) {
            String constraintName;
            if (readIf(CONSTRAINT)) {
                constraintName = readIdentifier();
            } else if (comment == null && (comment = readCommentIf()) != null) {
                // Compatibility: COMMENT may be specified appear after some constraint
                column.setComment(comment);
                continue;
            } else {
                constraintName = null;
            }
            if (!hasPrimaryKey && readIf(PRIMARY, KEY)) {
                hasPrimaryKey = true;
                boolean hash = readIf("HASH");
                AlterTableAddConstraint pk = new AlterTableAddConstraint(session, schema,
                        CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_PRIMARY_KEY, false);
                pk.setConstraintName(constraintName);
                pk.setPrimaryKeyHash(hash);
                pk.setTableName(tableName);
                pk.setIndexColumns(new IndexColumn[] { new IndexColumn(column.getName()) });
                command.addConstraintCommand(pk);
            } else if (readIf(UNIQUE)) {
                NullsDistinct nullsDistinct = readNullsDistinct(database.getMode().nullsDistinct);
                AlterTableAddConstraint unique = new AlterTableAddConstraint(session, schema,
                        CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_UNIQUE, false);
                unique.setConstraintName(constraintName);
                unique.setNullsDistinct(nullsDistinct);
                unique.setIndexColumns(new IndexColumn[] { new IndexColumn(column.getName()) });
                unique.setTableName(tableName);
                command.addConstraintCommand(unique);
            } else if (!hasNotNull
                    && (nullType = parseNotNullConstraint()) != NullConstraintType.NO_NULL_CONSTRAINT_FOUND) {
                hasNotNull = true;
                if (nullType == NullConstraintType.NULL_IS_NOT_ALLOWED) {
                    column.setNullable(false);
                } else if (nullType == NullConstraintType.NULL_IS_ALLOWED) {
                    if (column.isIdentity()) {
                        throw DbException.get(ErrorCode.COLUMN_MUST_NOT_BE_NULLABLE_1, column.getName());
                    }
                    column.setNullable(true);
                }
            } else if (readIf(CHECK)) {
                AlterTableAddConstraint check = new AlterTableAddConstraint(session, schema,
                        CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_CHECK, false);
                check.setConstraintName(constraintName);
                check.setTableName(tableName);
                check.setCheckExpression(readExpression());
                command.addConstraintCommand(check);
            } else if (readIf("REFERENCES")) {
                AlterTableAddConstraint ref = new AlterTableAddConstraint(session, schema,
                        CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_REFERENTIAL, false);
                ref.setConstraintName(constraintName);
                ref.setIndexColumns(new IndexColumn[] { new IndexColumn(column.getName()) });
                ref.setTableName(tableName);
                parseReferences(ref, schema, tableName);
                command.addConstraintCommand(ref);
            } else if (constraintName == null) {
                if (column.getIdentityOptions() != null || !parseCompatibilityIdentity(column, mode)) {
                    return;
                }
            } else {
                throw getSyntaxError();
            }
        }
    }

    private boolean parseCompatibilityIdentity(Column column, Mode mode) {
        if (mode.autoIncrementClause && readIf("AUTO_INCREMENT")) {
            parseCompatibilityIdentityOptions(column);
            return true;
        }
        if (mode.identityClause && readIf("IDENTITY")) {
            parseCompatibilityIdentityOptions(column);
            return true;
        }
        return false;
    }

    private void parseCreateTableMySQLTableOptions(CreateTable command) {
        boolean requireNext = false;
        for (;;) {
            if (readIf("AUTO_INCREMENT")) {
                readIf(EQUAL);
                Expression value = readExpression();
                set: {
                    AlterTableAddConstraint primaryKey = command.getPrimaryKey();
                    if (primaryKey != null) {
                        for (IndexColumn ic : primaryKey.getIndexColumns()) {
                            String columnName = ic.columnName;
                            for (Column column : command.getColumns()) {
                                if (database.equalsIdentifiers(column.getName(), columnName)) {
                                    SequenceOptions options = column.getIdentityOptions();
                                    if (options != null) {
                                        options.setStartValue(value);
                                        break set;
                                    }
                                }
                            }
                        }
                    }
                    throw DbException.get(ErrorCode.COLUMN_NOT_FOUND_1, "AUTO_INCREMENT PRIMARY KEY");
                }
            } else if (readIf(DEFAULT)) {
                if (!readIf("CHARACTER", SET)) {
                    readIf("CHARSET");
                    readIf("COLLATE");
                }
                readMySQLCharset();
            } else if (readIf("CHARACTER")) {
                read(SET);
                readMySQLCharset();
            } else if (readIf("COLLATE")) {
                readMySQLCharset();
            } else if (readIf("CHARSET")) {
                readMySQLCharset();
            } else if (readIf("COMMENT")) {
                readIf(EQUAL);
                command.setComment(readString());
            } else if (readIf("ENGINE")) {
                readIf(EQUAL);
                readIdentifier();
            } else if (readIf("ROW_FORMAT")) {
                readIf(EQUAL);
                readIdentifier();
            } else if (requireNext) {
                throw getSyntaxError();
            } else {
                break;
            }
            requireNext = readIf(COMMA);
        }
    }

    private void readMySQLCharset() {
        readIf(EQUAL);
        readIdentifier();
    }

    /**
     * Enumeration describing null constraints
     */
    private enum NullConstraintType {
        NULL_IS_ALLOWED, NULL_IS_NOT_ALLOWED, NO_NULL_CONSTRAINT_FOUND
    }

    private NullConstraintType parseNotNullConstraint(NullConstraintType nullConstraint) {
        if (nullConstraint == NullConstraintType.NO_NULL_CONSTRAINT_FOUND) {
            nullConstraint = parseNotNullConstraint();
        }
        return nullConstraint;
    }

    private NullConstraintType parseNotNullConstraint() {
        NullConstraintType nullConstraint;
        if (readIf(NOT, NULL)) {
            nullConstraint = NullConstraintType.NULL_IS_NOT_ALLOWED;
        } else if (readIf(NULL)) {
            nullConstraint = NullConstraintType.NULL_IS_ALLOWED;
        } else {
            return NullConstraintType.NO_NULL_CONSTRAINT_FOUND;
        }
        if (database.getMode().getEnum() == ModeEnum.Oracle) {
            nullConstraint = parseNotNullCompatibility(nullConstraint);
        }
        return nullConstraint;
    }

    private NullConstraintType parseNotNullCompatibility(NullConstraintType nullConstraint) {
        if (readIf("ENABLE")) {
            if (!readIf("VALIDATE") && readIf("NOVALIDATE")) {
                // Turn off constraint, allow NULLs
                nullConstraint = NullConstraintType.NULL_IS_ALLOWED;
            }
        } else if (readIf("DISABLE")) {
            // Turn off constraint, allow NULLs
            nullConstraint = NullConstraintType.NULL_IS_ALLOWED;
            if (!readIf("VALIDATE")) {
                readIf("NOVALIDATE");
            }
        }
        return nullConstraint;
    }

    private CreateSynonym parseCreateSynonym(boolean orReplace) {
        boolean ifNotExists = readIfNotExists();
        String name = readIdentifierWithSchema();
        Schema synonymSchema = getSchema();
        read(FOR);
        String tableName = readIdentifierWithSchema();

        Schema targetSchema = getSchema();
        CreateSynonym command = new CreateSynonym(session, synonymSchema);
        command.setName(name);
        command.setSynonymFor(tableName);
        command.setSynonymForSchema(targetSchema);
        command.setComment(readCommentIf());
        command.setIfNotExists(ifNotExists);
        command.setOrReplace(orReplace);
        return command;
    }

    private static int getCompareType(int tokenType) {
        switch (tokenType) {
        case EQUAL:
            return Comparison.EQUAL;
        case BIGGER_EQUAL:
            return Comparison.BIGGER_EQUAL;
        case BIGGER:
            return Comparison.BIGGER;
        case SMALLER:
            return Comparison.SMALLER;
        case SMALLER_EQUAL:
            return Comparison.SMALLER_EQUAL;
        case NOT_EQUAL:
            return Comparison.NOT_EQUAL;
        case SPATIAL_INTERSECTS:
            return Comparison.SPATIAL_INTERSECTS;
        default:
            return -1;
        }
    }

    /**
     * Add double quotes around an identifier if required.
     *
     * @param s the identifier
     * @param sqlFlags formatting flags
     * @return the quoted identifier
     */
    public static String quoteIdentifier(String s, int sqlFlags) {
        if (s == null) {
            return "\"\"";
        }
        if ((sqlFlags & HasSQL.QUOTE_ONLY_WHEN_REQUIRED) != 0 && ParserUtil.isSimpleIdentifier(s, false, false)) {
            return s;
        }
        return StringUtils.quoteIdentifier(s);
    }

    public void setLiteralsChecked(boolean literalsChecked) {
        this.literalsChecked = literalsChecked;
    }

    public void setRightsChecked(boolean rightsChecked) {
        this.rightsChecked = rightsChecked;
    }

    public void setSuppliedParameters(ArrayList<Parameter> suppliedParameters) {
        this.parameters = suppliedParameters;
    }

    /**
     * Parse a SQL code snippet that represents an expression.
     *
     * @param sql the code snippet
     * @return the expression object
     */
    public Expression parseExpression(String sql) {
        initialize(sql, null, false);
        read();
        return readExpression();
    }

    /**
     * Parse a SQL code snippet that represents an expression for a domain constraint.
     *
     * @param sql the code snippet
     * @return the expression object
     */
    public Expression parseDomainConstraintExpression(String sql) {
        initialize(sql, null, false);
        read();
        try {
            parseDomainConstraint = true;
            return readExpression();
        } finally {
            parseDomainConstraint = false;
        }
    }

    /**
     * Parse a SQL code snippet that represents a table name.
     *
     * @param sql the code snippet
     * @return the table object
     */
    public Table parseTableName(String sql) {
        initialize(sql, null, false);
        read();
        return readTableOrView();
    }

    /**
     * Parses a list of column names or numbers in parentheses.
     *
     * @param sql the source SQL
     * @param offset the initial offset
     * @return the array of column names ({@code String[]}) or numbers
     *         ({@code int[]})
     * @throws DbException on syntax error
     */
    public Object parseColumnList(String sql, int offset) {
        initialize(sql, null, true);
        for (int i = 0, l = tokens.size(); i < l; i++) {
            if (tokens.get(i).start() >= offset) {
                setTokenIndex(i);
                break;
            }
        }
        read(OPEN_PAREN);
        if (readIf(CLOSE_PAREN)) {
            return Utils.EMPTY_INT_ARRAY;
        }
        if (isIdentifier()) {
            ArrayList<String> list = Utils.newSmallArrayList();
            do {
                if (!isIdentifier()) {
                    throw getSyntaxError();
                }
                list.add(currentToken);
                read();
            } while (readIfMore());
            return list.toArray(new String[0]);
        } else if (currentTokenType == LITERAL) {
            ArrayList<Integer> list = Utils.newSmallArrayList();
            do {
                list.add(readInt());
            } while (readIfMore());
            int count = list.size();
            int[] array = new int[count];
            for (int i = 0; i < count; i++) {
                array[i] = list.get(i);
            }
            return array;
        } else {
            throw getSyntaxError();
        }
    }

    /**
     * Returns the last parse index.
     *
     * @return the last parse index
     */
    public int getLastParseIndex() {
        return token.start();
    }

    @Override
    public String toString() {
        return StringUtils.addAsterisk(sqlCommand, token.start());
    }
}
