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

org.hibernate.hql.internal.ast.QueryTranslatorImpl Maven / Gradle / Ivy

There is a newer version: 7.0.0.Alpha1
Show newest version
/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or .
 */
package org.hibernate.hql.internal.ast;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.QueryException;
import org.hibernate.engine.query.spi.EntityGraphQueryHint;
import org.hibernate.engine.spi.QueryParameters;
import org.hibernate.engine.spi.RowSelection;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.event.spi.EventSource;
import org.hibernate.hql.internal.QueryExecutionRequestException;
import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes;
import org.hibernate.hql.internal.antlr.HqlTokenTypes;
import org.hibernate.hql.internal.antlr.SqlTokenTypes;
import org.hibernate.hql.internal.ast.exec.BasicExecutor;
import org.hibernate.hql.internal.ast.exec.DeleteExecutor;
import org.hibernate.hql.internal.ast.exec.MultiTableDeleteExecutor;
import org.hibernate.hql.internal.ast.exec.MultiTableUpdateExecutor;
import org.hibernate.hql.internal.ast.exec.StatementExecutor;
import org.hibernate.hql.internal.ast.tree.AggregatedSelectExpression;
import org.hibernate.hql.internal.ast.tree.FromElement;
import org.hibernate.hql.internal.ast.tree.InsertStatement;
import org.hibernate.hql.internal.ast.tree.QueryNode;
import org.hibernate.hql.internal.ast.tree.Statement;
import org.hibernate.hql.internal.ast.tree.UpdateStatement;
import org.hibernate.hql.internal.ast.util.ASTPrinter;
import org.hibernate.hql.internal.ast.util.ASTUtil;
import org.hibernate.hql.internal.ast.util.NodeTraverser;
import org.hibernate.hql.internal.ast.util.TokenPrinters;
import org.hibernate.hql.spi.FilterTranslator;
import org.hibernate.hql.spi.ParameterTranslations;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.IdentitySet;
import org.hibernate.loader.hql.QueryLoader;
import org.hibernate.param.ParameterSpecification;
import org.hibernate.persister.entity.Queryable;
import org.hibernate.query.spi.ScrollableResultsImplementor;
import org.hibernate.type.Type;

import org.jboss.logging.Logger;

import antlr.ANTLRException;
import antlr.RecognitionException;
import antlr.TokenStreamException;
import antlr.collections.AST;

/**
 * A QueryTranslator that uses an Antlr-based parser.
 *
 * @author Joshua Davis ([email protected])
 */
public class QueryTranslatorImpl implements FilterTranslator {
	private static final CoreMessageLogger LOG = Logger.getMessageLogger(
			CoreMessageLogger.class,
			QueryTranslatorImpl.class.getName()
	);

	private SessionFactoryImplementor factory;

	private final String queryIdentifier;
	private String hql;
	private boolean shallowQuery;
	private Map tokenReplacements;

	//TODO:this is only needed during compilation .. can we eliminate the instvar?
	private Map enabledFilters;

	private boolean compiled;
	private QueryLoader queryLoader;
	private StatementExecutor statementExecutor;

	private Statement sqlAst;
	private String sql;

	private ParameterTranslations paramTranslations;
	private List collectedParameterSpecifications;
	
	private EntityGraphQueryHint entityGraphQueryHint;


	/**
	 * Creates a new AST-based query translator.
	 *
	 * @param queryIdentifier The query-identifier (used in stats collection)
	 * @param query The hql query to translate
	 * @param enabledFilters Currently enabled filters
	 * @param factory The session factory constructing this translator instance.
	 */
	public QueryTranslatorImpl(
			String queryIdentifier,
			String query,
			Map enabledFilters,
			SessionFactoryImplementor factory) {
		this.queryIdentifier = queryIdentifier;
		this.hql = query;
		this.compiled = false;
		this.shallowQuery = false;
		this.enabledFilters = enabledFilters;
		this.factory = factory;
	}
	
	public QueryTranslatorImpl(
			String queryIdentifier,
			String query,
			Map enabledFilters,
			SessionFactoryImplementor factory,
			EntityGraphQueryHint entityGraphQueryHint) {
		this( queryIdentifier, query, enabledFilters, factory );
		this.entityGraphQueryHint = entityGraphQueryHint;
	}

