oracle.kv.impl.query.compiler.Translator.java Source code

Java tutorial

Introduction

Here is the source code for oracle.kv.impl.query.compiler.Translator.java

Source

/*-
 * Copyright (C) 2011, 2017 Oracle and/or its affiliates. All rights reserved.
 *
 * This file was distributed by Oracle as part of a version of Oracle NoSQL
 * Database made available at:
 *
 * http://www.oracle.com/technetwork/database/database-technologies/nosqldb/downloads/index.html
 *
 * Please see the LICENSE file included in the top-level directory of the
 * appropriate version of Oracle NoSQL Database for a copy of the license and
 * additional information.
 */

package oracle.kv.impl.query.compiler;

import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.TimeUnit;

import oracle.kv.impl.api.table.EnumDefImpl;
import oracle.kv.impl.api.table.FieldDefFactory;
import oracle.kv.impl.api.table.FieldDefImpl;
import oracle.kv.impl.api.table.FieldMap;
import oracle.kv.impl.api.table.FieldValueImpl;
import oracle.kv.impl.api.table.FixedBinaryDefImpl;
import oracle.kv.impl.api.table.IndexImpl;
import oracle.kv.impl.api.table.IndexImpl.AnnotatedField;
import oracle.kv.impl.api.table.NullJsonValueImpl;
import oracle.kv.impl.api.table.RecordDefImpl;
import oracle.kv.impl.api.table.TableBuilder;
import oracle.kv.impl.api.table.TableBuilderBase;
import oracle.kv.impl.api.table.TableEvolver;
import oracle.kv.impl.api.table.TableImpl;
import oracle.kv.impl.api.table.TableMetadata;
import oracle.kv.impl.api.table.TablePath;
import oracle.kv.impl.api.table.TimestampDefImpl;
import oracle.kv.impl.query.QueryException;
import oracle.kv.impl.query.QueryException.Location;
import oracle.kv.impl.query.QueryStateException;
import oracle.kv.impl.query.compiler.Expr.ExprIter;
import oracle.kv.impl.query.compiler.Expr.ExprKind;
import oracle.kv.impl.query.compiler.ExprMapFilter.FilterKind;
import oracle.kv.impl.query.compiler.ExprSFW.FromClause;
import oracle.kv.impl.query.compiler.FunctionLib.FuncCode;
import oracle.kv.impl.query.compiler.parser.KVQLBaseListener;
import oracle.kv.impl.query.compiler.parser.KVQLParser;
import oracle.kv.impl.query.compiler.parser.KVQLParser.Es_propertiesContext;
import oracle.kv.impl.query.runtime.ArithUnaryOpIter;
import oracle.kv.impl.query.types.ExprType.Quantifier;
import oracle.kv.table.FieldDef;
import oracle.kv.table.FieldValue;
import oracle.kv.table.TimeToLive;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.antlr.v4.runtime.tree.RuleNode;
import org.antlr.v4.runtime.tree.TerminalNode;

import static java.util.Locale.ENGLISH;

/**
 * This class works with the Abstract Syntax Tree (AST) generated from KVSQL.g
 * by Antlr V4. It implements a parse tree visitor (extends KVQLBaseListener)
 * to walk the AST and generate an expression tree (in case of a DML statement)
 * or a single DdlOperation (in case of a DDL statement).
 *
 * Antlr parses the entire DDL/DML statement into the parse tree before any of
 * the TableListener methods are called. This means that the implementation
 * can rely on state that is guaranteed by the successful parse.
 *
 * If there was an error in processing of the AST, the Translator instance will
 * return false from its succeeded() method. In this case, there is no useful
 * state in the Translator object other than the exception returned by
 * getException().
 *
 * Use of TableMetadata for DDL.
 *
 * It would be possible to implement the translation without
 * access to TableMetadata.  In fact, the current code does not need metadata
 * for create/drop index and drop table.  The rationale for requiring
 * TableMetadata for some operations is just that it simplifies interactions
 * with TableBuilder and related classes in the case of table evolution and
 * child table creation.  This connection could be changed to have the
 * TableMetadata accessed only by callers.
 *
 * If so, it'd have the following implications: o creation of child tables
 * would need to add, and validate parent primary key information after the
 * fact.  o alter table would have to save its individual modifications for
 * application after the parse
 *
 * This may be desirable, but for now, this class uses TableMetadata directly.
 *
 * Usage warnings:
 * The syntax error messages are currently fairly
 * cryptic. oracle.kv.shell.ExecuteCmd implements a getUsage() method which
 * attempts to augment those messages. This should be moved into TableDdl.
 */
public class Translator extends KVQLBaseListener {

    private final static String ALL_PRIVS = "ALL";

    private final ParseTreeWalker theWalker = new ParseTreeWalker();

    private final TableMetadata theMetadata;

    /*
     * The query control block.
     */
    private final QueryControlBlock theQCB;

    /*
     * The library of build-in functions
     */
    private final FunctionLib theFuncLib;

    /*
     * The initial (root) static context of the query.
     */
    private final StaticContext theInitSctx;

    /*
     * The current static context.
     */
    private StaticContext theSctx;

    /*
     * A stack of static contexts to implement nested scopes.
     */
    private final Stack<StaticContext> theScopes = new Stack<StaticContext>();

    /*
     * For storing the sub-exprs of each expr. Sometimes, the parent expr is
     * created and placed in the stack before its subexpr. Other times, all of
     * the subexprs are translated first and then replaced in the stack by
     * their parent expr.
     */
    private final Stack<Expr> theExprs = new Stack<Expr>();

    /*
     * For storing the column names in a select clause.
     */
    private final Stack<String> theColNames = new Stack<String>();

    /*
     * For storing the sort specs in an order by clause.
     */
    private final Stack<SortSpec> theSortSpecs = new Stack<SortSpec>();

    private final Stack<FieldDefImpl> theTypes = new Stack<FieldDefImpl>();

    private final Stack<Quantifier> theQuantifiers = new Stack<Quantifier>();

    private final Stack<FieldDefHelper> theFields = new Stack<FieldDefHelper>();

    /*
     * Helper class to handle JSON fragments (used by full-text indexes)
     */
    private final JsonCollector jsonCollector = new JsonCollector();

    private TableImpl theTable;

    private String theTableAlias;

    /*
     * Counts the number of external variables in the query and serves to
     * assign a unique numeric id to each such var.
     */
    private int theExternalVarsCounter;

    private Expr theRootExpr = null;

    private RuntimeException theException;

    private TableBuilderBase theTableBuilder;

    /**
     * A flag that is set only inside DDL statements.
     */
    private boolean theInDDL = false;

    public Translator(QueryControlBlock qcb) {
        theQCB = qcb;
        theMetadata = qcb.getTableMeta();
        theInitSctx = qcb.getInitSctx();
        theSctx = theInitSctx;
        theFuncLib = CompilerAPI.getFuncLib();
        theScopes.push(theInitSctx);
    }

    /**
     * Returns a table if the parsed statement resulted in a table, otherwise
     * null.
     */
    public TableImpl getTable() {
        return theTable;
    }

    Expr getRootExpr() {
        return theRootExpr;
    }

    /**
     * Returns the CompilerException if an exception occurred.
     */
    public RuntimeException getException() {
        return theException;
    }

    public void setException(RuntimeException de) {
        theException = de;
    }

    public boolean succeeded() {
        return theException == null;
    }

    public boolean isQuery() {
        return theRootExpr != null;
    }

    /**
     * Returns whether to remove data as part of a drop table operation.
     * Unconditionally yes at this time.
     */
    public boolean getRemoveData() {
        return true;
    }

    /*
     * Implementation of the translator.
     *
     * TODO: look at using the BailErrorStrategy and setSLL(true) to do faster
     * parsing with a bailout.
     */
    public void translate(ParseTree tree) {

        try {
            /*
             * Walks the parse tree, acting on the rules encountered.
             */
            theWalker.walk(this, tree);
        } catch (DdlException e) {
            /*
             * DdlException is used to notify the caller that this is a DDL
             * statement that should be sent to the server without any further
             * processing from the compiler
             */
            throw e;
        } catch (RuntimeException e) {
            setException(e);
        }
    }

    void pushScope() {
        StaticContext sctx = new StaticContext(theScopes.peek());
        theScopes.push(sctx);
        theSctx = sctx;
    }

    void popScope() {
        theScopes.pop();
        theSctx = theScopes.peek();
    }

    @Override
    public void exitQuery(KVQLParser.QueryContext ctx) {

        theRootExpr = theExprs.pop();

        assert (theRootExpr != null);
        assert (theExprs.isEmpty());
        assert (theColNames.isEmpty());
        assert (theTypes.isEmpty());
    }

    /**
     * statement :
     *      (
     *      query
     *      | create_table_statement
     *      | create_index_statement
     *      | create_user_statement
     *      | create_role_statement
     *      | drop_index_statement
     *      | create_text_index_statement
     *      | drop_role_statement
     *      | drop_user_statement
     *      | alter_table_statement
     *      | alter_user_statement
     *      | drop_table_statement
     *      | grant_statement
     *      | revoke_statement
     *      | describe_statement
     *      | show_statement) ;
     */
    @Override
    public void enterStatement(KVQLParser.StatementContext ctx) {
        if (ctx.query() == null) {
            theInDDL = true;
        }
    }

    @Override
    public void exitStatement(KVQLParser.StatementContext ctx) {
        if (ctx.query() == null) {
            theInDDL = false;
        }
    }

    /*
     * query : prolog? sfw_expr ;
     *
     * prolog : DECLARE var_decl (var_decl)* SEMI;
     *
     * var_decl : var_name type_def;
     *
     * var_name : DOLLAR id;
     */
    @Override
    public void exitVar_decl(KVQLParser.Var_declContext ctx) {

        Location loc = getLocation(ctx);

        String varName = ctx.var_name().getText();

        FieldDefImpl varType = theTypes.pop();

        if (varName.equals(ExprVar.theElementVarName) || varName.equals(ExprVar.theElementPosVarName)
                || varName.equals(ExprVar.theKeyVarName) || varName.equals(ExprVar.theValueVarName)) {
            throw new QueryException(varName + " cannot be used as the name of an external variable");
        }

        ExprVar varExpr = new ExprVar(theQCB, theInitSctx, loc, varName, varType, theExternalVarsCounter++);

        theSctx.addVariable(varExpr);
    }

    /*
     * sfw_expr : select_clause
     *            from_clause
     *            where_clause?
     *            orderby_clause?
     *            limit_clause?
     *            offset_clause? ;
     */
    @Override
    public void enterSfw_expr(KVQLParser.Sfw_exprContext ctx) {

        Location loc = getLocation(ctx);

        theExprs.push(new ExprSFW(theQCB, theInitSctx, loc));

        enterFrom_clause(ctx.from_clause());
    }

    @Override
    public void exitSfw_expr(KVQLParser.Sfw_exprContext ctx) {

        ExprSFW sfw = (ExprSFW) theExprs.peek();

        sfw.analyseSort();

        for (int i = 0; i < sfw.getNumVars(); ++i) {
            popScope();
        }
    }

    /*
     * from_clause : FROM name_path (AS? id)? (COMMA expr (AS? var_name))* ;
     *
     * This method is called explicitly from enterSfw_expr, and then it is
     * also called from the antlr tree walker. The 2nd invocation should be
     * a noop. This is done by checking that the sfw expr at the top of the
     * exprs stack has a FromClause already.
     */
    @Override
    public void enterFrom_clause(KVQLParser.From_clauseContext ctx) {

        pushScope();

        Location loc = getLocation(ctx.name_path());

        ExprSFW sfwExpr = (ExprSFW) theExprs.peek();

        if (sfwExpr.getNumVars() > 0) {

            /*
             * This is the 2nd call to this method. The children of the
             * From_clauseContext node have been processed already. To
             * avoid reprocessing them, we remove all the childern here.
             */
            int numParseChildren = ctx.getChildCount();
            while (numParseChildren > 0) {
                ctx.removeLastChild();
                --numParseChildren;
            }
            return;
        }

        String[] pathName = getNamePath(ctx.name_path());

        theTable = getTable(pathName, loc);

        if (theTable == null) {
            throw new QueryException("Table " + concatPathName(pathName) + " does not exist", loc);
        }

        theTableAlias = (ctx.tab_alias() == null ? null : ctx.tab_alias().getText());

        String varName = "$$";
        if (theTableAlias == null) {
            varName += theTable.getFullName();
        } else if (theTableAlias.charAt(0) == '$') {
            varName = theTableAlias;
        } else {
            varName += theTableAlias;
        }

        Expr tableExpr = new ExprBaseTable(theQCB, theInitSctx, loc, theTable);

        FromClause fromClause = sfwExpr.addFromClause(tableExpr, varName);
        ExprVar varExpr = fromClause.getVar();

        theSctx.addVariable(varExpr);

        int numParseChildren = ctx.getChildCount();

        List<KVQLParser.ExprContext> exprCtxs = new ArrayList<KVQLParser.ExprContext>(numParseChildren);

        List<KVQLParser.Var_nameContext> varCtxs = new ArrayList<KVQLParser.Var_nameContext>(numParseChildren);

        for (int i = 0; i < numParseChildren; ++i) {

            ParseTree child = ctx.getChild(i);

            if (child instanceof KVQLParser.ExprContext) {
                exprCtxs.add((KVQLParser.ExprContext) child);
                continue;
            }

            if (child instanceof KVQLParser.Var_nameContext) {
                varCtxs.add((KVQLParser.Var_nameContext) child);
                continue;
            }
        }

        assert (exprCtxs.size() == varCtxs.size());

        for (int i = 0; i < exprCtxs.size(); ++i) {

            KVQLParser.ExprContext exprCtx = exprCtxs.get(i);
            KVQLParser.Var_nameContext varCtx = varCtxs.get(i);
            varName = varCtx.getText();

            theWalker.walk(this, exprCtx);

            Expr domainExpr = theExprs.pop();
            fromClause = sfwExpr.addFromClause(domainExpr, varName);
            varExpr = fromClause.getVar();
            pushScope();
            theSctx.addVariable(varExpr);
        }
    }

