Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<SelectStatement> 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<? extends JdbcOperationQuerySelect> 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<? extends MutationStatement> 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<? extends JdbcOperationQueryMutation> 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<QueryParameterImplementor<?>,
Map<SqmParameter<?>, List<JdbcParametersList>>> jdbcParamsXref =
SqmUtil.generateJdbcParamsXref(domainParameterXref, sqmInterpretation::getJdbcParamsBySqmParam);

Map<SqmParameter<?>, MappingModelExpressible<?>> sqmParamMappingTypeResolutions =
sqmInterpretation.getSqmParameterMappingModelTypeResolutions();

return SqmUtil.createJdbcParameterBindings(
execCtx.getQueryParameterBindings(),
domainParameterXref,
jdbcParamsXref,
factory.getRuntimeMetamodels().getMappingMetamodel(),
sqmInterpretation.getFromClauseAccess()::findTableGroup,
new SqmParameterMappingModelResolutionAccess() {
@Override
public <T> MappingModelExpressible<T> getResolvedMappingModelType(SqmParameter<T> parameter) {
return (MappingModelExpressible<T>) sqmParamMappingTypeResolutions.get(parameter);
}
},
execCtx.getSession()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();

Expand All @@ -61,23 +111,89 @@ public void testCriteriaAPI() {
Join<PostComment, Post> 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<Post> criteria = builder.createCriteriaUpdate(Post.class);
Root<Post> post = criteria.from(Post.class);

ParameterExpression<String> newTitle = builder.parameter(String.class, "newTitle");
ParameterExpression<String> titlePattern = builder.parameter(String.class, "titlePattern");

Path<String> 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<PostComment> criteria = builder.createCriteriaDelete(PostComment.class);
Root<PostComment> postComment = criteria.from(PostComment.class);

ParameterExpression<String> 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
);
});
}
Expand Down
Loading