Skip to content
Merged
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
Expand Up @@ -18,30 +18,20 @@
*/
package org.grails.orm.hibernate.query;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import groovy.lang.GString;

import jakarta.annotation.Nullable;

import org.hibernate.jpa.AvailableHints;

import org.grails.datastore.mapping.model.PersistentEntity;

import static org.grails.orm.hibernate.query.HqlQueryMethods.convertValue;

/**
* Immutable value object that holds all resolved HQL query state which can be computed without a
* Hibernate {@code Session}: the final HQL string, the result target class, any named parameters
* (including those expanded from a {@link GString}), and flags for whether the query is an update
* or native SQL.
* Hibernate {@code Session}: the final HQL string, the result target class, the {@link QueryParameters},
* the {@link QuerySettings}, and flags for whether the query is an update or native SQL.
*
* <p><strong>Security Note:</strong> The {@code hql} string must be trust-verified or
* properly parameterized (e.g. via {@link GString} expansion in {@link #prepare}) before
Expand All @@ -58,107 +48,151 @@
public record HqlQueryContext(
String hql,
Class<?> targetClass,
Map<String, Object> namedParams,
List<Object> positionalParams,
Map<String, Object> querySettings,
Map<String, Object> hints,
QueryParameters parameters,
QuerySettings settings,
boolean isUpdate,
boolean isNative) {

/**
* Constructs a context from raw map/list parameters and settings.
*
* @deprecated since 8.0.1, use {@link #HqlQueryContext(String, Class, QueryParameters, QuerySettings, boolean, boolean)} instead.
*/
@Deprecated(since = "8.0.1", forRemoval = true)
public HqlQueryContext(
String hql,
Class<?> targetClass,
Map<String, Object> namedParams,
List<Object> positionalParams,
Map<String, Object> querySettings,
Map<String, Object> hints,
boolean isUpdate,
boolean isNative) {
this(
hql,
targetClass,
QueryParameters.of(namedParams, positionalParams),
QuerySettings.of(querySettings, hints),
isUpdate,
isNative
);
}

// ─── Factory ─────────────────────────────────────────────────────────────

/**
* Resolves the final HQL string, the result target class, and expands any {@link GString} into
* named parameters. No {@code Session} is required.
*/
public static HqlQueryContext prepare(
PersistentEntity entity,
CharSequence queryCharseq,
Map<String, Object> namedParams,
Collection<Object> positionalParams,
Map<String, Object> querySettings,
Map<String, Object> hints,
boolean isNative,
boolean isUpdate) {
PersistentEntity entity,
CharSequence queryCharseq,
Map<String, Object> namedParams,
Collection<Object> positionalParams,
Map<String, Object> querySettings,
Map<String, Object> hints,
boolean isNative,
boolean isUpdate) {
return prepare(entity, queryCharseq, namedParams, positionalParams, querySettings, hints, isNative, isUpdate, null);
}

public static HqlQueryContext prepare(
PersistentEntity entity,
CharSequence queryCharseq,
Map<String, Object> namedParams,
Collection<Object> positionalParams,
Map<String, Object> querySettings,
Map<String, Object> hints,
boolean isNative,
boolean isUpdate,
Class<?> targetClassOverride) {

var namedParamsCopy = Optional.ofNullable(namedParams)
.map(HashMap::new)
.orElseGet(HashMap::new);

var positionalParamsCopy = Optional.ofNullable(positionalParams)
.map(ArrayList::new)
.orElseGet(ArrayList::new);

var querySettingsCopy = Optional.ofNullable(querySettings)
.map(HashMap::new)
.orElseGet(HashMap::new);

var filteredHints = Optional.ofNullable(hints)
.orElseGet(Collections::emptyMap)
.entrySet().stream()
.filter(e -> AvailableHints.getDefinedHints().contains(e.getKey()))
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));