    /*
     * where_clause : WHERE expr ;
     */
    @Override
    public void exitWhere_clause(KVQLParser.Where_clauseContext ctx) {

        Expr condExpr = theExprs.pop();
        ExprSFW sfwExpr = (ExprSFW) theExprs.peek();

        sfwExpr.addWhereClause(condExpr);
    }

    /*
     * select_clause :
     *     SELECT hint? (STAR | (expr col_alias (COMMA expr col_alias)*)) ;
     *
     * col_alias : (AS id)?
     */
    @Override
    public void enterSelect_clause(KVQLParser.Select_clauseContext ctx) {

        /*
         * If it's a "select *", add in the SELECT clause an expr for each
         * variables declared in the FROM clause.
         */
        if (ctx.STAR() != null) {

            ExprSFW sfw = (ExprSFW) theExprs.peek();

            int numColumns = sfw.getNumVars();

            ArrayList<Expr> colExprs = new ArrayList<Expr>(numColumns);
            ArrayList<String> colNames = new ArrayList<String>(numColumns);

            for (int i = 0; i < numColumns; ++i) {
                ExprVar varExpr = sfw.getVar(i);
                colExprs.add(varExpr);
                colNames.add(varExpr.getName().substring(1));
            }

            sfw.addSelectClause(colNames, colExprs, false/*hasAS*/);

        } else {
            /* Push a null sentinel in the stacks */
            theExprs.push(null);
            theColNames.push(null);
        }
    }

    @Override
    public void exitSelect_clause(KVQLParser.Select_clauseContext ctx) {

        if (ctx.STAR() != null) {
            return;
        }

        ArrayList<Expr> colExprs = new ArrayList<Expr>();
        ArrayList<String> colNames = new ArrayList<String>();

        Expr expr = theExprs.pop();
        String name = theColNames.pop();
        boolean hasASclauses = true;

        while (expr != null) {

            if (name == null) {
                hasASclauses = false;
                if (expr.getKind() == ExprKind.FIELD_STEP) {
                    name = ((ExprFieldStep) expr).getFieldName();
                } else if (expr.getKind() == ExprKind.VAR) {
                    name = ((ExprVar) expr).getName().substring(1);
                }
            }

            colExprs.add(expr);
            colNames.add(name);

            expr = theExprs.pop();
            name = theColNames.pop();
        }

        Collections.reverse(colExprs);
        Collections.reverse(colNames);

        for (int i = 0; i < colNames.size(); ++i) {
            if (colNames.get(i) == null) {
                colNames.set(i, ("Column_" + (i + 1)));
            }
        }

        HashSet<String> uniqueColNames = new HashSet<String>(colNames.size());

        for (int i = 0; i < colNames.size(); ++i) {
            String colName = colNames.get(i);
            if (!uniqueColNames.add(colName)) {
                throw new QueryException("Duplicate column name in SELECT clause: " + colName,
                        colExprs.get(i).getLocation());
            }
        }

        ExprSFW sfwExpr = (ExprSFW) theExprs.peek();

        sfwExpr.addSelectClause(colNames, colExprs, hasASclauses);
    }

    @Override
    public void enterCol_alias(KVQLParser.Col_aliasContext ctx) {

        if (ctx.id() == null) {
            theColNames.push(null);
        } else {
            theColNames.push(ctx.id().getText());
        }
    }

    /*
     *  hint : ( (PREFER_INDEXES LP name_path index_name* RP) |
     *           (FORCE_INDEX    LP name_path index_name  RP) |
     *           (PREFER_PRIMARY_INDEX LP name_path RP)       |
     *           (FORCE_PRIMARY_INDEX  LP name_path RP) ) STRING?;
     */
    @Override
    public void exitHint(KVQLParser.HintContext ctx) {

        ExprSFW sfwExpr = (ExprSFW) theExprs.peek();
        if (sfwExpr == null) {
            // skipping the null sentinel inserted by enterSelect_clause()
            sfwExpr = (ExprSFW) theExprs.get(theExprs.size() - 2);
            if (sfwExpr == null) {
                throw new QueryStateException("SFW expr not found.");
            }
        }

        assert ctx.name_path() != null : "Table name missing from " + " hint at: " + getLocation(ctx.name_path());

        ExprBaseTable exprBaseTable = sfwExpr.getTableExpr();
        String tableName = ctx.name_path().getText();

        if (!exprBaseTable.getTable().getFullName().equals(tableName)) {
            throw new QueryException(
                    "Table name specified in " + "hint doesn't match the table in the FROM statement.",
                    getLocation(ctx.name_path()));
        }

        if (ctx.PREFER_INDEXES() != null) {

            for (KVQLParser.Index_nameContext indxCtx : ctx.index_name()) {
                String indexName = indxCtx.getText();
                IndexImpl indx = (IndexImpl) exprBaseTable.getTable().getIndex(indexName);

                if (indx == null) {
                    /*
                     * Ignore the hint if the specified index does not actually
                     * exist. The index could have existed when the query was
                     * written, but it was dropped some time later. In this case
                     * we don't want existing queries to start throwing errors
                     * (of course, if there are any saved PreparedStatements,
                     * those should also be recompiled, but that a bigger TODO).
                     */
                    continue;
                }
                exprBaseTable.addIndexHint(indx, false, getLocation(indxCtx));
            }

        } else if (ctx.FORCE_INDEX() != null) {

            for (KVQLParser.Index_nameContext indxCtx : ctx.index_name()) {
                String indexName = indxCtx.getText();
                IndexImpl indx = (IndexImpl) exprBaseTable.getTable().getIndex(indexName);

                if (indx == null) {
                    /* throw new QueryException("No index found: " +
                                         indexName,
                                         getLocation(ctx.FORCE_INDEX())); */
                    continue;
                }

                exprBaseTable.addIndexHint(indx, true, getLocation(indxCtx));
            }

        } else if (ctx.PREFER_PRIMARY_INDEX() != null) {

            exprBaseTable.addIndexHint(null, false, getLocation(ctx));

        } else if (ctx.FORCE_PRIMARY_INDEX() != null) {

            exprBaseTable.addIndexHint(null, true, getLocation(ctx));

        }
    }

    /*
     * orderby_clause : ORDER BY expr sort_spec (COMMA expr sort_spec)* ;
     *
     * sort_spec : (ASC | DESC)? (NULLS (FIRST | LAST))? ;
     */
    @Override
    public void enterOrderby_clause(KVQLParser.Orderby_clauseContext ctx) {
        /* Push a null sentinel in the stacks */
        theExprs.push(null);
        theSortSpecs.push(null);
    }

    @Override
    public void exitOrderby_clause(KVQLParser.Orderby_clauseContext ctx) {

        ArrayList<Expr> sortExprs = new ArrayList<Expr>();
        ArrayList<SortSpec> sortSpecs = new ArrayList<SortSpec>();

        Expr expr = theExprs.pop();
        SortSpec spec = theSortSpecs.pop();

        while (expr != null) {
            sortExprs.add(expr);
            sortSpecs.add(spec);
            expr = theExprs.pop();
            spec = theSortSpecs.pop();
        }

        Collections.reverse(sortExprs);
        Collections.reverse(sortSpecs);

        ExprSFW sfwExpr = (ExprSFW) theExprs.peek();

        sfwExpr.addSortClause(sortExprs, sortSpecs);
    }

    @Override
    public void enterSort_spec(KVQLParser.Sort_specContext ctx) {

        boolean desc = false;
        boolean nullsFirst = false;

        if (ctx.DESC() != null) {
            desc = true;
        }

        if (ctx.NULLS() == null) {
            nullsFirst = desc;
        } else if (ctx.FIRST() != null) {
            nullsFirst = true;
        }

        theSortSpecs.push(new SortSpec(desc, nullsFirst));
    }

    /*
     * limit_clause : LIMIT add_expr
     */
    @Override
    public void exitLimit_clause(KVQLParser.Limit_clauseContext ctx) {

        Expr limitExpr = null;

        limitExpr = theExprs.pop();

        ExprSFW sfwExpr = (ExprSFW) theExprs.peek();

        sfwExpr.addLimit(limitExpr);
    }

    /*
     * offset_clause : OFFSET add_expr
     */
    @Override
    public void exitOffset_clause(KVQLParser.Offset_clauseContext ctx) {

        Expr offsetExpr = null;

        offsetExpr = theExprs.pop();

        ExprSFW sfwExpr = (ExprSFW) theExprs.peek();

        sfwExpr.addOffset(offsetExpr);
    }

    /*
     * case_expr : CASE WHEN expr THEN expr (WHEN expr THEN expr)* ELSE expr END;
     */
    @Override
    public void enterCase_expr(KVQLParser.Case_exprContext ctx) {

        theExprs.push(null);
    }

    @Override
    public void exitCase_expr(KVQLParser.Case_exprContext ctx) {

        Location loc = getLocation(ctx);

        ArrayList<Expr> exprs = new ArrayList<Expr>();

        if (ctx.ELSE() != null) {
            exprs.add(theExprs.pop());
        }

        Expr expr = theExprs.pop();

        while (expr != null) {
            exprs.add(expr);
            expr = theExprs.pop();
        }

        Collections.reverse(exprs);

        Expr caseExpr = new ExprCase(theQCB, theInitSctx, loc, exprs);
        theExprs.push(caseExpr);
    }

    /*
     * or_expr : and_expr | or_expr OR and_expr ;
     */
    @Override
    public void exitOr_expr(KVQLParser.Or_exprContext ctx) {

        if (ctx.OR() == null) {
            return;
        }

        Location loc = getLocation(ctx);
        Expr op2 = theExprs.pop();
        Expr op1 = theExprs.pop();

        ArrayList<Expr> args = new ArrayList<Expr>(2);

        if (op1.getKind() == ExprKind.FUNC_CALL) {

            ExprFuncCall fnCall = (ExprFuncCall) op1;

            if (fnCall.getFunction().getCode() == FuncCode.OP_OR) {
                flattenAndOrArgs(fnCall.getChildren(), args);
            } else {
                args.add(op1);
            }
        } else {
            args.add(op1);
        }

        if (op2.getKind() == ExprKind.FUNC_CALL) {

            ExprFuncCall fnCall = (ExprFuncCall) op2;

            if (fnCall.getFunction().getCode() == FuncCode.OP_OR) {
                flattenAndOrArgs(fnCall.getChildren(), args);
            } else {
                args.add(op2);
            }
        } else {
            args.add(op2);
        }

        Expr expr = ExprFuncCall.create(theQCB, theInitSctx, loc, theFuncLib.getFunc(FuncCode.OP_OR), args);
        theExprs.push(expr);
    }

    /*
     * and_expr : not_expr | and_expr AND not_expr ;
     */
    @Override
    public void exitAnd_expr(KVQLParser.And_exprContext ctx) {

        if (ctx.AND() == null) {
            return;
        }

        Location loc = getLocation(ctx);
        Expr op2 = theExprs.pop();
        Expr op1 = theExprs.pop();

        ArrayList<Expr> args = new ArrayList<Expr>(2);

        if (op1.getKind() == ExprKind.FUNC_CALL) {

            ExprFuncCall fnCall = (ExprFuncCall) op1;

            if (fnCall.getFunction().getCode() == FuncCode.OP_AND) {
                flattenAndOrArgs(fnCall.getChildren(), args);
            } else {
                args.add(op1);
            }
        } else {
            args.add(op1);
        }

        if (op2.getKind() == ExprKind.FUNC_CALL) {

            ExprFuncCall fnCall = (ExprFuncCall) op2;

            if (fnCall.getFunction().getCode() == FuncCode.OP_AND) {
                flattenAndOrArgs(fnCall.getChildren(), args);
            } else {
                args.add(op2);
            }
        } else {
            args.add(op2);
        }

        Expr expr = ExprFuncCall.create(theQCB, theInitSctx, loc, theFuncLib.getFunc(FuncCode.OP_AND), args);
        theExprs.push(expr);
    }

    private void flattenAndOrArgs(ExprIter children, List<Expr> args) {

        while (children.hasNext()) {
            Expr arg = children.next();
            children.remove(false/*destroy*/);
            args.add(arg);
        }
    }

