All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hibernate.query.hql.internal.SemanticQueryBuilder Maven / Gradle / Ivy

The newest version!

package org.hibernate.query.hql.internal;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.hibernate.QueryException;
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
import org.hibernate.boot.registry.classloading.spi.ClassLoadingException;
import org.hibernate.grammars.hql.HqlLexer;
import org.hibernate.grammars.hql.HqlParser;
import org.hibernate.grammars.hql.HqlParserBaseVisitor;
import org.hibernate.internal.util.CharSequenceHelper;
import org.hibernate.internal.util.QuotingHelper;
import org.hibernate.internal.util.collections.Stack;
import org.hibernate.internal.util.collections.StandardStack;
import org.hibernate.metamodel.CollectionClassification;
import org.hibernate.metamodel.mapping.CollectionPart;
import org.hibernate.metamodel.model.domain.BasicDomainType;
import org.hibernate.metamodel.model.domain.DomainType;
import org.hibernate.metamodel.model.domain.EntityDomainType;
import org.hibernate.metamodel.model.domain.IdentifiableDomainType;
import org.hibernate.metamodel.model.domain.ManagedDomainType;
import org.hibernate.metamodel.model.domain.PersistentAttribute;
import org.hibernate.metamodel.model.domain.PluralPersistentAttribute;
import org.hibernate.metamodel.model.domain.SingularPersistentAttribute;
import org.hibernate.metamodel.model.domain.internal.AnyDiscriminatorSqmPath;
import org.hibernate.metamodel.model.domain.internal.EntitySqmPathSource;
import org.hibernate.query.PathException;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.SemanticException;
import org.hibernate.query.criteria.JpaCteCriteria;
import org.hibernate.query.criteria.JpaCteCriteriaAttribute;
import org.hibernate.query.criteria.JpaCteCriteriaType;
import org.hibernate.query.criteria.JpaSearchOrder;
import org.hibernate.query.hql.HqlLogging;
import org.hibernate.query.hql.spi.DotIdentifierConsumer;
import org.hibernate.query.hql.spi.SemanticPathPart;
import org.hibernate.query.hql.spi.SqmCreationOptions;
import org.hibernate.query.hql.spi.SqmCreationProcessingState;
import org.hibernate.query.hql.spi.SqmCreationState;
import org.hibernate.query.hql.spi.SqmPathRegistry;
import org.hibernate.query.sqm.BinaryArithmeticOperator;
import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.query.sqm.FetchClauseType;
import org.hibernate.query.sqm.FrameExclusion;
import org.hibernate.query.sqm.FrameKind;
import org.hibernate.query.sqm.FrameMode;
import org.hibernate.query.sqm.LiteralNumberFormatException;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.NullPrecedence;
import org.hibernate.query.sqm.ParsingException;
import org.hibernate.query.sqm.SetOperator;
import org.hibernate.query.sqm.SortOrder;
import org.hibernate.query.sqm.SqmExpressible;
import org.hibernate.query.sqm.SqmPathSource;
import org.hibernate.query.sqm.SqmQuerySource;
import org.hibernate.query.sqm.SqmTreeCreationLogger;
import org.hibernate.query.sqm.StrictJpaComplianceViolation;
import org.hibernate.query.sqm.TemporalUnit;
import org.hibernate.query.sqm.TrimSpec;
import org.hibernate.query.sqm.UnaryArithmeticOperator;
import org.hibernate.query.sqm.UnknownEntityException;
import org.hibernate.query.sqm.function.FunctionKind;
import org.hibernate.query.sqm.function.NamedSqmFunctionDescriptor;
import org.hibernate.query.sqm.function.SqmFunctionDescriptor;
import org.hibernate.query.sqm.internal.ParameterCollector;
import org.hibernate.query.sqm.internal.SqmCreationProcessingStateImpl;
import org.hibernate.query.sqm.internal.SqmCriteriaNodeBuilder;
import org.hibernate.query.sqm.internal.SqmDmlCreationProcessingState;
import org.hibernate.query.sqm.internal.SqmQueryPartCreationProcessingStateStandardImpl;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.query.sqm.spi.ParameterDeclarationContext;
import org.hibernate.query.sqm.spi.SqmCreationContext;
import org.hibernate.query.sqm.tree.SqmJoinType;
import org.hibernate.query.sqm.tree.SqmQuery;
import org.hibernate.query.sqm.tree.SqmStatement;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.query.sqm.tree.cte.SqmCteContainer;
import org.hibernate.query.sqm.tree.cte.SqmCteStatement;
import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement;
import org.hibernate.query.sqm.tree.domain.AbstractSqmFrom;
import org.hibernate.query.sqm.tree.domain.SqmCorrelation;
import org.hibernate.query.sqm.tree.domain.SqmCteRoot;
import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot;
import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction;
import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmFkExpression;
import org.hibernate.query.sqm.tree.domain.SqmIndexAggregateFunction;
import org.hibernate.query.sqm.tree.domain.SqmListJoin;
import org.hibernate.query.sqm.tree.domain.SqmMapEntryReference;
import org.hibernate.query.sqm.tree.domain.SqmMapJoin;
import org.hibernate.query.sqm.tree.domain.SqmPath;
import org.hibernate.query.sqm.tree.domain.SqmPluralValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmPolymorphicRootDescriptor;
import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef;
import org.hibernate.query.sqm.tree.expression.SqmAny;
import org.hibernate.query.sqm.tree.expression.SqmAnyDiscriminatorValue;
import org.hibernate.query.sqm.tree.expression.SqmBinaryArithmetic;
import org.hibernate.query.sqm.tree.expression.SqmByUnit;
import org.hibernate.query.sqm.tree.expression.SqmCaseSearched;
import org.hibernate.query.sqm.tree.expression.SqmCaseSimple;
import org.hibernate.query.sqm.tree.expression.SqmCastTarget;
import org.hibernate.query.sqm.tree.expression.SqmCollation;
import org.hibernate.query.sqm.tree.expression.SqmCollectionSize;
import org.hibernate.query.sqm.tree.expression.SqmDistinct;
import org.hibernate.query.sqm.tree.expression.SqmDurationUnit;
import org.hibernate.query.sqm.tree.expression.SqmEvery;
import org.hibernate.query.sqm.tree.expression.SqmExpression;
import org.hibernate.query.sqm.tree.expression.SqmExtractUnit;
import org.hibernate.query.sqm.tree.expression.SqmFormat;
import org.hibernate.query.sqm.tree.expression.SqmFunction;
import org.hibernate.query.sqm.tree.expression.SqmLiteral;
import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType;
import org.hibernate.query.sqm.tree.expression.SqmLiteralNull;
import org.hibernate.query.sqm.tree.expression.SqmNamedParameter;
import org.hibernate.query.sqm.tree.expression.SqmOver;
import org.hibernate.query.sqm.tree.expression.SqmOverflow;
import org.hibernate.query.sqm.tree.expression.SqmParameter;
import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType;
import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter;
import org.hibernate.query.sqm.tree.expression.SqmStar;
import org.hibernate.query.sqm.tree.expression.SqmSummarization;
import org.hibernate.query.sqm.tree.expression.SqmToDuration;
import org.hibernate.query.sqm.tree.expression.SqmTrimSpecification;
import org.hibernate.query.sqm.tree.expression.SqmTuple;
import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation;
import org.hibernate.query.sqm.tree.from.SqmAttributeJoin;
import org.hibernate.query.sqm.tree.from.SqmCrossJoin;
import org.hibernate.query.sqm.tree.from.SqmCteJoin;
import org.hibernate.query.sqm.tree.from.SqmDerivedJoin;
import org.hibernate.query.sqm.tree.from.SqmEntityJoin;
import org.hibernate.query.sqm.tree.from.SqmFrom;
import org.hibernate.query.sqm.tree.from.SqmFromClause;
import org.hibernate.query.sqm.tree.from.SqmJoin;
import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin;
import org.hibernate.query.sqm.tree.from.SqmRoot;
import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
import org.hibernate.query.sqm.tree.insert.SqmInsertStatement;
import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement;
import org.hibernate.query.sqm.tree.insert.SqmValues;
import org.hibernate.query.sqm.tree.predicate.SqmBetweenPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmBooleanExpressionPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmComparisonPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmEmptinessPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmExistsPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmGroupedPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmInListPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmInSubQueryPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmJunctionPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmLikePredicate;
import org.hibernate.query.sqm.tree.predicate.SqmMemberOfPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmNegatablePredicate;
import org.hibernate.query.sqm.tree.predicate.SqmNegatedPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmNullnessPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmWhereClause;
import org.hibernate.query.sqm.tree.select.AbstractSqmSelectQuery;
import org.hibernate.query.sqm.tree.select.SqmAliasedNode;
import org.hibernate.query.sqm.tree.select.SqmDynamicInstantiation;
import org.hibernate.query.sqm.tree.select.SqmDynamicInstantiationArgument;
import org.hibernate.query.sqm.tree.select.SqmOrderByClause;
import org.hibernate.query.sqm.tree.select.SqmQueryGroup;
import org.hibernate.query.sqm.tree.select.SqmQueryPart;
import org.hibernate.query.sqm.tree.select.SqmQuerySpec;
import org.hibernate.query.sqm.tree.select.SqmSelectClause;
import org.hibernate.query.sqm.tree.select.SqmSelectQuery;
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
import org.hibernate.query.sqm.tree.select.SqmSelectableNode;
import org.hibernate.query.sqm.tree.select.SqmSelection;
import org.hibernate.query.sqm.tree.select.SqmSortSpecification;
import org.hibernate.query.sqm.tree.select.SqmSubQuery;
import org.hibernate.query.sqm.tree.update.SqmUpdateStatement;
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
import org.hibernate.sql.ast.tree.cte.CteMaterialization;
import org.hibernate.sql.ast.tree.cte.CteSearchClauseKind;
import org.hibernate.type.BasicType;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType;

import org.jboss.logging.Logger;

import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.metamodel.Bindable;
import jakarta.persistence.metamodel.SingularAttribute;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;

import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.hibernate.grammars.hql.HqlParser.ELEMENTS;
import static org.hibernate.grammars.hql.HqlParser.EXCEPT;
import static org.hibernate.grammars.hql.HqlParser.IDENTIFIER;
import static org.hibernate.grammars.hql.HqlParser.INDICES;
import static org.hibernate.grammars.hql.HqlParser.INTERSECT;
import static org.hibernate.grammars.hql.HqlParser.ListaggFunctionContext;
import static org.hibernate.grammars.hql.HqlParser.OnOverflowClauseContext;
import static org.hibernate.grammars.hql.HqlParser.PLUS;
import static org.hibernate.grammars.hql.HqlParser.UNION;
import static org.hibernate.query.sqm.TemporalUnit.DATE;
import static org.hibernate.query.sqm.TemporalUnit.DAY_OF_MONTH;
import static org.hibernate.query.sqm.TemporalUnit.DAY_OF_WEEK;
import static org.hibernate.query.sqm.TemporalUnit.DAY_OF_YEAR;
import static org.hibernate.query.sqm.TemporalUnit.NANOSECOND;
import static org.hibernate.query.sqm.TemporalUnit.OFFSET;
import static org.hibernate.query.sqm.TemporalUnit.TIME;
import static org.hibernate.query.sqm.TemporalUnit.TIMEZONE_HOUR;
import static org.hibernate.query.sqm.TemporalUnit.TIMEZONE_MINUTE;
import static org.hibernate.query.sqm.TemporalUnit.WEEK_OF_MONTH;
import static org.hibernate.query.sqm.TemporalUnit.WEEK_OF_YEAR;
import static org.hibernate.type.descriptor.DateTimeUtils.DATE_TIME;
import static org.hibernate.type.spi.TypeConfiguration.isJdbcTemporalType;

/**
 * Responsible for producing an SQM using visitation over an HQL parse tree generated by
 * ANTLR via {@link HqlParseTreeBuilder}.
 *
 * @author Steve Ebersole
 */
public class SemanticQueryBuilder extends HqlParserBaseVisitor implements SqmCreationState {

    private static final Logger log = Logger.getLogger( SemanticQueryBuilder.class );
    private static final Set JPA_STANDARD_FUNCTIONS;

    static {
        final Set jpaStandardFunctions = new HashSet<>();
        // Extracted from the BNF in JPA spec 4.14.
        jpaStandardFunctions.add( "avg" );
        jpaStandardFunctions.add( "max" );
        jpaStandardFunctions.add( "min" );
        jpaStandardFunctions.add( "sum" );
        jpaStandardFunctions.add( "count" );
        jpaStandardFunctions.add( "length" );
        jpaStandardFunctions.add( "locate" );
        jpaStandardFunctions.add( "abs" );
        jpaStandardFunctions.add( "sqrt" );
        jpaStandardFunctions.add( "mod" );
        jpaStandardFunctions.add( "size" );
        jpaStandardFunctions.add( "index" );
        jpaStandardFunctions.add( "current_date" );
        jpaStandardFunctions.add( "current_time" );
        jpaStandardFunctions.add( "current_timestamp" );
        jpaStandardFunctions.add( "concat" );
        jpaStandardFunctions.add( "substring" );
        jpaStandardFunctions.add( "trim" );
        jpaStandardFunctions.add( "lower" );
        jpaStandardFunctions.add( "upper" );
        jpaStandardFunctions.add( "coalesce" );
        jpaStandardFunctions.add( "nullif" );
        JPA_STANDARD_FUNCTIONS = jpaStandardFunctions;
    }

    /**
     * Main entry point into analysis of HQL/JPQL parse tree - producing
     * a semantic model of the query.
     */
    public static  SqmStatement buildSemanticModel(
            HqlParser.StatementContext hqlParseTree,
            Class expectedResultType,
            SqmCreationOptions creationOptions,
            SqmCreationContext creationContext) {
        return new SemanticQueryBuilder<>( expectedResultType, creationOptions, creationContext ).visitStatement( hqlParseTree );
    }

    private final Class expectedResultType;
    private final SqmCreationOptions creationOptions;
    private final SqmCreationContext creationContext;

    private final Stack dotIdentifierConsumerStack;

    private final Stack parameterDeclarationContextStack = new StandardStack<>( ParameterDeclarationContext.class );
    private final Stack processingStateStack = new StandardStack<>( SqmCreationProcessingState.class );

    private final BasicDomainType integerDomainType;
    private final JavaType> listJavaType;
    private final JavaType> mapJavaType;

    private ParameterCollector parameterCollector;
    private ParameterStyle parameterStyle;

    private boolean isExtractingJdbcTemporalType;
    // Provides access to the current CTE that is being processed, which is potentially recursive
    // This is necessary, so that the recursive query part of a CTE can access its own structure.
    // Note that the structure is based on the non-recursive query part, so there is no cycle
    private JpaCteCriteria currentPotentialRecursiveCte;

    public SemanticQueryBuilder(
            Class expectedResultType,
            SqmCreationOptions creationOptions,
            SqmCreationContext creationContext) {
        this.expectedResultType = expectedResultType;
        this.creationOptions = creationOptions;
        this.creationContext = creationContext;
        this.dotIdentifierConsumerStack = new StandardStack<>(
                DotIdentifierConsumer.class,
                new BasicDotIdentifierConsumer( this )
        );
        this.parameterStyle = creationOptions.useStrictJpaCompliance()
                ? ParameterStyle.UNKNOWN
                : ParameterStyle.MIXED;

        this.integerDomainType = creationContext
                .getNodeBuilder()
                .getTypeConfiguration()
                .standardBasicTypeForJavaType( Integer.class );
        this.listJavaType = creationContext
                .getNodeBuilder()
                .getTypeConfiguration()
                .getJavaTypeRegistry()
                .resolveDescriptor( List.class );
        this.mapJavaType = creationContext
                .getNodeBuilder()
                .getTypeConfiguration()
                .getJavaTypeRegistry()
                .resolveDescriptor( Map.class );
    }

    @Override
    public SqmCreationContext getCreationContext() {
        return creationContext;
    }

    @Override
    public SqmCreationOptions getCreationOptions() {
        return creationOptions;
    }

    public Stack getProcessingStateStack() {
        return processingStateStack;
    }

    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Grammar rules

    @Override
    public SqmStatement visitStatement(HqlParser.StatementContext ctx) {
        // parameters allow multi-valued bindings only in very limited cases, so for
        // the base case here we say false
        parameterDeclarationContextStack.push( () -> false );

        try {
            final ParseTree parseTree = ctx.getChild( 0 );
            if ( parseTree instanceof HqlParser.SelectStatementContext ) {
                final SqmSelectStatement selectStatement = visitSelectStatement( (HqlParser.SelectStatementContext) parseTree );
                selectStatement.getQueryPart().validateQueryStructureAndFetchOwners();
                return selectStatement;
            }
            else if ( parseTree instanceof HqlParser.InsertStatementContext ) {
                return visitInsertStatement( (HqlParser.InsertStatementContext) parseTree );
            }
            else if ( parseTree instanceof HqlParser.UpdateStatementContext ) {
                return visitUpdateStatement( (HqlParser.UpdateStatementContext) parseTree );
            }
            else if ( parseTree instanceof HqlParser.DeleteStatementContext ) {
                return visitDeleteStatement( (HqlParser.DeleteStatementContext) parseTree );
            }
        }
        finally {
            parameterDeclarationContextStack.pop();
        }

        throw new ParsingException( "Unexpected statement type [not INSERT, UPDATE, DELETE or SELECT] : " + ctx.getText() );
    }


    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Top-level statements

    @Override
    public SqmSelectStatement visitSelectStatement(HqlParser.SelectStatementContext ctx) {
        final HqlParser.QueryExpressionContext queryExpressionContext = ctx.queryExpression();
        final SqmSelectStatement selectStatement = new SqmSelectStatement<>( creationContext.getNodeBuilder() );

        parameterCollector = selectStatement;

        processingStateStack.push(
                new SqmQueryPartCreationProcessingStateStandardImpl(
                        processingStateStack.getCurrent(),
                        selectStatement,
                        this
                )
        );

        try {
            queryExpressionContext.accept( this );
        }
        finally {
            processingStateStack.pop();
        }

        return selectStatement;
    }