String resolved = positionalParamsCopy.isEmpty() ?
resolveHql(queryCharseq, namedParamsCopy) :
resolveHql(queryCharseq, isNative, positionalParamsCopy);
// native SQL is not HQL and is never read as such; an HQL query is read once, and its normalized form and
// its result type come from that reading, except an update, which runs as written
PersistentEntity entity,
CharSequence queryCharseq,
Map<String, Object> namedParams,
Collection<Object> positionalParams,
Map<String, Object> querySettings,
Map<String, Object> hints,
boolean isNative,
boolean isUpdate,
Class<?> targetClassOverride) {

QueryParameters.Expanded expanded = QueryParameters.expand(
queryCharseq, namedParams, positionalParams, isNative);
String resolved = expanded.query();
QueryParameters parameters = expanded.parameters();

HqlText reading = isNative ? null : HqlText.of(resolved);
var hql = Optional.ofNullable(isNative || isUpdate ? resolved : reading.normalized())
.filter(s -> !s.trim().isEmpty())
.orElseGet(() -> "from %s".formatted(entity.getName()));
.filter(s -> !s.trim().isEmpty())
.orElseGet(() -> "from %s".formatted(entity.getName()));

namedParamsCopy.replaceAll((k, v) -> convertValue(v));
positionalParamsCopy.replaceAll(HqlQueryMethods::convertValue);

// a native query always returns entities, as Hibernate 5 did by adding the entity to every native query
Class<?> targetClass = targetClassOverride != null ? targetClassOverride :
isNative ? entity.getJavaClass() : reading.target(entity.getJavaClass());

QuerySettings settings = QuerySettings.of(querySettings, hints);

return new HqlQueryContext(
hql,
targetClass,
namedParamsCopy,
positionalParamsCopy,
querySettingsCopy,
filteredHints,
isUpdate,
isNative);
hql,
targetClass,
parameters,
settings,
isUpdate,
isNative);
}

// ─── Backwards Compatibility Accessors ────────────────────────────────────

/**
* @deprecated since 8.0.1, use {@link #parameters()} and {@link QueryParameters#named()} instead.
*/
@Deprecated(since = "8.0.1", forRemoval = true)
public Map<String, Object> namedParams() {
return parameters != null ? parameters.named() : Collections.emptyMap();
}

/**
* @deprecated since 8.0.1, use {@link #parameters()} and {@link QueryParameters#positional()} instead.
*/
@Deprecated(since = "8.0.1", forRemoval = true)
public List<Object> positionalParams() {
return parameters != null ? parameters.positional() : Collections.emptyList();
}

/**
* @deprecated since 8.0.1, use {@link #settings()} and {@link QuerySettings#settings()} instead.
*/
@Deprecated(since = "8.0.1", forRemoval = true)
public Map<String, Object> querySettings() {
return settings != null ? settings.settings() : Collections.emptyMap();
}

// ─── HQL resolution ──────────────────────────────────────────────────────
/**
* @deprecated since 8.0.1, use {@link #settings()} and {@link QuerySettings#hints()} instead.
*/
@Deprecated(since = "8.0.1", forRemoval = true)
public Map<String, Object> hints() {
return settings != null ? settings.hints() : Collections.emptyMap();
}

// ─── HQL resolution delegation ───────────────────────────────────────────

/**
* @deprecated since 8.0.1, use {@link QueryParameters#normalizeMultiLineQueryString(String)} instead.
*/
@Deprecated(since = "8.0.1", forRemoval = true)
static String normalizeMultiLineQueryString(String query) {
return QueryParameters.normalizeMultiLineQueryString(query);
}

/**
* @deprecated since 8.0.1, use {@link QueryParameters#resolveHql(CharSequence, Map)} instead.
*/
@Deprecated(since = "8.0.1", forRemoval = true)
static @Nullable String resolveHql(CharSequence queryCharseq, Map<String, Object> namedParams) {
String raw = queryCharseq instanceof GString gstr ?
buildNamedParameterQueryFromGString(gstr, namedParams) :
queryCharseq != null ? queryCharseq.toString() : "";
return normalizeMultiLineQueryString(raw);
return QueryParameters.resolveHql(queryCharseq, namedParams);
}

/**
* @deprecated since 8.0.1, use {@link QueryParameters#resolveHql(CharSequence, boolean, Collection)} instead.
*/
@Deprecated(since = "8.0.1", forRemoval = true)
static @Nullable String resolveHql(
CharSequence queryCharseq, boolean isNative, Collection<Object> positionalParams) {
String raw = queryCharseq instanceof GString gstr ?
buildPositionalParameterQueryFromGString(gstr, positionalParams, isNative) :
queryCharseq != null ? queryCharseq.toString() : "";
return normalizeMultiLineQueryString(raw);
return QueryParameters.resolveHql(queryCharseq, isNative, positionalParams);
}

// ─── Projection analysis ─────────────────────────────────────────────────
// The query is read once, by HqlText; these entry points read a text of their own for the callers that have one.
// ─── Projection analysis delegation ──────────────────────────────────────

