diff --git a/hibernate-core/src/main/java/org/hibernate/sql/Template.java b/hibernate-core/src/main/java/org/hibernate/sql/Template.java index 05c5a78d028c..dc193c5d4e43 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/Template.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/Template.java @@ -194,6 +194,8 @@ else if ( LITERAL_PREFIXES.contains( lcToken ) ) { continue; } else if ( nextToken != null && Character.isWhitespace( nextToken.charAt( 0 ) ) ) { + final StringTokenizer lookahead = lookahead( sqlWhereString, symbols, tokens); + String lookaheadToken = lookahead.hasMoreTokens() ? lookahead.nextToken() : null; final StringBuilder additionalTokens = new StringBuilder(); TimeZoneTokens possibleNextToken = null; do { @@ -201,15 +203,18 @@ else if ( nextToken != null && Character.isWhitespace( nextToken.charAt( 0 ) ) ) ? TimeZoneTokens.getPossibleNextTokens( lcToken ) : possibleNextToken.nextToken(); do { - additionalTokens.append( nextToken ); - hasMore = tokens.hasMoreTokens(); - nextToken = tokens.nextToken(); - } while ( nextToken != null && Character.isWhitespace( nextToken.charAt( 0 ) ) ); - } while ( nextToken != null && possibleNextToken.isToken( nextToken ) ); - if ( "'".equals( nextToken ) ) { + additionalTokens.append( lookaheadToken ); + lookaheadToken = lookahead.hasMoreTokens() ? lookahead.nextToken() : null; + } while ( lookaheadToken != null && Character.isWhitespace( lookaheadToken.charAt( 0 ) ) ); + } while ( lookaheadToken != null && possibleNextToken.isToken( lookaheadToken ) ); + if ( "'".equals( lookaheadToken ) ) { // Don't prefix a literal result.append( token ); result.append( additionalTokens ); + while (tokens.countTokens() > lookahead.countTokens()) { + hasMore = tokens.hasMoreTokens(); + nextToken = hasMore ? tokens.nextToken() : null; + } continue; } else { @@ -401,6 +406,24 @@ else if ( inFromClause && ",".equals(lcToken) ) { return result.toString(); } + /** + * Clone the given token stream, returning a token stream which begins + * from the next token. + * + * @param sql the full SQL we are scanning + * @param symbols the delimiter symbols + * @param tokens the current token stream + * @return a cloned token stream + */ + private static StringTokenizer lookahead(String sql, String symbols, StringTokenizer tokens) { + final StringTokenizer lookahead = + new StringTokenizer( sql, symbols, true ); + while ( lookahead.countTokens() > tokens.countTokens() + 1) { + lookahead.nextToken(); + } + return lookahead; + } + private enum TimeZoneTokens { NONE, WITH, diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/joinsubquery/JoinSubqueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/joinsubquery/JoinSubqueryTest.java new file mode 100644 index 000000000000..d54a7421cbd7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/joinsubquery/JoinSubqueryTest.java @@ -0,0 +1,129 @@ +/* + * 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.orm.test.joinsubquery; + +import jakarta.persistence.*; +import org.hibernate.annotations.JoinColumnOrFormula; +import org.hibernate.annotations.JoinFormula; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DomainModel(annotatedClasses = { + JoinSubqueryTest.RecordItem.class, + JoinSubqueryTest.RecordType.class +}) +@SessionFactory +@JiraKey("HHH-19052") +class JoinSubqueryTest { + + @BeforeAll + static void setUp(SessionFactoryScope scope) throws Exception { + scope.inTransaction(session -> { + final var id = 1L; + final var typeId = 42L; + final var recordType = new RecordType(id, typeId); + session.persist(recordType); + final var item = new RecordItem(id, typeId, recordType); + session.persist(item); + }); + } + + @Test + void test(SessionFactoryScope scope) throws Exception { + scope.inSession(session -> { + final var item = session.get(RecordItem.class, 1L); + assertNotNull(item); + }); + } + + @Entity + @Table(name = "record_items") + public static class RecordItem implements Serializable { + + @Id + protected Long id; + + @Column(name = "type_id", insertable = false, updatable = false) + private Long typeId; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumnOrFormula(column = @JoinColumn(name = "type_id", referencedColumnName = "entity_id")) + @JoinColumnOrFormula(formula = @JoinFormula(value = "(SELECT x.id FROM record_types x WHERE x.entity_id = type_id)", referencedColumnName = "id")) + private RecordType type; + + RecordItem() { + } + + public RecordItem(Long id, Long typeId, RecordType type) { + this.id = id; + this.typeId = typeId; + this.type = type; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return this.id; + } + + public Long getTypeId() { + return typeId; + } + + public RecordType getType() { + return type; + } + + + } + + @Entity + @Table(name = "record_types") + public static class RecordType implements Serializable { + + @Id + protected Long id; + + @Column(name = "entity_id") + private Long entityId; + + RecordType() { + } + + public RecordType(Long id, Long entityId) { + this.id = id; + this.entityId = entityId; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return this.id; + } + + public Long getEntityId() { + return entityId; + } + + public void setEntityId(Long entityId) { + this.entityId = entityId; + } + + } +}