    /*
     * not_expr : NOT ? cond_expr
     *
     * cond_expr := comp_expr | exists_expr | is_of_type_expr;
     */
    @Override
    public void exitNot_expr(KVQLParser.Not_exprContext ctx) {

        if (ctx.NOT() == null) {
            return;
        }

        Location loc = getLocation(ctx);
        Expr input = theExprs.pop();

        ArrayList<Expr> args = new ArrayList<Expr>(1);
        args.add(input);

        Expr expr = ExprFuncCall.create(theQCB, theInitSctx, loc, theFuncLib.getFunc(FuncCode.OP_NOT), args);
        theExprs.push(expr);
    }

    /*
     * exists_expr : EXISTS add_expr ;
     */
    @Override
    public void exitExists_expr(KVQLParser.Exists_exprContext ctx) {

        Location loc = getLocation(ctx);

        Function func = theFuncLib.getFunc(FuncCode.OP_EXISTS);

        Expr input = theExprs.pop();

        ArrayList<Expr> args = new ArrayList<Expr>(1);
        args.add(input);

        Expr expr = ExprFuncCall.create(theQCB, theInitSctx, loc, func, args);
        theExprs.push(expr);
    }

    /*
     * comp_expr : add_expr ((comp_op | any_op) add_expr)? ;
     *
     * comp_op : EQ | NEQ | GT | GTE | LT | LTE ;
     *
     * any_op : EQ_ANY | NEQ_ANY | GT_ANY | GTE_ANY | LT_ANY | LTE_ANY;
     */
    @Override
    public void exitComp_expr(KVQLParser.Comp_exprContext ctx) {

        Location loc = getLocation(ctx);
        KVQLParser.Comp_opContext cmpctx = ctx.comp_op();
        KVQLParser.Any_opContext anyctx = ctx.any_op();

        if (cmpctx == null && anyctx == null) {
            return;
        }

        Expr op2 = theExprs.pop();
        Expr op1 = theExprs.pop();
        Function func;

        if (cmpctx != null) {
            if (cmpctx.EQ() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_EQ);
            } else if (cmpctx.NEQ() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_NEQ);
            } else if (cmpctx.GT() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_GT);
            } else if (cmpctx.GTE() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_GE);
            } else if (cmpctx.LT() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_LT);
            } else if (cmpctx.LTE() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_LE);
            } else {
                throw new QueryException("Unexpected comparison operator: " + cmpctx.getText(), loc);
            }
        } else {
            if (anyctx.EQ_ANY() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_EQ_ANY);
            } else if (anyctx.NEQ_ANY() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_NEQ_ANY);
            } else if (anyctx.GT_ANY() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_GT_ANY);
            } else if (anyctx.GTE_ANY() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_GE_ANY);
            } else if (anyctx.LT_ANY() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_LT_ANY);
            } else if (anyctx.LTE_ANY() != null) {
                func = theFuncLib.getFunc(FuncCode.OP_LE_ANY);
            } else {
                throw new QueryException("Unexpected comparison operator: " + anyctx.getText(), loc);
            }
        }

        ArrayList<Expr> args = new ArrayList<Expr>(2);
        args.add(op1);
        args.add(op2);

        Expr expr = ExprFuncCall.create(theQCB, theInitSctx, loc, func, args);
        theExprs.push(expr);
    }

    /*
     * quantified_type_def : type_def ( STAR | PLUS | QUESTION_MARK)? ;
     */
    @Override
    public void enterQuantified_type_def(KVQLParser.Quantified_type_defContext ctx) {
        if (ctx.PLUS() != null) {
            theQuantifiers.push(Quantifier.PLUS);
        } else if (ctx.STAR() != null) {
            theQuantifiers.push(Quantifier.STAR);
        } else if (ctx.QUESTION_MARK() != null) {
            theQuantifiers.push(Quantifier.QSTN);
        } else {
            theQuantifiers.push(Quantifier.ONE);
        }
    }

    /*
     * is_of_type_expr :
     *    add_expr IS NOT? OF TYPE?
     *    LP ONLY? quantified_type_def ( COMMA ONLY? quantified_type_def )* RP;
     */
    @Override
    public void exitIs_of_type_expr(KVQLParser.Is_of_type_exprContext ctx) {

        Location loc = getLocation(ctx);
        Expr input = theExprs.pop();
        boolean notFlag = ctx.NOT() != null;

        int numTypes = ctx.quantified_type_def().size();

        List<FieldDef> types = new ArrayList<FieldDef>(numTypes);
        List<Quantifier> quantifiers = new ArrayList<Quantifier>(numTypes);
        List<Boolean> onlyFlags = new ArrayList<Boolean>(numTypes);

        for (int i = 0; i < numTypes; i++) {
            FieldDefImpl typeDef = theTypes.pop();
            Quantifier typeQuantifier = theQuantifiers.pop();

            types.add(typeDef);
            quantifiers.add(typeQuantifier);
            onlyFlags.add(ctx.ONLY(i) != null);
        }

        ExprIsOfType expr = new ExprIsOfType(theQCB, theInitSctx, loc, input, notFlag, types, quantifiers,
                onlyFlags);
        theExprs.push(expr);
    }

    /*
     * cast_expr : CAST LP expr AS quantified_type_def RP ;
     */
    @Override
    public void exitCast_expr(KVQLParser.Cast_exprContext ctx) {
        Expr input = theExprs.pop();

        FieldDefImpl typeDefImpl = theTypes.pop();
        Quantifier typeQuantifier = theQuantifiers.pop();

        Expr expr = ExprCast.create(theQCB, theInitSctx, getLocation(ctx), input, typeDefImpl, typeQuantifier);

        theExprs.push(expr);
    }

    /*
     * add_expr : multiply_expr ((PLUS | MINUS) multiply_expr)* ;
     */
    @Override
    public void exitAdd_expr(KVQLParser.Add_exprContext ctx) {

        if (ctx.PLUS().isEmpty() && ctx.MINUS().isEmpty()) {
            return;
        }

        Location loc = getLocation(ctx);

        Function func = theFuncLib.getFunc(FuncCode.OP_ADD_SUB);

        /*
         * operations stores, as a "+" or "-" char, each operator that appears
         * in the additive expr processed. The ops are stored in the order of
         * their appearance in the expr. This state will get saved in a
         * ConstExpr, which is added as an arg to the ExprFuncCall created here,
         * and will eventually be passed to the ArithOpIter.
         */
        String operations = "";
        /*
         * args contain the operands in reverse query order because this is how
         * they are on the stack. After the for-loop is done, the order will be
         * reversed to follow the order of the specified ops in the query. The
         * last arg contains the ConstExpr holding the operations string.
         */
        ArrayList<Expr> args = new ArrayList<Expr>(ctx.getChildCount() / 2 + 2);

        for (int c = ctx.getChildCount() - 1; c >= 0; c = c - 2) {

            ParseTree pt = ctx.getChild(c);

            if (pt instanceof RuleNode) {

                Expr op = theExprs.pop();

                if (c > 0) {
                    ParseTree ptOp = ctx.getChild(c - 1);

                    if (ptOp instanceof TerminalNode) {
                        int tokenId = ((TerminalNode) ptOp).getSymbol().getType();

                        if (tokenId == KVQLParser.PLUS) {
                            operations = "+" + operations;
                            args.add(op);
                        } else if (tokenId == KVQLParser.MINUS) {
                            operations = "-" + operations;
                            args.add(op);
                        } else {
                            throw new QueryStateException("Unexpected arithmetic operator in: " + ctx.getText());
                        }
                    } else {
                        throw new QueryStateException("Unexpected arithmetic parse tree in: " + ctx.getText());
                    }
                } else {
                    // This is the 0 + operation.
                    operations = "+" + operations;
                    args.add(op);
                }

            } else {
                throw new QueryStateException("Unexpected arithmetic parse tree in: " + ctx.getText());
            }
        }

        /* reverse order of arguments */
        Collections.reverse(args);

        FieldValueImpl ops = FieldDefImpl.stringDef.createString(operations);

        args.add(new ExprConst(theQCB, theInitSctx, loc, ops));

        Expr expr = ExprFuncCall.create(theQCB, theInitSctx, loc, func, args);
        theExprs.push(expr);
    }

    /*
     * multiply_expr : unary_expr ((STAR | DIV) unary_expr)* ;
     */
    @Override
    public void exitMultiply_expr(KVQLParser.Multiply_exprContext ctx) {

        if (ctx.STAR().isEmpty() && ctx.DIV().isEmpty()) {
            return;
        }

        Location loc = getLocation(ctx);

        Function func = theFuncLib.getFunc(FuncCode.OP_MULT_DIV);

        /*
         * operations stores, as a "*" or "/" char, each operator that appears
         * in the additive expr processed. The ops are stored in the order of
         * their appearance in the expr. This state will get saved in a
         * ConstExpr, which is added as an arg to the ExprFuncCall created here,
         * and will eventually be passed to the ArithOpIter.
         */
        String operations = "";
        /*
         * args contain the operands in reverse query order because this is how
         * they are on the stack. After the for-loop is done, the order will be
         * reversed to follow the order of the specified ops in the query. The
         * last arg contains the ConstExpr holding the operations string.
         */
        ArrayList<Expr> args = new ArrayList<Expr>(ctx.getChildCount() / 2 + 2);

        for (int c = ctx.getChildCount() - 1; c >= 0; c = c - 2) {

            ParseTree pt = ctx.getChild(c);

            if (pt instanceof RuleNode) {

                Expr op = theExprs.pop();

                if (c > 0) {
                    ParseTree ptOp = ctx.getChild(c - 1);

                    if (ptOp instanceof TerminalNode) {
                        int tokenId = ((TerminalNode) ptOp).getSymbol().getType();

                        if (tokenId == KVQLParser.STAR) {
                            operations = "*" + operations;
                            args.add(op);
                        } else if (tokenId == KVQLParser.DIV) {
                            operations = "/" + operations;
                            args.add(op);
                        } else {
                            throw new QueryStateException("Unexpected arithmetic operator in: " + ctx.getText());
                        }
                    } else {
                        throw new QueryStateException("Unexpected arithmetic parse tree in: " + ctx.getText());
                    }
                } else {
                    // This is the 1 * operation.
                    operations = "*" + operations;
                    args.add(op);
                }

            } else {
                throw new QueryStateException("Unexpected arithmetic parse tree in: " + ctx.getText());
            }
        }

        /* reverse order of arguments to be in the query order */
        Collections.reverse(args);

        FieldValueImpl ops = FieldDefImpl.stringDef.createString(operations);

        args.add(new ExprConst(theQCB, theInitSctx, loc, ops));

        Expr expr = ExprFuncCall.create(theQCB, theInitSctx, loc, func, args);
        theExprs.push(expr);
    }

    /*
     * unary_expr : path_expr |
     *    (PLUS | MINUS) unary_expr ;
     */
    @Override
    public void exitUnary_expr(KVQLParser.Unary_exprContext ctx) {

        if (ctx.MINUS() == null) {
            return;
        }

        Location loc = getLocation(ctx);

        Function func = theFuncLib.getFunc(FuncCode.OP_ARITH_UNARY);

        Expr op = theExprs.pop();

        if (op.getKind() == ExprKind.CONST) {

            FieldValueImpl val = ((ExprConst) op).getValue();

            if (!val.isNull()) {
                FieldValueImpl negVal = ArithUnaryOpIter.getNegativeOfValue(val, getLocation(ctx));

                Expr expr = new ExprConst(theQCB, theInitSctx, loc, negVal);
                theExprs.push(expr);
                return;
            }
        }

        ArrayList<Expr> args = new ArrayList<Expr>(1);
        args.add(op);

        Expr expr = ExprFuncCall.create(theQCB, theInitSctx, loc, func, args);
        theExprs.push(expr);
    }

    /*
     * path_expr : primary_expr (map_step | array_step)* ;
     *
     * map_step : DOT ( map_filter_step | map_field_step );
     *
     * array_step : array_filter_step | array_slice_step;
     */

    /*
     * map_field_step :
     *     ( id | string | var_ref | parenthesized_expr | func_call );
     */
    @Override
    public void enterMap_field_step(KVQLParser.Map_field_stepContext ctx) {

        Location loc = getLocation(ctx);

        Expr inputExpr = theExprs.pop();

        ExprVar ctxItemVar = null;

        ExprFieldStep step = new ExprFieldStep(theQCB, theInitSctx, loc, inputExpr);

        if (ctx.id() == null && ctx.string() == null) {

            ctxItemVar = new ExprVar(theQCB, theInitSctx, loc, ExprVar.theCtxVarName, step);
            step.addCtxVars(ctxItemVar);

            pushScope();
            theSctx.addVariable(ctxItemVar);
        }

        theExprs.push(step);
    }

    @Override
    public void exitMap_field_step(KVQLParser.Map_field_stepContext ctx) {

        String fieldName = null;
        Expr fieldNameExpr = null;

        if (ctx.id() != null) {
            fieldName = ctx.id().getText();
        } else if (ctx.string() != null) {
            fieldName = stripFirstLast(ctx.string().getText());
        } else {
            fieldNameExpr = theExprs.pop();
        }

        ExprFieldStep step = (ExprFieldStep) theExprs.peek();

        step.addFieldNameExpr(fieldName, fieldNameExpr);

        if (ctx.id() == null && ctx.string() == null) {
            popScope();
        }
    }

    /*
     * map_filter_step : (KEYS | VALUES) LP expr? RP ;
     */
    @Override
    public void enterMap_filter_step(KVQLParser.Map_filter_stepContext ctx) {

        Location loc = getLocation(ctx);

        FilterKind kind = (ctx.KEYS() != null ? FilterKind.KEYS : FilterKind.VALUES);

        Expr inputExpr = theExprs.pop();

        ExprMapFilter step = new ExprMapFilter(theQCB, theInitSctx, loc, kind, inputExpr);

        if (ctx.expr() != null) {
            ExprVar ctxItemVar = new ExprVar(theQCB, theInitSctx, loc, ExprVar.theCtxVarName, step);

            ExprVar ctxElemVar = new ExprVar(theQCB, theInitSctx, loc, ExprVar.theValueVarName, step);

            ExprVar ctxKeyVar = new ExprVar(theQCB, theInitSctx, loc, ExprVar.theKeyVarName, step);

            step.addCtxVars(ctxItemVar, ctxElemVar, ctxKeyVar);

            pushScope();

            theSctx.addVariable(ctxItemVar);
            theSctx.addVariable(ctxElemVar);
            theSctx.addVariable(ctxKeyVar);
        }

        theExprs.push(step);
    }

    @Override
    public void exitMap_filter_step(KVQLParser.Map_filter_stepContext ctx) {

        ExprMapFilter filterStep;
        Expr predExpr = null;

        if (ctx.expr() != null) {
            predExpr = theExprs.pop();
            filterStep = (ExprMapFilter) theExprs.peek();
            filterStep.addPredExpr(predExpr);
            popScope();
        }
    }

    /*
     * array_slice_step : LBRACK expr? COLON expr? RBRACK ;
     */
    @Override
    public void enterArray_slice_step(KVQLParser.Array_slice_stepContext ctx) {

        Location loc = getLocation(ctx);

        Expr inputExpr = theExprs.pop();

        ExprVar ctxItemVar = null;

        ExprArraySlice step = new ExprArraySlice(theQCB, theInitSctx, loc, inputExpr);

        assert (ctx.COLON() != null);
        ctxItemVar = new ExprVar(theQCB, theInitSctx, loc, ExprVar.theCtxVarName, step);
        step.addCtxVars(ctxItemVar);

        pushScope();
        theSctx.addVariable(ctxItemVar);

        theExprs.push(step);
    }

    @Override
    public void exitArray_slice_step(KVQLParser.Array_slice_stepContext ctx) {

        Expr lowExpr = null;
        Expr highExpr = null;

        List<KVQLParser.ExprContext> args = ctx.expr();

        if (args.size() == 2) {
            highExpr = theExprs.pop();
            lowExpr = theExprs.pop();

        } else if (args.size() == 1) {

            if (ctx.getChild(1) instanceof KVQLParser.ExprContext) {
                lowExpr = theExprs.pop();
            } else {
                highExpr = theExprs.pop();
            }
        }

        ExprArraySlice step = (ExprArraySlice) theExprs.peek();

        step.addBoundaryExprs(lowExpr, highExpr);

        popScope();
    }

    /*
     * arrayt_filter_step : LBRACK expr RBRACK ;
     */
    @Override
    public void enterArray_filter_step(KVQLParser.Array_filter_stepContext ctx) {

        Location loc = getLocation(ctx);

        Expr inputExpr = theExprs.pop();

        ExprArrayFilter step = new ExprArrayFilter(theQCB, theInitSctx, loc, inputExpr);

        if (ctx.expr() != null) {
            ExprVar ctxItemVar = new ExprVar(theQCB, theInitSctx, loc, ExprVar.theCtxVarName, step);

            ExprVar ctxElemVar = new ExprVar(theQCB, theInitSctx, loc, ExprVar.theElementVarName, step);

            ExprVar ctxElemPosVar = new ExprVar(theQCB, theInitSctx, loc, ExprVar.theElementPosVarName, step);

            step.addCtxVars(ctxItemVar, ctxElemVar, ctxElemPosVar);

            pushScope();

            theSctx.addVariable(ctxItemVar);
            theSctx.addVariable(ctxElemVar);
            theSctx.addVariable(ctxElemPosVar);
        }

        theExprs.push(step);
    }

    @Override
    public void exitArray_filter_step(KVQLParser.Array_filter_stepContext ctx) {

        ExprArrayFilter filterStep;
        Expr predExpr = null;

        if (ctx.expr() != null) {
            predExpr = theExprs.pop();
            filterStep = (ExprArrayFilter) theExprs.pop();
            filterStep.addPredExpr(predExpr);
            popScope();
        } else {
            filterStep = (ExprArrayFilter) theExprs.pop();
        }

        /* Convert to slice step, if possible */
        Expr expr = filterStep.convertToSliceStep();
        theExprs.push(expr);
    }

    /*
     * primary_expr : const_expr |
     *                column_ref |
     *                var_ref |
     *                array_constructor |
     *                map_constructor |
     *                func_call |
     *                case_expr |
     *                parenthesized_expr ;
     */

    /*
     * array_constructor : LBRACK expr? (COMMA expr)* RBRACK ;
     */
    @Override
    public void enterArray_constructor(KVQLParser.Array_constructorContext ctx) {

        theExprs.push(null);
    }

    @Override
    public void exitArray_constructor(KVQLParser.Array_constructorContext ctx) {

        Location loc = getLocation(ctx);

        ArrayList<Expr> inputs = new ArrayList<Expr>();

        Expr input = theExprs.pop();
        while (input != null) {
            inputs.add(input);
            input = theExprs.pop();
        }

        Collections.reverse(inputs);

        Expr arrayConstr = new ExprArrayConstr(theQCB, theInitSctx, loc, inputs, false/*conditional*/);

        theExprs.push(arrayConstr);
    }

    /*
     * map_constructor :
     *     (LBRACE expr COLON expr (COMMA expr COLON expr)* RBRACE) |
     *     (LBRACE RBRACE) ;
     */
    @Override
    public void enterMap_constructor(KVQLParser.Map_constructorContext ctx) {

        theExprs.push(null);
    }

    @Override
    public void exitMap_constructor(KVQLParser.Map_constructorContext ctx) {

        Location loc = getLocation(ctx);

        ArrayList<Expr> inputs = new ArrayList<Expr>();

        Expr input = theExprs.pop();
        while (input != null) {
            inputs.add(input);
            input = theExprs.pop();
        }

        Collections.reverse(inputs);

        Expr mapConstr = new ExprMapConstr(theQCB, theInitSctx, loc, inputs);

        theExprs.push(mapConstr);
    }

    /*
     * func_call : id LP (expr (COMMA expr)*)? RP ;
     */
    @Override
    public void enterFunc_call(KVQLParser.Func_callContext ctx) {

        theExprs.push(null);
    }

    @Override
    public void exitFunc_call(KVQLParser.Func_callContext ctx) {

        Location loc = getLocation(ctx);

        ArrayList<Expr> inputs = new ArrayList<Expr>();

        Expr input = theExprs.pop();

        while (input != null) {
            inputs.add(input);
            input = theExprs.pop();
        }

        Collections.reverse(inputs);

        Function func = theSctx.findFunction(ctx.id().getText(), inputs.size());

        if (func == null) {
            throw new QueryException(
                    "Could not find function with name " + ctx.id().getText() + " and arity " + inputs.size(),
                    getLocation(ctx.id()));
        }

        Expr expr = ExprFuncCall.create(theQCB, theInitSctx, loc, func, inputs);
        theExprs.push(expr);
    }

    /*
     * var_ref : (DOLLAR DOLLAR? id) | (DOLLAR DOLLAR) ;
     */
    @Override
    public void exitVar_ref(KVQLParser.Var_refContext ctx) {

        String varName = ctx.getText();
        ExprVar varExpr = theScopes.peek().findVariable(varName);

        if (varExpr == null) {
            throw new QueryException(" Unknown variable " + varName, getLocation(ctx));
        }

        theExprs.push(varExpr);
    }

    /*
     * column_ref : id (DOT id)? ;
     *
     * If there are 2 ids, the first one refers to a table name/alias and the
     * second to a column in that table. A single id refers to a column in some
     * of the table in the FROM clause. If more than one table has a column of
     * that name, an error is thrown. In this case, the user has to rewrite the
     * query to use table aliases to resolve the ambiguity.
     */
    @Override
    public void exitColumn_ref(KVQLParser.Column_refContext ctx) {

        Location loc = getLocation(ctx);

        List<KVQLParser.IdContext> ids = ctx.id();

        String tableName;
        String varName;
        KVQLParser.IdContext col;

        if (ids.size() == 1) {
            col = ids.get(0);
            tableName = theTable.getFullName();
        } else {
            assert (ids.size() == 2);
            col = ids.get(1);
            tableName = ids.get(0).getText();

            if (!tableName.equalsIgnoreCase(theTable.getFullName())
                    && (theTableAlias == null || !tableName.equals(theTableAlias))) {

                throw new QueryException("Unknown table: " + tableName, getLocation(ids.get(0)));
            }

            tableName = theTable.getFullName();
        }

        if (theTable.getField(col.getText()) == null) {
            throw new QueryException("Table: " + tableName + " has no column named " + col.getText(),
                    getLocation(col));
        }

        if (theTableAlias == null) {
            varName = ("$$" + tableName);
        } else if (theTableAlias.charAt(0) == '$') {
            varName = theTableAlias;
        } else {
            varName = ("$$" + theTableAlias);
        }

        ExprVar varExpr = theScopes.peek().findVariable(varName);

        ExprFieldStep expr = new ExprFieldStep(theQCB, theInitSctx, loc, varExpr, col.getText());

        theExprs.push(expr);
    }

    /*
     * const_expr : INT | FLOAT | string | TRUE | FALSE | NULL;
     */
    @Override
    public void exitConst_expr(KVQLParser.Const_exprContext ctx) {

        Location loc = getLocation(ctx);

        FieldValue value;

        try {
            if (ctx.INT() != null) {
                Long val = Long.parseLong(ctx.INT().getText());

                if (Integer.MIN_VALUE <= val.longValue() && val.longValue() <= Integer.MAX_VALUE) {
                    value = FieldDefImpl.integerDef.createInteger(val.intValue());
                } else {
                    value = FieldDefImpl.longDef.createLong(val);
                }
            } else if (ctx.FLOAT() != null) {
                Double val = Double.parseDouble(ctx.FLOAT().getText());
                value = FieldDefImpl.doubleDef.createDouble(val);
            } else if (ctx.TRUE() != null) {
                Boolean val = Boolean.parseBoolean(ctx.TRUE().getText());
                value = FieldDefImpl.booleanDef.createBoolean(val);
            } else if (ctx.FALSE() != null) {
                Boolean val = Boolean.parseBoolean(ctx.FALSE().getText());
                value = FieldDefImpl.booleanDef.createBoolean(val);
            } else if (ctx.NULL() != null) {
                value = NullJsonValueImpl.getInstance();
            } else {
                String val = stripFirstLast(ctx.string().getText());
                value = FieldDefImpl.stringDef.createString(val);
            }
        } catch (NumberFormatException nfe) {
            throw new QueryException("Invalid numeric literal: " + ctx.getText(), loc);
        }

        ExprConst constExpr = new ExprConst(theQCB, theInitSctx, loc, (FieldValueImpl) value);

        theExprs.push(constExpr);
    }

    /*
     * This is a helper class used during the translation of field definitions.
     * It serves as a temporary place holder for the properties of the field
     * (its data type, nullability, default value, and associated comment).
     */
    private static class FieldDefHelper {

        final String name;

        final String comment;

        final QueryException.Location location;

        FieldDefImpl type = null;

        FieldValueImpl defaultValue = null;

        boolean nullable = true;

        FieldDefHelper(String name, String comment, QueryException.Location location) {
            this.name = name;
            this.comment = comment;
            this.location = location;
        }

        String getName() {
            return name;
        }

        void setType(FieldDefImpl t) {
            type = t;
        }

        FieldDefImpl getType() {
            return type;
        }

        void setNullable(boolean v) {
            nullable = v;
        }

        boolean getNullable() {
            return nullable;
        }

        void setDefault(String strval, KVQLParser.Default_valueContext ctx) {

            if (type == null) {
                throw new QueryStateException("Type must be set before " + "setting a default value.");
            }

            if (ctx.string() != null) {
                if (!type.isString() && !type.isTimestamp()) {
                    throw new QueryException("Quoted default value for a non-string field. " + "Field = " + name
                            + " Value = " + strval, getLocation(ctx));
                }

                strval = stripFirstLast(strval);
            }

            if (ctx.number() != null) {

                if (ctx.number().INT() != null) {
                    if (!type.isInteger() && !type.isLong() && !type.isFloat() && !type.isNumber()
                            && !type.isDouble() && !type.isTimestamp()) {
                        throw new QueryException("Integer default value for a non-numeric field. " + "Field = "
                                + name + " Value = " + strval, getLocation(ctx));
                    }
                }

                if (ctx.number().FLOAT() != null) {
                    if (!type.isFloat() && !type.isDouble() && !type.isNumber()) {
                        throw new QueryException("Float default value for a non-float field. " + "Field = " + name
                                + " Value = " + strval, getLocation(ctx));
                    }
                }
            }

            if (ctx.id() != null) {
                if (!type.isEnum()) {
                    throw new QueryException(
                            "id as default value for a non-enum field. " + "Field = " + name + " Value = " + strval,
                            getLocation(ctx));
                }
            }

            if (ctx.TRUE() != null || ctx.FALSE() != null) {
                if (!type.isBoolean()) {
                    throw new QueryException("Boolean default value for a non-boolean field. " + "Field = " + name
                            + " Value = " + strval, getLocation(ctx));
                }
            }

            try {
                switch (type.getType()) {
                case INTEGER:
                    defaultValue = (FieldValueImpl) type.createInteger(Integer.parseInt(strval));
                    break;
                case LONG:
                    defaultValue = (FieldValueImpl) type.createLong(Long.parseLong(strval));
                    break;
                case FLOAT:
                    defaultValue = (FieldValueImpl) type.createFloat(Float.parseFloat(strval));
                    break;
                case DOUBLE:
                    defaultValue = (FieldValueImpl) type.createDouble(Double.parseDouble(strval));
                    break;
                case NUMBER:
                    defaultValue = (FieldValueImpl) type.createNumber(new BigDecimal(strval));
                    break;
                case STRING:
                    defaultValue = (FieldValueImpl) type.createString(strval);
                    break;
                case ENUM:
                    defaultValue = type.createEnum(strval);
                    break;
                case BOOLEAN:
                    defaultValue = (FieldValueImpl) type.createBoolean(Boolean.parseBoolean(strval));
                    break;
                case TIMESTAMP:
                    if (ctx.string() != null) {
                        defaultValue = (FieldValueImpl) type.asTimestamp().fromString(strval);
                    } else {
                        assert (ctx.number().INT() != null);
                        defaultValue = ((TimestampDefImpl) type)
                                .createTimestamp(new Timestamp(Long.parseLong(strval)));
                    }

                    break;
                default:
                    throw new QueryException("Unexpected type for default value. Field = " + name + " Type = "
                            + type + " Value = " + strval, getLocation(ctx));
                }
            } catch (IllegalArgumentException iae) {
                throw new QueryException(iae.getMessage(), getLocation(ctx));
            }
        }

        FieldValueImpl getDefault() {
            return defaultValue;
        }

        /*
         * This method is called at the end of the parsing of a field
         * definition, just before the field definition is added to its
         * containing record or table definition.
         */
        void validate() {

            if (defaultValue == null && !nullable) {
                throw new QueryException("Non-nullable field without a default value. " + " Field = " + name,
                        location);
            }

            type.setDescription(comment);
        }
    }

    /*
     * type_def :
     *     binary_def         # Binary
     *   | array_def          # Array
     *   | boolean_def        # Boolean
     *   | enum_def           # Enum
     *   | float_def          # Float
     *   | integer_def        # Int
     *   | json_def           # JSON
     *   | map_def            # Map
     *   | record_def         # Record
     *   | string_def         # StringT
     *   | ANY_T              # Any
     *   | ANY_ATOMIC_T ;     # Any_atomic
     */

    /*
     * any_def : ANY_T ;
     */
    @Override
    public void enterAny(KVQLParser.AnyContext ctx) {
        if (theInDDL) {
            throw new QueryException("Type Any not allowed in DDL statements.", getLocation(ctx));
        }

        theTypes.push(FieldDefFactory.createAnyDef());
    }

    /*
     * anyAtomic_def : ANYATOMIC_T;
     */
    @Override
    public void enterAnyAtomic(KVQLParser.AnyAtomicContext ctx) {
        if (theInDDL) {
            throw new QueryException("Type AnyAtomic not allowed in DDL " + "statements.", getLocation(ctx));
        }

        theTypes.push(FieldDefFactory.createAnyAtomicDef());
    }

    /*
     * anyJsonAtomic_def : ANYJSONATOMIC_T;
     */
    @Override
    public void enterAnyJsonAtomic(KVQLParser.AnyJsonAtomicContext ctx) {
        if (theInDDL) {
            throw new QueryException("Type AnyJsonAtomic not allowed in DDL" + " statements.", getLocation(ctx));
        }

        theTypes.push(FieldDefFactory.createAnyJsonAtomicDef());
    }

    /*
     * json_def : JSON_T
     */
    @Override
    public void enterJSON(KVQLParser.JSONContext ctx) {

        theTypes.push(FieldDefFactory.createJsonDef());
    }

    /*
     * anyRecord_def : ANYRECORD_T;
     */
    @Override
    public void enterAnyRecord(KVQLParser.AnyRecordContext ctx) {
        if (theInDDL) {
            throw new QueryException("Type AnyRecord not allowed in DDL" + " statements.", getLocation(ctx));
        }

        theTypes.push(FieldDefFactory.createAnyRecordDef());
    }

    /*
     * record_def : RECORD_T LP field_def (COMMA field_def)* RP
     */
    @Override
    public void enterRecord(KVQLParser.RecordContext ctx) {

        /*
         * Push a null as a sentinel for the unknown number of field
         * definitions that will follow.
         */
        theFields.push(null);
    }

    @Override
    public void exitRecord(KVQLParser.RecordContext ctx) {

        FieldMap fieldMap = new FieldMap();

        FieldDefHelper field = theFields.pop();
        assert (field != null);

        while (field != null) {

            /* Records, enums, and fixed binaries require a name in Avro */
            setNameForNamedType(field.getName(), field.getType());

            field.validate();

            /* fieldMap.put() checks for duplicate fields */
            fieldMap.put(field.getName(), field.getType(), field.getNullable(), field.getDefault());

            field = theFields.pop();
        }

        fieldMap.reverseFieldOrder();

        RecordDefImpl type = FieldDefFactory.createRecordDef(fieldMap, null/*description*/);

        theTypes.push(type);
    }

    /*
     * field_def : id type_def default_def? comment?
     *
     * default_def : (default_value not_null?) | (not_null? default_value) ;
     *
     * comment : COMMENT string
     */
    @Override
    public void enterField_def(KVQLParser.Field_defContext ctx) {

        String name = ctx.id().getText();

        String comment = null;
        if (ctx.comment() != null) {
            comment = stripFirstLast(ctx.comment().string().getText());
        }

        FieldDefHelper field = new FieldDefHelper(name, comment, getLocation(ctx));
        theFields.push(field);
    }

    @Override
    public void exitField_def(KVQLParser.Field_defContext ctx) {

        assert (!theFields.empty());
        assert (!theTypes.empty());

        FieldDefHelper field = theFields.peek();

        field.setType(theTypes.pop());

        assert (theTypes.empty());
    }

    /*
     * default_value : DEFAULT (number | string | BOOLEAN_VALUE | id)
     *
     * not_null : NOT_NULL ;
     */
    @Override
    public void enterDefault_value(KVQLParser.Default_valueContext ctx) {

        assert (!theFields.empty());
        assert (!theTypes.empty());

        FieldDefHelper fieldDefHelper = theFields.peek();
        FieldDefImpl type = theTypes.peek();
        fieldDefHelper.setType(type);

        String strval = ctx.getChild(1).getText();

        /* validate and set the default value for the current field */
        fieldDefHelper.setDefault(strval, ctx);
    }

    /*
     * not_null : NOT_NULL;
     */
    @Override
    public void enterNot_null(KVQLParser.Not_nullContext ctx) {

        assert (!theFields.empty());
        FieldDefHelper field = theFields.peek();
        field.setNullable(false);
    }

    /*
     * array_def : ARRAY_T LP type_def RP
     */
    @Override
    public void exitArray(KVQLParser.ArrayContext ctx) {

        FieldDefImpl elemType = theTypes.pop();

        /* Record, enum, and fixed binary types require a name in Avro */
        setNameForNamedType(null/*name*/, elemType);

        FieldDefImpl type = FieldDefFactory.createArrayDef(elemType);
        theTypes.push(type);
    }

    /*
     * map_def : MAP_T LP type_def RP
     */
    @Override
    public void exitMap(KVQLParser.MapContext ctx) {

        FieldDefImpl elemType = theTypes.pop();

        /* Records enum, and fixed binary types require a name in Avro */
        setNameForNamedType(null/*name*/, elemType);

        FieldDefImpl type = FieldDefFactory.createMapDef(elemType);
        theTypes.push(type);
    }

    /*
     * integer_def : (INTEGER_T | LONG_T)
     */
    @Override
    public void enterInt(KVQLParser.IntContext ctx) {

        boolean isLong = ctx.integer_def().LONG_T() != null;

        FieldDefImpl type = (isLong ? FieldDefFactory.createLongDef() : FieldDefFactory.createIntegerDef());
        theTypes.push(type);
    }

    /*
     * float_def : (FLOAT_T | DOUBLE_T | NUMBER_T)
     */
    @Override
    public void enterFloat(KVQLParser.FloatContext ctx) {

        boolean isDouble = ctx.float_def().DOUBLE_T() != null;
        boolean isNumber = ctx.float_def().NUMBER_T() != null;

        FieldDefImpl type = (isDouble ? FieldDefFactory.createDoubleDef()
                : (isNumber ? FieldDefFactory.createNumberDef() : FieldDefFactory.createFloatDef()));

        theTypes.push(type);
    }

    /*
     * string_def : STRING_T
     */
    @Override
    public void enterStringT(KVQLParser.StringTContext ctx) {

        FieldDefImpl type = FieldDefFactory.createStringDef();
        theTypes.push(type);
    }

    /*
     * enum_def : ENUM_T id_list_with_paren
     *
     * id_list_with_paren : LP id_list RP
     *
     * id_list : id (COMMA id)*
     */
    @Override
    public void enterEnum(KVQLParser.EnumContext ctx) {

        String[] values = makeIdArray(ctx.enum_def().id_list().id());

        try {
            FieldDefImpl type = FieldDefFactory.createEnumDef(values);
            theTypes.push(type);
        } catch (IllegalArgumentException iae) {
            String msg = "Invalid ENUM type '" + ctx.enum_def().getText() + "': " + iae.getMessage();
            throw new QueryException(msg, getLocation(ctx));
        }
    }

    /*
     * boolean_def : BOOLEAN_T
     */
    @Override
    public void enterBoolean(KVQLParser.BooleanContext ctx) {

        FieldDefImpl type = FieldDefFactory.createBooleanDef();
        theTypes.push(type);
    }

    /*
     * binary_def : BINARY_T (LP INT RP)?
     */
    @Override
    public void enterBinary(KVQLParser.BinaryContext ctx) {

        int size = 0;
        if (ctx.binary_def().INT() != null) {
            size = Integer.parseInt(ctx.binary_def().INT().getText());
        }

        FieldDefImpl type = (size == 0 ? FieldDefFactory.createBinaryDef()
                : FieldDefFactory.createFixedBinaryDef(size));
        theTypes.push(type);
    }

    /*
     * timestamp_def : TIMESTAMP_T (LP INT RP)?
     */
    @Override
    public void enterTimestamp(KVQLParser.TimestampContext ctx) {

        int precision = -1;

        if (ctx.timestamp_def().INT() != null) {

            precision = Integer.parseInt(ctx.timestamp_def().INT().getText());

            if (precision > TimestampDefImpl.MAX_PRECISION) {
                throw new QueryException("Timestamp precision exceeds the maximum allowed " + "precision ("
                        + TimestampDefImpl.MAX_PRECISION + ")");
            }

            if (precision < 0) {
                throw new QueryException("Timestamp precision cannot be a negative number");
            }
        }

        if (theInDDL && precision < 0) {
            throw new QueryException("In DDL statements there is no default precision for the "
                    + "Timestamp type. The precision must be explicitly specified.", getLocation(ctx));
        }

        if (precision < 0) {
            precision = TimestampDefImpl.DEF_PRECISION;
        }

        FieldDefImpl type = FieldDefFactory.createTimestampDef(precision);
        theTypes.push(type);
    }

    /*
     * ???? Can we use the name of the associated field (if any) as the type
     * name? What is the scope of the uniqueness requirement?
     */
    private void setNameForNamedType(String name, FieldDef type) {

        if (type.isRecord()) {
            ((RecordDefImpl) type).setName(name != null ? name : theQCB.generateFieldName(getNamePrefix("RECORD")));
        } else if (type.isEnum()) {
            ((EnumDefImpl) type).setName(name != null ? name : theQCB.generateFieldName(getNamePrefix("ENUM")));
        } else if (type.isFixedBinary()) {
            ((FixedBinaryDefImpl) type)
                    .setName(name != null ? name : theQCB.generateFieldName(getNamePrefix("FIXEDBINARY")));
        }
    }

    private String getNamePrefix(String name) {
        if (theTableBuilder instanceof TableEvolver) {
            return name + ((TableEvolver) theTableBuilder).getTableVersion();
        }
        return name;
    }

    /*
     * create_table_statement :
     *     CREATE TABLE (IF NOT EXISTS)?
     *     name_path comment? LP table_def RP ttl_def?;
     *
     * table_def : (field_def | key_def) (COMMA (field_def | key_def))* ;
     */
    @Override
    public void enterCreate_table_statement(KVQLParser.Create_table_statementContext ctx) {

        /* only get the last component of the table path */
        String name = getPathLeaf(ctx.table_name().name_path());

        TableImpl parentTable = getParentTable(ctx.table_name().name_path());

        KVQLParser.Table_defContext table_def = ctx.table_def();

        /* Validate the number of primary keys. */
        if (table_def.key_def() == null || table_def.key_def().isEmpty() || table_def.key_def().size() > 1) {
            throw new QueryException("Table definition must contain a single primary " + "key definition",
                    getLocation(table_def));
        }

        String comment = null;
        if (ctx.comment() != null) {
            comment = stripFirstLast(ctx.comment().string().getText());
        }

        /*
         * Push a null as a sentinel for the unknown number of field
         * definitions that will follow.
         */
        theFields.push(null);

        /*
         * The TableBuilder constructor adds the columns of the parent's key to
         * its local FieldMap and primaryKey members.
         */
        theTableBuilder = TableBuilder.createTableBuilder(name, comment, parentTable);
    }

    @Override
    public void exitCreate_table_statement(KVQLParser.Create_table_statementContext ctx) {

        assert (theTableBuilder != null);
        assert (!theFields.isEmpty());
        assert (theTypes.isEmpty());

        ArrayList<FieldDefHelper> fields = new ArrayList<FieldDefHelper>();

        FieldDefHelper field = theFields.pop();
        assert (field != null);

        while (field != null) {

            /* Record, enum, and fixed binary types require a name in Avro */
            setNameForNamedType(field.getName(), field.getType());

            field.validate();
            fields.add(field);
            field = theFields.pop();
        }

        assert (theFields.isEmpty());

        Collections.reverse(fields);

        for (FieldDefHelper field1 : fields) {
            theTableBuilder.addField(field1.getName(), field1.getType(), field1.getNullable(), field1.getDefault());
        }

        try {

            /*
             * Some semantic errors are only caught when building the table.
             * Validation of primary key size is one of them. Re-throw as a
             * QueryException.
             *
             * Validate the primary key fields separately to avoid potential
             * issues with upgraded stores
             */
            theTableBuilder.validatePrimaryKeyFields();
            theTable = theTableBuilder.buildTable();
            theTableBuilder = null;
        } catch (Exception e) {
            throw new QueryException("Cannot build table: " + e.getMessage());
        }

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("CREATE TABLE must execute on a server");
        }

        boolean ifNotExists = (ctx.EXISTS() != null);

        theQCB.getStatementFactory().createTable(theTable, ifNotExists);
    }

    /**
     * key_def : PRIMARY_KEY LP (shard_key_def COMMA?)? id_list_with_size? RP ;
     *
     * id_list_with_size : id_with_size (COMMA id_with_size)* ;
     *
     * id_with_size : id ( LP 1..5 RP )?
     *
     * This is the Primary Key definition, which includes optional
     * specification of the shard key.
     *
     * The optional size specifier allows this:
     * 1. id field of primary key may take no more than 2 bytes of storage
     * when serialized:
     *   ... primary key (id(2))...
     * 2. id1 field of primary key may take no more than 2 bytes of storage
     *   ... primary key (shard(id1(2)), id2) ...
     */
    @Override
    public void enterKey_def(KVQLParser.Key_defContext ctx) {

        assert (theTableBuilder != null);

        try {

            /*
             * If it is a simple id list then there is no shard key
             * specified.
             */
            if (ctx.shard_key_def() == null) {
                /*
                 * Handle empty primary key (primary key()).
                 */
                if (ctx.id_list_with_size() == null) {
                    throw new QueryException("PRIMARY KEY must contain a list of fields", getLocation(ctx));
                }

                /*
                 * tableBuilder.primaryKey() checks that there no duplicate
                 * column names in the list, but does not check that the key
                 * columns have been declared. This is done by the TableImpl
                 * constructor.
                 */
                makePrimaryKey(ctx.id_list_with_size().id_with_size());
                return;
            }

            /*
             * There is shard key specified. Create a list from that, then add
             * the additional primary key fields.
             */
            List<KVQLParser.Id_with_sizeContext> shardKeyList = ctx.shard_key_def().id_list_with_size()
                    .id_with_size();

            /*
             * tableBuilder.shardKey() Checks that there no duplicate column
             * names in the list, but does not check that the key columns have
             * been declared. This is done by the TableImpl constructor.
             */
            theTableBuilder.shardKey(makeKeyIdArray(shardKeyList));

            List<KVQLParser.Id_with_sizeContext> pkey = new ArrayList<KVQLParser.Id_with_sizeContext>(shardKeyList);

            /*
             * Handle case where primary key == shard key and the user
             * specified shard(), even though it is redundant.  It's allowed,
             * just not needed.  E.g. create table foo (id integer, primary
             * key (shard(id))).
             */
            if (ctx.id_list_with_size() != null) {
                pkey.addAll(ctx.id_list_with_size().id_with_size());
            }

            makePrimaryKey(pkey);

        } catch (IllegalArgumentException iae) {
            throw new QueryException(iae.getMessage(), getLocation(ctx));
        }
    }

    @Override
    public void enterTtl_def(KVQLParser.Ttl_defContext ctx) {
        KVQLParser.DurationContext duration = ctx.duration();
        Location loc = getLocation(ctx);
        try {
            theTableBuilder.setDefaultTTL(TimeToLive.createTimeToLive(Integer.parseInt(duration.INT().getText()),
                    convertToTimeUnit(duration.TIME_UNIT())));
        } catch (NumberFormatException nfex) {
            String msg = "Invalid TTL value: " + duration.INT().getText() + " in " + duration.INT().getText() + " "
                    + duration.TIME_UNIT().getText();
            throw new QueryException(msg, loc);
        } catch (IllegalArgumentException iae) {
            String msg = "Invalid TTL Unit: " + convertToTimeUnit(duration.TIME_UNIT()) + " in "
                    + duration.INT().getText() + " " + duration.TIME_UNIT().getText();
            throw new QueryException(msg, loc);
        }
    }

    /*
     * alter_table_statement : ALTER TABLE table_name alter_field_statement ;
     *
     * alter_field_statement :
     * LP
     * (add_field_statement | drop_field_statement | modify_field_statement)
     * (COMMA
     * (add_field_statement | drop_field_statement | modify_field_statement))*
     * RP ;
     *
     * add_field_statement : ADD schema_path type_def default_def?
     *                          comment? ;
     *
     * drop_field_statement : DROP schema_path ;
     *
     * modify_field_statement : MODIFY schema_path type_def default_def?
     *                              comment? ;
     *
     * schema_path : schema_path_step (DOT schema_path_step)*;
     *
     * schema_path_step : id (LBRACK RBRACK)* | VALUES LP RP;
     */
    @Override
    public void enterAlter_table_statement(KVQLParser.Alter_table_statementContext ctx) {

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("ALTER TABLE must execute on a server");
        }

        String[] pathName = getNamePath(ctx.table_name().name_path());

        TableImpl currentTable = getTable(pathName, getLocation(ctx));

        if (currentTable == null) {
            noTable(pathName, getLocation(ctx));
        }

        theTableBuilder = TableEvolver.createTableEvolver(currentTable);
    }

    @Override
    public void exitAlter_table_statement(KVQLParser.Alter_table_statementContext ctx) {

        TableEvolver evolver = (TableEvolver) theTableBuilder;

        try {
            theTable = evolver.evolveTable();
        } catch (IllegalArgumentException iae) {
            throw new QueryException(iae.getMessage(), getLocation(ctx));
        }

        theTableBuilder = null;
        theQCB.getStatementFactory().evolveTable(theTable);
    }

    /*
     * add_field_statement : ADD schema_path type_def default_def?
     * comment? ;
     *
     * default_def : (default_value not_null?) | (not_null? default_value) ;
     *
     * comment : COMMENT string
     */
    @Override
    public void enterAdd_field_statement(KVQLParser.Add_field_statementContext ctx) {

        String comment = null;
        if (ctx.comment() != null) {
            comment = stripFirstLast(ctx.comment().string().getText());
        }

        /*
         * This FieldDefHelper is used primarily for the translation of
         * the default_def.
        */
        FieldDefHelper fieldDefHelper = new FieldDefHelper("", comment, getLocation(ctx));
        theFields.push(fieldDefHelper);
    }

    @Override
    public void exitAdd_field_statement(KVQLParser.Add_field_statementContext ctx) {

        assert (!theFields.empty());
        assert (!theTypes.empty());
        assert (theTableBuilder != null);

        TableEvolver evolver = (TableEvolver) theTableBuilder;

        FieldDefHelper field = theFields.pop();

        field.setType(theTypes.pop());
        // the default value is set in enterDefault_value() method

        assert (theTypes.empty());

        List<String> stepsList = getStepsList(ctx.schema_path());

        /* Record, enum, and fixed binary types require a name in Avro */
        /* Use the last component of the path name as the type name */
        String newFieldName = stepsList.get(stepsList.size() - 1);
        setNameForNamedType(newFieldName, field.getType());

        field.validate();

        try {
            evolver.addField(new TablePath(evolver.getFieldMap(), stepsList), field.getType(), field.getNullable(),
                    field.getDefault());
        } catch (IllegalArgumentException iae) {
            throw new QueryException(iae.getMessage(), getLocation(ctx.schema_path()));
        }
    }

    /*
     * drop_field_statement : DROP schema_path ;
     */
    @Override
    public void exitDrop_field_statement(KVQLParser.Drop_field_statementContext ctx) {

        List<String> stepsList = getStepsList(ctx.schema_path());

        try {
            theTableBuilder.removeField(new TablePath(theTableBuilder.getFieldMap(), stepsList));
        } catch (IllegalArgumentException iae) {
            throw new QueryException(iae.getMessage(), iae, getLocation(ctx.schema_path()));
        }
    }

    /*
     * schema_path : init_schema_path_step (DOT schema_path_step)*;
     *
     * init_schema_path_step : id (LBRACK RBRACK)* ;
     *
     * schema_path_step : id (LBRACK RBRACK)* | VALUES LP RP ;
     */
    @SuppressWarnings("unused")
    static List<String> getStepsList(KVQLParser.Schema_pathContext schemaPathCtx) {

        KVQLParser.Init_schema_path_stepContext initStep = schemaPathCtx.init_schema_path_step();

        List<KVQLParser.Schema_path_stepContext> steps = schemaPathCtx.schema_path_step();

        List<String> stepsList = new ArrayList<String>(steps.size() + 1);

        stepsList.add(initStep.id().getText());

        if (initStep.LBRACK() != null) {
            assert initStep.RBRACK() != null;
            for (TerminalNode t : initStep.LBRACK()) {
                stepsList.add(TableImpl.BRACKETS);
            }
        }

        for (KVQLParser.Schema_path_stepContext step : steps) {

            if (step.id() != null) {
                stepsList.add(step.id().getText());

                if (step.LBRACK() != null) {
                    assert step.RBRACK() != null;
                    for (TerminalNode t : step.LBRACK()) {
                        stepsList.add(TableImpl.BRACKETS);
                    }
                }
            } else {
                stepsList.add(TableImpl.BRACKETS);
            }
        }
        return stepsList;
    }

    /**
     * In the current TableBuilder/TableEvolver model a new field cannot be
     * added over top of an existing field, so remove the field first so the
     * add later works.  The actual modification is validated in
     * TableImpl.evolve().
     */
    @Override
    public void enterModify_field_statement(KVQLParser.Modify_field_statementContext ctx) {

        throw new QueryException("MODIFY is not supported at this time", getLocation(ctx));
    }

    /*
     * drop_table_statement : DROP TABLE (IF_EXISTS)? name_path ;
     */
    @Override
    public void enterDrop_table_statement(KVQLParser.Drop_table_statementContext ctx) {

        boolean ifExists = (ctx.EXISTS() != null);

        String[] tableName = getNamePath(ctx.name_path());
        theTable = getTableSilently(tableName);

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("DROP TABLE must execute on a server");
        }

        theQCB.getStatementFactory().dropTable(concatPathName(tableName), theTable, ifExists, getRemoveData());
    }

    /*
     * create_index_statement :
     *     CREATE INDEX (IF NOT EXISTS)? index_name ON table_name
     *     ((LP index_path_list RP)
     *     comment?;
     *
     * index_name : id ;
     *
     * index_path_list : index_path (COMMA index_path)* ;
     *
     * index_path : (name_path | keys_expr | values_expr | brackets_expr) ;
     *
     * keys_expr : KEYS LP name_path RP
     *
     * Compat:
     *    KEYOF LP name_path RP
     *    KEYS LP name_path RP
     *
     * values_expr : name_path DOT VALUES LP RP ('.' name_path)?
     * Compat:
     *  ELEMENTOF LP name_path RP
     *
     * brackets_expr : name_path LBRACK RBRACK ('.' name_path)? ;
     *
     * GMF: TODO: this doesn't yet work for compatibility syntax. The
     * translation needs to be done here or IndexImpl needs to be modified
     * to accept the old syntax.
     */
    @Override
    public void enterCreate_index_statement(KVQLParser.Create_index_statementContext ctx) {

        boolean ifNotExists = (ctx.EXISTS() != null);

        String[] tableName = getNamePath(ctx.table_name().name_path());
        String indexName = ctx.index_name().id().getText();

        String[] fieldNames = getIndexFieldNames(ctx.index_path_list().index_path());

        String indexComment = null;
        if (ctx.comment() != null) {
            indexComment = stripFirstLast(ctx.comment().string().getText());
        }

        theTable = getTable(tableName, getLocation(ctx));

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("CREATE INDEX must execute on a server");
        }

        theQCB.getStatementFactory().createIndex(concatPathName(tableName), theTable, indexName, fieldNames,
                null /* annotatedFields */, null /* properties */, indexComment, ifNotExists, false /* override */);
    }

    static private String[] getIndexFieldNames(List<KVQLParser.Index_pathContext> list) {

        String[] names = new String[list.size()];
        int i = 0;
        for (KVQLParser.Index_pathContext path : list) {
            names[i++] = path.getText();
        }
        return names;
    }

    @Override
    public void enterDrop_index_statement(KVQLParser.Drop_index_statementContext ctx) {

        boolean ifExists = (ctx.EXISTS() != null);
        boolean override = (ctx.OVERRIDE() != null);

        String[] tableName = getNamePath(ctx.name_path());
        String indexName = ctx.index_name().id().getText();

        theTable = getTableSilently(tableName);

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("DROP INDEX must execute on a server");
        }

        theQCB.getStatementFactory().dropIndex(concatPathName(tableName), theTable, indexName, ifExists, override);
    }

    @Override
    public void exitCreate_text_index_statement(KVQLParser.Create_text_index_statementContext ctx) {

        boolean ifNotExists = false;
        if (ctx.EXISTS() != null) {
            ifNotExists = true;
        }
        boolean override = (ctx.OVERRIDE() != null);
        String[] tableName = getNamePath(ctx.table_name().name_path());
        String indexName = ctx.index_name().id().getText();
        AnnotatedField[] ftsFieldArray = makeFtsFieldArray(ctx.fts_field_list().fts_path_list().fts_path());

        Map<String, String> properties = new HashMap<String, String>();

        Es_propertiesContext propCtx = ctx.es_properties();
        if (propCtx != null) {
            for (KVQLParser.Es_property_assignmentContext prop : propCtx.es_property_assignment()) {

                if (prop.ES_SHARDS() != null) {
                    String shards = prop.INT().toString();
                    if (Integer.parseInt(shards) < 1) {
                        throw new DdlException(
                                "The " + prop.ES_SHARDS() + " value of " + shards + " is not allowed.");
                    }
                    properties.put(prop.ES_SHARDS().toString(), shards);
                } else if (prop.ES_REPLICAS() != null) {
                    String replicas = prop.INT().toString();
                    if (Integer.parseInt(replicas) < 0) {
                        throw new DdlException(
                                "The " + prop.ES_REPLICAS() + " value of " + replicas + " is not allowed.");
                    }
                    properties.put(prop.ES_REPLICAS().toString(), replicas);
                }
            }
        }

        /* Don't carry an empty map around if we don't need it. */
        if (properties.isEmpty()) {
            properties = null;
        }

        String indexComment = null;
        if (ctx.comment() != null) {
            indexComment = stripFirstLast(ctx.comment().string().getText());
        }
        theTable = getTable(tableName, getLocation(ctx));

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("CREATE FULLTEXT INDEX must execute on a server");
        }

        theQCB.getStatementFactory().createIndex(concatPathName(tableName), theTable, indexName, null,
                ftsFieldArray, properties, indexComment, ifNotExists, override);
    }

    @Override
    public void enterDescribe_statement(KVQLParser.Describe_statementContext ctx) {

        String[] tableName = null;
        String indexName = null;
        List<List<String>> schemaPaths = null;

        if (ctx.name_path() != null) {

            tableName = getNamePath(ctx.name_path());
            if (getTable(tableName, getLocation(ctx.name_path())) == null) {
                noTable(tableName, getLocation(ctx.name_path()));
            }

            if (ctx.schema_path_list() != null) {

                List<KVQLParser.Schema_pathContext> pathCtxList = ctx.schema_path_list().schema_path();

                schemaPaths = new ArrayList<List<String>>(pathCtxList.size());

                for (KVQLParser.Schema_pathContext spctx : pathCtxList) {
                    schemaPaths.add(getStepsList(spctx));
                }
            }

            if (ctx.index_name() != null) {
                indexName = ctx.index_name().id().getText();
            }
        }

        boolean describeAsJson = (ctx.JSON() != null);

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("DESCRIBE TABLE must execute on a server");
        }

        theQCB.getStatementFactory().describeTable(concatPathName(tableName), indexName, schemaPaths,
                describeAsJson);
    }

    /**
     * Very similar to DESCRIBE, with other options
     * show_statment: SHOW AS_JSON?
     *      (TABLES |
     *      ROLES |
     *      USERS |
     *      ROLE role_name |
     *      USER user_name |
     *      INDEXES ON table_name |
     *      TABLE table_name) ;
     */
    @Override
    public void enterShow_statement(KVQLParser.Show_statementContext ctx) {

        /* Try to identify as a Show User or Show Role operation */
        if (getShowUserOrRoleOp(ctx)) {
            return;
        }

        String[] tableName = null;
        boolean showTables = false;
        boolean showIndexes = false;

        /* Try to identify as a Show Table or Show Index operation */
        if (ctx.name_path() != null) {
            tableName = getNamePath(ctx.name_path());
            if (getTable(tableName, getLocation(ctx.name_path())) == null) {
                noTable(tableName, getLocation(ctx.name_path()));
            }
            if (ctx.INDEXES() != null) {
                showIndexes = true;
            }
        } else {
            /*
             * The grammar does not allow table name and TABLES in the same
             * statement.
             */
            assert ctx.TABLES() != null;
            showTables = true;
        }

        boolean describeAsJson = (ctx.JSON() != null);

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("SHOW TABLE|INDEX must execute on a server");
        }

        theQCB.getStatementFactory().showTableOrIndex(concatPathName(tableName), showTables, showIndexes,
                describeAsJson);
    }

    /*
     * For security related commands
     */

    /*
     * create_user_statement :
     *     CREATE USER create_user_identified_clause account_lock? ADMIN? ;
     *
     * create_user_identified_clause :
     *    id identified_clause (PASSWORD EXPIRE)? password_lifetime? |
     *    string IDENTIFIED EXTERNALLY ;
     */
    @Override
    public void exitCreate_user_statement(KVQLParser.Create_user_statementContext ctx) {

        final String userName = getIdentifierName(ctx.create_user_identified_clause(), "user");

        final boolean isExternal = ctx.create_user_identified_clause().IDENTIFIED_EXTERNALLY() != null ? true
                : false;

        final boolean isAdmin = (ctx.ADMIN() != null);
        final boolean passExpired = (ctx.create_user_identified_clause().PASSWORD_EXPIRE() != null);

        final boolean isEnabled = ctx.account_lock() != null ? !isAccountLocked(ctx.account_lock()) : true;

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("CREATE USER must execute on a server");
        }

        if (!isExternal) {
            Long pwdLifetimeInMillis = ctx.create_user_identified_clause().password_lifetime() == null ? null
                    : resolvePassLifeTime(ctx.create_user_identified_clause().password_lifetime());

            final String plainPass = resolvePlainPassword(ctx.create_user_identified_clause().identified_clause());

            if (passExpired) {
                pwdLifetimeInMillis = -1L;
            }

            theQCB.getStatementFactory().createUser(userName, isEnabled, isAdmin, plainPass, pwdLifetimeInMillis);
        } else {
            theQCB.getStatementFactory().createExternalUser(userName, isEnabled, isAdmin);
        }
    }

    @Override
    public void exitCreate_role_statement(KVQLParser.Create_role_statementContext ctx) {

        final String roleName = getIdentifierName(ctx.id(), "role");

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("CREATE ROLE must execute on a server");
        }

        theQCB.getStatementFactory().createRole(roleName);
    }

    @Override
    public void exitAlter_user_statement(KVQLParser.Alter_user_statementContext ctx) {

        final String userName = getIdentifierName(ctx.identifier_or_string(), "user");
        boolean retainPassword = false;
        String newPass = null;

        final KVQLParser.Reset_password_clauseContext resetPassCtx = ctx.reset_password_clause();

        if (resetPassCtx != null) {
            newPass = resolvePlainPassword(resetPassCtx.identified_clause());
            retainPassword = (resetPassCtx.RETAIN_CURRENT_PASSWORD() != null);
        }

        final boolean clearRetainedPassword = (ctx.CLEAR_RETAINED_PASSWORD() != null);
        final boolean passwordExpire = (ctx.PASSWORD_EXPIRE() != null);

        Long pwdLifetimeInMillis = ctx.password_lifetime() == null ? null
                : resolvePassLifeTime(ctx.password_lifetime());

        final Boolean isEnabled = ctx.account_lock() != null ? !isAccountLocked(ctx.account_lock()) : null;

        if (passwordExpire) {
            pwdLifetimeInMillis = -1L;
        }

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("ALTER USER must execute on a server");
        }

        theQCB.getStatementFactory().alterUser(userName, isEnabled, newPass, retainPassword, clearRetainedPassword,
                pwdLifetimeInMillis);
    }

    @Override
    public void exitDrop_user_statement(KVQLParser.Drop_user_statementContext ctx) {

        final String userName = getIdentifierName(ctx.identifier_or_string(), "user");
        final boolean cascade = (ctx.CASCADE() != null);

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("DROP USER must execute on a server");
        }

        theQCB.getStatementFactory().dropUser(userName, cascade);
    }

    @Override
    public void exitDrop_role_statement(KVQLParser.Drop_role_statementContext ctx) {

        final String roleName = getIdentifierName(ctx.id(), "role");

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("DROP ROLE must execute on a server");
        }

        theQCB.getStatementFactory().dropRole(roleName);
    }

    @Override
    public void exitGrant_statement(KVQLParser.Grant_statementContext ctx) {

        final Set<String> privSet = new HashSet<String>();
        final List<KVQLParser.Priv_itemContext> privItemList;
        final String roleName;

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("GRANT must execute on a server");
        }

        /* The GRANT roles TO user/role case */
        if (ctx.grant_roles() != null) {
            String[] roleNames = makeIdArray(ctx.grant_roles().id_list().id());
            final String grantee;
            if (ctx.grant_roles().principal().USER() != null) {
                assert (ctx.grant_roles().principal().ROLE() == null);
                grantee = getIdentifierName(ctx.grant_roles().principal().identifier_or_string(), "user");

                theQCB.getStatementFactory().grantRolesToUser(grantee, roleNames);
            } else {
                grantee = getIdentifierName(ctx.grant_roles().principal().id(), "role");

                theQCB.getStatementFactory().grantRolesToRole(grantee, roleNames);
            }
            return;
        }

        /* The GRANT system_privilegs TO role case */
        if (ctx.grant_system_privileges() != null) {
            privItemList = ctx.grant_system_privileges().sys_priv_list().priv_item();
            getPrivSet(privItemList, privSet);

            roleName = getIdentifierName(ctx.grant_system_privileges().id(), "role");

            theQCB.getStatementFactory().grantPrivileges(roleName, null, // tableName
                    privSet);
            return;
        }

        /* The GRANT object_privilege ON object TO role case */
        if (ctx.grant_object_privileges() != null) {
            if (!ctx.grant_object_privileges().obj_priv_list().ALL().isEmpty()) {
                privSet.add(ALL_PRIVS);
            } else {
                privItemList = ctx.grant_object_privileges().obj_priv_list().priv_item();
                getPrivSet(privItemList, privSet);
            }
            roleName = getIdentifierName(ctx.grant_object_privileges().id(), "role");
            final String[] onTable = getNamePath(ctx.grant_object_privileges().object().name_path());

            theQCB.getStatementFactory().grantPrivileges(roleName, concatPathName(onTable), privSet);
        }
    }

    @Override
    public void exitRevoke_statement(KVQLParser.Revoke_statementContext ctx) {

        final Set<String> privSet = new HashSet<String>();
        final List<KVQLParser.Priv_itemContext> privItemList;
        final String roleName;

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("REVOKE must execute on a server");
        }

        /* The REVOKE roles FROM user/role case */
        if (ctx.revoke_roles() != null) {
            String[] roleNames = makeIdArray(ctx.revoke_roles().id_list().id());
            final String revokee;
            if (ctx.revoke_roles().principal().USER() != null) {
                assert (ctx.revoke_roles().principal().ROLE() == null);
                revokee = getIdentifierName(ctx.revoke_roles().principal().identifier_or_string(), "user");

                theQCB.getStatementFactory().revokeRolesFromUser(revokee, roleNames);
            } else {
                revokee = getIdentifierName(ctx.revoke_roles().principal().id(), "role");

                theQCB.getStatementFactory().revokeRolesFromRole(revokee, roleNames);
            }
            return;
        }

        /* The REVOKE system_privilegs FROM role case */
        if (ctx.revoke_system_privileges() != null) {
            privItemList = ctx.revoke_system_privileges().sys_priv_list().priv_item();
            getPrivSet(privItemList, privSet);

            roleName = getIdentifierName(ctx.revoke_system_privileges().id(), "role");

            theQCB.getStatementFactory().revokePrivileges(roleName, null, // tableName
                    privSet);
            return;
        }

        /* The REVOKE object_privilege ON object FROM role case */
        if (ctx.revoke_object_privileges() != null) {
            if (!ctx.revoke_object_privileges().obj_priv_list().ALL().isEmpty()) {
                privSet.add(ALL_PRIVS);
            } else {
                privItemList = ctx.revoke_object_privileges().obj_priv_list().priv_item();
                getPrivSet(privItemList, privSet);
            }
            roleName = getIdentifierName(ctx.revoke_object_privileges().id(), "role");
            final String[] onTable = getNamePath(ctx.revoke_object_privileges().object().name_path());

            theQCB.getStatementFactory().revokePrivileges(roleName, concatPathName(onTable), privSet);
        }
    }

    /* Callbacks for embedded JSON parsing. */
    @Override
    public void exitJsonAtom(KVQLParser.JsonAtomContext ctx) {
        jsonCollector.exitJsonAtom(ctx);
    }

    @Override
    public void exitJsonArrayValue(KVQLParser.JsonArrayValueContext ctx) {

        jsonCollector.exitJsonArrayValue(ctx);
    }

    @Override
    public void exitJsonObjectValue(KVQLParser.JsonObjectValueContext ctx) {

        jsonCollector.exitJsonObjectValue(ctx);
    }

    @Override
    public void exitJsonPair(KVQLParser.JsonPairContext ctx) {
        jsonCollector.exitJsonPair(ctx);
    }

    @Override
    public void exitArrayOfJsonValues(KVQLParser.ArrayOfJsonValuesContext ctx) {

        jsonCollector.exitArrayOfJsonValues(ctx);
    }

    @Override
    public void exitEmptyJsonArray(KVQLParser.EmptyJsonArrayContext ctx) {

        jsonCollector.exitEmptyJsonArray(ctx);
    }

    @Override
    public void exitJsonObject(KVQLParser.JsonObjectContext ctx) {
        jsonCollector.exitJsonObject(ctx);
    }

    @Override
    public void exitEmptyJsonObject(KVQLParser.EmptyJsonObjectContext ctx) {

        jsonCollector.exitEmptyJsonObject(ctx);
    }

    @Override
    public void exitJson_text(KVQLParser.Json_textContext ctx) {
        jsonCollector.exitJson_text(ctx);
    }

    /*
     * Internal functions and classes
     */

    private boolean getShowUserOrRoleOp(KVQLParser.Show_statementContext ctx) {

        final boolean asJson = (ctx.JSON() != null);

        if (theQCB.getStatementFactory() == null) {
            throw new DdlException("SHOW must execute on a server");
        }

        if (ctx.identifier_or_string() != null && ctx.USER() != null) {
            final String name = getIdentifierName(ctx.identifier_or_string(), "user");
            theQCB.getStatementFactory().showUser(name, asJson);
            return true;
        }
        if (ctx.id() != null && ctx.ROLE() != null) {
            final String name = getIdentifierName(ctx.id(), "role");
            theQCB.getStatementFactory().showRole(name, asJson);
            return true;
        }
        if (ctx.USERS() != null) {
            theQCB.getStatementFactory().showUser(null, asJson);
            return true;
        } else if (ctx.ROLES() != null) {
            theQCB.getStatementFactory().showRole(null, asJson);
            return true;
        }
        return false;
    }

    private static boolean isAccountLocked(KVQLParser.Account_lockContext ctx) {

        if (ctx.LOCK() != null) {
            assert (ctx.UNLOCK() == null);
            return true;
        }
        return false;
    }

    private static String getIdentifierName(KVQLParser.IdContext ctx, String idType) {

        if (ctx != null) {
            return ctx.getText();
        }
        throw new QueryException("Invalid empty name of " + idType, getLocation(ctx));
    }

    private static String getIdentifierName(KVQLParser.Identifier_or_stringContext ctx, String idType) {

        if (ctx.id() != null) {
            return getIdentifierName(ctx.id(), idType);
        }
        if (ctx.string() != null) {
            final String result = stripFirstLast(ctx.string().getText());
            if (!result.equals("")) {
                return result;
            }
        }
        throw new QueryException("Invalid empty name of " + idType, getLocation(ctx));
    }

    private static String getIdentifierName(KVQLParser.Create_user_identified_clauseContext ctx, String idType) {

        if (ctx.identified_clause() != null && ctx.id() != null) {
            return getIdentifierName(ctx.id(), idType);
        }
        if (ctx.IDENTIFIED_EXTERNALLY() != null && ctx.string() != null) {
            final String result = stripFirstLast(ctx.string().getText());
            if (!result.equals("")) {
                return result;
            }
        }
        throw new QueryException("Invalid empty name of " + idType, getLocation(ctx));
    }

    private static String resolvePlainPassword(KVQLParser.Identified_clauseContext ctx) {

        final String passStr = ctx.by_password().string().getText();
        if (passStr.isEmpty() || passStr.length() <= 2) {
            throw new QueryException("Invalid empty password", getLocation(ctx));
        }
        return passStr;
    }

    private static long resolvePassLifeTime(KVQLParser.Password_lifetimeContext ctx) {

        final long timeValue;
        final TimeUnit timeUnit;
        try {
            timeValue = Integer.parseInt(ctx.duration().INT().getText());
            if (timeValue <= 0) {
                throw new QueryException("Time value must not be zero or negative", getLocation(ctx));
            }
        } catch (NumberFormatException nfe) {
            throw new QueryException("Invalid numeric value for time value", getLocation(ctx));
        }

        timeUnit = convertToTimeUnit(ctx.duration().TIME_UNIT());
        return TimeUnit.MILLISECONDS.convert(timeValue, timeUnit);
    }

    enum DDLTimeUnit {
        S() {
            @Override
            TimeUnit getUnit() {
                return TimeUnit.SECONDS;
            }
        },

        M() {
            @Override
            TimeUnit getUnit() {
                return TimeUnit.MINUTES;
            }
        },

        H() {
            @Override
            TimeUnit getUnit() {
                return TimeUnit.HOURS;
            }
        },

        D() {
            @Override
            TimeUnit getUnit() {
                return TimeUnit.DAYS;
            }
        };

        abstract TimeUnit getUnit();
    }

    private static TimeUnit convertToTimeUnit(TerminalNode node) {
        String unitStr = node.getText();
        try {
            return TimeUnit.valueOf(unitStr.toUpperCase(ENGLISH));
        } catch (IllegalArgumentException iae) {
            try {
                return DDLTimeUnit.valueOf(unitStr.toUpperCase(ENGLISH)).getUnit();
            } catch (IllegalArgumentException iae2) {
                /* Fall through */
            }
        }
        throw new QueryException("Unrecognized time unit " + unitStr, getLocation(node));
    }

    /**
     * Returns all the components of a path name as an array of strings.
     */
    static private String[] getNamePath(KVQLParser.Name_pathContext ctx) {

        List<KVQLParser.IdContext> steps = ctx.id();

        String[] result = new String[steps.size()];

        int i = 0;
        for (KVQLParser.IdContext step : steps) {
            result[i] = step.getText();
            ++i;
        }

        return result;
    }

    /*
     * Returns all the components of a path name, except from the last one,
     * as an array of strings.
     */
    static private String[] getParentPath(KVQLParser.Name_pathContext ctx) {

        List<KVQLParser.IdContext> steps = ctx.id();

        if (steps.size() == 1) {
            return null;
        }

        String[] result = new String[steps.size() - 1];

        int i = 0;
        for (KVQLParser.IdContext step : steps) {
            result[i] = step.getText();
            ++i;
            if (i == steps.size() - 1) {
                break;
            }
        }

        return result;
    }

    static private String getPathLeaf(KVQLParser.Name_pathContext ctx) {

        List<KVQLParser.IdContext> steps = ctx.id();

        return steps.get(steps.size() - 1).getText();
    }

    static private String concatPathName(String[] pathName) {

        if (pathName == null) {
            return null;
        }

        int numSteps = pathName.length;
        StringBuilder name = new StringBuilder();
        for (int i = 0; i < numSteps; ++i) {
            name.append(pathName[i]);
            if (i < numSteps - 1) {
                name.append('.');
            }
        }
        return name.toString();
    }

    /**
     * Given a full name_path for a table, return the parent table, if any.
     */
    private TableImpl getParentTable(KVQLParser.Name_pathContext ctx) {

        String[] parentPath = getParentPath(ctx);

        if (parentPath == null) {
            return null;
        }

        TableImpl parent = getTable(parentPath, getLocation(ctx));
        if (parent == null) {
            String fullPath = concatPathName(getNamePath(ctx));
            noParentTable(concatPathName(parentPath), fullPath, getLocation(ctx));
        }
        return parent;
    }

    /**
     * Returns the named table if it exists in the table metadata.
     *
     * @return the table if it exists, null if not
     * @throws QueryException if TableMetadata is null
     */
    private TableImpl getTable(String[] pathName, QueryException.Location location) {

        if (theMetadata == null) {
            throw new QueryException("No metadata found for table " + concatPathName(pathName), location);
        }

        return theMetadata.getTable(pathName);
    }

    /**
     * Returns the named table if it exists in table metadata.  Null will be
     * returned if either the table metadata is null, or the table does not
     * exist.
     */
    private TableImpl getTableSilently(String[] pathName) {
        return theMetadata == null ? null : theMetadata.getTable(pathName);
    }

    static private String[] makeIdArray(List<KVQLParser.IdContext> list) {

        String[] ids = new String[list.size()];
        int i = 0;
        for (KVQLParser.IdContext idCtx : list) {
            ids[i++] = idCtx.getText();
        }
        return ids;
    }

    static private String[] makeKeyIdArray(List<KVQLParser.Id_with_sizeContext> list) {
        String[] ids = new String[list.size()];
        int i = 0;
        for (KVQLParser.Id_with_sizeContext idCtx : list) {
            ids[i++] = idCtx.id().getText();
        }
        return ids;
    }

    private void makePrimaryKey(List<KVQLParser.Id_with_sizeContext> list) {
        for (KVQLParser.Id_with_sizeContext idCtx : list) {
            String keyField = idCtx.id().getText();
            theTableBuilder.primaryKey(keyField);
            if (idCtx.storage_size() != null) {
                int size = Integer.parseInt(idCtx.storage_size().INT().getText());
                theTableBuilder.primaryKeySize(keyField, size);
            }
        }
    }

    static private void getPrivSet(List<KVQLParser.Priv_itemContext> pCtxList, Set<String> privSet) {

        for (KVQLParser.Priv_itemContext privItem : pCtxList) {
            if (privItem.ALL_PRIVILEGES() != null) {
                privSet.add(ALL_PRIVS);
            } else {
                privSet.add(getIdentifierName(privItem.id(), "privilege"));
            }
        }
    }

    private AnnotatedField[] makeFtsFieldArray(List<KVQLParser.Fts_pathContext> list) {

        final AnnotatedField[] fieldspecs = new AnnotatedField[list.size()];

        int i = 0;
        for (KVQLParser.Fts_pathContext pctx : list) {
            KVQLParser.Index_pathContext path = pctx.index_path();
            String fieldName = path.getText();
            String jsonStr = jsonCollector.get(pctx.jsobject());
            fieldspecs[i++] = new AnnotatedField(fieldName, jsonStr);
        }
        return fieldspecs;
    }

    static private String stripFirstLast(String s) {
        return s.substring(1, s.length() - 1);
    }

    static private void noTable(String[] pathName, QueryException.Location location) {
        throw new QueryException("Table does not exist: " + concatPathName(pathName), location);
    }

    static private void noParentTable(String parentName, String fullName, QueryException.Location location) {
        throw new QueryException("Parent table does not exist (" + parentName + ") in table path " + fullName,
                location);
    }

    private static QueryException.Location getLocation(ParserRuleContext ctx) {
        int startLine = -1;
        int startColumn = -1;
        int endLine = -1;
        int endColumn = -1;

        if (ctx != null && ctx.getStart() != null) {
            startLine = ctx.getStart().getLine();
            startColumn = ctx.getStart().getCharPositionInLine();
        }

        if (ctx != null && ctx.getStop() != null) {
            endLine = ctx.getStop().getLine();
            endColumn = ctx.getStop().getCharPositionInLine();
        }

        return new QueryException.Location(startLine, startColumn, endLine, endColumn);
    }

    private static QueryException.Location getLocation(TerminalNode node) {
        int startLine = -1;
        int startColumn = -1;
        int endLine = -1;
        int endColumn = -1;

        if (node != null && node.getSymbol() != null) {
            startLine = node.getSymbol().getLine();
            startColumn = node.getSymbol().getCharPositionInLine();
            endLine = node.getSymbol().getLine();
            endColumn = node.getSymbol().getCharPositionInLine();
        }

        return new QueryException.Location(startLine, startColumn, endLine, endColumn);
    }

}