diff --git a/hypersistence-utils-hibernate-63/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java b/hypersistence-utils-hibernate-63/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java index 4a6891e56..8e9609d34 100644 --- a/hypersistence-utils-hibernate-63/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java +++ b/hypersistence-utils-hibernate-63/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java @@ -1,19 +1,32 @@ package io.hypersistence.utils.hibernate.query; -import io.hypersistence.utils.common.ReflectionUtils; import jakarta.persistence.Query; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.spi.QueryImplementor; -import org.hibernate.query.spi.QueryInterpretationCache; -import org.hibernate.query.spi.SelectQueryPlan; -import org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.query.spi.QueryParameterImplementor; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.QuerySqmImpl; -import org.hibernate.query.sqm.internal.SqmInterpretationsKey; +import org.hibernate.query.sqm.internal.SqmUtil; +import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; +import org.hibernate.query.sqm.sql.SqmTranslation; +import org.hibernate.query.sqm.tree.SqmDmlStatement; +import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcParametersList; -import java.util.function.Supplier; +import java.util.List; +import java.util.Map; /** * The {@link SQLExtractor} allows you to extract the @@ -36,42 +49,124 @@ protected SQLExtractor() { * @return the underlying SQL generated by the provided JPA query */ public static String from(Query query) { - if(query instanceof SqmInterpretationsKey.InterpretationsKeySource && - query instanceof QueryImplementor && - query instanceof QuerySqmImpl) { - QueryInterpretationCache.Key cacheKey = SqmInterpretationsKey.createInterpretationsKey((SqmInterpretationsKey.InterpretationsKeySource) query); + if (query instanceof QuerySqmImpl) { QuerySqmImpl querySqm = (QuerySqmImpl) query; - Supplier buildSelectQueryPlan = () -> ReflectionUtils.invokeMethod(querySqm, "buildSelectQueryPlan"); - SelectQueryPlan plan = cacheKey != null ? ((QueryImplementor) query).getSession().getFactory().getQueryEngine() - .getInterpretationCache() - .resolveSelectQueryPlan(cacheKey, buildSelectQueryPlan) : - (SelectQueryPlan) buildSelectQueryPlan.get(); - if(plan instanceof ConcreteSqmSelectQueryPlan) { - ConcreteSqmSelectQueryPlan selectQueryPlan = (ConcreteSqmSelectQueryPlan) plan; - Object cacheableSqmInterpretation = ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "cacheableSqmInterpretation"); - if(cacheableSqmInterpretation == null) { - DomainQueryExecutionContext domainQueryExecutionContext = DomainQueryExecutionContext.class.cast(querySqm); - cacheableSqmInterpretation = ReflectionUtils.invokeStaticMethod( - ReflectionUtils.getMethod( - ConcreteSqmSelectQueryPlan.class, - "buildCacheableSqmInterpretation", - SqmSelectStatement.class, - DomainParameterXref.class, - DomainQueryExecutionContext.class - ), - ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "sqm"), - ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "domainParameterXref"), - domainQueryExecutionContext - ); - } - if (cacheableSqmInterpretation != null) { - JdbcOperationQuerySelect jdbcSelect = ReflectionUtils.getFieldValueOrNull(cacheableSqmInterpretation, "jdbcSelect"); - if (jdbcSelect != null) { - return jdbcSelect.getSqlString(); - } - } + SqmStatement sqmStatement = querySqm.getSqmStatement(); + + if (sqmStatement instanceof SqmSelectStatement) { + return extractSelectSql(querySqm, (SqmSelectStatement) sqmStatement); + } + if (sqmStatement instanceof SqmDmlStatement) { + return extractMutationSql(querySqm, (SqmDmlStatement) sqmStatement); } + return querySqm.getQueryString(); } - return ReflectionUtils.invokeMethod(query, "getQueryString"); + throw new IllegalArgumentException("Unsupported query type: " + (query == null ? "null" : query.getClass().getName())); + } + + /** + * Extract SQL from an HQL select statement using the public SQM translation pipeline. + */ + private static String extractSelectSql(QuerySqmImpl querySqm, SqmSelectStatement sqmSelect) { + DomainQueryExecutionContext execCtx = (DomainQueryExecutionContext) querySqm; + SharedSessionContractImplementor session = execCtx.getSession(); + SessionFactoryImplementor factory = session.getFactory(); + QueryOptions queryOptions = execCtx.getQueryOptions(); + QueryParameterBindings paramBindings = execCtx.getQueryParameterBindings(); + DomainParameterXref domainParameterXref = querySqm.getDomainParameterXref(); + + // Step 1: Translate SQM to SQL AST + SqmTranslation sqmInterpretation = factory.getQueryEngine() + .getSqmTranslatorFactory() + .createSelectTranslator( + sqmSelect, + queryOptions, + domainParameterXref, + paramBindings, + session.getLoadQueryInfluencers(), + factory, + false + ) + .translate(); + + // Step 2: Build JDBC parameter bindings (supports parameterized queries) + JdbcParameterBindings jdbcParameterBindings = buildJdbcParameterBindings( + execCtx, factory, domainParameterXref, sqmInterpretation); + + // Step 3: Translate SQL AST to JDBC operation (produces the SQL string) + SqlAstTranslator translator = factory.getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildSelectTranslator(factory, sqmInterpretation.getSqlAst()); + + JdbcOperationQuerySelect jdbcSelect = translator.translate(jdbcParameterBindings, queryOptions); + return jdbcSelect.getSqlString(); + } + + /** + * Extract SQL from an HQL update/delete statement using the public SQM translation pipeline. + */ + private static String extractMutationSql(QuerySqmImpl querySqm, SqmDmlStatement sqmDml) { + DomainQueryExecutionContext execCtx = (DomainQueryExecutionContext) querySqm; + SharedSessionContractImplementor session = execCtx.getSession(); + SessionFactoryImplementor factory = session.getFactory(); + QueryOptions queryOptions = execCtx.getQueryOptions(); + QueryParameterBindings paramBindings = execCtx.getQueryParameterBindings(); + DomainParameterXref domainParameterXref = querySqm.getDomainParameterXref(); + + // Step 1: Translate SQM to SQL AST + SqmTranslation sqmInterpretation = factory.getQueryEngine() + .getSqmTranslatorFactory() + .createMutationTranslator( + sqmDml, + queryOptions, + domainParameterXref, + paramBindings, + session.getLoadQueryInfluencers(), + factory + ) + .translate(); + + // Step 2: Build JDBC parameter bindings (supports parameterized queries) + JdbcParameterBindings jdbcParameterBindings = buildJdbcParameterBindings( + execCtx, factory, domainParameterXref, sqmInterpretation); + + // Step 3: Translate SQL AST to JDBC operation (produces the SQL string) + SqlAstTranslator translator = factory.getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildMutationTranslator(factory, sqmInterpretation.getSqlAst()); + + JdbcOperationQueryMutation jdbcMutation = translator.translate(jdbcParameterBindings, queryOptions); + return jdbcMutation.getSqlString(); + } + + private static JdbcParameterBindings buildJdbcParameterBindings( + DomainQueryExecutionContext execCtx, + SessionFactoryImplementor factory, + DomainParameterXref domainParameterXref, + SqmTranslation sqmInterpretation) { + + Map, + Map, List>> jdbcParamsXref = + SqmUtil.generateJdbcParamsXref(domainParameterXref, sqmInterpretation::getJdbcParamsBySqmParam); + + Map, MappingModelExpressible> sqmParamMappingTypeResolutions = + sqmInterpretation.getSqmParameterMappingModelTypeResolutions(); + + return SqmUtil.createJdbcParameterBindings( + execCtx.getQueryParameterBindings(), + domainParameterXref, + jdbcParamsXref, + factory.getRuntimeMetamodels().getMappingMetamodel(), + sqmInterpretation.getFromClauseAccess()::findTableGroup, + new SqmParameterMappingModelResolutionAccess() { + @Override + public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { + return (MappingModelExpressible) sqmParamMappingTypeResolutions.get(parameter); + } + }, + execCtx.getSession() + ); } } diff --git a/hypersistence-utils-hibernate-63/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java b/hypersistence-utils-hibernate-63/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java index e74d81673..847bf858e 100644 --- a/hypersistence-utils-hibernate-63/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java +++ b/hypersistence-utils-hibernate-63/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java @@ -3,8 +3,12 @@ import io.hypersistence.utils.hibernate.util.AbstractPostgreSQLIntegrationTest; import jakarta.persistence.*; import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Root; import org.junit.Test; @@ -26,32 +30,78 @@ protected Class[] entities() { } @Test - public void testJPQL() { + public void testSelectJPQL() { doInJPA(entityManager -> { Query jpql = entityManager - .createQuery( - "select " + - " YEAR(p.createdOn) as year, " + - " count(p) as postCount " + - "from " + - " Post p " + - "group by " + - " YEAR(p.createdOn)", Tuple.class); + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class); String sql = SQLExtractor.from(jpql); assertNotNull(sql); LOGGER.info( - "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", - jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), - sql + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); }); } @Test - public void testCriteriaAPI() { + public void testUpdateJPQL() { + doInJPA(entityManager -> { + Query jpql = entityManager.createQuery( + "update Post " + + "set title = :newTitle " + + "where title like :titlePattern" + ); + + jpql.setParameter("newTitle", "Hibernate Tips"); + jpql.setParameter("titlePattern", "%Java%"); + + String sql = SQLExtractor.from(jpql); + + assertNotNull(sql); + + LOGGER.info( + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testDeleteJPQL() { + doInJPA(entityManager -> { + Query jpql = entityManager.createQuery( + "delete from PostComment " + + "where review like :reviewPattern" + ); + + jpql.setParameter("reviewPattern", "%spam%"); + + String sql = SQLExtractor.from(jpql); + + assertNotNull(sql); + + LOGGER.info( + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testSelectCriteriaAPI() { doInJPA(entityManager -> { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); @@ -61,23 +111,89 @@ public void testCriteriaAPI() { Join post = postComment.join("post"); criteria.where( - builder.like(post.get("title"), "%Java%") + builder.like(post.get("title"), "%Java%") ); criteria.orderBy( - builder.asc(postComment.get("id")) + builder.asc(postComment.get("id")) + ); + + Query criteriaQuery = entityManager.createQuery(criteria); + + String sql = SQLExtractor.from(criteriaQuery); + + assertNotNull(sql); + + LOGGER.info( + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); + }); + } + + @Test + public void testUpdateCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaUpdate criteria = builder.createCriteriaUpdate(Post.class); + Root post = criteria.from(Post.class); + + ParameterExpression newTitle = builder.parameter(String.class, "newTitle"); + ParameterExpression titlePattern = builder.parameter(String.class, "titlePattern"); + + Path title = post.get("title"); + + criteria + .set(title, newTitle) + .where( + builder.like(title, titlePattern) + ); Query criteriaQuery = entityManager.createQuery(criteria); + criteriaQuery.setParameter("newTitle", "Hibernate Tips"); + criteriaQuery.setParameter("titlePattern", "%Java%"); + + String sql = SQLExtractor.from(criteriaQuery); + + assertNotNull(sql); + + LOGGER.info( + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testDeleteCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaDelete criteria = builder.createCriteriaDelete(PostComment.class); + Root postComment = criteria.from(PostComment.class); + + ParameterExpression reviewPattern = builder.parameter(String.class, "reviewPattern"); + + criteria.where( + builder.like(postComment.get("review"), reviewPattern) + ); + + Query criteriaQuery = entityManager.createQuery(criteria); + + criteriaQuery.setParameter("reviewPattern", "%spam%"); + String sql = SQLExtractor.from(criteriaQuery); assertNotNull(sql); LOGGER.info( - "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", - criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), - sql + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); }); } diff --git a/hypersistence-utils-hibernate-70/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java b/hypersistence-utils-hibernate-70/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java index 9c2a79855..c69e09625 100644 --- a/hypersistence-utils-hibernate-70/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java +++ b/hypersistence-utils-hibernate-70/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java @@ -1,20 +1,32 @@ package io.hypersistence.utils.hibernate.query; -import io.hypersistence.utils.common.ReflectionUtils; import jakarta.persistence.Query; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.spi.QueryImplementor; -import org.hibernate.query.spi.QueryInterpretationCache; -import org.hibernate.query.spi.SelectQueryPlan; -import org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.query.spi.QueryParameterImplementor; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.QuerySqmImpl; -import org.hibernate.query.sqm.internal.SqmInterpretationsKey; -import org.hibernate.query.sqm.spi.InterpretationsKeySource; +import org.hibernate.query.sqm.internal.SqmUtil; +import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; +import org.hibernate.query.sqm.sql.SqmTranslation; +import org.hibernate.query.sqm.tree.SqmDmlStatement; +import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcParametersList; -import java.util.function.Supplier; +import java.util.List; +import java.util.Map; /** * The {@link SQLExtractor} allows you to extract the @@ -37,42 +49,120 @@ protected SQLExtractor() { * @return the underlying SQL generated by the provided JPA query */ public static String from(Query query) { - if(query instanceof InterpretationsKeySource && - query instanceof QueryImplementor && - query instanceof QuerySqmImpl) { - QueryInterpretationCache.Key cacheKey = SqmInterpretationsKey.createInterpretationsKey((InterpretationsKeySource) query); - QuerySqmImpl querySqm = (QuerySqmImpl) query; - Supplier buildSelectQueryPlan = () -> ReflectionUtils.invokeMethod(querySqm, "buildSelectQueryPlan"); - SelectQueryPlan plan = cacheKey != null ? ((QueryImplementor) query).getSession().getFactory().getQueryEngine() - .getInterpretationCache() - .resolveSelectQueryPlan(cacheKey, buildSelectQueryPlan) : - (SelectQueryPlan) buildSelectQueryPlan.get(); - if(plan instanceof ConcreteSqmSelectQueryPlan) { - ConcreteSqmSelectQueryPlan selectQueryPlan = (ConcreteSqmSelectQueryPlan) plan; - Object cacheableSqmInterpretation = ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "cacheableSqmInterpretation"); - if(cacheableSqmInterpretation == null) { - DomainQueryExecutionContext domainQueryExecutionContext = DomainQueryExecutionContext.class.cast(querySqm); - cacheableSqmInterpretation = ReflectionUtils.invokeStaticMethod( - ReflectionUtils.getMethod( - ConcreteSqmSelectQueryPlan.class, - "buildCacheableSqmInterpretation", - SqmSelectStatement.class, - DomainParameterXref.class, - DomainQueryExecutionContext.class - ), - ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "sqm"), - ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "domainParameterXref"), - domainQueryExecutionContext - ); - } - if (cacheableSqmInterpretation != null) { - JdbcOperationQuerySelect jdbcSelect = ReflectionUtils.getFieldValueOrNull(cacheableSqmInterpretation, "jdbcSelect"); - if (jdbcSelect != null) { - return jdbcSelect.getSqlString(); - } - } + if (query instanceof QuerySqmImpl querySqm) { + SqmStatement sqmStatement = querySqm.getSqmStatement(); + + if (sqmStatement instanceof SqmSelectStatement sqmSelectStatement) { + return extractSelectSql(querySqm, sqmSelectStatement); + } + if (sqmStatement instanceof SqmDmlStatement sqmDmlStatement) { + return extractMutationSql(querySqm, sqmDmlStatement); } + return querySqm.getQueryString(); } - return ReflectionUtils.invokeMethod(query, "getQueryString"); + throw new IllegalArgumentException("Unsupported query type: " + (query == null ? "null" : query.getClass().getName())); + } + + /** + * Extract SQL from an HQL select statement using the public SQM translation pipeline. + */ + private static String extractSelectSql(QuerySqmImpl querySqm, SqmSelectStatement sqmSelect) { + DomainQueryExecutionContext execCtx = (DomainQueryExecutionContext) querySqm; + SharedSessionContractImplementor session = execCtx.getSession(); + SessionFactoryImplementor factory = session.getFactory(); + QueryOptions queryOptions = execCtx.getQueryOptions(); + QueryParameterBindings paramBindings = execCtx.getQueryParameterBindings(); + DomainParameterXref domainParameterXref = querySqm.getDomainParameterXref(); + + // Step 1: Translate SQM to SQL AST + SqmTranslation sqmInterpretation = factory.getQueryEngine() + .getSqmTranslatorFactory() + .createSelectTranslator( + sqmSelect, + queryOptions, + domainParameterXref, + paramBindings, + session.getLoadQueryInfluencers(), + factory.getSqlTranslationEngine(), + false + ) + .translate(); + + // Step 2: Build JDBC parameter bindings (supports parameterized queries) + JdbcParameterBindings jdbcParameterBindings = buildJdbcParameterBindings( + execCtx, domainParameterXref, sqmInterpretation); + + // Step 3: Translate SQL AST to JDBC operation (produces the SQL string) + SqlAstTranslator translator = factory.getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildSelectTranslator(factory, sqmInterpretation.getSqlAst()); + + JdbcOperationQuerySelect jdbcSelect = translator.translate(jdbcParameterBindings, queryOptions); + return jdbcSelect.getSqlString(); + } + + /** + * Extract SQL from an HQL update/delete statement using the public SQM translation pipeline. + */ + private static String extractMutationSql(QuerySqmImpl querySqm, SqmDmlStatement sqmDml) { + DomainQueryExecutionContext execCtx = (DomainQueryExecutionContext) querySqm; + SharedSessionContractImplementor session = execCtx.getSession(); + SessionFactoryImplementor factory = session.getFactory(); + QueryOptions queryOptions = execCtx.getQueryOptions(); + QueryParameterBindings paramBindings = execCtx.getQueryParameterBindings(); + DomainParameterXref domainParameterXref = querySqm.getDomainParameterXref(); + + // Step 1: Translate SQM to SQL AST + SqmTranslation sqmInterpretation = factory.getQueryEngine() + .getSqmTranslatorFactory() + .createMutationTranslator( + sqmDml, + queryOptions, + domainParameterXref, + paramBindings, + session.getLoadQueryInfluencers(), + factory.getSqlTranslationEngine() + ) + .translate(); + + // Step 2: Build JDBC parameter bindings (supports parameterized queries) + JdbcParameterBindings jdbcParameterBindings = buildJdbcParameterBindings( + execCtx, domainParameterXref, sqmInterpretation); + + // Step 3: Translate SQL AST to JDBC operation (produces the SQL string) + SqlAstTranslator translator = factory.getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildMutationTranslator(factory, sqmInterpretation.getSqlAst()); + + JdbcOperationQueryMutation jdbcMutation = translator.translate(jdbcParameterBindings, queryOptions); + return jdbcMutation.getSqlString(); + } + + private static JdbcParameterBindings buildJdbcParameterBindings( + DomainQueryExecutionContext execCtx, + DomainParameterXref domainParameterXref, + SqmTranslation sqmInterpretation) { + + Map, + Map, List>> jdbcParamsXref = + SqmUtil.generateJdbcParamsXref(domainParameterXref, sqmInterpretation::getJdbcParamsBySqmParam); + + Map, MappingModelExpressible> sqmParamMappingTypeResolutions = + sqmInterpretation.getSqmParameterMappingModelTypeResolutions(); + + return SqmUtil.createJdbcParameterBindings( + execCtx.getQueryParameterBindings(), + domainParameterXref, + jdbcParamsXref, + new SqmParameterMappingModelResolutionAccess() { + @Override + public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { + return (MappingModelExpressible) sqmParamMappingTypeResolutions.get(parameter); + } + }, + execCtx.getSession() + ); } } diff --git a/hypersistence-utils-hibernate-70/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java b/hypersistence-utils-hibernate-70/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java index e74d81673..847bf858e 100644 --- a/hypersistence-utils-hibernate-70/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java +++ b/hypersistence-utils-hibernate-70/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java @@ -3,8 +3,12 @@ import io.hypersistence.utils.hibernate.util.AbstractPostgreSQLIntegrationTest; import jakarta.persistence.*; import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Root; import org.junit.Test; @@ -26,32 +30,78 @@ protected Class[] entities() { } @Test - public void testJPQL() { + public void testSelectJPQL() { doInJPA(entityManager -> { Query jpql = entityManager - .createQuery( - "select " + - " YEAR(p.createdOn) as year, " + - " count(p) as postCount " + - "from " + - " Post p " + - "group by " + - " YEAR(p.createdOn)", Tuple.class); + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class); String sql = SQLExtractor.from(jpql); assertNotNull(sql); LOGGER.info( - "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", - jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), - sql + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); }); } @Test - public void testCriteriaAPI() { + public void testUpdateJPQL() { + doInJPA(entityManager -> { + Query jpql = entityManager.createQuery( + "update Post " + + "set title = :newTitle " + + "where title like :titlePattern" + ); + + jpql.setParameter("newTitle", "Hibernate Tips"); + jpql.setParameter("titlePattern", "%Java%"); + + String sql = SQLExtractor.from(jpql); + + assertNotNull(sql); + + LOGGER.info( + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testDeleteJPQL() { + doInJPA(entityManager -> { + Query jpql = entityManager.createQuery( + "delete from PostComment " + + "where review like :reviewPattern" + ); + + jpql.setParameter("reviewPattern", "%spam%"); + + String sql = SQLExtractor.from(jpql); + + assertNotNull(sql); + + LOGGER.info( + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testSelectCriteriaAPI() { doInJPA(entityManager -> { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); @@ -61,23 +111,89 @@ public void testCriteriaAPI() { Join post = postComment.join("post"); criteria.where( - builder.like(post.get("title"), "%Java%") + builder.like(post.get("title"), "%Java%") ); criteria.orderBy( - builder.asc(postComment.get("id")) + builder.asc(postComment.get("id")) + ); + + Query criteriaQuery = entityManager.createQuery(criteria); + + String sql = SQLExtractor.from(criteriaQuery); + + assertNotNull(sql); + + LOGGER.info( + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); + }); + } + + @Test + public void testUpdateCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaUpdate criteria = builder.createCriteriaUpdate(Post.class); + Root post = criteria.from(Post.class); + + ParameterExpression newTitle = builder.parameter(String.class, "newTitle"); + ParameterExpression titlePattern = builder.parameter(String.class, "titlePattern"); + + Path title = post.get("title"); + + criteria + .set(title, newTitle) + .where( + builder.like(title, titlePattern) + ); Query criteriaQuery = entityManager.createQuery(criteria); + criteriaQuery.setParameter("newTitle", "Hibernate Tips"); + criteriaQuery.setParameter("titlePattern", "%Java%"); + + String sql = SQLExtractor.from(criteriaQuery); + + assertNotNull(sql); + + LOGGER.info( + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testDeleteCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaDelete criteria = builder.createCriteriaDelete(PostComment.class); + Root postComment = criteria.from(PostComment.class); + + ParameterExpression reviewPattern = builder.parameter(String.class, "reviewPattern"); + + criteria.where( + builder.like(postComment.get("review"), reviewPattern) + ); + + Query criteriaQuery = entityManager.createQuery(criteria); + + criteriaQuery.setParameter("reviewPattern", "%spam%"); + String sql = SQLExtractor.from(criteriaQuery); assertNotNull(sql); LOGGER.info( - "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", - criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), - sql + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); }); } diff --git a/hypersistence-utils-hibernate-71/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java b/hypersistence-utils-hibernate-71/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java index 39a1a627d..fa26e6f0c 100644 --- a/hypersistence-utils-hibernate-71/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java +++ b/hypersistence-utils-hibernate-71/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java @@ -1,20 +1,32 @@ package io.hypersistence.utils.hibernate.query; -import io.hypersistence.utils.common.ReflectionUtils; import jakarta.persistence.Query; -import org.hibernate.internal.util.MutableObject; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.spi.QueryImplementor; -import org.hibernate.query.spi.QueryInterpretationCache; -import org.hibernate.query.spi.SelectQueryPlan; -import org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.query.spi.QueryParameterImplementor; import org.hibernate.query.sqm.internal.DomainParameterXref; -import org.hibernate.query.sqm.internal.SqmInterpretationsKey; -import org.hibernate.query.sqm.spi.InterpretationsKeySource; +import org.hibernate.query.sqm.internal.SqmQueryImpl; +import org.hibernate.query.sqm.internal.SqmUtil; +import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; +import org.hibernate.query.sqm.sql.SqmTranslation; +import org.hibernate.query.sqm.tree.SqmDmlStatement; +import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcParametersList; -import java.util.function.Supplier; +import java.util.List; +import java.util.Map; /** * The {@link SQLExtractor} allows you to extract the @@ -37,43 +49,120 @@ protected SQLExtractor() { * @return the underlying SQL generated by the provided JPA query */ public static String from(Query query) { - if(query instanceof InterpretationsKeySource && - query instanceof QueryImplementor) { - QueryInterpretationCache.Key cacheKey = SqmInterpretationsKey.createInterpretationsKey((InterpretationsKeySource) query); - Supplier buildSelectQueryPlan = () -> ReflectionUtils.invokeMethod(query, "buildSelectQueryPlan"); - SelectQueryPlan plan = cacheKey != null ? ((QueryImplementor) query).getSession().getFactory().getQueryEngine() - .getInterpretationCache() - .resolveSelectQueryPlan(cacheKey, buildSelectQueryPlan) : - (SelectQueryPlan) buildSelectQueryPlan.get(); - if(plan instanceof ConcreteSqmSelectQueryPlan) { - ConcreteSqmSelectQueryPlan selectQueryPlan = (ConcreteSqmSelectQueryPlan) plan; - Object cacheableSqmInterpretation = ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "cacheableSqmInterpretation"); - if(cacheableSqmInterpretation == null) { - DomainQueryExecutionContext domainQueryExecutionContext = DomainQueryExecutionContext.class.cast(query); - final MutableObject mutableObject = new MutableObject<>(); - cacheableSqmInterpretation = ReflectionUtils.invokeStaticMethod( - ReflectionUtils.getMethod( - ConcreteSqmSelectQueryPlan.class, - "buildInterpretation", - SqmSelectStatement.class, - DomainParameterXref.class, - DomainQueryExecutionContext.class, - MutableObject.class - ), - ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "sqm"), - ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "domainParameterXref"), - domainQueryExecutionContext, - mutableObject - ); - } - if (cacheableSqmInterpretation != null) { - JdbcOperation jdbcSelect = ReflectionUtils.getFieldValueOrNull(cacheableSqmInterpretation, "jdbcOperation"); - if (jdbcSelect != null) { - return jdbcSelect.getSqlString(); - } - } + if (query instanceof SqmQueryImpl querySqm) { + SqmStatement sqmStatement = querySqm.getSqmStatement(); + + if (sqmStatement instanceof SqmSelectStatement sqmSelectStatement) { + return extractSelectSql(querySqm, sqmSelectStatement); + } + if (sqmStatement instanceof SqmDmlStatement sqmDmlStatement) { + return extractMutationSql(querySqm, sqmDmlStatement); } + return querySqm.getQueryString(); } - return ReflectionUtils.invokeMethod(query, "getQueryString"); + throw new IllegalArgumentException("Unsupported query type: " + (query == null ? "null" : query.getClass().getName())); + } + + /** + * Extract SQL from an HQL select statement using the public SQM translation pipeline. + */ + private static String extractSelectSql(SqmQueryImpl querySqm, SqmSelectStatement sqmSelect) { + DomainQueryExecutionContext execCtx = (DomainQueryExecutionContext) querySqm; + SharedSessionContractImplementor session = execCtx.getSession(); + SessionFactoryImplementor factory = session.getFactory(); + QueryOptions queryOptions = execCtx.getQueryOptions(); + QueryParameterBindings paramBindings = execCtx.getQueryParameterBindings(); + DomainParameterXref domainParameterXref = querySqm.getDomainParameterXref(); + + // Step 1: Translate SQM to SQL AST + SqmTranslation sqmInterpretation = factory.getQueryEngine() + .getSqmTranslatorFactory() + .createSelectTranslator( + sqmSelect, + queryOptions, + domainParameterXref, + paramBindings, + session.getLoadQueryInfluencers(), + factory.getSqlTranslationEngine(), + false + ) + .translate(); + + // Step 2: Build JDBC parameter bindings (supports parameterized queries) + JdbcParameterBindings jdbcParameterBindings = buildJdbcParameterBindings( + execCtx, domainParameterXref, sqmInterpretation); + + // Step 3: Translate SQL AST to JDBC operation (produces the SQL string) + SqlAstTranslator translator = factory.getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildSelectTranslator(factory, sqmInterpretation.getSqlAst()); + + JdbcOperationQuerySelect jdbcSelect = translator.translate(jdbcParameterBindings, queryOptions); + return jdbcSelect.getSqlString(); + } + + /** + * Extract SQL from an HQL update/delete statement using the public SQM translation pipeline. + */ + private static String extractMutationSql(SqmQueryImpl querySqm, SqmDmlStatement sqmDml) { + DomainQueryExecutionContext execCtx = (DomainQueryExecutionContext) querySqm; + SharedSessionContractImplementor session = execCtx.getSession(); + SessionFactoryImplementor factory = session.getFactory(); + QueryOptions queryOptions = execCtx.getQueryOptions(); + QueryParameterBindings paramBindings = execCtx.getQueryParameterBindings(); + DomainParameterXref domainParameterXref = querySqm.getDomainParameterXref(); + + // Step 1: Translate SQM to SQL AST + SqmTranslation sqmInterpretation = factory.getQueryEngine() + .getSqmTranslatorFactory() + .createMutationTranslator( + sqmDml, + queryOptions, + domainParameterXref, + paramBindings, + session.getLoadQueryInfluencers(), + factory.getSqlTranslationEngine() + ) + .translate(); + + // Step 2: Build JDBC parameter bindings (supports parameterized queries) + JdbcParameterBindings jdbcParameterBindings = buildJdbcParameterBindings( + execCtx, domainParameterXref, sqmInterpretation); + + // Step 3: Translate SQL AST to JDBC operation (produces the SQL string) + SqlAstTranslator translator = factory.getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildMutationTranslator(factory, sqmInterpretation.getSqlAst()); + + JdbcOperationQueryMutation jdbcMutation = translator.translate(jdbcParameterBindings, queryOptions); + return jdbcMutation.getSqlString(); + } + + private static JdbcParameterBindings buildJdbcParameterBindings( + DomainQueryExecutionContext execCtx, + DomainParameterXref domainParameterXref, + SqmTranslation sqmInterpretation) { + + Map, + Map, List>> jdbcParamsXref = + SqmUtil.generateJdbcParamsXref(domainParameterXref, sqmInterpretation::getJdbcParamsBySqmParam); + + Map, MappingModelExpressible> sqmParamMappingTypeResolutions = + sqmInterpretation.getSqmParameterMappingModelTypeResolutions(); + + return SqmUtil.createJdbcParameterBindings( + execCtx.getQueryParameterBindings(), + domainParameterXref, + jdbcParamsXref, + new SqmParameterMappingModelResolutionAccess() { + @Override + public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { + return (MappingModelExpressible) sqmParamMappingTypeResolutions.get(parameter); + } + }, + execCtx.getSession() + ); } } diff --git a/hypersistence-utils-hibernate-71/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java b/hypersistence-utils-hibernate-71/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java index e74d81673..847bf858e 100644 --- a/hypersistence-utils-hibernate-71/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java +++ b/hypersistence-utils-hibernate-71/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java @@ -3,8 +3,12 @@ import io.hypersistence.utils.hibernate.util.AbstractPostgreSQLIntegrationTest; import jakarta.persistence.*; import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Root; import org.junit.Test; @@ -26,32 +30,78 @@ protected Class[] entities() { } @Test - public void testJPQL() { + public void testSelectJPQL() { doInJPA(entityManager -> { Query jpql = entityManager - .createQuery( - "select " + - " YEAR(p.createdOn) as year, " + - " count(p) as postCount " + - "from " + - " Post p " + - "group by " + - " YEAR(p.createdOn)", Tuple.class); + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class); String sql = SQLExtractor.from(jpql); assertNotNull(sql); LOGGER.info( - "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", - jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), - sql + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); }); } @Test - public void testCriteriaAPI() { + public void testUpdateJPQL() { + doInJPA(entityManager -> { + Query jpql = entityManager.createQuery( + "update Post " + + "set title = :newTitle " + + "where title like :titlePattern" + ); + + jpql.setParameter("newTitle", "Hibernate Tips"); + jpql.setParameter("titlePattern", "%Java%"); + + String sql = SQLExtractor.from(jpql); + + assertNotNull(sql); + + LOGGER.info( + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testDeleteJPQL() { + doInJPA(entityManager -> { + Query jpql = entityManager.createQuery( + "delete from PostComment " + + "where review like :reviewPattern" + ); + + jpql.setParameter("reviewPattern", "%spam%"); + + String sql = SQLExtractor.from(jpql); + + assertNotNull(sql); + + LOGGER.info( + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testSelectCriteriaAPI() { doInJPA(entityManager -> { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); @@ -61,23 +111,89 @@ public void testCriteriaAPI() { Join post = postComment.join("post"); criteria.where( - builder.like(post.get("title"), "%Java%") + builder.like(post.get("title"), "%Java%") ); criteria.orderBy( - builder.asc(postComment.get("id")) + builder.asc(postComment.get("id")) + ); + + Query criteriaQuery = entityManager.createQuery(criteria); + + String sql = SQLExtractor.from(criteriaQuery); + + assertNotNull(sql); + + LOGGER.info( + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); + }); + } + + @Test + public void testUpdateCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaUpdate criteria = builder.createCriteriaUpdate(Post.class); + Root post = criteria.from(Post.class); + + ParameterExpression newTitle = builder.parameter(String.class, "newTitle"); + ParameterExpression titlePattern = builder.parameter(String.class, "titlePattern"); + + Path title = post.get("title"); + + criteria + .set(title, newTitle) + .where( + builder.like(title, titlePattern) + ); Query criteriaQuery = entityManager.createQuery(criteria); + criteriaQuery.setParameter("newTitle", "Hibernate Tips"); + criteriaQuery.setParameter("titlePattern", "%Java%"); + + String sql = SQLExtractor.from(criteriaQuery); + + assertNotNull(sql); + + LOGGER.info( + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testDeleteCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaDelete criteria = builder.createCriteriaDelete(PostComment.class); + Root postComment = criteria.from(PostComment.class); + + ParameterExpression reviewPattern = builder.parameter(String.class, "reviewPattern"); + + criteria.where( + builder.like(postComment.get("review"), reviewPattern) + ); + + Query criteriaQuery = entityManager.createQuery(criteria); + + criteriaQuery.setParameter("reviewPattern", "%spam%"); + String sql = SQLExtractor.from(criteriaQuery); assertNotNull(sql); LOGGER.info( - "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", - criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), - sql + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); }); } diff --git a/hypersistence-utils-hibernate-73/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java b/hypersistence-utils-hibernate-73/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java index 39a1a627d..65bf25ea4 100644 --- a/hypersistence-utils-hibernate-73/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java +++ b/hypersistence-utils-hibernate-73/src/main/java/io/hypersistence/utils/hibernate/query/SQLExtractor.java @@ -1,20 +1,32 @@ package io.hypersistence.utils.hibernate.query; -import io.hypersistence.utils.common.ReflectionUtils; import jakarta.persistence.Query; -import org.hibernate.internal.util.MutableObject; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.spi.QueryImplementor; -import org.hibernate.query.spi.QueryInterpretationCache; -import org.hibernate.query.spi.SelectQueryPlan; -import org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.query.spi.QueryParameterImplementor; import org.hibernate.query.sqm.internal.DomainParameterXref; -import org.hibernate.query.sqm.internal.SqmInterpretationsKey; -import org.hibernate.query.sqm.spi.InterpretationsKeySource; +import org.hibernate.query.sqm.internal.SqmQueryImpl; +import org.hibernate.query.sqm.internal.SqmUtil; +import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; +import org.hibernate.query.sqm.sql.SqmTranslation; +import org.hibernate.query.sqm.tree.SqmDmlStatement; +import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcParametersList; -import java.util.function.Supplier; +import java.util.List; +import java.util.Map; /** * The {@link SQLExtractor} allows you to extract the @@ -37,43 +49,120 @@ protected SQLExtractor() { * @return the underlying SQL generated by the provided JPA query */ public static String from(Query query) { - if(query instanceof InterpretationsKeySource && - query instanceof QueryImplementor) { - QueryInterpretationCache.Key cacheKey = SqmInterpretationsKey.createInterpretationsKey((InterpretationsKeySource) query); - Supplier buildSelectQueryPlan = () -> ReflectionUtils.invokeMethod(query, "buildSelectQueryPlan"); - SelectQueryPlan plan = cacheKey != null ? ((QueryImplementor) query).getSession().getFactory().getQueryEngine() - .getInterpretationCache() - .resolveSelectQueryPlan(cacheKey, buildSelectQueryPlan) : - (SelectQueryPlan) buildSelectQueryPlan.get(); - if(plan instanceof ConcreteSqmSelectQueryPlan) { - ConcreteSqmSelectQueryPlan selectQueryPlan = (ConcreteSqmSelectQueryPlan) plan; - Object cacheableSqmInterpretation = ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "cacheableSqmInterpretation"); - if(cacheableSqmInterpretation == null) { - DomainQueryExecutionContext domainQueryExecutionContext = DomainQueryExecutionContext.class.cast(query); - final MutableObject mutableObject = new MutableObject<>(); - cacheableSqmInterpretation = ReflectionUtils.invokeStaticMethod( - ReflectionUtils.getMethod( - ConcreteSqmSelectQueryPlan.class, - "buildInterpretation", - SqmSelectStatement.class, - DomainParameterXref.class, - DomainQueryExecutionContext.class, - MutableObject.class - ), - ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "sqm"), - ReflectionUtils.getFieldValueOrNull(selectQueryPlan, "domainParameterXref"), - domainQueryExecutionContext, - mutableObject - ); - } - if (cacheableSqmInterpretation != null) { - JdbcOperation jdbcSelect = ReflectionUtils.getFieldValueOrNull(cacheableSqmInterpretation, "jdbcOperation"); - if (jdbcSelect != null) { - return jdbcSelect.getSqlString(); - } - } + if (query instanceof SqmQueryImpl querySqm) { + SqmStatement sqmStatement = querySqm.getSqmStatement(); + + if (sqmStatement instanceof SqmSelectStatement sqmSelectStatement) { + return extractSelectSql(querySqm, sqmSelectStatement); + } + if (sqmStatement instanceof SqmDmlStatement sqmDmlStatement) { + return extractMutationSql(querySqm, sqmDmlStatement); } + return querySqm.getQueryString(); } - return ReflectionUtils.invokeMethod(query, "getQueryString"); + throw new IllegalArgumentException("Unsupported query type: " + (query == null ? "null" : query.getClass().getName())); + } + + /** + * Extract SQL from an HQL select statement using the public SQM translation pipeline. + */ + private static String extractSelectSql(SqmQueryImpl querySqm, SqmSelectStatement sqmSelect) { + DomainQueryExecutionContext execCtx = (DomainQueryExecutionContext) querySqm; + SharedSessionContractImplementor session = execCtx.getSession(); + SessionFactoryImplementor factory = session.getFactory(); + QueryOptions queryOptions = execCtx.getQueryOptions(); + QueryParameterBindings paramBindings = execCtx.getQueryParameterBindings(); + DomainParameterXref domainParameterXref = querySqm.getDomainParameterXref(); + + // Step 1: Translate SQM to SQL AST + SqmTranslation sqmInterpretation = factory.getQueryEngine() + .getSqmTranslatorFactory() + .createSelectTranslator( + sqmSelect, + queryOptions, + domainParameterXref, + paramBindings, + session.getLoadQueryInfluencers(), + factory.getSqlTranslationEngine(), + false + ) + .translate(); + + // Step 2: Build JDBC parameter bindings (supports parameterized queries) + JdbcParameterBindings jdbcParameterBindings = buildJdbcParameterBindings( + execCtx, domainParameterXref, sqmInterpretation); + + // Step 3: Translate SQL AST to JDBC operation (produces the SQL string) + SqlAstTranslator translator = factory.getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildSelectTranslator(factory, sqmInterpretation.getSqlAst()); + + JdbcOperation jdbcSelect = translator.translate(jdbcParameterBindings, queryOptions); + return jdbcSelect.getSqlString(); + } + + /** + * Extract SQL from an HQL update/delete statement using the public SQM translation pipeline. + */ + private static String extractMutationSql(SqmQueryImpl querySqm, SqmDmlStatement sqmDml) { + DomainQueryExecutionContext execCtx = (DomainQueryExecutionContext) querySqm; + SharedSessionContractImplementor session = execCtx.getSession(); + SessionFactoryImplementor factory = session.getFactory(); + QueryOptions queryOptions = execCtx.getQueryOptions(); + QueryParameterBindings paramBindings = execCtx.getQueryParameterBindings(); + DomainParameterXref domainParameterXref = querySqm.getDomainParameterXref(); + + // Step 1: Translate SQM to SQL AST + SqmTranslation sqmInterpretation = factory.getQueryEngine() + .getSqmTranslatorFactory() + .createMutationTranslator( + sqmDml, + queryOptions, + domainParameterXref, + paramBindings, + session.getLoadQueryInfluencers(), + factory.getSqlTranslationEngine() + ) + .translate(); + + // Step 2: Build JDBC parameter bindings (supports parameterized queries) + JdbcParameterBindings jdbcParameterBindings = buildJdbcParameterBindings( + execCtx, domainParameterXref, sqmInterpretation); + + // Step 3: Translate SQL AST to JDBC operation (produces the SQL string) + SqlAstTranslator translator = factory.getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildMutationTranslator(factory, sqmInterpretation.getSqlAst()); + + JdbcOperationQueryMutation jdbcMutation = translator.translate(jdbcParameterBindings, queryOptions); + return jdbcMutation.getSqlString(); + } + + private static JdbcParameterBindings buildJdbcParameterBindings( + DomainQueryExecutionContext execCtx, + DomainParameterXref domainParameterXref, + SqmTranslation sqmInterpretation) { + + Map, + Map, List>> jdbcParamsXref = + SqmUtil.generateJdbcParamsXref(domainParameterXref, sqmInterpretation::getJdbcParamsBySqmParam); + + Map, MappingModelExpressible> sqmParamMappingTypeResolutions = + sqmInterpretation.getSqmParameterMappingModelTypeResolutions(); + + return SqmUtil.createJdbcParameterBindings( + execCtx.getQueryParameterBindings(), + domainParameterXref, + jdbcParamsXref, + new SqmParameterMappingModelResolutionAccess() { + @Override + public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { + return (MappingModelExpressible) sqmParamMappingTypeResolutions.get(parameter); + } + }, + execCtx.getSession() + ); } } diff --git a/hypersistence-utils-hibernate-73/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java b/hypersistence-utils-hibernate-73/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java index e74d81673..847bf858e 100644 --- a/hypersistence-utils-hibernate-73/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java +++ b/hypersistence-utils-hibernate-73/src/test/java/io/hypersistence/utils/hibernate/query/SQLExtractorTest.java @@ -3,8 +3,12 @@ import io.hypersistence.utils.hibernate.util.AbstractPostgreSQLIntegrationTest; import jakarta.persistence.*; import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Root; import org.junit.Test; @@ -26,32 +30,78 @@ protected Class[] entities() { } @Test - public void testJPQL() { + public void testSelectJPQL() { doInJPA(entityManager -> { Query jpql = entityManager - .createQuery( - "select " + - " YEAR(p.createdOn) as year, " + - " count(p) as postCount " + - "from " + - " Post p " + - "group by " + - " YEAR(p.createdOn)", Tuple.class); + .createQuery( + "select " + + " YEAR(p.createdOn) as year, " + + " count(p) as postCount " + + "from " + + " Post p " + + "group by " + + " YEAR(p.createdOn)", Tuple.class); String sql = SQLExtractor.from(jpql); assertNotNull(sql); LOGGER.info( - "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", - jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), - sql + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); }); } @Test - public void testCriteriaAPI() { + public void testUpdateJPQL() { + doInJPA(entityManager -> { + Query jpql = entityManager.createQuery( + "update Post " + + "set title = :newTitle " + + "where title like :titlePattern" + ); + + jpql.setParameter("newTitle", "Hibernate Tips"); + jpql.setParameter("titlePattern", "%Java%"); + + String sql = SQLExtractor.from(jpql); + + assertNotNull(sql); + + LOGGER.info( + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testDeleteJPQL() { + doInJPA(entityManager -> { + Query jpql = entityManager.createQuery( + "delete from PostComment " + + "where review like :reviewPattern" + ); + + jpql.setParameter("reviewPattern", "%spam%"); + + String sql = SQLExtractor.from(jpql); + + assertNotNull(sql); + + LOGGER.info( + "The JPQL query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + jpql.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testSelectCriteriaAPI() { doInJPA(entityManager -> { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); @@ -61,23 +111,89 @@ public void testCriteriaAPI() { Join post = postComment.join("post"); criteria.where( - builder.like(post.get("title"), "%Java%") + builder.like(post.get("title"), "%Java%") ); criteria.orderBy( - builder.asc(postComment.get("id")) + builder.asc(postComment.get("id")) + ); + + Query criteriaQuery = entityManager.createQuery(criteria); + + String sql = SQLExtractor.from(criteriaQuery); + + assertNotNull(sql); + + LOGGER.info( + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); + }); + } + + @Test + public void testUpdateCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaUpdate criteria = builder.createCriteriaUpdate(Post.class); + Root post = criteria.from(Post.class); + + ParameterExpression newTitle = builder.parameter(String.class, "newTitle"); + ParameterExpression titlePattern = builder.parameter(String.class, "titlePattern"); + + Path title = post.get("title"); + + criteria + .set(title, newTitle) + .where( + builder.like(title, titlePattern) + ); Query criteriaQuery = entityManager.createQuery(criteria); + criteriaQuery.setParameter("newTitle", "Hibernate Tips"); + criteriaQuery.setParameter("titlePattern", "%Java%"); + + String sql = SQLExtractor.from(criteriaQuery); + + assertNotNull(sql); + + LOGGER.info( + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql + ); + }); + } + + @Test + public void testDeleteCriteriaAPI() { + doInJPA(entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + + CriteriaDelete criteria = builder.createCriteriaDelete(PostComment.class); + Root postComment = criteria.from(PostComment.class); + + ParameterExpression reviewPattern = builder.parameter(String.class, "reviewPattern"); + + criteria.where( + builder.like(postComment.get("review"), reviewPattern) + ); + + Query criteriaQuery = entityManager.createQuery(criteria); + + criteriaQuery.setParameter("reviewPattern", "%spam%"); + String sql = SQLExtractor.from(criteriaQuery); assertNotNull(sql); LOGGER.info( - "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", - criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), - sql + "The Criteria API query: [\n{}\n]\ngenerates the following SQL query: [\n{}\n]", + criteriaQuery.unwrap(org.hibernate.query.Query.class).getQueryString(), + sql ); }); }