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