	/**
	 * Compile a "normal" query. This method may be called multiple
	 * times. Subsequent invocations are no-ops.
	 *
	 * @param replacements Defined query substitutions.
	 * @param shallow      Does this represent a shallow (scalar or entity-id) select?
	 * @throws QueryException   There was a problem parsing the query string.
	 * @throws MappingException There was a problem querying defined mappings.
	 */
	@Override
	public void compile(
			Map replacements,
			boolean shallow) throws QueryException, MappingException {
		doCompile( replacements, shallow, null );
	}

	/**
	 * Compile a filter. This method may be called multiple
	 * times. Subsequent invocations are no-ops.
	 *
	 * @param collectionRole the role name of the collection used as the basis for the filter.
	 * @param replacements   Defined query substitutions.
	 * @param shallow        Does this represent a shallow (scalar or entity-id) select?
	 * @throws QueryException   There was a problem parsing the query string.
	 * @throws MappingException There was a problem querying defined mappings.
	 */
	@Override
	public void compile(
			String collectionRole,
			Map replacements,
			boolean shallow) throws QueryException, MappingException {
		doCompile( replacements, shallow, collectionRole );
	}

	/**
	 * Performs both filter and non-filter compiling.
	 *
	 * @param replacements   Defined query substitutions.
	 * @param shallow        Does this represent a shallow (scalar or entity-id) select?
	 * @param collectionRole the role name of the collection used as the basis for the filter, NULL if this
	 *                       is not a filter.
	 */
	private synchronized void doCompile(Map replacements, boolean shallow, String collectionRole) {
		// If the query is already compiled, skip the compilation.
		if ( compiled ) {
			LOG.debug( "compile() : The query is already compiled, skipping..." );
			return;
		}

		// Remember the parameters for the compilation.
		this.tokenReplacements = replacements;
		if ( tokenReplacements == null ) {
			tokenReplacements = new HashMap();
		}
		this.shallowQuery = shallow;

		try {
			// PHASE 1 : Parse the HQL into an AST.
			final HqlParser parser = parse( true );

			// PHASE 2 : Analyze the HQL AST, and produce an SQL AST.
			final HqlSqlWalker w = analyze( parser, collectionRole );

			sqlAst = (Statement) w.getAST();

			// at some point the generate phase needs to be moved out of here,
			// because a single object-level DML might spawn multiple SQL DML
			// command executions.
			//
			// Possible to just move the sql generation for dml stuff, but for
			// consistency-sake probably best to just move responsiblity for
			// the generation phase completely into the delegates
			// (QueryLoader/StatementExecutor) themselves.  Also, not sure why
			// QueryLoader currently even has a dependency on this at all; does
			// it need it?  Ideally like to see the walker itself given to the delegates directly...

			if ( sqlAst.needsExecutor() ) {
				statementExecutor = buildAppropriateStatementExecutor( w );
			}
			else {
				// PHASE 3 : Generate the SQL.
				generate( (QueryNode) sqlAst );
				queryLoader = new QueryLoader( this, factory, w.getSelectClause() );
			}

			compiled = true;
		}
		catch ( QueryException qe ) {
			if ( qe.getQueryString() == null ) {
				throw qe.wrapWithQueryString( hql );
			}
			else {
				throw qe;
			}
		}
		catch ( RecognitionException e ) {
			// we do not actually propagate ANTLRExceptions as a cause, so
			// log it here for diagnostic purposes
			LOG.trace( "Converted antlr.RecognitionException", e );
			throw QuerySyntaxException.convert( e, hql );
		}
		catch ( ANTLRException e ) {
			// we do not actually propagate ANTLRExceptions as a cause, so
			// log it here for diagnostic purposes
			LOG.trace( "Converted antlr.ANTLRException", e );
			throw new QueryException( e.getMessage(), hql );
		}
		catch ( IllegalArgumentException e ) {
			// translate this into QueryException
			LOG.trace( "Converted IllegalArgumentException", e );
			throw new QueryException( e.getMessage(), hql );
		}

		//only needed during compilation phase...
		this.enabledFilters = null;
	}

	private void generate(AST sqlAst) throws QueryException, RecognitionException {
		if ( sql == null ) {
			final SqlGenerator gen = new SqlGenerator( factory );
			gen.statement( sqlAst );
			sql = gen.getSQL();
			if ( LOG.isDebugEnabled() ) {
				LOG.debugf( "HQL: %s", hql );
				LOG.debugf( "SQL: %s", sql );
			}
			gen.getParseErrorHandler().throwQueryException();
			if ( collectedParameterSpecifications == null ) {
				collectedParameterSpecifications = gen.getCollectedParameters();
			}
			else {
				collectedParameterSpecifications.addAll( gen.getCollectedParameters() );
			}
		}
	}