    @Override
    public SqmRoot visitTargetEntity(HqlParser.TargetEntityContext dmlTargetContext) {
        final HqlParser.EntityNameContext entityNameContext = (HqlParser.EntityNameContext) dmlTargetContext.getChild( 0 );
        final String identificationVariable;
        if ( dmlTargetContext.getChildCount() == 1 ) {
            identificationVariable = null;
        }
        else {
            identificationVariable = applyJpaCompliance(
                    visitVariable(
                            (HqlParser.VariableContext) dmlTargetContext.getChild( 1 )
                    )
            );
        }
        //noinspection unchecked
        return new SqmRoot<>(
                (EntityDomainType) visitEntityName( entityNameContext ),
                identificationVariable,
                false,
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public SqmInsertStatement visitInsertStatement(HqlParser.InsertStatementContext ctx) {
        final int dmlTargetIndex;
        if ( ctx.getChild( 1 ) instanceof HqlParser.TargetEntityContext ) {
            dmlTargetIndex = 1;
        }
        else {
            dmlTargetIndex = 2;
        }
        final HqlParser.TargetEntityContext dmlTargetContext = (HqlParser.TargetEntityContext) ctx.getChild( dmlTargetIndex );
        final HqlParser.TargetFieldsContext targetFieldsSpecContext = (HqlParser.TargetFieldsContext) ctx.getChild(
                dmlTargetIndex + 1
        );
        final SqmRoot root = visitTargetEntity( dmlTargetContext );
        if ( root.getModel() instanceof SqmPolymorphicRootDescriptor ) {
            throw new SemanticException(
                    String.format(
                            "Target type '%s' in insert statement is not an entity",
                            root.getModel().getHibernateEntityName()
                    )
            );
        }

        final HqlParser.QueryExpressionContext queryExpressionContext = ctx.queryExpression();
        if ( queryExpressionContext != null ) {
            final SqmInsertSelectStatement insertStatement = new SqmInsertSelectStatement<>( root, creationContext.getNodeBuilder() );
            parameterCollector = insertStatement;
            final SqmDmlCreationProcessingState processingState = new SqmDmlCreationProcessingState(
                    insertStatement,
                    this
            );

            processingStateStack.push( processingState );

            try {
                queryExpressionContext.accept( this );

                final SqmCreationProcessingState stateFieldsProcessingState = new SqmCreationProcessingStateImpl(
                        insertStatement,
                        this
                );
                stateFieldsProcessingState.getPathRegistry().register( root );

                processingStateStack.push( stateFieldsProcessingState );
                try {
                    for ( HqlParser.SimplePathContext stateFieldCtx : targetFieldsSpecContext.simplePath() ) {
                        final SqmPath stateField = (SqmPath) visitSimplePath( stateFieldCtx );
                        insertStatement.addInsertTargetStateField( stateField );
                    }
                }
                finally {
                    processingStateStack.pop();
                }

                return insertStatement;
            }
            finally {
                processingStateStack.pop();
            }

        }
        else {
            final SqmInsertValuesStatement insertStatement = new SqmInsertValuesStatement<>( root, creationContext.getNodeBuilder() );
            parameterCollector = insertStatement;
            final SqmDmlCreationProcessingState processingState = new SqmDmlCreationProcessingState(
                    insertStatement,
                    this
            );

            processingStateStack.push( processingState );
            processingState.getPathRegistry().register( root );

            try {
                final HqlParser.ValuesListContext valuesListContext = ctx.valuesList();
                for ( int i = 1; i < valuesListContext.getChildCount(); i += 2 ) {
                    final ParseTree values = valuesListContext.getChild( i );
                    final SqmValues sqmValues = new SqmValues();
                    for ( int j = 1; j < values.getChildCount(); j += 2 ) {
                        sqmValues.getExpressions().add( (SqmExpression) values.getChild( j ).accept( this ) );
                    }
                    insertStatement.getValuesList().add( sqmValues );
                }

                for ( HqlParser.SimplePathContext stateFieldCtx : targetFieldsSpecContext.simplePath() ) {
                    final SqmPath stateField = (SqmPath) visitSimplePath( stateFieldCtx );
                    insertStatement.addInsertTargetStateField( stateField );
                }

                return insertStatement;
            }
            finally {
                processingStateStack.pop();
            }
        }
    }

    @Override
    public SqmUpdateStatement visitUpdateStatement(HqlParser.UpdateStatementContext ctx) {
        final boolean versioned = !( ctx.getChild( 1 ) instanceof HqlParser.TargetEntityContext );
        final int dmlTargetIndex = versioned ? 2 : 1;
        final HqlParser.TargetEntityContext dmlTargetContext = (HqlParser.TargetEntityContext) ctx.getChild( dmlTargetIndex );
        final SqmRoot root = visitTargetEntity( dmlTargetContext );
        if ( root.getModel() instanceof SqmPolymorphicRootDescriptor ) {
            throw new SemanticException(
                    String.format(
                            "Target type '%s' in update statement is not an entity",
                            root.getModel().getHibernateEntityName()
                    )
            );
        }

        final SqmUpdateStatement updateStatement = new SqmUpdateStatement<>( root, creationContext.getNodeBuilder() );
        parameterCollector = updateStatement;
        final SqmDmlCreationProcessingState processingState = new SqmDmlCreationProcessingState(
                updateStatement,
                this
        );
        processingStateStack.push( processingState );
        processingState.getPathRegistry().register( root );

        try {
            updateStatement.versioned( versioned );
            final HqlParser.SetClauseContext setClauseCtx = (HqlParser.SetClauseContext) ctx.getChild( dmlTargetIndex + 1 );
            for ( ParseTree subCtx : setClauseCtx.children ) {
                if ( subCtx instanceof HqlParser.AssignmentContext ) {
                    final HqlParser.AssignmentContext assignmentContext = (HqlParser.AssignmentContext) subCtx;
                    //noinspection unchecked
                    final SqmPath targetPath = (SqmPath) consumeDomainPath( (HqlParser.SimplePathContext) assignmentContext.getChild( 0 ) );
                    final Class targetPathJavaType = targetPath.getJavaType();
                    final boolean isEnum = targetPathJavaType != null && targetPathJavaType.isEnum();
                    final ParseTree rightSide = assignmentContext.getChild( 2 );
                    final HqlParser.ExpressionContext expressionContext;
                    final Map, Enum> possibleEnumValues;
                    final SqmExpression value;
                    if ( isEnum && rightSide.getChild( 0 ) instanceof HqlParser.ExpressionContext
                            && ( possibleEnumValues = getPossibleEnumValues( expressionContext = (HqlParser.ExpressionContext) rightSide.getChild( 0 ) ) ) != null ) {
                        value = resolveEnumShorthandLiteral(
                                expressionContext,
                                possibleEnumValues,
                                targetPathJavaType
                        );
                    }
                    else {
                        value = (SqmExpression) rightSide.accept( this );
                    }
                    updateStatement.applyAssignment( targetPath, value );
                }
            }

            if ( dmlTargetIndex + 2 <= ctx.getChildCount() ) {
                updateStatement.applyPredicate(
                        visitWhereClause( (HqlParser.WhereClauseContext) ctx.getChild( dmlTargetIndex + 2 ) )
                );
            }

            return updateStatement;
        }
        finally {
            processingStateStack.pop();
        }
    }

    @Override
    public SqmDeleteStatement visitDeleteStatement(HqlParser.DeleteStatementContext ctx) {
        final int dmlTargetIndex;
        if ( ctx.getChild( 1 ) instanceof HqlParser.TargetEntityContext ) {
            dmlTargetIndex = 1;
        }
        else {
            dmlTargetIndex = 2;
        }
        final HqlParser.TargetEntityContext dmlTargetContext = (HqlParser.TargetEntityContext) ctx.getChild( dmlTargetIndex );
        final SqmRoot root = visitTargetEntity( dmlTargetContext );

        final SqmDeleteStatement deleteStatement = new SqmDeleteStatement<>( root, SqmQuerySource.HQL, creationContext.getNodeBuilder() );

        parameterCollector = deleteStatement;

        final SqmDmlCreationProcessingState sqmDeleteCreationState = new SqmDmlCreationProcessingState(
                deleteStatement,
                this
        );

        sqmDeleteCreationState.getPathRegistry().register( root );

        processingStateStack.push( sqmDeleteCreationState );
        try {
            if ( dmlTargetIndex + 1 <= ctx.getChildCount() ) {
                deleteStatement.applyPredicate(
                        visitWhereClause( (HqlParser.WhereClauseContext) ctx.getChild( dmlTargetIndex + 1 ) )
                );
            }

            return deleteStatement;
        }
        finally {
            processingStateStack.pop();
        }
    }


    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Query spec

    @Override
    public Object visitWithClause(HqlParser.WithClauseContext ctx) {
        if ( creationOptions.useStrictJpaCompliance() ) {
            throw new StrictJpaComplianceViolation(
                    StrictJpaComplianceViolation.Type.CTES
            );
        }
        final List children = ctx.children;
        for ( int i = 1; i < children.size(); i += 2 ) {
            visitCte( (HqlParser.CteContext) children.get( i ) );
        }
        return null;
    }

    @Override
    public Object visitCte(HqlParser.CteContext ctx) {
        final SqmCteContainer cteContainer = (SqmCteContainer) processingStateStack.getCurrent().getProcessingQuery();
        final String name = visitIdentifier( (HqlParser.IdentifierContext) ctx.children.get( 0 ) );
        final TerminalNode thirdChild = (TerminalNode) ctx.getChild( 2 );
        final int queryExpressionIndex;
        final CteMaterialization materialization;
        switch ( thirdChild.getSymbol().getType() ) {
            case HqlParser.NOT:
                materialization = CteMaterialization.NOT_MATERIALIZED;
                queryExpressionIndex = 5;
                break;
            case HqlParser.MATERIALIZED:
                materialization = CteMaterialization.MATERIALIZED;
                queryExpressionIndex = 4;
                break;
            default:
                materialization = null;
                queryExpressionIndex = 3;
                break;
        }

        final HqlParser.QueryExpressionContext queryExpressionContext = (HqlParser.QueryExpressionContext) ctx.getChild( queryExpressionIndex );
        final SqmSelectQuery cte;
        if ( cteContainer instanceof SqmSubQuery ) {
            cte = new SqmSubQuery<>(
                    processingStateStack.getCurrent().getProcessingQuery(),
                    creationContext.getNodeBuilder()
            );
        }
        else {
            cte = new SqmSelectStatement<>( creationContext.getNodeBuilder() );
        }
        processingStateStack.push(
                new SqmQueryPartCreationProcessingStateStandardImpl(
                        processingStateStack.getCurrent(),
                        cte,
                        this
                )
        );
        final JpaCteCriteria oldCte = currentPotentialRecursiveCte;
        try {
            currentPotentialRecursiveCte = null;
            if ( queryExpressionContext instanceof HqlParser.SetQueryGroupContext ) {
                final HqlParser.SetQueryGroupContext setContext = (HqlParser.SetQueryGroupContext) queryExpressionContext;
                // A recursive query is only possible if the child count is lower than 5 e.g. `withClause? q1 op q2`
                if ( setContext.getChildCount() < 5 ) {
                    final SetOperator setOperator = (SetOperator) setContext.getChild( setContext.getChildCount() - 2 )
                            .accept( this );
                    switch ( setOperator ) {
                        case UNION:
                        case UNION_ALL:
                            final HqlParser.OrderedQueryContext nonRecursiveQueryContext;
                            final HqlParser.OrderedQueryContext recursiveQueryContext;
                            // On count == 4, we have a withClause at index 0
                            if ( setContext.getChildCount() == 4 ) {
                                nonRecursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 1 );
                                recursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 3 );
                            }
                            else {
                                nonRecursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 0 );
                                recursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 2 );
                            }
                            // First visit the non-recursive part
                            nonRecursiveQueryContext.accept( this );

                            // Visiting the possibly recursive part must happen within the call to SqmCteContainer.with,
                            // because in there, the SqmCteStatement/JpaCteCriteria is available for use in the recursive part.
                            // The structure (SqmCteTable) for the SqmCteStatement is based on the non-recursive part,
                            // which is necessary to have, so that the SqmCteRoot/SqmCteJoin can resolve sub-paths.
                            final SqmSelectStatement recursivePart = new SqmSelectStatement<>( creationContext.getNodeBuilder() );

                            processingStateStack.pop();
                            processingStateStack.push(
                                    new SqmQueryPartCreationProcessingStateStandardImpl(
                                            processingStateStack.getCurrent(),
                                            recursivePart,
                                            this
                                    )
                            );
                            final JpaCteCriteria cteDefinition;
                            if ( setOperator == SetOperator.UNION ) {
                                cteDefinition = cteContainer.withRecursiveUnionDistinct(
                                        name,
                                        cte,
                                        cteCriteria -> {
                                            currentPotentialRecursiveCte = cteCriteria;
                                            recursiveQueryContext.accept( this );
                                            return recursivePart;
                                        }
                                );
                            }
                            else {
                                cteDefinition = cteContainer.withRecursiveUnionAll(
                                        name,
                                        cte,
                                        cteCriteria -> {
                                            currentPotentialRecursiveCte = cteCriteria;
                                            recursiveQueryContext.accept( this );
                                            return recursivePart;
                                        }
                                );
                            }
                            if ( materialization != null ) {
                                cteDefinition.setMaterialization( materialization );
                            }
                            final ParseTree lastChild = ctx.getChild( ctx.getChildCount() - 1 );
                            final ParseTree potentialSearchClause;
                            if ( lastChild instanceof HqlParser.CycleClauseContext ) {
                                applyCycleClause( cteDefinition, (HqlParser.CycleClauseContext) lastChild );
                                potentialSearchClause = ctx.getChild( ctx.getChildCount() - 2 );
                            }
                            else {
                                potentialSearchClause = lastChild;
                            }
                            if ( potentialSearchClause instanceof HqlParser.SearchClauseContext ) {
                                applySearchClause( cteDefinition, (HqlParser.SearchClauseContext) potentialSearchClause );
                            }
                            return null;
                    }
                }
            }
            queryExpressionContext.accept( this );
            final JpaCteCriteria cteDefinition = cteContainer.with( name, cte );
            if ( materialization != null ) {
                cteDefinition.setMaterialization( materialization );
            }
        }
        finally {
            processingStateStack.pop();
            currentPotentialRecursiveCte = oldCte;
        }
        return null;
    }

    private void applyCycleClause(JpaCteCriteria cteDefinition, HqlParser.CycleClauseContext ctx) {
        final HqlParser.CteAttributesContext attributesContext = (HqlParser.CteAttributesContext) ctx.getChild( 1 );
        final String cycleMarkAttributeName = visitIdentifier( (HqlParser.IdentifierContext) ctx.getChild( 3 ) );
        final List cycleAttributes = new ArrayList<>( ( attributesContext.getChildCount() + 1 ) >> 1 );
        final List children = attributesContext.children;
        final JpaCteCriteriaType type = cteDefinition.getType();
        for ( int i = 0; i < children.size(); i += 2 ) {
            final String attributeName = visitIdentifier( (HqlParser.IdentifierContext) children.get( i ) );
            final JpaCteCriteriaAttribute attribute = type.getAttribute( attributeName );
            if ( attribute == null ) {
                throw new SemanticException(
                        String.format(
                                "Cycle attribute '%s' not found in the CTE %s",
                                attributeName,
                                cteDefinition.getName()
                        )
                );
            }
            cycleAttributes.add( attribute );
        }

        final String cyclePathAttributeName;
        final Object cycleValue;
        final Object noCycleValue;
        if ( ctx.getChildCount() > 4 ) {
            if ( ctx.getChildCount() > 6 ) {
                final SqmLiteral cycleLiteral = (SqmLiteral) visitLiteral( (HqlParser.LiteralContext) ctx.getChild( 5 ) );
                final SqmLiteral noCycleLiteral = (SqmLiteral) visitLiteral( (HqlParser.LiteralContext) ctx.getChild( 7 ) );
                cycleValue = cycleLiteral.getLiteralValue();
                noCycleValue = noCycleLiteral.getLiteralValue();
            }
            else {
                cycleValue = Boolean.TRUE;
                noCycleValue = Boolean.FALSE;
            }
            final ParseTree lastChild = ctx.getChild( ctx.getChildCount() - 1 );
            if ( lastChild instanceof HqlParser.IdentifierContext ) {
                cyclePathAttributeName = visitIdentifier( (HqlParser.IdentifierContext) lastChild );
            }
            else {
                cyclePathAttributeName = null;
            }
        }
        else {
            cyclePathAttributeName = null;
            cycleValue = Boolean.TRUE;
            noCycleValue = Boolean.FALSE;
        }

        cteDefinition.cycleUsing( cycleMarkAttributeName, cyclePathAttributeName, cycleValue, noCycleValue, cycleAttributes );
    }

    private void applySearchClause(JpaCteCriteria cteDefinition, HqlParser.SearchClauseContext ctx) {
        final CteSearchClauseKind kind;
        if ( ( (TerminalNode) ctx.getChild( 1 ) ).getSymbol().getType() == HqlParser.BREADTH ) {
            kind = CteSearchClauseKind.BREADTH_FIRST;
        }
        else {
            kind = CteSearchClauseKind.DEPTH_FIRST;
        }
        final String searchAttributeName = visitIdentifier( (HqlParser.IdentifierContext) ctx.getChild( ctx.getChildCount() - 1 ) );
        final HqlParser.SearchSpecificationsContext searchCtx = (HqlParser.SearchSpecificationsContext) ctx.getChild( 4 );
        final List searchOrders = new ArrayList<>( ( searchCtx.getChildCount() + 1 ) >> 1 );
        final List children = searchCtx.children;
        final JpaCteCriteriaType type = cteDefinition.getType();
        for ( int i = 0; i < children.size(); i += 2 ) {
            final HqlParser.SearchSpecificationContext specCtx = (HqlParser.SearchSpecificationContext) children.get( i );
            final String attributeName = visitIdentifier( (HqlParser.IdentifierContext) specCtx.getChild( 0 ) );
            final JpaCteCriteriaAttribute attribute = type.getAttribute( attributeName );
            if ( attribute == null ) {
                throw new SemanticException(
                        String.format(
                                "Search attribute '%s' not found in the CTE %s",
                                attributeName,
                                cteDefinition.getName()
                        )
                );
            }
            SortOrder sortOrder = SortOrder.ASCENDING;
            NullPrecedence nullPrecedence = NullPrecedence.NONE;
            int index = 1;
            if ( index < specCtx.getChildCount() ) {
                if ( specCtx.getChild( index ) instanceof HqlParser.SortDirectionContext ) {
                    final HqlParser.SortDirectionContext sortCtx = (HqlParser.SortDirectionContext) specCtx.getChild( index );
                    switch ( ( (TerminalNode) sortCtx.getChild( 0 ) ).getSymbol().getType() ) {
                        case HqlParser.ASC:
                            sortOrder = SortOrder.ASCENDING;
                            break;
                        case HqlParser.DESC:
                            sortOrder = SortOrder.DESCENDING;
                            break;
                        default:
                            throw new SemanticException( "Unrecognized sort ordering: " + sortCtx.getText() );
                    }
                    index++;
                }
                if ( index < specCtx.getChildCount() ) {
                    final HqlParser.NullsPrecedenceContext nullsPrecedenceContext = (HqlParser.NullsPrecedenceContext) specCtx.getChild( index );
                    switch ( ( (TerminalNode) nullsPrecedenceContext.getChild( 1 ) ).getSymbol().getType() ) {
                        case HqlParser.FIRST:
                            nullPrecedence = NullPrecedence.FIRST;
                            break;
                        case HqlParser.LAST:
                            nullPrecedence = NullPrecedence.LAST;
                            break;
                        default:
                            throw new SemanticException( "Unrecognized null precedence: " + nullsPrecedenceContext.getText() );
                    }
                }
            }
            searchOrders.add(
                    creationContext.getNodeBuilder().search(
                            attribute,
                            sortOrder,
                            nullPrecedence
                    )
            );
        }
        cteDefinition.search( kind, searchAttributeName, searchOrders );
    }

    @Override
    public SqmQueryPart visitSimpleQueryGroup(HqlParser.SimpleQueryGroupContext ctx) {
        final int lastChild = ctx.getChildCount() - 1;
        if ( lastChild != 0 ) {
            ctx.getChild( 0 ).accept( this );
        }
        //noinspection unchecked
        return (SqmQueryPart) ctx.getChild( lastChild ).accept( this );
    }

    @Override
    public SqmQueryPart visitQuerySpecExpression(HqlParser.QuerySpecExpressionContext ctx) {
        final List children = ctx.children;
        final SqmQueryPart queryPart = visitQuery( (HqlParser.QueryContext) children.get( 0 ) );
        if ( children.size() > 1 ) {
            visitQueryOrder( queryPart, (HqlParser.QueryOrderContext) children.get( 1 ) );
        }
        return queryPart;
    }

    @Override
    public SqmQueryPart visitNestedQueryExpression(HqlParser.NestedQueryExpressionContext ctx) {
        final List children = ctx.children;
        //noinspection unchecked
        final SqmQueryPart queryPart = (SqmQueryPart) children.get( 1 ).accept( this );
        if ( children.size() > 3 ) {
            final SqmCreationProcessingState firstProcessingState = processingStateStack.pop();
            processingStateStack.push(
                    new SqmQueryPartCreationProcessingStateStandardImpl(
                            processingStateStack.getCurrent(),
                            firstProcessingState.getProcessingQuery(),
                            this
                    )
            );
            visitQueryOrder( queryPart, (HqlParser.QueryOrderContext) children.get( 3 ) );
        }
        return queryPart;
    }

    @Override
    public SqmQueryGroup visitSetQueryGroup(HqlParser.SetQueryGroupContext ctx) {
        final List children = ctx.children;
        final int firstIndex;
        if ( children.get( 0 ) instanceof HqlParser.WithClauseContext ) {
            children.get( 0 ).accept( this );
            firstIndex = 1;
        }
        else {
            firstIndex = 0;
        }
        if ( creationOptions.useStrictJpaCompliance() ) {
            throw new StrictJpaComplianceViolation(
                    StrictJpaComplianceViolation.Type.SET_OPERATIONS
            );
        }
        //noinspection unchecked
        final SqmQueryPart firstQueryPart = (SqmQueryPart) children.get( firstIndex ).accept( this );
        SqmQueryGroup queryGroup;
        if ( firstQueryPart instanceof SqmQueryGroup) {
            queryGroup = (SqmQueryGroup) firstQueryPart;
        }
        else {
            queryGroup = new SqmQueryGroup<>( firstQueryPart );
        }
        setCurrentQueryPart( queryGroup );
        final int size = children.size();
        final SqmCreationProcessingState firstProcessingState = processingStateStack.pop();
        for ( int i = firstIndex + 1; i < size; i += 2 ) {
            final SetOperator operator = visitSetOperator( (HqlParser.SetOperatorContext) children.get( i ) );
            final HqlParser.OrderedQueryContext simpleQueryCtx =
                    (HqlParser.OrderedQueryContext) children.get( i + 1 );
            final List> queryParts;
            processingStateStack.push(
                    new SqmQueryPartCreationProcessingStateStandardImpl(
                            processingStateStack.getCurrent(),
                            firstProcessingState.getProcessingQuery(),
                            this
                    )
            );
            if ( queryGroup.getSetOperator() == null || queryGroup.getSetOperator() == operator ) {
                queryGroup.setSetOperator( operator );
                queryParts = queryGroup.queryParts();
            }
            else {
                queryParts = new ArrayList<>( size - ( i >> 1 ) );
                queryParts.add( queryGroup );
                queryGroup = new SqmQueryGroup<>(
                        creationContext.getNodeBuilder(),
                        operator,
                        queryParts
                );
                setCurrentQueryPart( queryGroup );
            }

            final SqmQueryPart queryPart;
            try {
                final List subChildren = simpleQueryCtx.children;
                if ( subChildren.get( 0 ) instanceof HqlParser.QueryContext ) {
                    final SqmQuerySpec querySpec = new SqmQuerySpec<>( creationContext.getNodeBuilder() );
                    queryParts.add( querySpec );
                    visitQuerySpecExpression( (HqlParser.QuerySpecExpressionContext) simpleQueryCtx );
                }
                else {
                    try {
                        final SqmSelectStatement selectStatement = new SqmSelectStatement<>( creationContext.getNodeBuilder() );
                        processingStateStack.push(
                                new SqmQueryPartCreationProcessingStateStandardImpl(
                                        processingStateStack.getCurrent(),
                                        selectStatement,
                                        this
                                )
                        );
                        queryPart = visitNestedQueryExpression( (HqlParser.NestedQueryExpressionContext) simpleQueryCtx );
                        queryParts.add( queryPart );
                    }
                    finally {
                        processingStateStack.pop();
                    }
                }
            }
            finally {
                processingStateStack.pop();
            }
        }
        processingStateStack.push( firstProcessingState );

        return queryGroup;
    }

    @Override
    public SetOperator visitSetOperator(HqlParser.SetOperatorContext ctx) {
        final Token token = ( (TerminalNode) ctx.getChild( 0 ) ).getSymbol();
        final boolean all = ctx.getChildCount() == 2;
        switch ( token.getType() ) {
            case UNION:
                return all ? SetOperator.UNION_ALL : SetOperator.UNION;
            case INTERSECT:
                return all ? SetOperator.INTERSECT_ALL : SetOperator.INTERSECT;
            case EXCEPT:
                return all ? SetOperator.EXCEPT_ALL : SetOperator.EXCEPT;
        }
        throw new SemanticException( "Illegal set operator token: " + token.getText() );
    }

    protected void visitQueryOrder(SqmQueryPart sqmQueryPart, HqlParser.QueryOrderContext ctx) {
        if ( ctx == null ) {
            return;
        }
        final SqmOrderByClause orderByClause;
        final HqlParser.OrderByClauseContext orderByClauseContext = (HqlParser.OrderByClauseContext) ctx.getChild( 0 );
        if ( orderByClauseContext != null ) {
            if ( creationOptions.useStrictJpaCompliance() && processingStateStack.depth() > 1 ) {
                throw new StrictJpaComplianceViolation(
                        StrictJpaComplianceViolation.Type.SUBQUERY_ORDER_BY
                );
            }

            orderByClause = visitOrderByClause( orderByClauseContext );
            sqmQueryPart.setOrderByClause( orderByClause );
        }
        else {
            orderByClause = null;
        }

        int currentIndex = 1;
        final HqlParser.LimitClauseContext limitClauseContext;
        if ( currentIndex < ctx.getChildCount() && ctx.getChild( currentIndex ) instanceof HqlParser.LimitClauseContext ) {
            limitClauseContext = (HqlParser.LimitClauseContext) ctx.getChild( currentIndex++ );
        }
        else {
            limitClauseContext = null;
        }
        final HqlParser.OffsetClauseContext offsetClauseContext;
        if ( currentIndex < ctx.getChildCount() && ctx.getChild( currentIndex ) instanceof HqlParser.OffsetClauseContext ) {
            offsetClauseContext = (HqlParser.OffsetClauseContext) ctx.getChild( currentIndex++ );
        }
        else {
            offsetClauseContext = null;
        }
        final HqlParser.FetchClauseContext fetchClauseContext;
        if ( currentIndex < ctx.getChildCount() && ctx.getChild( currentIndex ) instanceof HqlParser.FetchClauseContext ) {
            fetchClauseContext = (HqlParser.FetchClauseContext) ctx.getChild( currentIndex++ );
        }
        else {
            fetchClauseContext = null;
        }
        if ( currentIndex != 1 ) {
            if ( getCreationOptions().useStrictJpaCompliance() ) {
                throw new StrictJpaComplianceViolation(
                        StrictJpaComplianceViolation.Type.LIMIT_OFFSET_CLAUSE
                );
            }

            if ( processingStateStack.depth() > 1 && orderByClause == null ) {
                throw new SemanticException(
                        "limit, offset and fetch clause require an order-by clause when used in sub-query"
                );
            }

            sqmQueryPart.setOffsetExpression( visitOffsetClause( offsetClauseContext ) );
            if ( limitClauseContext == null ) {
                sqmQueryPart.setFetchExpression( visitFetchClause( fetchClauseContext ), visitFetchClauseType( fetchClauseContext ) );
            }
            else if ( fetchClauseContext == null ) {
                sqmQueryPart.setFetchExpression( visitLimitClause( limitClauseContext ) );
            }
            else {
                throw new SemanticException("Can't use both limit and fetch clause" );
            }
        }
    }

    @Override
    public SqmQuerySpec visitQuery(HqlParser.QueryContext ctx) {
        final SqmQuerySpec sqmQuerySpec = currentQuerySpec();

        // visit from-clause first!!!
        visitFromClause( ctx.fromClause() );

        final SqmSelectClause selectClause;
        if ( ctx.selectClause() == null ) {
            if ( creationOptions.useStrictJpaCompliance() ) {
                throw new StrictJpaComplianceViolation(
                        "Encountered implicit select-clause, but strict JPQL compliance was requested",
                        StrictJpaComplianceViolation.Type.IMPLICIT_SELECT
                );
            }
            log.debugf( "Encountered implicit select clause : %s", ctx.getText() );
            selectClause = buildInferredSelectClause( sqmQuerySpec.getFromClause() );
        }
        else {
            selectClause = visitSelectClause( ctx.selectClause() );
        }
        sqmQuerySpec.setSelectClause( selectClause );

        final SqmWhereClause whereClause = new SqmWhereClause( creationContext.getNodeBuilder() );
        if ( ctx.whereClause() != null ) {
            whereClause.setPredicate( (SqmPredicate) ctx.whereClause().accept( this ) );
        }
        sqmQuerySpec.setWhereClause( whereClause );

        if ( ctx.groupByClause() != null ) {
            sqmQuerySpec.setGroupByClauseExpressions( visitGroupByClause( ctx.groupByClause() ) );
        }
        if ( ctx.havingClause() != null ) {
            sqmQuerySpec.setHavingClausePredicate( visitHavingClause( ctx.havingClause() ) );
        }

        return sqmQuerySpec;
    }

    protected SqmSelectClause buildInferredSelectClause(SqmFromClause fromClause) {
        // for now, this is slightly different than the legacy behavior where
        // the root and each non-fetched-join was selected.  For now, here, we simply
        // select the root
        final SqmSelectClause selectClause;

        final boolean expectingArray = expectedResultType != null && expectedResultType.isArray();
        if ( expectingArray ) {
            // triggers legacy interpretation of returning all roots
            // and non-fetched joins
            selectClause = new SqmSelectClause(
                    false,
                    creationContext.getNodeBuilder()
            );
        }
        else {
            selectClause = new SqmSelectClause(
                    false,
                    fromClause.getNumberOfRoots(),
                    creationContext.getNodeBuilder()
            );
        }

        fromClause.visitRoots( (sqmRoot) -> {
            selectClause.addSelection( new SqmSelection<>( sqmRoot, sqmRoot.getAlias(), creationContext.getNodeBuilder() ) );
            if ( expectingArray ) {
                applyJoinsToInferredSelectClause( sqmRoot, selectClause );
            }
        } );

        return selectClause;
    }

    private void applyJoinsToInferredSelectClause(SqmFrom sqm, SqmSelectClause selectClause) {
        sqm.visitSqmJoins( (sqmJoin) -> {
            selectClause.addSelection( new SqmSelection<>( sqmJoin, sqmJoin.getAlias(), creationContext.getNodeBuilder() ) );
            applyJoinsToInferredSelectClause( sqmJoin, selectClause );
        } );
    }

    @Override
    public SqmSelectClause visitSelectClause(HqlParser.SelectClauseContext ctx) {
        // todo (6.0) : primer a select-clause-specific SemanticPathPart into the stack
        final int selectionListIndex;
        if ( ctx.getChild( 1 ) instanceof HqlParser.SelectionListContext ) {
            selectionListIndex = 1;
        }
        else {
            selectionListIndex = 2;
        }

        final SqmSelectClause selectClause = new SqmSelectClause(
                selectionListIndex == 2,
                creationContext.getNodeBuilder()
        );
        final HqlParser.SelectionListContext selectionListContext = (HqlParser.SelectionListContext) ctx.getChild(
                selectionListIndex
        );
        for ( ParseTree subCtx : selectionListContext.children ) {
            if ( subCtx instanceof HqlParser.SelectionContext ) {
                selectClause.addSelection( visitSelection( (HqlParser.SelectionContext) subCtx ) );
            }
        }
        return selectClause;
    }

    @Override
    public SqmSelection visitSelection(HqlParser.SelectionContext ctx) {
        final String resultIdentifier;
        if ( ctx.getChildCount() == 1 ) {
            resultIdentifier = null;
        }
        else {
            resultIdentifier = applyJpaCompliance(
                    visitVariable( (HqlParser.VariableContext) ctx.getChild( 1 ) )
            );
        }
        final SqmSelectableNode selectableNode = visitSelectableNode( ctx );

        final SqmSelection selection = new SqmSelection<>(
                selectableNode,
                // NOTE : SqmSelection forces the alias down to its selectableNode.
                //		- no need to do that here
                resultIdentifier,
                creationContext.getNodeBuilder()
        );

        // if the node is not a dynamic-instantiation, register it with
        // the path-registry
        //noinspection StatementWithEmptyBody
        if ( selectableNode instanceof SqmDynamicInstantiation ) {
            // nothing else to do (avoid kludgy `! ( instanceof )` syntax
        }
        else {
            getCurrentProcessingState().getPathRegistry().register( selection );
        }

        return selection;
    }

    private SqmSelectableNode visitSelectableNode(HqlParser.SelectionContext ctx) {
        final ParseTree subCtx = ctx.getChild( 0 ).getChild( 0 );
        if ( subCtx instanceof HqlParser.ExpressionOrPredicateContext ) {
            final SqmExpression sqmExpression = (SqmExpression) subCtx.accept( this );
            if ( sqmExpression instanceof SqmPath ) {
                final SqmPath sqmPath = (SqmPath) sqmExpression;
                if ( sqmPath.getReferencedPathSource() instanceof PluralPersistentAttribute ) {
                    // for plural-attribute selections, use the element path as the selection
                    //		- this is not strictly JPA compliant
                    if ( creationOptions.useStrictJpaCompliance() ) {
                        SqmTreeCreationLogger.LOGGER.debugf(
                                "Raw selection of plural attribute not supported by JPA: %s.  Use `value(%s)` or `key(%s)` to indicate what part of the collection to select",
                                sqmPath.getAlias(),
                                sqmPath.getAlias(),
                                sqmPath.getAlias()
                        );
                    }

                    final SqmPath elementPath = sqmPath.resolvePathPart( CollectionPart.Nature.ELEMENT.getName(), true, this );
                    processingStateStack.getCurrent().getPathRegistry().register( elementPath );
                    return elementPath;
                }
            }

            return sqmExpression;
        }
        return (SqmSelectableNode) subCtx.accept( this );
    }

    @Override
    public SqmDynamicInstantiation visitInstantiation(HqlParser.InstantiationContext ctx) {
        final SqmDynamicInstantiation dynamicInstantiation;
        final ParseTree instantiationTarget = ctx.instantiationTarget().getChild( 0 );
        if ( instantiationTarget instanceof HqlParser.SimplePathContext ) {
            final String className = instantiationTarget.getText();
            try {
                final JavaType jtd = resolveInstantiationTargetJtd( className );
                dynamicInstantiation = SqmDynamicInstantiation.forClassInstantiation(
                        jtd,
                        creationContext.getNodeBuilder()
                );
            }
            catch (ClassLoadingException e) {
                throw new SemanticException( "Could not resolve class '" + className + "' named for instantiation" );
            }
        }
        else {
            final TerminalNode terminalNode = (TerminalNode) instantiationTarget;
            switch ( terminalNode.getSymbol().getType() ) {
                case HqlParser.MAP:
                    dynamicInstantiation = SqmDynamicInstantiation.forMapInstantiation(
                            mapJavaType,
                            creationContext.getNodeBuilder()
                    );
                    break;
                case HqlParser.LIST:
                    dynamicInstantiation = SqmDynamicInstantiation.forListInstantiation(
                            listJavaType,
                            creationContext.getNodeBuilder()
                    );
                    break;
                default:
                    throw new UnsupportedOperationException( "Unsupported instantiation target: " + terminalNode );
            }
        }

        for ( HqlParser.InstantiationArgumentContext arg : ctx.instantiationArguments().instantiationArgument() ) {
            dynamicInstantiation.addArgument( visitInstantiationArgument( arg ) );
        }

        return dynamicInstantiation;
    }

    private JavaType resolveInstantiationTargetJtd(String className) {
        final Class targetJavaType = classForName( creationContext.getJpaMetamodel().qualifyImportableName( className ) );
        return creationContext.getJpaMetamodel()
                .getTypeConfiguration()
                .getJavaTypeRegistry()
                .resolveDescriptor( targetJavaType );
    }

    private Class classForName(String className) {
        return creationContext.getServiceRegistry().getService( ClassLoaderService.class ).classForName( className );
    }

    @Override
    public SqmDynamicInstantiationArgument visitInstantiationArgument(HqlParser.InstantiationArgumentContext ctx) {
        final String alias;
        if ( ctx.getChildCount() > 1 ) {
            alias = visitVariable( (HqlParser.VariableContext) ctx.getChild( ctx.getChildCount() - 1 ) );
        }
        else {
            alias = null;
        }

        final SqmSelectableNode argExpression = (SqmSelectableNode) ctx.getChild( 0 ).accept( this );

        final SqmDynamicInstantiationArgument argument = new SqmDynamicInstantiationArgument<>(
                argExpression,
                alias,
                creationContext.getNodeBuilder()
        );

        //noinspection StatementWithEmptyBody
        if ( argExpression instanceof SqmDynamicInstantiation ) {
            // nothing else to do (avoid kludgy `! ( instanceof )` syntax
        }
        else {
            getCurrentProcessingState().getPathRegistry().register( argument );
        }

        return argument;
    }

    @Override
    public SqmPath visitJpaSelectObjectSyntax(HqlParser.JpaSelectObjectSyntaxContext ctx) {
        final String alias = ctx.getChild( 2 ).getText();
        final SqmFrom sqmFromByAlias = processingStateStack.getCurrent().getPathRegistry().findFromByAlias(
                alias,
                true
        );
        if ( sqmFromByAlias == null ) {
            throw new SemanticException( "Unable to resolve alias [" +  alias + "] in selection [" + ctx.getText() + "]" );
        }
        return sqmFromByAlias;
    }

    @Override
    public List> visitGroupByClause(HqlParser.GroupByClauseContext ctx) {
        final int size = ctx.getChildCount();
        // Shift 1 bit instead of division by 2
        final int estimateExpressionsCount = ( size >> 1 ) - 1;
        final List> expressions = new ArrayList<>( estimateExpressionsCount );
        for ( int i = 0; i < size; i++ ) {
            final ParseTree parseTree = ctx.getChild( i );
            if ( parseTree instanceof HqlParser.GroupByExpressionContext ) {
                expressions.add( (SqmExpression) parseTree.accept( this ) );
            }
        }
        return expressions;
    }

    private SqmExpression resolveOrderByOrGroupByExpression(
            ParseTree child,
            boolean definedCollate,
            boolean allowPositionalOrAliases) {
        final SqmCreationProcessingState processingState = getCurrentProcessingState();
        final SqmQuery processingQuery = processingState.getProcessingQuery();
        final SqmQueryPart queryPart;
        if ( processingQuery instanceof SqmInsertSelectStatement ) {
            queryPart = ( (SqmInsertSelectStatement) processingQuery ).getSelectQueryPart();
        }
        else if ( processingQuery instanceof SqmSelectQuery ) {
            queryPart = ( (SqmSelectQuery) processingQuery ).getQueryPart();
        }
        else {
            queryPart = null;
        }
        if ( child instanceof TerminalNode ) {
            if ( definedCollate ) {
                // This is syntactically disallowed
                throw new ParsingException( "COLLATE is not allowed for position based order-by or group-by items" );
            }
            else if ( !allowPositionalOrAliases ) {
                // This is syntactically disallowed
                throw new ParsingException( "Position based order-by is not allowed in OVER or WITHIN GROUP clauses" );
            }

            final int position = Integer.parseInt( child.getText() );

            // make sure this selection exists
            final SqmAliasedNode nodeByPosition;
            if ( queryPart instanceof SqmQueryGroup ) {
                final List> selections = queryPart.getFirstQuerySpec().getSelectClause().getSelections();
                nodeByPosition = position <= selections.size() ? selections.get( position - 1 ) : null;
            }
            else {
                nodeByPosition = processingState.getPathRegistry().findAliasedNodeByPosition( position );
            }
            if ( nodeByPosition == null ) {
                throw new ParsingException( "Numeric literal '" + position + "' used in group-by does not match a registered select-item" );
            }

            return new SqmAliasedNodeRef( position, integerDomainType, creationContext.getNodeBuilder() );
        }
        else if ( child instanceof HqlParser.IdentifierContext ) {
            final String identifierText = visitIdentifier( (HqlParser.IdentifierContext) child );
            if ( queryPart instanceof SqmQueryGroup ) {
                // If the current query part is a query group, check if the text matches
                // an attribute name of one of the selected SqmFrom elements or the path source name of a SqmPath
                SqmFrom found = null;
                int sqmPosition = 0;
                final List> selections = queryPart.getFirstQuerySpec().getSelectClause().getSelections();
                for ( int i = 0; i < selections.size(); i++ ) {
                    final SqmSelection sqmSelection = selections.get( i );
                    if ( identifierText.equals( sqmSelection.getAlias() ) ) {
                        return new SqmAliasedNodeRef(
                                i + 1,
                                creationContext.getNodeBuilder().getIntegerType(),
                                creationContext.getNodeBuilder()
                        );
                    }
                    final SqmSelectableNode selectableNode = sqmSelection.getSelectableNode();
                    if ( selectableNode instanceof SqmFrom ) {
                        final SqmFrom fromElement = (SqmFrom) selectableNode;
                        final SqmPathSource pathSource = fromElement.getReferencedPathSource();
                        if ( pathSource.findSubPathSource( identifierText ) != null ) {
                            if ( sqmPosition != 0 ) {
                                throw new IllegalStateException(
                                        "Multiple from-elements expose unqualified attribute : " + identifierText );
                            }
                            found = fromElement;
                            sqmPosition = i + 1;
                        }
                    }
                    else if ( selectableNode instanceof SqmPath ) {
                        final SqmPath path = (SqmPath) selectableNode;
                        if ( identifierText.equals( path.getReferencedPathSource().getPathName() ) ) {
                            if ( sqmPosition != 0 ) {
                                throw new IllegalStateException(
                                        "Multiple from-elements expose unqualified attribute : " + identifierText );
                            }
                            sqmPosition = i + 1;
                        }
                    }
                }
                if ( found != null ) {
                    return new SqmAliasedNodeRef(
                            sqmPosition,
                            found.get( identifierText ).getNavigablePath(),
                            creationContext.getNodeBuilder().getIntegerType(),
                            creationContext.getNodeBuilder()
                    );
                }
                else if ( sqmPosition != 0 ) {
                    return new SqmAliasedNodeRef(
                            sqmPosition,
                            creationContext.getNodeBuilder().getIntegerType(),
                            creationContext.getNodeBuilder()
                    );
                }
            }
            else {
                final Integer correspondingPosition = allowPositionalOrAliases ?
                        processingState.getPathRegistry().findAliasedNodePosition( identifierText ) :
                        null;
                if ( correspondingPosition != null ) {
                    if ( definedCollate ) {
                        // This is syntactically disallowed
                        throw new ParsingException( "COLLATE is not allowed for alias based order-by or group-by items" );
                    }
                    return new SqmAliasedNodeRef(
                            correspondingPosition,
                            integerDomainType,
                            creationContext.getNodeBuilder()
                    );
                }

                final SqmFrom sqmFrom = processingState.getPathRegistry().findFromByAlias(
                        identifierText,
                        true
                );
                if ( sqmFrom != null ) {
                    if ( definedCollate ) {
                        // This is syntactically disallowed
                        throw new ParsingException( "COLLATE is not allowed for alias based order-by or group-by items" );
                    }
                    // this will group-by all of the sub-parts in the from-element's model part
                    return sqmFrom;
                }

                final DotIdentifierConsumer dotIdentifierConsumer = dotIdentifierConsumerStack.getCurrent();
                dotIdentifierConsumer.consumeIdentifier( identifierText, true, true );
                return (SqmExpression) dotIdentifierConsumer.getConsumedPart();
            }
        }

        return (SqmExpression) child.accept( this );
    }

    @Override
    public SqmExpression visitGroupByExpression(HqlParser.GroupByExpressionContext ctx) {
        return resolveOrderByOrGroupByExpression( ctx.getChild( 0 ), ctx.getChildCount() > 1, true );
    }

    @Override
    public SqmPredicate visitHavingClause(HqlParser.HavingClauseContext ctx) {
        return (SqmPredicate) ctx.getChild( 1 ).accept( this );
    }

    @Override
    public SqmOrderByClause visitOrderByClause(HqlParser.OrderByClauseContext ctx) {
        return visitOrderByClause( ctx, true );
    }

    private SqmOrderByClause visitOrderByClause(HqlParser.OrderByClauseContext ctx, boolean allowPositionalOrAliases) {
        final int size = ctx.getChildCount();
        // Shift 1 bit instead of division by 2
        final int estimateExpressionsCount = ( size >> 1 ) - 1;
        final SqmOrderByClause orderByClause = new SqmOrderByClause( estimateExpressionsCount );
        for ( int i = 0; i < size; i++ ) {
            final ParseTree parseTree = ctx.getChild( i );
            if ( parseTree instanceof HqlParser.SortSpecificationContext ) {
                orderByClause.addSortSpecification( visitSortSpecification(
                        (HqlParser.SortSpecificationContext) parseTree,
                        allowPositionalOrAliases
                ) );
            }
        }
        return orderByClause;
    }

    @Override
    public SqmSortSpecification visitSortSpecification(HqlParser.SortSpecificationContext ctx) {
        return visitSortSpecification( ctx, true );
    }

    private SqmSortSpecification visitSortSpecification(
            HqlParser.SortSpecificationContext ctx,
            boolean allowPositionalOrAliases) {
        final SqmExpression sortExpression = visitSortExpression(
                (HqlParser.SortExpressionContext) ctx.getChild( 0 ),
                allowPositionalOrAliases
        );
        if ( sortExpression == null ) {
            throw new ParsingException( "Could not resolve sort-expression : " + ctx.getChild( 0 ).getText() );
        }
        if ( sortExpression instanceof SqmLiteral || sortExpression instanceof SqmParameter ) {
            HqlLogging.QUERY_LOGGER.debugf( "Questionable sorting by constant value : %s", sortExpression );
        }

        final SortOrder sortOrder;
        final NullPrecedence nullPrecedence;
        int nextIndex = 1;
        if ( nextIndex < ctx.getChildCount() ) {
            ParseTree parseTree = ctx.getChild( nextIndex );
            if ( parseTree instanceof HqlParser.SortDirectionContext ) {
                switch ( ( (TerminalNode) parseTree.getChild( 0 ) ).getSymbol().getType() ) {
                    case HqlParser.ASC:
                        sortOrder = SortOrder.ASCENDING;
                        break;
                    case HqlParser.DESC:
                        sortOrder = SortOrder.DESCENDING;
                        break;
                    default:
                        throw new SemanticException( "Unrecognized sort ordering: " + parseTree.getText() );
                }
                nextIndex++;
            }
            else {
                sortOrder = SortOrder.ASCENDING;
            }
            parseTree = ctx.getChild( nextIndex );
            if ( parseTree instanceof HqlParser.NullsPrecedenceContext ) {
                switch ( ( (TerminalNode) parseTree.getChild( 1 ) ).getSymbol().getType() ) {
                    case HqlParser.FIRST:
                        nullPrecedence = NullPrecedence.FIRST;
                        break;
                    case HqlParser.LAST:
                        nullPrecedence = NullPrecedence.LAST;
                        break;
                    default:
                        throw new SemanticException( "Unrecognized null precedence: " + parseTree.getText() );
                }
            }
            else {
                nullPrecedence = NullPrecedence.NONE;
            }
        }
        else {
            sortOrder = SortOrder.ASCENDING;
            nullPrecedence = NullPrecedence.NONE;
        }

        return new SqmSortSpecification( sortExpression, sortOrder, nullPrecedence );
    }

    @Override
    public SqmExpression visitSortExpression(HqlParser.SortExpressionContext ctx) {
        return visitSortExpression( ctx, true );
    }

    public SqmExpression visitSortExpression(HqlParser.SortExpressionContext ctx, boolean allowPositionalOrAliases) {
        return resolveOrderByOrGroupByExpression( ctx.getChild( 0 ), ctx.getChildCount() > 1, allowPositionalOrAliases );
    }

    private SqmQuerySpec currentQuerySpec() {
        SqmQuery processingQuery = processingStateStack.getCurrent().getProcessingQuery();
        if ( processingQuery instanceof SqmInsertSelectStatement ) {
            return ( (SqmInsertSelectStatement) processingQuery ).getSelectQueryPart().getLastQuerySpec();
        }
        else {
            return ( (SqmSelectQuery) processingQuery ).getQueryPart().getLastQuerySpec();
        }
    }

    private  void setCurrentQueryPart(SqmQueryPart queryPart) {
        @SuppressWarnings("unchecked")
        final SqmQuery processingQuery = (SqmQuery) processingStateStack.getCurrent().getProcessingQuery();
        if ( processingQuery instanceof SqmInsertSelectStatement ) {
            ( (SqmInsertSelectStatement) processingQuery ).setSelectQueryPart( queryPart );
        }
        else {
            ( (AbstractSqmSelectQuery) processingQuery ).setQueryPart( queryPart );
        }
    }

    @Override
    public SqmExpression visitLimitClause(HqlParser.LimitClauseContext ctx) {
        if ( ctx == null ) {
            return null;
        }

        return (SqmExpression) ctx.getChild( 1 ).accept( this );
    }

    @Override
    public SqmExpression visitOffsetClause(HqlParser.OffsetClauseContext ctx) {
        if ( ctx == null ) {
            return null;
        }

        return (SqmExpression) ctx.getChild( 1 ).accept( this );
    }

    @Override
    public SqmExpression visitFetchClause(HqlParser.FetchClauseContext ctx) {
        if ( ctx == null ) {
            return null;
        }

        return (SqmExpression) ctx.getChild( 2 ).accept( this );
    }

    private FetchClauseType visitFetchClauseType(HqlParser.FetchClauseContext ctx) {
        if ( ctx == null ) {
            return FetchClauseType.ROWS_ONLY;
        }
        final int thirdSymbolType = ( (TerminalNode) ctx.getChild( 3 ) ).getSymbol().getType();
        final int lastSymbolType = ( (TerminalNode) ctx.getChild( ctx.getChildCount() - 1 ) ).getSymbol().getType();
        if ( lastSymbolType == HqlParser.TIES ) {
            return thirdSymbolType == HqlParser.PERCENT ? FetchClauseType.PERCENT_WITH_TIES : FetchClauseType.ROWS_WITH_TIES;
        }
        else {
            return thirdSymbolType == HqlParser.PERCENT ? FetchClauseType.PERCENT_ONLY : FetchClauseType.ROWS_ONLY;
        }
    }

    @Override
    public Object visitSyntacticPathExpression(HqlParser.SyntacticPathExpressionContext ctx) {
        SemanticPathPart part = visitSyntacticDomainPath( (HqlParser.SyntacticDomainPathContext) ctx.getChild( 0 ) );
        if ( ctx.getChildCount() == 2 ) {
            dotIdentifierConsumerStack.push(
                    new BasicDotIdentifierConsumer( part, this ) {
                        @Override
                        protected void reset() {
                        }
                    }
            );
            try {
                part = (SemanticPathPart) ctx.getChild( 1 ).accept( this );
            }
            finally {
                dotIdentifierConsumerStack.pop();
            }
        }
        if ( part instanceof DomainPathPart ) {
            return ( (DomainPathPart) part ).getSqmExpression();
        }
        return part;
    }

    @Override
    public Object visitGeneralPathExpression(HqlParser.GeneralPathExpressionContext ctx) {
        final SemanticPathPart part = visitGeneralPathFragment( (HqlParser.GeneralPathFragmentContext) ctx.getChild( 0 ) );
        if ( part instanceof DomainPathPart ) {
            return ( (DomainPathPart) part ).getSqmExpression();
        }
        return part;
    }

    @Override
    public SqmExpression visitFunctionExpression(HqlParser.FunctionExpressionContext ctx) {
        return (SqmExpression) ctx.getChild( 0 ).accept( this );
    }

    @Override
    public SqmExpression visitParameterOrIntegerLiteral(HqlParser.ParameterOrIntegerLiteralContext ctx) {
        final ParseTree firstChild = ctx.getChild( 0 );
        if ( firstChild instanceof TerminalNode ) {
            return integerLiteral( firstChild.getText() );
        }
        return (SqmExpression) firstChild.accept( this );
    }

    @Override
    public SqmExpression visitParameterOrNumberLiteral(HqlParser.ParameterOrNumberLiteralContext ctx) {
        if ( ctx.INTEGER_LITERAL() != null ) {
            return integerLiteral( ctx.INTEGER_LITERAL().getText() );
        }
        if ( ctx.FLOAT_LITERAL() != null ) {
            return floatLiteral( ctx.FLOAT_LITERAL().getText() );
        }
        if ( ctx.DOUBLE_LITERAL() != null ) {
            return doubleLiteral( ctx.DOUBLE_LITERAL().getText() );
        }
        if ( ctx.parameter() != null ) {
            return (SqmExpression) ctx.parameter().accept( this );
        }
        final ParseTree firstChild = ctx.getChild( 0 );
        if ( firstChild instanceof TerminalNode ) {
            switch ( ( (TerminalNode) firstChild ).getSymbol().getType() ) {
                case HqlParser.INTEGER_LITERAL:
                    return integerLiteral( firstChild.getText() );
                case HqlParser.FLOAT_LITERAL:
                    return floatLiteral( firstChild.getText() );
                case HqlParser.DOUBLE_LITERAL:
                    return doubleLiteral( firstChild.getText() );
                default:
                    throw new UnsupportedOperationException( "Unsupported literal: " + firstChild.getText() );
            }
        }
        return (SqmExpression) firstChild.accept( this );
    }

    public String getEntityName(HqlParser.EntityNameContext parserEntityName) {
        final StringBuilder sb = new StringBuilder();
        final int end = parserEntityName.getChildCount();
        sb.append( visitIdentifier( (HqlParser.IdentifierContext) parserEntityName.getChild( 0 ) ) );
        for ( int i = 2; i < end; i += 2 ) {
            sb.append( '.' );
            sb.append( visitIdentifier( (HqlParser.IdentifierContext) parserEntityName.getChild( i ) ) );
        }
        return sb.toString();
    }

    @Override
    public String visitIdentifier(HqlParser.IdentifierContext ctx) {
        final ParseTree child = ctx.getChild( 0 );
        if ( child instanceof TerminalNode ) {
            return child.getText();
        }
        return visitNakedIdentifier( (HqlParser.NakedIdentifierContext) child );
    }

    @Override
    public String visitNakedIdentifier(HqlParser.NakedIdentifierContext ctx) {
        final TerminalNode node = (TerminalNode) ctx.getChild( 0 );
        if ( node.getSymbol().getType() == HqlParser.QUOTED_IDENTIFIER ) {
            return QuotingHelper.unquoteIdentifier( node.getText() );
        }
        return node.getText();
    }

    @Override
    public EntityDomainType visitEntityName(HqlParser.EntityNameContext parserEntityName) {
        final String entityName = getEntityName( parserEntityName );
        final EntityDomainType entityReference = getCreationContext()
                .getJpaMetamodel()
                .getHqlEntityReference( entityName );
        if ( entityReference == null ) {
            throw new UnknownEntityException( "Could not resolve target entity '" + entityName + "'", entityName );
        }
        checkFQNEntityNameJpaComplianceViolationIfNeeded( entityName, entityReference );
        if ( entityReference instanceof SqmPolymorphicRootDescriptor && getCreationOptions().useStrictJpaCompliance() ) {
            throw new StrictJpaComplianceViolation(
                    "Encountered the use of a non entity name [" + entityName + "], " +
                            "but strict JPQL compliance was requested which doesn't allow this",
                    StrictJpaComplianceViolation.Type.NON_ENTITY_NAME
            );
        }
        return entityReference;
    }

    @Override
    public SqmFromClause visitFromClause(HqlParser.FromClauseContext parserFromClause) {
        final SqmFromClause fromClause;
        if ( parserFromClause == null ) {
            fromClause = new SqmFromClause();
            currentQuerySpec().setFromClause( fromClause );
        }
        else {
            final int size = parserFromClause.getChildCount();
            // Shift 1 bit instead of division by 2
            final int estimatedSize = size >> 1;
            fromClause = new SqmFromClause( estimatedSize );
            currentQuerySpec().setFromClause( fromClause );
            for ( int i = 0; i < size; i++ ) {
                final ParseTree parseTree = parserFromClause.getChild( i );
                if ( parseTree instanceof HqlParser.EntityWithJoinsContext ) {
                    visitEntityWithJoins( (HqlParser.EntityWithJoinsContext) parseTree );
                }
            }
        }
        return fromClause;
    }

    @Override
    public SqmRoot visitEntityWithJoins(HqlParser.EntityWithJoinsContext parserSpace) {
        final SqmRoot sqmRoot = (SqmRoot) parserSpace.getChild( 0 ).accept( this );
        final SqmFromClause fromClause = currentQuerySpec().getFromClause();
        // Correlations are implicitly added to the from clause
        if ( !( sqmRoot instanceof SqmCorrelation ) ) {
            fromClause.addRoot( sqmRoot );
        }
        final int size = parserSpace.getChildCount();
        for ( int i = 1; i < size; i++ ) {
            final ParseTree parseTree = parserSpace.getChild( i );
            if ( parseTree instanceof HqlParser.CrossJoinContext ) {
                consumeCrossJoin( (HqlParser.CrossJoinContext) parseTree, sqmRoot );
            }
            else if ( parseTree instanceof HqlParser.JoinContext ) {
                consumeJoin( (HqlParser.JoinContext) parseTree, sqmRoot );
            }
            else if ( parseTree instanceof HqlParser.JpaCollectionJoinContext ) {
                consumeJpaCollectionJoin( (HqlParser.JpaCollectionJoinContext) parseTree, sqmRoot );
            }
        }

        return sqmRoot;
    }

    @Override
    @SuppressWarnings( { "rawtypes", "unchecked" } )
    public SqmRoot visitRootEntity(HqlParser.RootEntityContext ctx) {
        final HqlParser.EntityNameContext entityNameContext = (HqlParser.EntityNameContext) ctx.getChild( 0 );
        final List entityNameParseTreeChildren = entityNameContext.children;
        final String name = getEntityName( entityNameContext );

        log.debugf( "Handling root path - %s", name );
        final EntityDomainType entityDescriptor = getCreationContext()
                .getJpaMetamodel()
                .getHqlEntityReference( name );

        final HqlParser.VariableContext identificationVariableDefContext;
        if ( ctx.getChildCount() > 1 ) {
            identificationVariableDefContext = (HqlParser.VariableContext) ctx.getChild( 1 );
        }
        else {
            identificationVariableDefContext = null;
        }
        final String alias = applyJpaCompliance(
                visitVariable( identificationVariableDefContext )
        );

        final SqmCreationProcessingState processingState = processingStateStack.getCurrent();
        final SqmPathRegistry pathRegistry = processingState.getPathRegistry();
        if ( entityDescriptor == null ) {
            final int size = entityNameParseTreeChildren.size();
            // Handle the use of a correlation path in subqueries
            if ( processingStateStack.depth() > 1 && size > 2 ) {
                final String parentAlias = entityNameParseTreeChildren.get( 0 ).getText();
                final AbstractSqmFrom correlation = processingState.getPathRegistry()
                        .findFromByAlias( parentAlias, true );
                if ( correlation instanceof SqmCorrelation ) {
                    final DotIdentifierConsumer dotIdentifierConsumer = new QualifiedJoinPathConsumer(
                            correlation,
                            SqmJoinType.INNER,
                            false,
                            alias,
                            this
                    );
                    final int lastIdx = size - 1;
                    for ( int i = 2; i != lastIdx; i += 2 ) {
                        dotIdentifierConsumer.consumeIdentifier(
                                entityNameParseTreeChildren.get( i ).getText(),
                                false,
                                false
                        );
                    }
                    dotIdentifierConsumer.consumeIdentifier(
                            entityNameParseTreeChildren.get( lastIdx ).getText(),
                            false,
                            true
                    );
                    return ( (SqmCorrelation) correlation ).getCorrelatedRoot();
                }
                throw new SemanticException( "Could not resolve entity or correlation path '" + name + "'" );
            }
            final SqmCteStatement cteStatement = findCteStatement( name );
            if ( cteStatement != null ) {
                final SqmCteRoot root = new SqmCteRoot<>( cteStatement, alias );
                pathRegistry.register( root );
                return root;
            }
            throw new UnknownEntityException( "Could not resolve root entity '" + name + "'", name );
        }
        checkFQNEntityNameJpaComplianceViolationIfNeeded( name, entityDescriptor );

        if ( entityDescriptor instanceof SqmPolymorphicRootDescriptor ) {
            if ( getCreationOptions().useStrictJpaCompliance() ) {
                throw new StrictJpaComplianceViolation(
                        "Encountered unmapped polymorphic reference [" + entityDescriptor.getHibernateEntityName()
                                + "], but strict JPQL compliance was requested",
                        StrictJpaComplianceViolation.Type.UNMAPPED_POLYMORPHISM
                );
            }

            if ( processingStateStack.depth() > 1 ) {
                throw new SemanticException(
                        "Illegal implicit-polymorphic domain path in subquery '" + entityDescriptor.getName() +"'"
                );
            }
        }

        final SqmRoot sqmRoot = new SqmRoot<>( entityDescriptor, alias, true, creationContext.getNodeBuilder() );

        pathRegistry.register( sqmRoot );

        return sqmRoot;
    }

    @Override
    public SqmCteStatement findCteStatement(String name) {
        if ( currentPotentialRecursiveCte != null && name.equals( currentPotentialRecursiveCte.getName() ) ) {
            return (SqmCteStatement) currentPotentialRecursiveCte;
        }
        return processingStateStack.findCurrentFirstWithParameter( name, SemanticQueryBuilder::matchCteStatement );
    }

    private static SqmCteStatement matchCteStatement(SqmCreationProcessingState state, String n) {
        if ( state.getProcessingQuery() instanceof SqmCteContainer ) {
            final SqmCteContainer container = (SqmCteContainer) state.getProcessingQuery();
            return container.getCteStatement( n );
        }
        return null;
    }

    @Override
    public SqmRoot visitRootSubquery(HqlParser.RootSubqueryContext ctx) {
        if ( getCreationOptions().useStrictJpaCompliance() ) {
            throw new StrictJpaComplianceViolation(
                    "The JPA specification does not support subqueries in the from clause. " +
                            "Please disable the JPA query compliance if you want to use this feature.",
                    StrictJpaComplianceViolation.Type.FROM_SUBQUERY
            );
        }

        final SqmSubQuery subQuery = (SqmSubQuery) ctx.getChild(1).accept( this );

        final ParseTree lastChild = ctx.getChild( ctx.getChildCount() - 1 );
        final HqlParser.VariableContext identificationVariableDefContext;
        if ( lastChild instanceof HqlParser.VariableContext ) {
            identificationVariableDefContext = (HqlParser.VariableContext) lastChild;
        }
        else {
            identificationVariableDefContext = null;
        }
        final String alias = applyJpaCompliance(
                visitVariable( identificationVariableDefContext )
        );

        final SqmCreationProcessingState processingState = processingStateStack.getCurrent();
        final SqmPathRegistry pathRegistry = processingState.getPathRegistry();
        final SqmRoot sqmRoot = new SqmDerivedRoot<>( subQuery, alias );

        pathRegistry.register( sqmRoot );

        return sqmRoot;
    }

    @Override
    public String visitVariable(HqlParser.VariableContext ctx) {
        if ( ctx == null ) {
            return null;
        }
        final ParseTree lastChild = ctx.getChild( ctx.getChildCount() - 1 );
        if ( lastChild instanceof HqlParser.IdentifierContext ) {
            final HqlParser.IdentifierContext identifierContext = (HqlParser.IdentifierContext) lastChild;
            // in this branch, the alias could be a reserved word ("keyword as identifier")
            // which JPA disallows...
            if ( getCreationOptions().useStrictJpaCompliance() ) {
                final Token identificationVariableToken = identifierContext.getStart();
                if ( identificationVariableToken.getType() != IDENTIFIER ) {
                    throw new StrictJpaComplianceViolation(
                            String.format(
                                    Locale.ROOT,
                                    "Strict JPQL compliance was violated : %s [%s]",
                                    StrictJpaComplianceViolation.Type.RESERVED_WORD_USED_AS_ALIAS.description(),
                                    identificationVariableToken.getText()
                            ),
                            StrictJpaComplianceViolation.Type.RESERVED_WORD_USED_AS_ALIAS
                    );
                }
            }
            return visitIdentifier( identifierContext );
        }
        else {
            final HqlParser.NakedIdentifierContext identifierContext = (HqlParser.NakedIdentifierContext) lastChild;
            // in this branch, the alias could be a reserved word ("keyword as identifier")
            // which JPA disallows...
            if ( getCreationOptions().useStrictJpaCompliance() ) {
                final Token identificationVariableToken = identifierContext.getStart();
                if ( identificationVariableToken.getType() != IDENTIFIER ) {
                    throw new StrictJpaComplianceViolation(
                            String.format(
                                    Locale.ROOT,
                                    "Strict JPQL compliance was violated : %s [%s]",
                                    StrictJpaComplianceViolation.Type.RESERVED_WORD_USED_AS_ALIAS.description(),
                                    identificationVariableToken.getText()
                            ),
                            StrictJpaComplianceViolation.Type.RESERVED_WORD_USED_AS_ALIAS
                    );
                }
            }
            return visitNakedIdentifier( identifierContext );
        }
    }

    private String applyJpaCompliance(String text) {
        if ( text == null ) {
            return null;
        }

        if ( getCreationOptions().useStrictJpaCompliance() ) {
            return text.toLowerCase( Locale.getDefault() );
        }

        return text;
    }

    @Override
    public final SqmCrossJoin visitCrossJoin(HqlParser.CrossJoinContext ctx) {
        throw new UnsupportedOperationException( "Unexpected call to #visitCrossJoin, see #consumeCrossJoin" );
    }

    private  void consumeCrossJoin(HqlParser.CrossJoinContext parserJoin, SqmRoot sqmRoot) {
        final HqlParser.EntityNameContext entityNameContext = (HqlParser.EntityNameContext) parserJoin.getChild( 2 );
        final String name = getEntityName( entityNameContext );

        SqmTreeCreationLogger.LOGGER.debugf( "Handling root path - %s", name );

        final EntityDomainType entityDescriptor = getCreationContext().getJpaMetamodel()
                .resolveHqlEntityReference( name );

        if ( entityDescriptor instanceof SqmPolymorphicRootDescriptor ) {
            throw new SemanticException( "Unmapped polymorphic reference cannot be used as a CROSS JOIN target" );
        }
        final HqlParser.VariableContext identificationVariableDefContext;
        if ( parserJoin.getChildCount() > 3 ) {
            identificationVariableDefContext = (HqlParser.VariableContext) parserJoin.getChild( 3 );
        }
        else {
            identificationVariableDefContext = null;
        }
        final SqmCrossJoin join = new SqmCrossJoin<>(
                entityDescriptor,
                visitVariable( identificationVariableDefContext ),
                sqmRoot
        );

        processingStateStack.getCurrent().getPathRegistry().register( join );

        // CROSS joins are always added to the root
        sqmRoot.addSqmJoin( join );
    }

    @Override
    public final SqmJoin visitJoin(HqlParser.JoinContext parserJoin) {
        throw new UnsupportedOperationException( "Unexpected call to #visitJoin, see #consumeJoin" );
    }

    protected  void consumeJoin(HqlParser.JoinContext parserJoin, SqmRoot sqmRoot) {
        final SqmJoinType joinType;
        final int firstJoinTypeSymbolType;
        if ( parserJoin.getChild( 0 ) instanceof HqlParser.JoinTypeContext
                && parserJoin.getChild( 0 ).getChildCount() != 0 ) {
            firstJoinTypeSymbolType = ( (TerminalNode) parserJoin.getChild( 0 ).getChild( 0 ) ).getSymbol().getType();
        }
        else {
            firstJoinTypeSymbolType = HqlParser.INNER;
        }
        switch ( firstJoinTypeSymbolType ) {
            case HqlParser.FULL:
                joinType = SqmJoinType.FULL;
                break;
            case HqlParser.RIGHT:
                joinType = SqmJoinType.RIGHT;
                break;
            // For some reason, we also support `outer join` syntax..
            case HqlParser.OUTER:
            case HqlParser.LEFT:
                joinType = SqmJoinType.LEFT;
                break;
            default:
                joinType = SqmJoinType.INNER;
                break;
        }

        final HqlParser.JoinTargetContext qualifiedJoinTargetContext = parserJoin.joinTarget();
        final ParseTree lastChild = qualifiedJoinTargetContext.getChild( qualifiedJoinTargetContext.getChildCount() - 1 );
        final HqlParser.VariableContext identificationVariableDefContext;
        if ( lastChild instanceof HqlParser.VariableContext ) {
            identificationVariableDefContext = (HqlParser.VariableContext) lastChild;
        }
        else {
            identificationVariableDefContext = null;
        }
        final String alias = visitVariable( identificationVariableDefContext );
        final boolean fetch = parserJoin.getChild( 2 ) instanceof TerminalNode;

        if ( fetch && processingStateStack.depth() > 1 ) {
            throw new SemanticException( "fetch not allowed in subquery from-elements" );
        }

        dotIdentifierConsumerStack.push(
                new QualifiedJoinPathConsumer(
                        sqmRoot,
                        joinType,
                        fetch,
                        alias,
                        this
                )
        );
        try {
            final SqmQualifiedJoin join;
            if ( qualifiedJoinTargetContext instanceof HqlParser.JoinPathContext ) {
                //noinspection unchecked
                join = (SqmQualifiedJoin) qualifiedJoinTargetContext.getChild( 0 ).accept( this );
            }
            else {
                if ( fetch ) {
                    throw new SemanticException( "fetch not allowed for subquery join" );
                }
                if ( getCreationOptions().useStrictJpaCompliance() ) {
                    throw new StrictJpaComplianceViolation(
                            "The JPA specification does not support subqueries in the from clause. " +
                                    "Please disable the JPA query compliance if you want to use this feature.",
                            StrictJpaComplianceViolation.Type.FROM_SUBQUERY
                    );
                }
                final TerminalNode terminalNode = (TerminalNode) qualifiedJoinTargetContext.getChild( 0 );
                final boolean lateral = terminalNode.getSymbol().getType() == HqlParser.LATERAL;
                final int subqueryIndex = lateral ? 2 : 1;
                final DotIdentifierConsumer identifierConsumer = dotIdentifierConsumerStack.pop();
                final SqmSubQuery subQuery = (SqmSubQuery) qualifiedJoinTargetContext.getChild( subqueryIndex ).accept( this );
                dotIdentifierConsumerStack.push( identifierConsumer );
                //noinspection unchecked,rawtypes
                join = new SqmDerivedJoin( subQuery, alias, joinType, lateral, sqmRoot );
                processingStateStack.getCurrent().getPathRegistry().register( join );
            }

            final HqlParser.JoinRestrictionContext qualifiedJoinRestrictionContext = parserJoin.joinRestriction();
            if ( join instanceof SqmEntityJoin || join instanceof SqmDerivedJoin || join instanceof SqmCteJoin ) {
                sqmRoot.addSqmJoin( join );
            }
            else if ( join instanceof SqmAttributeJoin ) {
                final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) join;
                if ( getCreationOptions().useStrictJpaCompliance() ) {
                    if ( join.getExplicitAlias() != null ) {
                        if ( attributeJoin.isFetched() ) {
                            throw new StrictJpaComplianceViolation(
                                    "Encountered aliased fetch join, but strict JPQL compliance was requested",
                                    StrictJpaComplianceViolation.Type.ALIASED_FETCH_JOIN
                            );
                        }
                    }
                }
                if ( qualifiedJoinRestrictionContext != null && attributeJoin.isFetched() ) {
                    throw new SemanticException( "with-clause not allowed on fetched associations; use filters" );
                }
            }

            if ( qualifiedJoinRestrictionContext != null ) {
                dotIdentifierConsumerStack.push( new QualifiedJoinPredicatePathConsumer( join, this ) );
                try {
                    join.setJoinPredicate( (SqmPredicate) qualifiedJoinRestrictionContext.getChild( 1 ).accept( this ) );
                }
                finally {
                    dotIdentifierConsumerStack.pop();
                }
            }
        }
        finally {
            dotIdentifierConsumerStack.pop();
        }
    }

    @Override
    public SqmJoin visitJpaCollectionJoin(HqlParser.JpaCollectionJoinContext ctx) {
        throw new UnsupportedOperationException();
    }

    protected void consumeJpaCollectionJoin(
            HqlParser.JpaCollectionJoinContext ctx,
            SqmRoot sqmRoot) {
        final HqlParser.VariableContext identificationVariableDefContext;
        if ( ctx.getChildCount() > 5 ) {
            identificationVariableDefContext = (HqlParser.VariableContext) ctx.getChild( 5 );
        }
        else {
            identificationVariableDefContext = null;
        }
        final String alias = visitVariable( identificationVariableDefContext );
        dotIdentifierConsumerStack.push(
                new QualifiedJoinPathConsumer(
                        sqmRoot,
                        // According to JPA spec 4.4.6 this is an inner join
                        SqmJoinType.INNER,
                        false,
                        alias,
                        this
                )
        );

        try {
            consumePluralAttributeReference( (HqlParser.PathContext) ctx.getChild( 3 ) );
        }
        finally {
            dotIdentifierConsumerStack.pop();
        }
    }


    // Predicates (and `whereClause`)

    @Override
    public SqmPredicate visitWhereClause(HqlParser.WhereClauseContext ctx) {
        if ( ctx == null || ctx.getChildCount() != 2 ) {
            return null;
        }

        return (SqmPredicate) ctx.getChild( 1 ).accept( this );

    }

    @Override
    public SqmGroupedPredicate visitGroupedPredicate(HqlParser.GroupedPredicateContext ctx) {
        return new SqmGroupedPredicate(
                (SqmPredicate) ctx.getChild( 1 ).accept( this ),
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public SqmPredicate visitAndPredicate(HqlParser.AndPredicateContext ctx) {
        return junction(
                Predicate.BooleanOperator.AND,
                (SqmPredicate) ctx.getChild( 0 ).accept( this ),
                (SqmPredicate) ctx.getChild( 2 ).accept( this )
        );
    }

    @Override
    public SqmPredicate visitOrPredicate(HqlParser.OrPredicateContext ctx) {
        return junction(
                Predicate.BooleanOperator.OR,
                (SqmPredicate) ctx.getChild( 0 ).accept( this ),
                (SqmPredicate) ctx.getChild( 2 ).accept( this )
        );
    }

    private SqmPredicate junction(Predicate.BooleanOperator operator, SqmPredicate lhs, SqmPredicate rhs) {
        if ( lhs instanceof SqmJunctionPredicate ) {
            final SqmJunctionPredicate junction = (SqmJunctionPredicate) lhs;
            if ( junction.getOperator() == operator ) {
                junction.getPredicates().add( rhs );
                return junction;
            }
        }
        if ( rhs instanceof SqmJunctionPredicate ) {
            final SqmJunctionPredicate junction = (SqmJunctionPredicate) rhs;
            if ( junction.getOperator() == operator ) {
                junction.getPredicates().add( 0, lhs );
                return junction;
            }
        }
        return new SqmJunctionPredicate(
                operator,
                lhs,
                rhs,
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public SqmPredicate visitNegatedPredicate(HqlParser.NegatedPredicateContext ctx) {
        SqmPredicate predicate = (SqmPredicate) ctx.getChild( 1 ).accept( this );
        if ( predicate instanceof SqmNegatablePredicate ) {
            ( (SqmNegatablePredicate) predicate ).negate();
            return predicate;
        }
        else {
            return new SqmNegatedPredicate( predicate, creationContext.getNodeBuilder() );
        }
    }

    @Override
    public SqmBetweenPredicate visitBetweenPredicate(HqlParser.BetweenPredicateContext ctx) {
        final boolean negated = ( (TerminalNode) ctx.getChild( 1 ) ).getSymbol().getType() == HqlParser.NOT;
        final int startIndex = negated ? 3 : 2;
        return new SqmBetweenPredicate(
                (SqmExpression) ctx.getChild( 0 ).accept( this ),
                (SqmExpression) ctx.getChild( startIndex ).accept( this ),
                (SqmExpression) ctx.getChild( startIndex + 2 ).accept( this ),
                negated,
                creationContext.getNodeBuilder()
        );
    }


    @Override
    public SqmNullnessPredicate visitIsNullPredicate(HqlParser.IsNullPredicateContext ctx) {
        final boolean negated = ctx.getChildCount() == 4;
        return new SqmNullnessPredicate(
                (SqmExpression) ctx.getChild( 0 ).accept( this ),
                negated,
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public SqmEmptinessPredicate visitIsEmptyPredicate(HqlParser.IsEmptyPredicateContext ctx) {
        final boolean negated = ctx.getChildCount() == 4;
        return new SqmEmptinessPredicate(
                (SqmPluralValuedSimplePath) ctx.getChild( 0 ).accept( this ),
                negated,
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public Object visitComparisonOperator(HqlParser.ComparisonOperatorContext ctx) {
        final TerminalNode firstToken = (TerminalNode) ctx.getChild( 0 );
        switch ( firstToken.getSymbol().getType() ) {
            case HqlLexer.EQUAL:
                return ComparisonOperator.EQUAL;
            case HqlLexer.NOT_EQUAL:
                return ComparisonOperator.NOT_EQUAL;
            case HqlLexer.LESS:
                return ComparisonOperator.LESS_THAN;
            case HqlLexer.LESS_EQUAL:
                return ComparisonOperator.LESS_THAN_OR_EQUAL;
            case HqlLexer.GREATER:
                return ComparisonOperator.GREATER_THAN;
            case HqlLexer.GREATER_EQUAL:
                return ComparisonOperator.GREATER_THAN_OR_EQUAL;
            case HqlLexer.IS: {
                final TerminalNode secondToken = (TerminalNode) ctx.getChild( 1 );
                return secondToken.getSymbol().getType() == HqlLexer.NOT
                        ? ComparisonOperator.NOT_DISTINCT_FROM
                        : ComparisonOperator.DISTINCT_FROM;
            }
        }
        throw new QueryException("missing operator");
    }

    @Override
    public SqmPredicate visitComparisonPredicate(HqlParser.ComparisonPredicateContext ctx) {
        final ComparisonOperator comparisonOperator = (ComparisonOperator) ctx.getChild( 1 ).accept( this );
        final SqmExpression left;
        final SqmExpression right;
        final HqlParser.ExpressionContext leftExpressionContext = (HqlParser.ExpressionContext) ctx.getChild( 0 );
        final HqlParser.ExpressionContext rightExpressionContext = (HqlParser.ExpressionContext) ctx.getChild( 2 );
        switch (comparisonOperator) {
            case EQUAL:
            case NOT_EQUAL:
            case DISTINCT_FROM:
            case NOT_DISTINCT_FROM: {
                Map, Enum> possibleEnumValues;
                if ( ( possibleEnumValues = getPossibleEnumValues( leftExpressionContext ) ) != null ) {
                    right = (SqmExpression) rightExpressionContext.accept( this );
                    left = resolveEnumShorthandLiteral(
                            leftExpressionContext,
                            possibleEnumValues,
                            right.getJavaType()
                    );
                    break;
                }
                else if ( ( possibleEnumValues = getPossibleEnumValues( rightExpressionContext ) ) != null ) {
                    left = (SqmExpression) leftExpressionContext.accept( this );
                    right = resolveEnumShorthandLiteral(
                            rightExpressionContext,
                            possibleEnumValues,
                            left.getJavaType()
                    );
                    break;
                }
                final SqmExpression l = (SqmExpression) leftExpressionContext.accept( this );
                final SqmExpression r = (SqmExpression) rightExpressionContext.accept( this );
                if ( l instanceof AnyDiscriminatorSqmPath && r instanceof SqmLiteralEntityType ) {
                    left = l;
                    right = createDiscriminatorValue( (AnyDiscriminatorSqmPath) left, rightExpressionContext );
                }
                else if ( r instanceof AnyDiscriminatorSqmPath && l instanceof SqmLiteralEntityType ) {
                    left = createDiscriminatorValue( (AnyDiscriminatorSqmPath) r, leftExpressionContext );
                    right = r;
                }
                else {
                    left = l;
                    right = r;
                }

                // This is something that we used to support before 6 which is also used in our testsuite
                if ( left instanceof SqmLiteralNull ) {
                    return new SqmNullnessPredicate(
                            right,
                            comparisonOperator == ComparisonOperator.NOT_EQUAL
                                    || comparisonOperator == ComparisonOperator.DISTINCT_FROM,
                            creationContext.getNodeBuilder()
                    );
                }
                else if ( right instanceof SqmLiteralNull ) {
                    return new SqmNullnessPredicate(
                            left,
                            comparisonOperator == ComparisonOperator.NOT_EQUAL
                                    || comparisonOperator == ComparisonOperator.DISTINCT_FROM,
                            creationContext.getNodeBuilder()
                    );
                }
                break;
            }
            default: {
                left = (SqmExpression) leftExpressionContext.accept( this );
                right = (SqmExpression) rightExpressionContext.accept( this );
                break;
            }
        }
        ( (SqmCriteriaNodeBuilder) creationContext.getNodeBuilder() ).assertComparable( left, right );
        return new SqmComparisonPredicate(
                left,
                comparisonOperator,
                right,
                creationContext.getNodeBuilder()
        );
    }

    private  SqmExpression createDiscriminatorValue(
            AnyDiscriminatorSqmPath anyDiscriminatorTypeSqmPath,
            HqlParser.ExpressionContext valueExpressionContext) {
        return new SqmAnyDiscriminatorValue<>(
                anyDiscriminatorTypeSqmPath.getNodeType().getPathName(),
                creationContext.getJpaMetamodel().resolveHqlEntityReference( valueExpressionContext.getText() ),
                anyDiscriminatorTypeSqmPath.getExpressible().getSqmPathType(),
                creationContext.getNodeBuilder()
        );
    }

    private SqmExpression resolveEnumShorthandLiteral(HqlParser.ExpressionContext expressionContext, Map, Enum> possibleEnumValues, Class enumType) {
        final Enum enumValue;
        if ( possibleEnumValues != null && ( enumValue = possibleEnumValues.get( enumType ) ) != null ) {
            DotIdentifierConsumer dotIdentifierConsumer = dotIdentifierConsumerStack.getCurrent();
            dotIdentifierConsumer.consumeIdentifier( enumValue.getClass().getName(), true, false );
            dotIdentifierConsumer.consumeIdentifier( enumValue.name(), false, true );
            return (SqmExpression) dotIdentifierConsumerStack.getCurrent().getConsumedPart();
        }
        else {
            return (SqmExpression) expressionContext.accept( this );
        }
    }

    private Map, Enum> getPossibleEnumValues(HqlParser.ExpressionContext expressionContext) {
        ParseTree ctx;
        // Traverse the expression structure according to the grammar
        if ( expressionContext instanceof HqlParser.BarePrimaryExpressionContext && expressionContext.getChildCount() == 1 ) {
            ctx = expressionContext.getChild( 0 );

            while ( ctx instanceof HqlParser.PrimaryExpressionContext && ctx.getChildCount() == 1 ) {
                ctx = ctx.getChild( 0 );
            }

            if ( ctx instanceof HqlParser.GeneralPathFragmentContext && ctx.getChildCount() == 1 ) {
                ctx = ctx.getChild( 0 );

                if ( ctx instanceof HqlParser.SimplePathContext ) {
                    return creationContext.getJpaMetamodel().getAllowedEnumLiteralTexts().get( ctx.getText() );
                }
            }
        }

        return null;
    }

    @Override
    public SqmPredicate visitLikePredicate(HqlParser.LikePredicateContext ctx) {
        final boolean negated = ( (TerminalNode) ctx.getChild( 1 ) ).getSymbol().getType() == HqlParser.NOT;
        final int startIndex = negated ? 3 : 2;
        final boolean caseSensitive = ( (TerminalNode) ctx.getChild( negated ? 2 : 1 ) ).getSymbol()
                .getType() == HqlParser.LIKE;
        if ( ctx.getChildCount() == startIndex + 2 ) {
            return new SqmLikePredicate(
                    (SqmExpression) ctx.getChild( 0 ).accept( this ),
                    (SqmExpression) ctx.getChild( startIndex ).accept( this ),
                    (SqmExpression) ctx.getChild( startIndex + 1 ).accept( this ),
                    negated,
                    caseSensitive,
                    creationContext.getNodeBuilder()
            );
        }
        else {
            return new SqmLikePredicate(
                    (SqmExpression) ctx.getChild( 0 ).accept( this ),
                    (SqmExpression) ctx.getChild( startIndex ).accept( this ),
                    negated,
                    caseSensitive,
                    creationContext.getNodeBuilder()
            );
        }
    }

    @Override
    public Object visitLikeEscape(HqlParser.LikeEscapeContext ctx) {
        final ParseTree child = ctx.getChild( 1 );
        if ( child instanceof HqlParser.NamedParameterContext ) {
            return visitNamedParameter(
                    (HqlParser.NamedParameterContext) child,
                    creationContext.getNodeBuilder().getCharacterType()
            );
        }
        else if ( child instanceof HqlParser.PositionalParameterContext ) {
            return visitPositionalParameter(
                    (HqlParser.PositionalParameterContext) child,
                    creationContext.getNodeBuilder().getCharacterType()
            );
        }
        else {
            assert child instanceof TerminalNode;
            final TerminalNode terminalNode = (TerminalNode) child;
            final String escape = QuotingHelper.unquoteStringLiteral( terminalNode.getText() );
            if ( escape.length() != 1 ) {
                throw new SemanticException(
                        "Escape character literals must have exactly a single character, but found: " + escape
                );
            }
            return new SqmLiteral<>(
                    escape.charAt( 0 ),
                    creationContext.getNodeBuilder().getCharacterType(),
                    creationContext.getNodeBuilder()
            );
        }
    }

    @Override
    public SqmPredicate visitMemberOfPredicate(HqlParser.MemberOfPredicateContext ctx) {
        final boolean negated = ctx.NOT() != null;
        final SqmPath sqmPluralPath = consumeDomainPath(
                (HqlParser.PathContext) ctx.getChild( ctx.getChildCount() - 1 )
        );

        if ( sqmPluralPath.getReferencedPathSource() instanceof PluralPersistentAttribute ) {
            return new SqmMemberOfPredicate(
                    (SqmExpression) ctx.getChild( 0 ).accept( this ),
                    sqmPluralPath,
                    negated,
                    creationContext.getNodeBuilder()
            );
        }
        else {
            throw new SemanticException( "Path argument to MEMBER OF must be a plural attribute" );
        }
    }

    @Override
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public SqmPredicate visitInPredicate(HqlParser.InPredicateContext ctx) {
        final boolean negated = ctx.getChildCount() == 4;
        final SqmExpression testExpression = (SqmExpression) ctx.getChild( 0 ).accept( this );
        final HqlParser.InListContext inListContext = (HqlParser.InListContext) ctx.getChild( ctx.getChildCount() - 1 );
        if ( inListContext instanceof HqlParser.ExplicitTupleInListContext ) {
            final HqlParser.ExplicitTupleInListContext tupleExpressionListContext = (HqlParser.ExplicitTupleInListContext) inListContext;
            final int size = tupleExpressionListContext.getChildCount();
            final int estimatedSize = size >> 1;
            final Class testExpressionJavaType = testExpression.getJavaType();
            final boolean isEnum = testExpressionJavaType != null && testExpressionJavaType.isEnum();
            // Multi-valued bindings are only allowed if there is a single list item, hence size 3 (LP, RP and param)
            parameterDeclarationContextStack.push( () -> size == 3 );
            try {
                final List> listExpressions = new ArrayList<>( estimatedSize );
                for ( int i = 1; i < size; i++ ) {
                    final ParseTree parseTree = tupleExpressionListContext.getChild( i );
                    if ( parseTree instanceof HqlParser.ExpressionOrPredicateContext ) {
                        final ParseTree child = parseTree.getChild( 0 );
                        final HqlParser.ExpressionContext expressionContext;
                        final Map, Enum> possibleEnumValues;
                        if ( isEnum && child instanceof HqlParser.ExpressionContext
                                && ( possibleEnumValues = getPossibleEnumValues( expressionContext = (HqlParser.ExpressionContext) child ) ) != null ) {
                            listExpressions.add(
                                    resolveEnumShorthandLiteral(
                                            expressionContext,
                                            possibleEnumValues,
                                            testExpressionJavaType
                                    )
                            );
                        }
                        else {
                            listExpressions.add( (SqmExpression) child.accept( this ) );
                        }
                    }
                }

                return new SqmInListPredicate(
                        testExpression,
                        listExpressions,
                        negated,
                        creationContext.getNodeBuilder()
                );
            }
            finally {
                parameterDeclarationContextStack.pop();
            }
        }
        else if ( inListContext instanceof HqlParser.ParamInListContext ) {
            final HqlParser.ParamInListContext tupleExpressionListContext = (HqlParser.ParamInListContext) inListContext;
            parameterDeclarationContextStack.push( () -> true );
            try {
                return new SqmInListPredicate(
                        testExpression,
                        Collections.singletonList( tupleExpressionListContext.getChild( 0 ).accept( this ) ),
                        negated,
                        creationContext.getNodeBuilder()
                );
            }
            finally {
                parameterDeclarationContextStack.pop();
            }
        }
        else if ( inListContext instanceof HqlParser.SubqueryInListContext ) {
            final HqlParser.SubqueryInListContext subQueryOrParamInListContext = (HqlParser.SubqueryInListContext) inListContext;
            return new SqmInSubQueryPredicate(
                    testExpression,
                    visitSubquery( (HqlParser.SubqueryContext) subQueryOrParamInListContext.getChild( 1 ) ),
                    negated,
                    creationContext.getNodeBuilder()
            );
        }
        else if ( inListContext instanceof HqlParser.PersistentCollectionReferenceInListContext ) {
            if ( getCreationOptions().useStrictJpaCompliance() ) {
                throw new StrictJpaComplianceViolation( StrictJpaComplianceViolation.Type.HQL_COLLECTION_FUNCTION );
            }
            final HqlParser.PersistentCollectionReferenceInListContext collectionReferenceInListContext = (HqlParser.PersistentCollectionReferenceInListContext) inListContext;
            return new SqmInSubQueryPredicate<>(
                    testExpression,
                    createCollectionReferenceSubQuery(
                            (HqlParser.SimplePathContext) collectionReferenceInListContext.getChild( 2 ),
                            (TerminalNode) collectionReferenceInListContext.getChild( 0 )
                    ),
                    negated,
                    creationContext.getNodeBuilder()
            );
        }
        else {
            throw new ParsingException( "Unexpected IN predicate type [" + ctx.getClass().getSimpleName() + "] : " + ctx.getText() );
        }
    }

    @Override
    public SqmPredicate visitExistsCollectionPartPredicate(HqlParser.ExistsCollectionPartPredicateContext ctx) {
        final SqmSubQuery subQuery = createCollectionReferenceSubQuery(
                (HqlParser.SimplePathContext) ctx.getChild( 3 ),
                null
        );
        return new SqmExistsPredicate( subQuery, creationContext.getNodeBuilder() );
    }

    @Override
    public SqmPredicate visitExistsPredicate(HqlParser.ExistsPredicateContext ctx) {
        final SqmExpression expression = (SqmExpression) ctx.getChild( 1 ).accept( this );
        return new SqmExistsPredicate( expression, creationContext.getNodeBuilder() );
    }

    @Override @SuppressWarnings("rawtypes")
    public SqmPredicate visitBooleanExpressionPredicate(HqlParser.BooleanExpressionPredicateContext ctx) {
        final SqmExpression expression = (SqmExpression) ctx.expression().accept( this );
        if ( expression.getJavaType() != Boolean.class ) {
            throw new SemanticException( "Non-boolean expression used in predicate context: " + ctx.getText() );
        }
        @SuppressWarnings("unchecked")
        final SqmExpression booleanExpression = expression;
        return new SqmBooleanExpressionPredicate( booleanExpression, creationContext.getNodeBuilder() );
    }

    @Override
    public Object visitEntityTypeExpression(HqlParser.EntityTypeExpressionContext ctx) {
        final ParseTree pathOrParameter = ctx.getChild( 0 ).getChild( 2 );
        // can be one of 2 forms:
        //		1) TYPE( some.path )
        //		2) TYPE( :someParam )
        if ( pathOrParameter instanceof HqlParser.ParameterContext ) {
            // we have form (2)
            return new SqmParameterizedEntityType<>(
                    (SqmParameter) pathOrParameter.accept( this ),
                    creationContext.getNodeBuilder()
            );
        }
        else if ( pathOrParameter instanceof HqlParser.PathContext ) {
            // we have form (1)
            return ( (SqmPath) pathOrParameter.accept( this ) ).type();
        }

        throw new ParsingException( "Could not interpret grammar context as 'entity type' expression : " + ctx.getText() );
    }

    @Override
    public SqmExpression visitEntityIdExpression(HqlParser.EntityIdExpressionContext ctx) {
        return visitEntityIdReference( (HqlParser.EntityIdReferenceContext) ctx.getChild( 0 ) );
    }

    @Override
    public SqmPath visitEntityIdReference(HqlParser.EntityIdReferenceContext ctx) {
        final SqmPath sqmPath = consumeDomainPath( (HqlParser.PathContext) ctx.getChild( 2 ) );
        final DomainType sqmPathType = sqmPath.getReferencedPathSource().getSqmPathType();

        if ( sqmPathType instanceof IdentifiableDomainType ) {
            final SqmPathSource identifierDescriptor = ( (IdentifiableDomainType) sqmPathType ).getIdentifierDescriptor();
            final SqmPath idPath = sqmPath.get( identifierDescriptor.getPathName() );

            if ( ctx.getChildCount() != 5 ) {
                return idPath;
            }

            throw new UnsupportedOperationException( "Path continuation from `id()` reference not yet implemented" );
        }

        throw new SemanticException( "Path does not resolve to an entity type '" + sqmPath.getNavigablePath() + "'" );
    }

    @Override
    public SqmExpression visitEntityVersionExpression(HqlParser.EntityVersionExpressionContext ctx) {
        return visitEntityVersionReference( (HqlParser.EntityVersionReferenceContext) ctx.getChild( 0 ) );
    }

    @Override
    public SqmPath visitEntityVersionReference(HqlParser.EntityVersionReferenceContext ctx) {
        final SqmPath sqmPath = consumeDomainPath( (HqlParser.PathContext) ctx.getChild( 2 ) );
        final DomainType sqmPathType = sqmPath.getReferencedPathSource().getSqmPathType();

        if ( sqmPathType instanceof IdentifiableDomainType ) {
            @SuppressWarnings("unchecked")
            final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType;
            final SingularPersistentAttribute versionAttribute = identifiableType.findVersionAttribute();
            if ( versionAttribute == null ) {
                throw new SemanticException(
                        String.format(
                                "Path '%s' resolved to entity type '%s' which does not define a version",
                                sqmPath.getNavigablePath(),
                                identifiableType.getTypeName()
                        )
                );
            }

            return sqmPath.get( versionAttribute );
        }

        throw new SemanticException( "Path does not resolve to an entity type '" + sqmPath.getNavigablePath() + "'" );
    }

    @Override
    public SqmPath visitEntityNaturalIdExpression(HqlParser.EntityNaturalIdExpressionContext ctx) {
        return visitEntityNaturalIdReference( (HqlParser.EntityNaturalIdReferenceContext) ctx.getChild( 0 ) );
    }

    @Override
    public SqmPath visitEntityNaturalIdReference(HqlParser.EntityNaturalIdReferenceContext ctx) {
        final SqmPath sqmPath = consumeDomainPath( (HqlParser.PathContext) ctx.getChild( 2 ) );
        final DomainType sqmPathType = sqmPath.getReferencedPathSource().getSqmPathType();

        if ( sqmPathType instanceof IdentifiableDomainType ) {
            @SuppressWarnings("unchecked")
            final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType;
            final List> attributes = identifiableType.findNaturalIdAttributes();
            if ( attributes == null ) {
                throw new SemanticException(
                        String.format(
                                "Path '%s' resolved to entity type '%s' which does not define a natural id",
                                sqmPath.getNavigablePath(),
                                identifiableType.getTypeName()
                        )
                );
            }
            else if ( attributes.size() >1 ) {
                throw new SemanticException(
                        String.format(
                                "Path '%s' resolved to entity type '%s' which defines multiple natural ids",
                                sqmPath.getNavigablePath(),
                                identifiableType.getTypeName()
                        )
                );
            }

            @SuppressWarnings("unchecked")
            SingularAttribute naturalIdAttribute
                    = (SingularAttribute) attributes.get(0);
            return sqmPath.get( naturalIdAttribute );
        }

        throw new SemanticException( "Path does not resolve to an entity type '" + sqmPath.getNavigablePath() + "'" );
    }

    @Override
    public Object visitToOneFkExpression(HqlParser.ToOneFkExpressionContext ctx) {
        return visitToOneFkReference( (HqlParser.ToOneFkReferenceContext) ctx.getChild( 0 ) );
    }

    @Override
    public SqmFkExpression visitToOneFkReference(HqlParser.ToOneFkReferenceContext ctx) {
        final SqmPath sqmPath = consumeDomainPath( (HqlParser.PathContext) ctx.getChild( 2 ) );
        final SqmPathSource toOneReference = sqmPath.getReferencedPathSource();

        final boolean validToOneRef = toOneReference.getBindableType() == Bindable.BindableType.SINGULAR_ATTRIBUTE
                && toOneReference instanceof EntitySqmPathSource;

        if ( !validToOneRef ) {
            throw new SemanticException(
                    String.format(
                            Locale.ROOT,
                            "`%s` used in `fk()` only supported for to-one mappings, but found `%s`",
                            sqmPath.getNavigablePath(),
                            toOneReference
                    )
            );

        }

        return new SqmFkExpression<>( (SqmEntityValuedSimplePath) sqmPath, creationContext.getNodeBuilder() );
    }

    @Override
    public SqmMapEntryReference visitMapEntrySelection(HqlParser.MapEntrySelectionContext ctx) {
        return new SqmMapEntryReference<>(
                consumePluralAttributeReference( (HqlParser.PathContext) ctx.getChild( 2 ) ),
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public SqmExpression visitConcatenationExpression(HqlParser.ConcatenationExpressionContext ctx) {
        if ( ctx.getChildCount() != 3 ) {
            throw new ParsingException( "Expecting 2 operands to the concat operator" );
        }
        return getFunctionDescriptor( "concat" ).generateSqmExpression(
                asList(
                        (SqmExpression) ctx.getChild( 0 ).accept( this ),
                        (SqmExpression) ctx.getChild( 2 ).accept( this )
                ),
                null,
                creationContext.getQueryEngine(),
                creationContext.getJpaMetamodel().getTypeConfiguration()
        );
    }

    @Override
    public Object visitSignOperator(HqlParser.SignOperatorContext ctx) {
        switch ( ( (TerminalNode) ctx.getChild( 0 ) ).getSymbol().getType() ) {
            case HqlParser.PLUS:
                return UnaryArithmeticOperator.UNARY_PLUS;
            case HqlParser.MINUS:
                return UnaryArithmeticOperator.UNARY_MINUS;
            default:
                throw new QueryException( "missing operator" );
        }
    }

    @Override
    public Object visitAdditiveOperator(HqlParser.AdditiveOperatorContext ctx) {
        switch ( ( (TerminalNode) ctx.getChild( 0 ) ).getSymbol().getType() ) {
            case HqlParser.PLUS:
                return BinaryArithmeticOperator.ADD;
            case HqlParser.MINUS:
                return BinaryArithmeticOperator.SUBTRACT;
            default:
                throw new QueryException( "missing operator" );
        }
    }

    @Override
    public Object visitMultiplicativeOperator(HqlParser.MultiplicativeOperatorContext ctx) {
        switch ( ( (TerminalNode) ctx.getChild( 0 ) ).getSymbol().getType() ) {
            case HqlParser.ASTERISK:
                return BinaryArithmeticOperator.MULTIPLY;
            case HqlParser.SLASH:
                return BinaryArithmeticOperator.DIVIDE;
            case HqlParser.PERCENT_OP:
                return BinaryArithmeticOperator.MODULO;
            default:
                throw new QueryException( "missing operator" );
        }
    }

    @Override
    public Object visitAdditionExpression(HqlParser.AdditionExpressionContext ctx) {
        if ( ctx.getChildCount() != 3 ) {
            throw new ParsingException( "Expecting 2 operands to the additive operator" );
        }

        return new SqmBinaryArithmetic<>(
                (BinaryArithmeticOperator) ctx.getChild( 1 ).accept( this ),
                (SqmExpression) ctx.getChild( 0 ).accept( this ),
                (SqmExpression) ctx.getChild( 2 ).accept( this ),
                creationContext.getJpaMetamodel(),
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public Object visitMultiplicationExpression(HqlParser.MultiplicationExpressionContext ctx) {
        if ( ctx.getChildCount() != 3 ) {
            throw new ParsingException( "Expecting 2 operands to the multiplicative operator" );
        }

        final SqmExpression left = (SqmExpression) ctx.getChild( 0 ).accept( this );
        final SqmExpression right = (SqmExpression) ctx.getChild( 2 ).accept( this );
        final BinaryArithmeticOperator operator = (BinaryArithmeticOperator) ctx.getChild( 1 ).accept( this );

        if ( operator == BinaryArithmeticOperator.MODULO ) {
            return getFunctionDescriptor("mod").generateSqmExpression(
                    asList( left, right ),
                    null,
                    creationContext.getQueryEngine(),
                    creationContext.getJpaMetamodel().getTypeConfiguration()
            );
        }
        else {
            return new SqmBinaryArithmetic<>(
                    operator,
                    left,
                    right,
                    creationContext.getJpaMetamodel(),
                    creationContext.getNodeBuilder()
            );
        }
    }

    @Override
    public Object visitToDurationExpression(HqlParser.ToDurationExpressionContext ctx) {
        return new SqmToDuration<>(
                (SqmExpression) ctx.getChild( 0 ).accept( this ),
                toDurationUnit( (SqmExtractUnit) ctx.getChild( 1 ).accept( this ) ),
                resolveExpressibleTypeBasic( Duration.class ),
                creationContext.getNodeBuilder()
        );
    }

    private SqmDurationUnit toDurationUnit(SqmExtractUnit extractUnit) {
        return new SqmDurationUnit<>(
                extractUnit.getUnit(),
                resolveExpressibleTypeBasic( Long.class ),
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public Object visitFromDurationExpression(HqlParser.FromDurationExpressionContext ctx) {
        return new SqmByUnit(
                toDurationUnit( (SqmExtractUnit) ctx.getChild( 2 ).accept( this ) ),
                (SqmExpression) ctx.getChild( 0 ).accept( this ),
                resolveExpressibleTypeBasic( Long.class ),
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public SqmUnaryOperation visitUnaryExpression(HqlParser.UnaryExpressionContext ctx) {
        return new SqmUnaryOperation<>(
                (UnaryArithmeticOperator) ctx.getChild( 0 ).accept( this ),
                (SqmExpression) ctx.getChild( 1 ).accept( this )
        );
    }

    @Override
    public Object visitGroupedExpression(HqlParser.GroupedExpressionContext ctx) {
        return ctx.getChild( 1 ).accept( this );
    }

    @Override
    public Object visitCollateFunction(HqlParser.CollateFunctionContext ctx) {
        if ( creationOptions.useStrictJpaCompliance() ) {
            throw new StrictJpaComplianceViolation(
                    StrictJpaComplianceViolation.Type.COLLATIONS
            );
        }

        final SqmExpression expressionToCollate = (SqmExpression) ctx.getChild( 2 ).accept( this );
        final SqmCollation castTargetExpression = (SqmCollation) ctx.getChild( 4 ).accept( this );

        return getFunctionDescriptor("collate").generateSqmExpression(
                asList( expressionToCollate, castTargetExpression ),
                null, //why not string?
                creationContext.getQueryEngine(),
                creationContext.getJpaMetamodel().getTypeConfiguration()
        );
    }

    @Override
    public Object visitCollation(HqlParser.CollationContext ctx) {
        return new SqmCollation(
                ctx.getChild( 0 ).getText(),
                null,
                creationContext.getNodeBuilder() );
    }

    @Override
    public Object visitTupleExpression(HqlParser.TupleExpressionContext ctx) {
        if ( creationOptions.useStrictJpaCompliance() ) {
            throw new StrictJpaComplianceViolation(
                    StrictJpaComplianceViolation.Type.TUPLES
            );
        }
        final List> expressions = visitExpressions( ctx );
        return new SqmTuple<>( expressions, creationContext.getNodeBuilder() );
    }

    private List> visitExpressions(ParserRuleContext parentContext) {
        final int size = parentContext.getChildCount();
        // Shift 1 bit instead of division by 2
        final int estimateExpressionsCount = ( size >> 1 ) - 1;
        final List> expressions = new ArrayList<>( estimateExpressionsCount );
        for ( int i = 0; i < size; i++ ) {
            final ParseTree parseTree = parentContext.getChild( i );
            if ( parseTree instanceof HqlParser.ExpressionOrPredicateContext ) {
                expressions.add( (SqmExpression) parseTree.accept( this ) );
            }
        }
        return expressions;
    }

    @Override
    public Object visitCaseExpression(HqlParser.CaseExpressionContext ctx) {
        return ctx.getChild( 0 ).accept( this );
    }

    @Override
    public SqmCaseSimple visitSimpleCaseList(HqlParser.SimpleCaseListContext ctx) {
        final int size = ctx.getChildCount();
        //noinspection unchecked
        final SqmCaseSimple caseExpression = new SqmCaseSimple<>(
                (SqmExpression) ctx.getChild( 1 ).accept( this ),
                null,
                size - 3,
                creationContext.getNodeBuilder()
        );

        for ( int i = 2; i < size; i++ ) {
            final ParseTree parseTree = ctx.getChild( i );
            if ( parseTree instanceof HqlParser.SimpleCaseWhenContext ) {
                //noinspection unchecked
                caseExpression.when(
                        (SqmExpression) parseTree.getChild( 1 ).accept( this ),
                        (SqmExpression) parseTree.getChild( 3 ).accept( this )
                );
            }
        }

        final ParseTree lastChild = ctx.getChild( ctx.getChildCount() - 2 );
        if ( lastChild instanceof HqlParser.CaseOtherwiseContext ) {
            //noinspection unchecked
            caseExpression.otherwise( (SqmExpression) lastChild.getChild( 1 ).accept( this ) );
        }

        return caseExpression;
    }

    @Override
    public SqmCaseSearched visitSearchedCaseList(HqlParser.SearchedCaseListContext ctx) {
        final int size = ctx.getChildCount();
        final SqmCaseSearched caseExpression = new SqmCaseSearched<>(
                null,
                size - 2,
                creationContext.getNodeBuilder()
        );

        for ( int i = 1; i < size; i++ ) {
            final ParseTree parseTree = ctx.getChild( i );
            if ( parseTree instanceof HqlParser.SearchedCaseWhenContext ) {
                //noinspection unchecked
                caseExpression.when(
                        (SqmPredicate) parseTree.getChild( 1 ).accept( this ),
                        (SqmExpression) parseTree.getChild( 3 ).accept( this )
                );
            }
        }

        final ParseTree lastChild = ctx.getChild( ctx.getChildCount() - 2 );
        if ( lastChild instanceof HqlParser.CaseOtherwiseContext ) {
            //noinspection unchecked
            caseExpression.otherwise( (SqmExpression) lastChild.getChild( 1 ).accept( this ) );
        }

        return caseExpression;
    }

    @Override
    public SqmExpression visitCurrentDateFunction(HqlParser.CurrentDateFunctionContext ctx) {
        return getFunctionDescriptor("current_date")
                .generateSqmExpression(
                        resolveExpressibleTypeBasic( Date.class ),
                        creationContext.getQueryEngine(),
                        creationContext.getJpaMetamodel().getTypeConfiguration()
                );
    }

    @Override
    public SqmExpression visitCurrentTimeFunction(HqlParser.CurrentTimeFunctionContext ctx) {
        return getFunctionDescriptor("current_time")
                .generateSqmExpression(
                        resolveExpressibleTypeBasic( Time.class ),
                        creationContext.getQueryEngine(),
                        creationContext.getJpaMetamodel().getTypeConfiguration()
                );
    }

    @Override
    public SqmExpression visitCurrentTimestampFunction(HqlParser.CurrentTimestampFunctionContext ctx) {
        return getFunctionDescriptor("current_timestamp")
                .generateSqmExpression(
                        resolveExpressibleTypeBasic( Timestamp.class ),
                        creationContext.getQueryEngine(),
                        creationContext.getJpaMetamodel().getTypeConfiguration()
                );
    }

    @Override
    public SqmExpression visitInstantFunction(HqlParser.InstantFunctionContext ctx) {
        return getFunctionDescriptor("instant")
                .generateSqmExpression(
                        resolveExpressibleTypeBasic( Instant.class ),
                        creationContext.getQueryEngine(),
                        creationContext.getJpaMetamodel().getTypeConfiguration()
                );
    }

    @Override
    public SqmExpression visitLocalDateFunction(HqlParser.LocalDateFunctionContext ctx) {
        return getFunctionDescriptor("local_date")
                .generateSqmExpression(
                        resolveExpressibleTypeBasic( LocalDate.class ),
                        creationContext.getQueryEngine(),
                        creationContext.getJpaMetamodel().getTypeConfiguration()
                );
    }

    @Override
    public SqmExpression visitLocalTimeFunction(HqlParser.LocalTimeFunctionContext ctx) {
        return getFunctionDescriptor("local_time")
                .generateSqmExpression(
                        resolveExpressibleTypeBasic( LocalTime.class ),
                        creationContext.getQueryEngine(),
                        creationContext.getJpaMetamodel().getTypeConfiguration()
                );
    }

    @Override
    public SqmExpression visitLocalDateTimeFunction(HqlParser.LocalDateTimeFunctionContext ctx) {
        return getFunctionDescriptor("local_datetime")
                .generateSqmExpression(
                        resolveExpressibleTypeBasic( LocalDateTime.class ),
                        creationContext.getQueryEngine(),
                        creationContext.getJpaMetamodel().getTypeConfiguration()
                );
    }

    @Override
    public SqmExpression visitOffsetDateTimeFunction(HqlParser.OffsetDateTimeFunctionContext ctx) {
        return getFunctionDescriptor("offset_datetime")
                .generateSqmExpression(
                        resolveExpressibleTypeBasic( OffsetDateTime.class ),
                        creationContext.getQueryEngine(),
                        creationContext.getJpaMetamodel().getTypeConfiguration()
                );
    }

    @Override
    public SqmExpression visitLiteralExpression(HqlParser.LiteralExpressionContext ctx) {
        return (SqmExpression) ctx.getChild( 0 ).accept( this );
    }

    @Override
    public SqmExpression visitUnaryNumericLiteralExpression(HqlParser.UnaryNumericLiteralExpressionContext ctx) {
        final TerminalNode node = (TerminalNode) ctx.getChild( 1 ).getChild( 0 );
        final String text;
        if ( ( (TerminalNode) ctx.getChild( 0 ).getChild( 0 ) ).getSymbol().getType() == HqlParser.MINUS ) {
            text = "-" + node.getText();
        }
        else {
            text = node.getText();
        }
        switch ( node.getSymbol().getType() ) {
            case HqlParser.INTEGER_LITERAL:
                return integerOrLongLiteral( text );
            case HqlParser.LONG_LITERAL:
                return longLiteral( text );
            case HqlParser.BIG_INTEGER_LITERAL:
                return bigIntegerLiteral( text );
            case HqlParser.HEX_LITERAL:
                return hexLiteral( text );
            case HqlParser.FLOAT_LITERAL:
                return floatLiteral( text );
            case HqlParser.DOUBLE_LITERAL:
                return doubleLiteral( text );
            case HqlParser.BIG_DECIMAL_LITERAL:
                return bigDecimalLiteral( text );
            default:
                throw new ParsingException("Unexpected terminal node [" + text + "]");
        }
    }

    @Override
    public Object visitBinaryLiteral(HqlParser.BinaryLiteralContext ctx) {
        final TerminalNode firstNode = (TerminalNode) ctx.getChild( 0 );
        if ( firstNode.getSymbol().getType() == HqlParser.BINARY_LITERAL ) {
            return binaryLiteral( firstNode.getText() );
        }
        else {
            final StringBuilder text = new StringBuilder( "x'" );
            final int size = ctx.getChildCount();
            for ( int i = 0; i < size; i++ ) {
                final TerminalNode hex = (TerminalNode) ctx.getChild( i );
                if ( hex.getSymbol().getType() == HqlParser.HEX_LITERAL ) {
                    final String hexText = hex.getText();
                    if ( hexText.length() != 4 ) {
                        throw new LiteralNumberFormatException( "not a byte: " + hexText );
                    }
                    text.append( hexText, 2, hexText.length() );
                }
            }
            return binaryLiteral( text.append( "'" ).toString() );
        }
    }

    @Override
    public Object visitGeneralizedLiteral(HqlParser.GeneralizedLiteralContext ctx) {
        throw new UnsupportedOperationException();
    }

    @Override
    public SqmExpression visitTerminal(TerminalNode node) {
        if ( node.getSymbol().getType() == HqlLexer.EOF ) {
            return null;
        }
        switch ( node.getSymbol().getType() ) {
            case HqlParser.STRING_LITERAL:
                return stringLiteral( node.getText() );
            case HqlParser.JAVA_STRING_LITERAL:
                return javaStringLiteral( node.getText() );
            case HqlParser.INTEGER_LITERAL:
                return integerOrLongLiteral( node.getText() );
            case HqlParser.LONG_LITERAL:
                return longLiteral( node.getText() );
            case HqlParser.BIG_INTEGER_LITERAL:
                return bigIntegerLiteral( node.getText() );
            case HqlParser.HEX_LITERAL:
                return hexLiteral( node.getText() );
            case HqlParser.FLOAT_LITERAL:
                return floatLiteral( node.getText() );
            case HqlParser.DOUBLE_LITERAL:
                return doubleLiteral( node.getText() );
            case HqlParser.BIG_DECIMAL_LITERAL:
                return bigDecimalLiteral( node.getText() );
            case HqlParser.FALSE:
                return booleanLiteral( false );
            case HqlParser.TRUE:
                return booleanLiteral( true );
            case HqlParser.NULL:
                return new SqmLiteralNull<>( creationContext.getQueryEngine().getCriteriaBuilder() );
            case HqlParser.BINARY_LITERAL:
                return binaryLiteral( node.getText() );
            default:
                throw new ParsingException("Unexpected terminal node [" + node.getText() + "]");
        }
    }

    @Override
    public Object visitDateTimeLiteral(HqlParser.DateTimeLiteralContext ctx) {
        return ctx.getChild( 0 ).accept( this );
    }

    @Override
    public Object visitLocalDateTimeLiteral(HqlParser.LocalDateTimeLiteralContext ctx) {
        return ctx.localDateTime().accept( this );
    }

    @Override
    public Object visitZonedDateTimeLiteral(HqlParser.ZonedDateTimeLiteralContext ctx) {
        return ctx.zonedDateTime().accept( this );
    }

    @Override
    public Object visitOffsetDateTimeLiteral(HqlParser.OffsetDateTimeLiteralContext ctx) {
        if ( ctx.offsetDateTime() != null ) {
            return ctx.offsetDateTime().accept(this);
        }
        else if ( ctx.offsetDateTimeWithMinutes() != null ) {
            return ctx.offsetDateTimeWithMinutes().accept(this);
        }
        else {
            return null;
        }
    }

    @Override
    public Object visitDateLiteral(HqlParser.DateLiteralContext ctx) {
        return ctx.getChild( 1 ).accept( this );
    }

    @Override
    public Object visitTimeLiteral(HqlParser.TimeLiteralContext ctx) {
        return ctx.getChild( 1 ).accept( this );
    }

    @Override
    public Object visitJdbcTimestampLiteral(HqlParser.JdbcTimestampLiteralContext ctx) {
        final ParseTree parseTree = ctx.getChild( 1 );
        if ( parseTree instanceof HqlParser.DateTimeContext ) {
            return parseTree.accept( this );
        }
        else {
            return sqlTimestampLiteralFrom( parseTree.getText() );
        }
    }

    @Override
    public Object visitJdbcDateLiteral(HqlParser.JdbcDateLiteralContext ctx) {
        final ParseTree parseTree = ctx.getChild( 1 );
        if ( parseTree instanceof HqlParser.DateContext ) {
            return parseTree.accept( this );
        }
        else {
            return sqlDateLiteralFrom( parseTree.getText() );
        }
    }

    @Override
    public Object visitJdbcTimeLiteral(HqlParser.JdbcTimeLiteralContext ctx) {
        final ParseTree parseTree = ctx.getChild( 1 );
        if ( parseTree instanceof HqlParser.TimeContext ) {
            return parseTree.accept( this );
        }
        else {
            return sqlTimeLiteralFrom( parseTree.getText() );
        }
    }

    @Override
    public Object visitDateTime(HqlParser.DateTimeContext ctx) {
        return ctx.getChild( 0 ).accept( this );
    }

    @Override
    public Object visitLocalDateTime(HqlParser.LocalDateTimeContext ctx) {
        return localDateTimeLiteralFrom( ctx.date(), ctx.time() );
    }

    @Override
    public Object visitOffsetDateTime(HqlParser.OffsetDateTimeContext ctx) {
        return offsetDatetimeLiteralFrom( ctx.date(), ctx.time(), ctx.offset() );
    }

    @Override
    public Object visitOffsetDateTimeWithMinutes(HqlParser.OffsetDateTimeWithMinutesContext ctx) {
        return offsetDatetimeLiteralFrom( ctx.date(), ctx.time(), ctx.offsetWithMinutes() );
    }

    @Override
    public Object visitZonedDateTime(HqlParser.ZonedDateTimeContext ctx) {
        return zonedDateTimeLiteralFrom( ctx.date(), ctx.time(), ctx.zoneId() );
    }

    private SqmLiteral localDateTimeLiteralFrom(
            HqlParser.DateContext date,
            HqlParser.TimeContext time) {
        return new SqmLiteral<>(
                LocalDateTime.of( localDate( date ), localTime( time ) ),
                resolveExpressibleTypeBasic( LocalDateTime.class ),
                creationContext.getNodeBuilder()
        );
    }

    private SqmLiteral zonedDateTimeLiteralFrom(
            HqlParser.DateContext date,
            HqlParser.TimeContext time,
            HqlParser.ZoneIdContext timezone) {
        return new SqmLiteral<>(
                ZonedDateTime.of( localDate( date ), localTime( time ), visitZoneId( timezone ) ),
                resolveExpressibleTypeBasic( ZonedDateTime.class ),
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public ZoneId visitZoneId(HqlParser.ZoneIdContext ctx) {
        final TerminalNode firstChild = (TerminalNode) ctx.getChild( 0 );
        final String timezoneText;
        if ( firstChild.getSymbol().getType() == HqlParser.STRING_LITERAL ) {
            timezoneText = QuotingHelper.unquoteStringLiteral( ctx.getText() );
        }
        else {
            timezoneText = ctx.getText();
        }
        final String timezoneFullName = ZoneId.SHORT_IDS.get( timezoneText );
        if ( timezoneFullName == null ) {
            return ZoneId.of( timezoneText );
        }
        else {
            return ZoneId.of( timezoneFullName );
        }
    }

    private SqmLiteral offsetDatetimeLiteralFrom(
            HqlParser.DateContext date,
            HqlParser.TimeContext time,
            HqlParser.OffsetContext offset) {
        return new SqmLiteral<>(
                OffsetDateTime.of( localDate( date ), localTime( time ), zoneOffset( offset ) ),
                resolveExpressibleTypeBasic( OffsetDateTime.class ),
                creationContext.getNodeBuilder()
        );
    }

    private SqmLiteral offsetDatetimeLiteralFrom(
            HqlParser.DateContext date,
            HqlParser.TimeContext time,
            HqlParser.OffsetWithMinutesContext offset) {
        return new SqmLiteral<>(
                OffsetDateTime.of( localDate( date ), localTime( time ), zoneOffset( offset ) ),
                resolveExpressibleTypeBasic( OffsetDateTime.class ),
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public Object visitDate(HqlParser.DateContext ctx) {
        return new SqmLiteral<>(
                localDate( ctx ),
                resolveExpressibleTypeBasic( LocalDate.class ),
                creationContext.getNodeBuilder()
        );
    }

    @Override
    public Object visitTime(HqlParser.TimeContext ctx) {
        return new SqmLiteral<>(
                localTime( ctx ),
                resolveExpressibleTypeBasic( LocalTime.class ),
                creationContext.getNodeBuilder()
        );
    }

    private static LocalTime localTime(HqlParser.TimeContext ctx) {
        final int hour = Integer.parseInt( ctx.hour().getText() );
        final int minute = Integer.parseInt( ctx.minute().getText() );
        final HqlParser.SecondContext secondContext = ctx.second();
        if ( secondContext != null ) {
            final String secondText = secondContext.getText();
            final int index = secondText.indexOf( '.' );
            if ( index < 0 ) {
                return LocalTime.of(
                        hour,
                        minute,
                        Integer.parseInt( secondText )
                );
            }
            else {
                final String secondFractions = secondText.substring( index + 1 );
                return LocalTime.of(
                        hour,
                        minute,
                        Integer.parseInt( secondText.substring( 0, index ) ),
                        Integer.parseInt( secondFractions ) * (int) Math.pow( 10, 9 - secondFractions.length() )
                );
            }
        }
        else {
            return LocalTime.of( hour, minute );
        }
    }

    private static LocalDate localDate(HqlParser.DateContext ctx) {
        return LocalDate.of(
                Integer.parseInt( ctx.year().getText() ),
                Integer.parseInt( ctx.month().getText() ),
                Integer.parseInt( ctx.day().getText() )
        );
    }

    private static ZoneOffset zoneOffset(HqlParser.OffsetContext offset) {
        final int factor = ( (TerminalNode) offset.getChild( 0 ) ).getSymbol().getType() == PLUS ? 1 : -1;
        final int hour = factor * Integer.parseInt( offset.hour().getText() );
        if ( offset.getChildCount() == 2 ) {
            return ZoneOffset.ofHours( hour );
        }
        return ZoneOffset.ofHoursMinutes(
                hour,
                factor * Integer.parseInt( offset.minute().getText() )
        );
    }

    private static ZoneOffset zoneOffset(HqlParser.OffsetWithMinutesContext offset) {
        final int factor = ( (TerminalNode) offset.getChild( 0 ) ).getSymbol().getType() == PLUS ? 1 : -1;
        final int hour = factor * Integer.parseInt( offset.hour().getText() );
        if ( offset.getChildCount() == 2 ) {
            return ZoneOffset.ofHours( hour );
        }
        return ZoneOffset.ofHoursMinutes(
                hour,
                factor * Integer.parseInt( offset.minute().getText() )
        );
    }

    private SqmLiteral sqlTimestampLiteralFrom(String literalText) {
        final TemporalAccessor parsed = DATE_TIME.parse( literalText.subSequence( 1, literalText.length() - 1 ) );
        try {
            final ZonedDateTime zonedDateTime = ZonedDateTime.from( parsed );
            final Calendar literal = GregorianCalendar.from( zonedDateTime );
            return new SqmLiteral<>(
                    literal,
                    resolveExpressibleTypeBasic( Calendar.class ),
                    creationContext.getNodeBuilder()
            );
        }
        catch (DateTimeException dte) {
            final LocalDateTime localDateTime = LocalDateTime.from( parsed );
            final Timestamp literal = Timestamp.valueOf( localDateTime );
            return new SqmLiteral<>(
                    literal,
                    resolveExpressibleTypeBasic( Timestamp.class ),
                    creationContext.getNodeBuilder()
            );
        }
    }

    private SqmLiteral sqlDateLiteralFrom(String literalText) {
        final LocalDate localDate = LocalDate.from( ISO_LOCAL_DATE.parse( literalText.subSequence( 1, literalText.length() - 1 ) ) );
        final Date literal = Date.valueOf( localDate );
        return new SqmLiteral<>(
                literal,
                resolveExpressibleTypeBasic( Date.class ),
                creationContext.getNodeBuilder()
        );
    }

    private SqmLiteral