diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java index ece52baace44..481126e79960 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerDialect.java @@ -43,6 +43,7 @@ import org.hibernate.query.common.TemporalUnit; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.SetOperator; +import org.hibernate.query.sqm.TrimSpec; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.spi.LockingClauseStrategy; @@ -653,6 +654,15 @@ public static Replacer datetimeFormat(String format) { .replace("xx", "%z"); //note special case } + @Override + public String trimPattern(TrimSpec specification, boolean isWhitespace) { + return switch ( specification ) { + case LEADING -> isWhitespace ? "ltrim(?1)" : "ltrim(?1, ?2)"; + case TRAILING -> isWhitespace ? "rtrim(?1)" : "rtrim(?1, ?2)"; + default -> isWhitespace ? "trim(?1)" : "trim(?1, ?2)"; + }; + } + /* DDL-related functions */ @Override @@ -955,6 +965,24 @@ public String getSetOperatorSqlString(SetOperator operator) { }; } + @Override + public String getDual() { + return "unnest([1])"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual() + " dual"; + } + + @Override + public boolean supportsLateral() { + // Spanner does not support the `LATERAL` keyword natively. + // However, we return true here because `SpannerSqlAstTranslator` emulates + // lateral joins using the `UNNEST(ARRAY(select as struct..)) alias` syntax. + return true; + } + /* Type conversion and casting */ /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SpannerSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SpannerSqlAstTranslator.java index cd25879460b8..3c68a21d6fab 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SpannerSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/SpannerSqlAstTranslator.java @@ -20,6 +20,10 @@ import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.DerivedTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.QueryPartTableReference; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.predicate.InArrayPredicate; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; @@ -112,6 +116,8 @@ protected void renderDerivedTableReference(DerivedTableReference tableReference) if ( correlated ) { this.correlated = oldCorrelated; appendSql( CLOSE_PARENTHESIS ); + // Spanner requires the alias to be outside the parentheses UNNEST(... ) alias + super.renderTableReferenceIdentificationVariable( tableReference ); } } @@ -142,6 +148,19 @@ protected void visitUpdateStatementOnly(UpdateStatement statement) { } } + @Override + protected void renderTableReferenceIdentificationVariable(TableReference tableReference) { + // Spanner requires `UNNEST(...) alias`. Standard rendering places the alias + // inside the parentheses UNNEST(... alias). We suppress it here to manually + // render it outside the UNNEST wrapper in `renderDerivedTableReference`. + if ( correlated + && tableReference instanceof DerivedTableReference + && ((DerivedTableReference) tableReference).isLateral() ) { + return; + } + super.renderTableReferenceIdentificationVariable( tableReference ); + } + @Override protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { super.renderDmlTargetTableExpression( tableReference ); @@ -150,4 +169,30 @@ protected void renderDmlTargetTableExpression(NamedTableReference tableReference } } + @Override + protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableReference tableReference) { + renderTableReferenceIdentificationVariable( tableReference ); + } + + @Override + public void visitQueryPartTableReference(QueryPartTableReference tableReference) { + emulateQueryPartTableReferenceColumnAliasing( tableReference ); + } + + @Override + public void visitInArrayPredicate(InArrayPredicate inArrayPredicate) { + inArrayPredicate.getTestExpression().accept( this ); + appendSql( " in unnest(" ); + inArrayPredicate.getArrayParameter().accept( this ); + appendSql( ')' ); + } + + @Override + public void visitLikePredicate(LikePredicate likePredicate) { + if ( likePredicate.getEscapeCharacter() != null ) { + throw new UnsupportedOperationException( "Escape character is not supported by Spanner" ); + } + super.visitLikePredicate( likePredicate ); + } + } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ILikeCriteriaTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ILikeCriteriaTest.java index c7a7a7285ab7..a794ec2c13ed 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ILikeCriteriaTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ILikeCriteriaTest.java @@ -9,6 +9,7 @@ import jakarta.persistence.criteria.Root; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.SpannerDialect; import org.hibernate.query.Query; import org.hibernate.query.criteria.HibernateCriteriaBuilder; @@ -19,6 +20,7 @@ import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.Setting; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -88,6 +90,7 @@ public void testLike(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = SpannerDialect.class, reason = "Spanner does not support escape character") public void testLikeEscape(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -140,6 +143,7 @@ public void testNotLike(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = SpannerDialect.class, reason = "Spanner does not support escape character") public void testNotLikeEscape(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -192,6 +196,7 @@ public void testIlike(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = SpannerDialect.class, reason = "Spanner does not support escape character") public void testIlikeEscape(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -244,6 +249,7 @@ public void testNotIlike(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = SpannerDialect.class, reason = "Spanner does not support escape character") public void testNotIlikeEscape(SessionFactoryScope scope) { scope.inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ILikeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ILikeTest.java index 2f1542fcf44b..f04e249c156c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ILikeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ILikeTest.java @@ -6,6 +6,7 @@ import java.util.List; +import org.hibernate.dialect.SpannerDialect; import org.hibernate.query.Query; import org.hibernate.testing.orm.domain.StandardDomainModel; @@ -14,6 +15,7 @@ import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -85,6 +87,7 @@ public void testNotLike(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = SpannerDialect.class, reason = "Spanner does not support escape character") public void testLikeEscape(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -96,6 +99,7 @@ public void testLikeEscape(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = SpannerDialect.class, reason = "Spanner does not support escape character") public void testLikeEscapeParam(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -108,6 +112,7 @@ public void testLikeEscapeParam(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = SpannerDialect.class, reason = "Spanner does not support escape character") public void testNotLikeEscape(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -141,6 +146,7 @@ public void testNotIlike(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = SpannerDialect.class, reason = "Spanner does not support escape character") public void testIlikeEscape(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -152,6 +158,7 @@ public void testIlikeEscape(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = SpannerDialect.class, reason = "Spanner does not support escape character") public void testNotIlikeEscape(SessionFactoryScope scope) { scope.inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/returns/ScrollableResultsTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/returns/ScrollableResultsTests.java index cd6b3d0a0fa9..f8b77a9062da 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/returns/ScrollableResultsTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/returns/ScrollableResultsTests.java @@ -11,6 +11,7 @@ import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.dialect.HANADialect; +import org.hibernate.dialect.SpannerDialect; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.query.Query; @@ -53,6 +54,7 @@ public void cleanUpTestData(SessionFactoryScope scope) { @Test @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA supports only ResultSet.TYPE_FORWARD_ONLY") + @SkipForDialect(dialectClass = SpannerDialect.class, reason = "Spanner supports only ResultSet.TYPE_FORWARD_ONLY") public void testCursorPositioning(SessionFactoryScope scope) { // create an extra row so we can better test cursor positioning scope.inTransaction( @@ -262,8 +264,9 @@ private static void verifyScroll(Query query, Consumer validator) { } final SessionImplementor session = (SessionImplementor) query.getSession(); - // HANA supports only ResultSet.TYPE_FORWARD_ONLY - if ( !( session.getFactory().getJdbcServices().getDialect() instanceof HANADialect ) ) { + // HANA and Spanner support only ResultSet.TYPE_FORWARD_ONLY + if ( !(session.getFactory().getJdbcServices().getDialect() instanceof HANADialect) && + !(session.getFactory().getJdbcServices().getDialect() instanceof SpannerDialect) ) { try (final ScrollableResults results = query.scroll( ScrollMode.SCROLL_INSENSITIVE )) { assertThat( results.next(), is( true ) ); validator.accept( results.get() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/BasicSelectionQueryTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/BasicSelectionQueryTests.java index 70c1f4921f97..48a3871a63c5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/BasicSelectionQueryTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/BasicSelectionQueryTests.java @@ -14,6 +14,7 @@ import org.hibernate.ScrollMode; import org.hibernate.dialect.HANADialect; +import org.hibernate.dialect.SpannerDialect; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.query.IllegalSelectQueryException; import org.hibernate.query.SelectionQuery; @@ -39,6 +40,7 @@ ) @SessionFactory @SkipForDialect( dialectClass = HANADialect.class, reason = "HANA does not support scrollable results") +@SkipForDialect( dialectClass = SpannerDialect.class, reason = "Spanner does not support scrollable results") public class BasicSelectionQueryTests { @Test public void typedEntitySelectTest(SessionFactoryScope scope) {