	private HqlSqlWalker analyze(HqlParser parser, String collectionRole) throws QueryException, RecognitionException {
		final HqlSqlWalker w = new HqlSqlWalker( this, factory, parser, tokenReplacements, collectionRole );
		final AST hqlAst = parser.getAST();

		// Transform the tree.
		w.statement( hqlAst );

		if ( LOG.isDebugEnabled() ) {
			LOG.debug( TokenPrinters.SQL_TOKEN_PRINTER.showAsString( w.getAST(), "--- SQL AST ---" ) );
		}

		w.getParseErrorHandler().throwQueryException();

		return w;
	}

	private HqlParser parse(boolean filter) throws TokenStreamException {
		// Parse the query string into an HQL AST.
		final HqlParser parser = HqlParser.getInstance( hql );
		parser.setFilter( filter );

		LOG.debugf( "parse() - HQL: %s", hql );
		try {
			parser.statement();
		}
		catch (RecognitionException e) {
			throw new HibernateException( "Unexpected error parsing HQL", e );
		}

		final AST hqlAst = parser.getAST();
		parser.getParseErrorHandler().throwQueryException();

		final NodeTraverser walker = new NodeTraverser( new JavaConstantConverter( factory ) );
		walker.traverseDepthFirst( hqlAst );

		showHqlAst( hqlAst );

		return parser;
	}

	void showHqlAst(AST hqlAst) {
		if ( LOG.isDebugEnabled() ) {
			LOG.debug( TokenPrinters.HQL_TOKEN_PRINTER.showAsString( hqlAst, "--- HQL AST ---" ) );
		}
	}

	private void errorIfDML() throws HibernateException {
		if ( sqlAst.needsExecutor() ) {
			throw new QueryExecutionRequestException( "Not supported for DML operations", hql );
		}
	}

	private void errorIfSelect() throws HibernateException {
		if ( !sqlAst.needsExecutor() ) {
			throw new QueryExecutionRequestException( "Not supported for select queries", hql );
		}
	}
	@Override
	public String getQueryIdentifier() {
		return queryIdentifier;
	}

	public Statement getSqlAST() {
		return sqlAst;
	}

	private HqlSqlWalker getWalker() {
		return sqlAst.getWalker();
	}

	/**
	 * Types of the return values of an iterate() style query.
	 *
	 * @return an array of Types.
	 */
	@Override
	public Type[] getReturnTypes() {
		errorIfDML();
		return getWalker().getReturnTypes();
	}
	@Override
	public String[] getReturnAliases() {
		errorIfDML();
		return getWalker().getReturnAliases();
	}
	@Override
	public String[][] getColumnNames() {
		errorIfDML();
		return getWalker().getSelectClause().getColumnNames();
	}
	@Override
	public Set getQuerySpaces() {
		return getWalker().getQuerySpaces();
	}

	@Override
	public List list(SharedSessionContractImplementor session, QueryParameters queryParameters)
			throws HibernateException {
		// Delegate to the QueryLoader...
		errorIfDML();

		final QueryNode query = (QueryNode) sqlAst;
		final boolean hasLimit = queryParameters.getRowSelection() != null && queryParameters.getRowSelection().definesLimits();
		final boolean needsDistincting = (
				query.getSelectClause().isDistinct() ||
				getEntityGraphQueryHint() != null ||
				hasLimit )
		&& containsCollectionFetches();

		QueryParameters queryParametersToUse;
		if ( hasLimit && containsCollectionFetches() ) {
			boolean fail = session.getFactory().getSessionFactoryOptions().isFailOnPaginationOverCollectionFetchEnabled();
			if (fail) {
				throw new HibernateException("firstResult/maxResults specified with collection fetch. " +
						"In memory pagination was about to be applied. " +
						"Failing because 'Fail on pagination over collection fetch' is enabled.");
			}
			else {
				LOG.firstOrMaxResultsSpecifiedWithCollectionFetch();
			}
			RowSelection selection = new RowSelection();
			selection.setFetchSize( queryParameters.getRowSelection().getFetchSize() );
			selection.setTimeout( queryParameters.getRowSelection().getTimeout() );
			queryParametersToUse = queryParameters.createCopyUsing( selection );
		}
		else {
			queryParametersToUse = queryParameters;
		}

		List results = queryLoader.list( session, queryParametersToUse );

		if ( needsDistincting ) {
			int includedCount = -1;
			// NOTE : firstRow is zero-based
			int first = !hasLimit || queryParameters.getRowSelection().getFirstRow() == null
						? 0
						: queryParameters.getRowSelection().getFirstRow();
			int max = !hasLimit || queryParameters.getRowSelection().getMaxRows() == null
						? -1
						: queryParameters.getRowSelection().getMaxRows();
			List tmp = new ArrayList();
			IdentitySet distinction = new IdentitySet();
			for ( final Object result : results ) {
				if ( !distinction.add( result ) ) {
					continue;
				}
				includedCount++;
				if ( includedCount < first ) {
					continue;
				}
				tmp.add( result );
				// NOTE : ( max - 1 ) because first is zero-based while max is not...
				if ( max >= 0 && ( includedCount - first ) >= ( max - 1 ) ) {
					break;
				}
			}
			results = tmp;
		}

		return results;
	}

