1 /*
2 * Hibernate, Relational Persistence for Idiomatic Java
3 *
4 * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as
5 * indicated by the @author tags or express copyright attribution
6 * statements applied by the authors. All third-party contributions are
7 * distributed under license by Red Hat Middleware LLC.
8 *
9 * This copyrighted material is made available to anyone wishing to use, modify,
10 * copy, or redistribute it subject to the terms and conditions of the GNU
11 * Lesser General Public License, as published by the Free Software Foundation.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
16 * for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public License
19 * along with this distribution; if not, write to:
20 * Free Software Foundation, Inc.
21 * 51 Franklin Street, Fifth Floor
22 * Boston, MA 02110-1301 USA
23 *
24 */
25 package org.hibernate.hql.ast;
26
27 import antlr.ANTLRException;
28 import antlr.RecognitionException;
29 import antlr.TokenStreamException;
30 import antlr.collections.AST;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33 import org.hibernate.HibernateException;
34 import org.hibernate.MappingException;
35 import org.hibernate.QueryException;
36 import org.hibernate.ScrollableResults;
37 import org.hibernate.engine.QueryParameters;
38 import org.hibernate.engine.RowSelection;
39 import org.hibernate.engine.SessionFactoryImplementor;
40 import org.hibernate.engine.SessionImplementor;
41 import org.hibernate.event.EventSource;
42 import org.hibernate.hql.FilterTranslator;
43 import org.hibernate.hql.QueryExecutionRequestException;
44 import org.hibernate.hql.ParameterTranslations;
45 import org.hibernate.hql.antlr.HqlSqlTokenTypes;
46 import org.hibernate.hql.antlr.HqlTokenTypes;
47 import org.hibernate.hql.antlr.SqlTokenTypes;
48 import org.hibernate.hql.ast.exec.BasicExecutor;
49 import org.hibernate.hql.ast.exec.MultiTableDeleteExecutor;
50 import org.hibernate.hql.ast.exec.MultiTableUpdateExecutor;
51 import org.hibernate.hql.ast.exec.StatementExecutor;
52 import org.hibernate.hql.ast.tree.FromElement;
53 import org.hibernate.hql.ast.tree.InsertStatement;
54 import org.hibernate.hql.ast.tree.QueryNode;
55 import org.hibernate.hql.ast.tree.Statement;
56 import org.hibernate.hql.ast.util.ASTPrinter;
57 import org.hibernate.hql.ast.util.NodeTraverser;
58 import org.hibernate.hql.ast.util.ASTUtil;
59 import org.hibernate.loader.hql.QueryLoader;
60 import org.hibernate.persister.entity.Queryable;
61 import org.hibernate.type.Type;
62 import org.hibernate.util.IdentitySet;
63 import org.hibernate.util.StringHelper;
64 import org.hibernate.util.ReflectHelper;
65
66 import java.util.HashMap;
67 import java.util.Iterator;
68 import java.util.List;
69 import java.util.Map;
70 import java.util.Set;
71 import java.util.ArrayList;
72
73 /**
74 * A QueryTranslator that uses an Antlr-based parser.
75 *
76 * @author Joshua Davis (pgmjsd@sourceforge.net)
77 */
78 public class QueryTranslatorImpl implements FilterTranslator {
79
80 private static final Logger log = LoggerFactory.getLogger( QueryTranslatorImpl.class );
81 private static final Logger AST_LOG = LoggerFactory.getLogger( "org.hibernate.hql.ast.AST" );
82
83 private SessionFactoryImplementor factory;
84
85 private final String queryIdentifier;
86 private String hql;
87 private boolean shallowQuery;
88 private Map tokenReplacements;
89
90 private Map enabledFilters; //TODO:this is only needed during compilation .. can we eliminate the instvar?
91
92 private boolean compiled;
93 private QueryLoader queryLoader;
94 private StatementExecutor statementExecutor;
95
96 private Statement sqlAst;
97 private String sql;
98
99 private ParameterTranslations paramTranslations;
100 private List collectedParameterSpecifications;
101
102
103 /**
104 * Creates a new AST-based query translator.
105 *
106 * @param queryIdentifier The query-identifier (used in stats collection)
107 * @param query The hql query to translate
108 * @param enabledFilters Currently enabled filters
109 * @param factory The session factory constructing this translator instance.
110 */
111 public QueryTranslatorImpl(
112 String queryIdentifier,
113 String query,
114 Map enabledFilters,
115 SessionFactoryImplementor factory) {
116 this.queryIdentifier = queryIdentifier;
117 this.hql = query;
118 this.compiled = false;
119 this.shallowQuery = false;
120 this.enabledFilters = enabledFilters;
121 this.factory = factory;
122 }
123
124 /**
125 * Compile a "normal" query. This method may be called multiple
126 * times. Subsequent invocations are no-ops.
127 *
128 * @param replacements Defined query substitutions.
129 * @param shallow Does this represent a shallow (scalar or entity-id) select?
130 * @throws QueryException There was a problem parsing the query string.
131 * @throws MappingException There was a problem querying defined mappings.
132 */
133 public void compile(
134 Map replacements,
135 boolean shallow) throws QueryException, MappingException {
136 doCompile( replacements, shallow, null );
137 }
138
139 /**
140 * Compile a filter. This method may be called multiple
141 * times. Subsequent invocations are no-ops.
142 *
143 * @param collectionRole the role name of the collection used as the basis for the filter.
144 * @param replacements Defined query substitutions.
145 * @param shallow Does this represent a shallow (scalar or entity-id) select?
146 * @throws QueryException There was a problem parsing the query string.
147 * @throws MappingException There was a problem querying defined mappings.
148 */
149 public void compile(
150 String collectionRole,
151 Map replacements,
152 boolean shallow) throws QueryException, MappingException {
153 doCompile( replacements, shallow, collectionRole );
154 }
155
156 /**
157 * Performs both filter and non-filter compiling.
158 *
159 * @param replacements Defined query substitutions.
160 * @param shallow Does this represent a shallow (scalar or entity-id) select?
161 * @param collectionRole the role name of the collection used as the basis for the filter, NULL if this
162 * is not a filter.
163 */
164 private synchronized void doCompile(Map replacements, boolean shallow, String collectionRole) {
165 // If the query is already compiled, skip the compilation.
166 if ( compiled ) {
167 if ( log.isDebugEnabled() ) {
168 log.debug( "compile() : The query is already compiled, skipping..." );
169 }
170 return;
171 }
172
173 // Remember the parameters for the compilation.
174 this.tokenReplacements = replacements;
175 if ( tokenReplacements == null ) {
176 tokenReplacements = new HashMap();
177 }
178 this.shallowQuery = shallow;
179
180 try {
181 // PHASE 1 : Parse the HQL into an AST.
182 HqlParser parser = parse( true );
183
184 // PHASE 2 : Analyze the HQL AST, and produce an SQL AST.
185 HqlSqlWalker w = analyze( parser, collectionRole );
186
187 sqlAst = ( Statement ) w.getAST();
188
189 // at some point the generate phase needs to be moved out of here,
190 // because a single object-level DML might spawn multiple SQL DML
191 // command executions.
192 //
193 // Possible to just move the sql generation for dml stuff, but for
194 // consistency-sake probably best to just move responsiblity for
195 // the generation phase completely into the delegates
196 // (QueryLoader/StatementExecutor) themselves. Also, not sure why
197 // QueryLoader currently even has a dependency on this at all; does
198 // it need it? Ideally like to see the walker itself given to the delegates directly...
199
200 if ( sqlAst.needsExecutor() ) {
201 statementExecutor = buildAppropriateStatementExecutor( w );
202 }
203 else {
204 // PHASE 3 : Generate the SQL.
205 generate( ( QueryNode ) sqlAst );
206 queryLoader = new QueryLoader( this, factory, w.getSelectClause() );
207 }
208
209 compiled = true;
210 }
211 catch ( QueryException qe ) {
212 qe.setQueryString( hql );
213 throw qe;
214 }
215 catch ( RecognitionException e ) {
216 // we do not actually propogate ANTLRExceptions as a cause, so
217 // log it here for diagnostic purposes
218 if ( log.isTraceEnabled() ) {
219 log.trace( "converted antlr.RecognitionException", e );
220 }
221 throw QuerySyntaxException.convert( e, hql );
222 }
223 catch ( ANTLRException e ) {
224 // we do not actually propogate ANTLRExceptions as a cause, so
225 // log it here for diagnostic purposes
226 if ( log.isTraceEnabled() ) {
227 log.trace( "converted antlr.ANTLRException", e );
228 }
229 throw new QueryException( e.getMessage(), hql );
230 }
231
232 this.enabledFilters = null; //only needed during compilation phase...
233 }
234
235 private void generate(AST sqlAst) throws QueryException, RecognitionException {
236 if ( sql == null ) {
237 SqlGenerator gen = new SqlGenerator(factory);
238 gen.statement( sqlAst );
239 sql = gen.getSQL();
240 if ( log.isDebugEnabled() ) {
241 log.debug( "HQL: " + hql );
242 log.debug( "SQL: " + sql );
243 }
244 gen.getParseErrorHandler().throwQueryException();
245 collectedParameterSpecifications = gen.getCollectedParameters();
246 }
247 }
248
249 private HqlSqlWalker analyze(HqlParser parser, String collectionRole) throws QueryException, RecognitionException {
250 HqlSqlWalker w = new HqlSqlWalker( this, factory, parser, tokenReplacements, collectionRole );
251 AST hqlAst = parser.getAST();
252
253 // Transform the tree.
254 w.statement( hqlAst );
255
256 if ( AST_LOG.isDebugEnabled() ) {
257 ASTPrinter printer = new ASTPrinter( SqlTokenTypes.class );
258 AST_LOG.debug( printer.showAsString( w.getAST(), "--- SQL AST ---" ) );
259 }
260
261 w.getParseErrorHandler().throwQueryException();
262
263 return w;
264 }
265
266 private HqlParser parse(boolean filter) throws TokenStreamException, RecognitionException {
267 // Parse the query string into an HQL AST.
268 HqlParser parser = HqlParser.getInstance( hql );
269 parser.setFilter( filter );
270
271 if ( log.isDebugEnabled() ) {
272 log.debug( "parse() - HQL: " + hql );
273 }
274 parser.statement();
275
276 AST hqlAst = parser.getAST();
277
278 JavaConstantConverter converter = new JavaConstantConverter();
279 NodeTraverser walker = new NodeTraverser( converter );
280 walker.traverseDepthFirst( hqlAst );
281
282 showHqlAst( hqlAst );
283
284 parser.getParseErrorHandler().throwQueryException();
285 return parser;
286 }
287
288 void showHqlAst(AST hqlAst) {
289 if ( AST_LOG.isDebugEnabled() ) {
290 ASTPrinter printer = new ASTPrinter( HqlTokenTypes.class, false );
291 AST_LOG.debug( printer.showAsString( hqlAst, "--- HQL AST ---" ) );
292 }
293 }
294
295 private void errorIfDML() throws HibernateException {
296 if ( sqlAst.needsExecutor() ) {
297 throw new QueryExecutionRequestException( "Not supported for DML operations", hql );
298 }
299 }
300
301 private void errorIfSelect() throws HibernateException {
302 if ( !sqlAst.needsExecutor() ) {
303 throw new QueryExecutionRequestException( "Not supported for select queries", hql );
304 }
305 }
306
307 public String getQueryIdentifier() {
308 return queryIdentifier;
309 }
310
311 public Statement getSqlAST() {
312 return sqlAst;
313 }
314
315 private HqlSqlWalker getWalker() {
316 return sqlAst.getWalker();
317 }
318
319 /**
320 * Types of the return values of an <tt>iterate()</tt> style query.
321 *
322 * @return an array of <tt>Type</tt>s.
323 */
324 public Type[] getReturnTypes() {
325 errorIfDML();
326 return getWalker().getReturnTypes();
327 }
328
329 public String[] getReturnAliases() {
330 errorIfDML();
331 return getWalker().getReturnAliases();
332 }
333
334 public String[][] getColumnNames() {
335 errorIfDML();
336 return getWalker().getSelectClause().getColumnNames();
337 }
338
339 public Set getQuerySpaces() {
340 return getWalker().getQuerySpaces();
341 }
342
343 public List list(SessionImplementor session, QueryParameters queryParameters)
344 throws HibernateException {
345 // Delegate to the QueryLoader...
346 errorIfDML();
347 QueryNode query = ( QueryNode ) sqlAst;
348 boolean hasLimit = queryParameters.getRowSelection() != null && queryParameters.getRowSelection().definesLimits();
349 boolean needsDistincting = ( query.getSelectClause().isDistinct() || hasLimit ) && containsCollectionFetches();
350
351 QueryParameters queryParametersToUse;
352 if ( hasLimit && containsCollectionFetches() ) {
353 log.warn( "firstResult/maxResults specified with collection fetch; applying in memory!" );
354 RowSelection selection = new RowSelection();
355 selection.setFetchSize( queryParameters.getRowSelection().getFetchSize() );
356 selection.setTimeout( queryParameters.getRowSelection().getTimeout() );
357 queryParametersToUse = queryParameters.createCopyUsing( selection );
358 }
359 else {
360 queryParametersToUse = queryParameters;
361 }
362
363 List results = queryLoader.list( session, queryParametersToUse );
364
365 if ( needsDistincting ) {
366 int includedCount = -1;
367 // NOTE : firstRow is zero-based
368 int first = !hasLimit || queryParameters.getRowSelection().getFirstRow() == null
369 ? 0
370 : queryParameters.getRowSelection().getFirstRow().intValue();
371 int max = !hasLimit || queryParameters.getRowSelection().getMaxRows() == null
372 ? -1
373 : queryParameters.getRowSelection().getMaxRows().intValue();
374 int size = results.size();
375 List tmp = new ArrayList();
376 IdentitySet distinction = new IdentitySet();
377 for ( int i = 0; i < size; i++ ) {
378 final Object result = results.get( i );
379 if ( !distinction.add( result ) ) {
380 continue;
381 }
382 includedCount++;
383 if ( includedCount < first ) {
384 continue;
385 }
386 tmp.add( result );
387 // NOTE : ( max - 1 ) because first is zero-based while max is not...
388 if ( max >= 0 && ( includedCount - first ) >= ( max - 1 ) ) {
389 break;
390 }
391 }
392 results = tmp;
393 }
394
395 return results;
396 }
397
398 /**
399 * Return the query results as an iterator
400 */
401 public Iterator iterate(QueryParameters queryParameters, EventSource session)
402 throws HibernateException {
403 // Delegate to the QueryLoader...
404 errorIfDML();
405 return queryLoader.iterate( queryParameters, session );
406 }
407
408 /**
409 * Return the query results, as an instance of <tt>ScrollableResults</tt>
410 */
411 public ScrollableResults scroll(QueryParameters queryParameters, SessionImplementor session)
412 throws HibernateException {
413 // Delegate to the QueryLoader...
414 errorIfDML();
415 return queryLoader.scroll( queryParameters, session );
416 }
417
418 public int executeUpdate(QueryParameters queryParameters, SessionImplementor session)
419 throws HibernateException {
420 errorIfSelect();
421 return statementExecutor.execute( queryParameters, session );
422 }
423
424 /**
425 * The SQL query string to be called; implemented by all subclasses
426 */
427 public String getSQLString() {
428 return sql;
429 }
430
431 public List collectSqlStrings() {
432 ArrayList list = new ArrayList();
433 if ( isManipulationStatement() ) {
434 String[] sqlStatements = statementExecutor.getSqlStatements();
435 for ( int i = 0; i < sqlStatements.length; i++ ) {
436 list.add( sqlStatements[i] );
437 }
438 }
439 else {
440 list.add( sql );
441 }
442 return list;
443 }
444
445 // -- Package local methods for the QueryLoader delegate --
446
447 public boolean isShallowQuery() {
448 return shallowQuery;
449 }
450
451 public String getQueryString() {
452 return hql;
453 }
454
455 public Map getEnabledFilters() {
456 return enabledFilters;
457 }
458
459 public int[] getNamedParameterLocs(String name) {
460 return getWalker().getNamedParameterLocations( name );
461 }
462
463 public boolean containsCollectionFetches() {
464 errorIfDML();
465 List collectionFetches = ( ( QueryNode ) sqlAst ).getFromClause().getCollectionFetches();
466 return collectionFetches != null && collectionFetches.size() > 0;
467 }
468
469 public boolean isManipulationStatement() {
470 return sqlAst.needsExecutor();
471 }
472
473 public void validateScrollability() throws HibernateException {
474 // Impl Note: allows multiple collection fetches as long as the
475 // entire fecthed graph still "points back" to a single
476 // root entity for return
477
478 errorIfDML();
479
480 QueryNode query = ( QueryNode ) sqlAst;
481
482 // If there are no collection fetches, then no further checks are needed
483 List collectionFetches = query.getFromClause().getCollectionFetches();
484 if ( collectionFetches.isEmpty() ) {
485 return;
486 }
487
488 // A shallow query is ok (although technically there should be no fetching here...)
489 if ( isShallowQuery() ) {
490 return;
491 }
492
493 // Otherwise, we have a non-scalar select with defined collection fetch(es).
494 // Make sure that there is only a single root entity in the return (no tuples)
495 if ( getReturnTypes().length > 1 ) {
496 throw new HibernateException( "cannot scroll with collection fetches and returned tuples" );
497 }
498
499 FromElement owner = null;
500 Iterator itr = query.getSelectClause().getFromElementsForLoad().iterator();
501 while ( itr.hasNext() ) {
502 // should be the first, but just to be safe...
503 final FromElement fromElement = ( FromElement ) itr.next();
504 if ( fromElement.getOrigin() == null ) {
505 owner = fromElement;
506 break;
507 }
508 }
509
510 if ( owner == null ) {
511 throw new HibernateException( "unable to locate collection fetch(es) owner for scrollability checks" );
512 }
513
514 // This is not strictly true. We actually just need to make sure that
515 // it is ordered by root-entity PK and that that order-by comes before
516 // any non-root-entity ordering...
517
518 AST primaryOrdering = query.getOrderByClause().getFirstChild();
519 if ( primaryOrdering != null ) {
520 // TODO : this is a bit dodgy, come up with a better way to check this (plus see above comment)
521 String [] idColNames = owner.getQueryable().getIdentifierColumnNames();
522 String expectedPrimaryOrderSeq = StringHelper.join(
523 ", ",
524 StringHelper.qualify( owner.getTableAlias(), idColNames )
525 );
526 if ( !primaryOrdering.getText().startsWith( expectedPrimaryOrderSeq ) ) {
527 throw new HibernateException( "cannot scroll results with collection fetches which are not ordered primarily by the root entity's PK" );
528 }
529 }
530 }
531
532 private StatementExecutor buildAppropriateStatementExecutor(HqlSqlWalker walker) {
533 Statement statement = ( Statement ) walker.getAST();
534 if ( walker.getStatementType() == HqlSqlTokenTypes.DELETE ) {
535 FromElement fromElement = walker.getFinalFromClause().getFromElement();
536 Queryable persister = fromElement.getQueryable();
537 if ( persister.isMultiTable() ) {
538 return new MultiTableDeleteExecutor( walker );
539 }
540 else {
541 return new BasicExecutor( walker, persister );
542 }
543 }
544 else if ( walker.getStatementType() == HqlSqlTokenTypes.UPDATE ) {
545 FromElement fromElement = walker.getFinalFromClause().getFromElement();
546 Queryable persister = fromElement.getQueryable();
547 if ( persister.isMultiTable() ) {
548 // even here, if only properties mapped to the "base table" are referenced
549 // in the set and where clauses, this could be handled by the BasicDelegate.
550 // TODO : decide if it is better performance-wise to perform that check, or to simply use the MultiTableUpdateDelegate
551 return new MultiTableUpdateExecutor( walker );
552 }
553 else {
554 return new BasicExecutor( walker, persister );
555 }
556 }
557 else if ( walker.getStatementType() == HqlSqlTokenTypes.INSERT ) {
558 return new BasicExecutor( walker, ( ( InsertStatement ) statement ).getIntoClause().getQueryable() );
559 }
560 else {
561 throw new QueryException( "Unexpected statement type" );
562 }
563 }
564
565 public ParameterTranslations getParameterTranslations() {
566 if ( paramTranslations == null ) {
567 paramTranslations = new ParameterTranslationsImpl( getWalker().getParameters() );
568 // paramTranslations = new ParameterTranslationsImpl( collectedParameterSpecifications );
569 }
570 return paramTranslations;
571 }
572
573 public List getCollectedParameterSpecifications() {
574 return collectedParameterSpecifications;
575 }
576
577 public static class JavaConstantConverter implements NodeTraverser.VisitationStrategy {
578 private AST dotRoot;
579 public void visit(AST node) {
580 if ( dotRoot != null ) {
581 // we are already processing a dot-structure
582 if ( ASTUtil.isSubtreeChild( dotRoot, node ) ) {
583 // igndore it...
584 return;
585 }
586 else {
587 // we are now at a new tree level
588 dotRoot = null;
589 }
590 }
591
592 if ( dotRoot == null && node.getType() == HqlTokenTypes.DOT ) {
593 dotRoot = node;
594 handleDotStructure( dotRoot );
595 }
596 }
597 private void handleDotStructure(AST dotStructureRoot) {
598 String expression = ASTUtil.getPathText( dotStructureRoot );
599 Object constant = ReflectHelper.getConstantValue( expression );
600 if ( constant != null ) {
601 dotStructureRoot.setFirstChild( null );
602 dotStructureRoot.setType( HqlTokenTypes.JAVA_CONSTANT );
603 dotStructureRoot.setText( expression );
604 }
605 }
606 }
607 }