static Class<?> getTarget(CharSequence hql, Class<?> clazz) {
return HqlText.of(hql).target(clazz);
Expand All @@ -179,50 +213,4 @@ static int getCommas(String sel) {
static boolean isHasAlias(String s, int cur, int tokenEnd) {
return HqlText.isClauseAlias(s, cur, tokenEnd);
}

private static String normalizeMultiLineQueryString(String query) {
if (query == null || query.indexOf('\n') == -1) return query;
return query.trim().replace("\n", " ");
}

private static String buildNamedParameterQueryFromGString(GString query, Map<String, Object> params) {
StringBuilder sql = new StringBuilder();
Object[] values = query.getValues();
String[] strings = query.getStrings();
for (int i = 0; i < strings.length; i++) {
sql.append(strings[i]);
if (i < values.length) {
if (!sql.isEmpty() && !Character.isWhitespace(sql.charAt(sql.length() - 1))) {
sql.append(' ');
}
String name = "p" + i;
sql.append(':').append(name);
params.put(name, values[i]);
}
}
return sql.toString();
}

private static String buildPositionalParameterQueryFromGString(
GString query, Collection<Object> positionalParams, boolean isNative) {
StringBuilder sql = new StringBuilder();
Object[] values = query.getValues();
String[] strings = query.getStrings();
for (int i = 0; i < strings.length; i++) {
sql.append(strings[i]);
if (i < values.length) {
if (!sql.isEmpty() && !Character.isWhitespace(sql.charAt(sql.length() - 1))) {
sql.append(' ');
}
if (isNative) {
sql.append('?');
} else {
sql.append('?').append(positionalParams.size() + 1);
}
Object value = values[i];
positionalParams.add(value);
}
}
return sql.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@
*/
package org.grails.orm.hibernate.query;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

Expand Down Expand Up @@ -87,9 +84,19 @@ default void populateHints(HqlQueryDelegate d, Map<String, Object> hints) {
hints.forEach(d::setHint);
}

/**
* @deprecated since 8.0.1, use {@link #populateParameters(HqlQueryDelegate, QueryParameters)} instead.
*/
@Deprecated(since = "8.0.1", forRemoval = true)
static void populateParameters(HqlQueryDelegate d, HqlQueryContext queryContext) {
if (queryContext.namedParams() != null && !queryContext.namedParams().isEmpty()) {
queryContext.namedParams().forEach((key, value) -> {
if (queryContext != null) {
populateParameters(d, queryContext.parameters());
}
}

static void populateParameters(HqlQueryDelegate d, QueryParameters parameters) {
if (parameters != null && parameters.hasNamed()) {
parameters.named().forEach((key, value) -> {
if (INTERNAL_SETTINGS.contains(key)) return;
Object val = convertValue(value);
if (val instanceof Collection) {
Expand All @@ -100,33 +107,15 @@ static void populateParameters(HqlQueryDelegate d, HqlQueryContext queryContext)
d.setParameter(key, val);
}
});
} else if (queryContext.positionalParams() != null && !queryContext.positionalParams().isEmpty()) {
for (int i = 0; i < queryContext.positionalParams().size(); i++) {
Object val = convertValue(queryContext.positionalParams().get(i));
} else if (parameters != null && parameters.hasPositional()) {
for (int i = 0; i < parameters.positional().size(); i++) {
Object val = convertValue(parameters.positional().get(i));
d.setParameter(i + 1, val);
}
}
}

static Object convertValue(Object value) {
if (value instanceof CharSequence) {
return value.toString();
}
if (value instanceof Collection<?> coll) {
List<Object> newList = new ArrayList<>(coll.size());
for (Object o : coll) {
newList.add(convertValue(o));
}
return newList;
}
if (value != null && value.getClass().isArray()) {
int length = Array.getLength(value);
Object newArray = Array.newInstance(value.getClass().getComponentType() == CharSequence.class || CharSequence.class.isAssignableFrom(value.getClass().getComponentType()) ? String.class : value.getClass().getComponentType(), length);
for (int i = 0; i < length; i++) {
Array.set(newArray, i, convertValue(Array.get(value, i)));
}
return newArray;
}
return value;
return QueryParameters.convertValue(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ public int executeUpdate() {
}

protected void applyQuerySettings(HqlQueryDelegate d) {
populateQuerySettings(d, queryContext.querySettings());
populateHints(d, queryContext.hints());
HqlQueryMethods.populateParameters(d, queryContext);
populateQuerySettings(d, queryContext.settings().settings());
populateHints(d, queryContext.settings().hints());
HqlQueryMethods.populateParameters(d, queryContext.parameters());
}

@Override
Expand Down
Loading
Loading