	/**
	 * Return the query results as an iterator
	 */
	@Override
	public Iterator iterate(QueryParameters queryParameters, EventSource session)
			throws HibernateException {
		// Delegate to the QueryLoader...
		errorIfDML();
		return queryLoader.iterate( queryParameters, session );
	}

	/**
	 * Return the query results, as an instance of ScrollableResults
	 */
	@Override
	public ScrollableResultsImplementor scroll(QueryParameters queryParameters, SharedSessionContractImplementor session)
			throws HibernateException {
		// Delegate to the QueryLoader...
		errorIfDML();
		return queryLoader.scroll( queryParameters, session );
	}
	@Override
	public int executeUpdate(QueryParameters queryParameters, SharedSessionContractImplementor session)
			throws HibernateException {
		errorIfSelect();
		return statementExecutor.execute( queryParameters, session );
	}

	/**
	 * The SQL query string to be called; implemented by all subclasses
	 */
	@Override
	public String getSQLString() {
		return sql;
	}
	@Override
	public List collectSqlStrings() {
		ArrayList list = new ArrayList();
		if ( isManipulationStatement() ) {
			String[] sqlStatements = statementExecutor.getSqlStatements();
			Collections.addAll( list, sqlStatements );
		}
		else {
			list.add( sql );
		}
		return list;
	}

	// -- Package local methods for the QueryLoader delegate --

	public boolean isShallowQuery() {
		return shallowQuery;
	}
	@Override
	public String getQueryString() {
		return hql;
	}
	@Override
	public Map getEnabledFilters() {
		return enabledFilters;
	}

	public int[] getNamedParameterLocs(String name) {
		return getWalker().getNamedParameterLocations( name );
	}
	@Override
	public boolean containsCollectionFetches() {
		errorIfDML();
		List collectionFetches = ( (QueryNode) sqlAst ).getFromClause().getCollectionFetches();
		return collectionFetches != null && collectionFetches.size() > 0;
	}
	@Override
	public boolean isManipulationStatement() {
		return sqlAst.needsExecutor();
	}
	@Override
	public boolean isUpdateStatement() {
		return SqlTokenTypes.UPDATE == sqlAst.getStatementType();
	}
	@Override
	public void validateScrollability() throws HibernateException {
		// Impl Note: allows multiple collection fetches as long as the
		// entire fecthed graph still "points back" to a single
		// root entity for return

		errorIfDML();

		final QueryNode query = (QueryNode) sqlAst;

		// If there are no collection fetches, then no further checks are needed
		List collectionFetches = query.getFromClause().getCollectionFetches();
		if ( collectionFetches.isEmpty() ) {
			return;
		}

		// A shallow query is ok (although technically there should be no fetching here...)
		if ( isShallowQuery() ) {
			return;
		}

		// Otherwise, we have a non-scalar select with defined collection fetch(es).
		// Make sure that there is only a single root entity in the return (no tuples)
		if ( getReturnTypes().length > 1 ) {
			throw new HibernateException( "cannot scroll with collection fetches and returned tuples" );
		}

		FromElement owner = null;
		for ( Object o : query.getSelectClause().getFromElementsForLoad() ) {
			// should be the first, but just to be safe...
			final FromElement fromElement = (FromElement) o;
			if ( fromElement.getOrigin() == null ) {
				owner = fromElement;
				break;
			}
		}

		if ( owner == null ) {
			throw new HibernateException( "unable to locate collection fetch(es) owner for scrollability checks" );
		}

		// This is not strictly true.  We actually just need to make sure that
		// it is ordered by root-entity PK and that that order-by comes before
		// any non-root-entity ordering...

		AST primaryOrdering = query.getOrderByClause().getFirstChild();
		if ( primaryOrdering != null ) {
			// TODO : this is a bit dodgy, come up with a better way to check this (plus see above comment)
			String [] idColNames = owner.getQueryable().getIdentifierColumnNames();
			String expectedPrimaryOrderSeq = String.join(
					", ",
					StringHelper.qualify( owner.getTableAlias(), idColNames )
			);
			if (  !primaryOrdering.getText().startsWith( expectedPrimaryOrderSeq ) ) {
				throw new HibernateException( "cannot scroll results with collection fetches which are not ordered primarily by the root entity's PK" );
			}
		}
	}

	private StatementExecutor buildAppropriateStatementExecutor(HqlSqlWalker walker) {
		final Statement statement = (Statement) walker.getAST();
		if ( walker.getStatementType() == HqlSqlTokenTypes.DELETE ) {
			final FromElement fromElement = walker.getFinalFromClause().getFromElement();
			final Queryable persister = fromElement.getQueryable();
			if ( persister.isMultiTable() ) {
				return new MultiTableDeleteExecutor( walker );
			}
			else {
				return new DeleteExecutor( walker, persister );
			}
		}
		else if ( walker.getStatementType() == HqlSqlTokenTypes.UPDATE ) {
			final FromElement fromElement = walker.getFinalFromClause().getFromElement();
			final Queryable persister = fromElement.getQueryable();
			if ( persister.isMultiTable() ) {
				// even here, if only properties mapped to the "base table" are referenced
				// in the set and where clauses, this could be handled by the BasicDelegate.
				// TODO : decide if it is better performance-wise to doAfterTransactionCompletion that check, or to simply use the MultiTableUpdateDelegate
				return new MultiTableUpdateExecutor( walker );
			}
			else {
				return new BasicExecutor( walker, persister );
			}
		}
		else if ( walker.getStatementType() == HqlSqlTokenTypes.INSERT ) {
			return new BasicExecutor( walker, ( (InsertStatement) statement ).getIntoClause().getQueryable() );
		}
		else {
			throw new QueryException( "Unexpected statement type" );
		}
	}
	@Override
	public ParameterTranslations getParameterTranslations() {
		if ( paramTranslations == null ) {
			paramTranslations = new ParameterTranslationsImpl( getWalker().getParameterSpecs() );
		}
		return paramTranslations;
	}

	public List getCollectedParameterSpecifications() {
		return collectedParameterSpecifications;
	}

	@Override
	public Class getDynamicInstantiationResultType() {
		AggregatedSelectExpression aggregation = queryLoader.getAggregatedSelectExpression();
		return aggregation == null ? null : aggregation.getAggregationResultType();
	}

	public static class JavaConstantConverter implements NodeTraverser.VisitationStrategy {
		private final SessionFactoryImplementor factory;
		private AST dotRoot;

		public JavaConstantConverter(SessionFactoryImplementor factory) {
			this.factory = factory;
		}

		@Override
		public void visit(AST node) {
			if ( dotRoot != null ) {
				// we are already processing a dot-structure
				if ( ASTUtil.isSubtreeChild( dotRoot, node ) ) {
					return;
				}
				// we are now at a new tree level
				dotRoot = null;
			}

			if ( node.getType() == HqlTokenTypes.DOT ) {
				dotRoot = node;
				handleDotStructure( dotRoot );
			}
		}
		private void handleDotStructure(AST dotStructureRoot) {
			final String expression = ASTUtil.getPathText( dotStructureRoot );
			final Object constant = ReflectHelper.getConstantValue( expression, factory );
			if ( constant != null ) {
				dotStructureRoot.setFirstChild( null );
				dotStructureRoot.setType( HqlTokenTypes.JAVA_CONSTANT );
				dotStructureRoot.setText( expression );
			}
		}
	}

	public EntityGraphQueryHint getEntityGraphQueryHint() {
		return entityGraphQueryHint;
	}

	public void setEntityGraphQueryHint(EntityGraphQueryHint entityGraphQueryHint) {
		this.entityGraphQueryHint = entityGraphQueryHint;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy