You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@grails.apache.org by "matrei (via GitHub)" <gi...@apache.org> on 2026/10/04 06:53:38 UTC
[PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
matrei opened a new pull request, #16505:
URL: https://github.com/apache/grails-core/pull/16505
## Description
The Hibernate 7 guide documents `sqlRestriction` in criteria queries, but the Hibernate 7 `HibernateCriteriaBuilder` had no such method, so a criteria query using it failed with a `MissingMethodException`. This adds it, with the same signatures as on Hibernate 5:
```groovy
Person.createCriteria().list {
sqlRestriction 'char_length({alias}.first_name) < ? and char_length({alias}.first_name) > ?', [maxValue, minValue]
}
```
- The SQL is added to the `where` clause as written.
- Each `?` is bound as a parameter to the value at the same position. A mismatch between the number of `?` placeholders and values, or a `null` value, is rejected with an `IllegalArgumentException` when `sqlRestriction` is called.
- `{alias}` stands for the table alias of the queried class, or of the association inside an association block such as `pets { ... }`. This matches Hibernate 5, where Hibernate's `Restrictions.sqlRestriction` replaces it.
### Implementation
The builder adds a `SqlRestriction` criterion, which `PredicateGenerator` translates to a call of a SQL function, `grails_sql_restriction`, registered with a `FunctionContributor` like the existing `rlike` function. The function:
- is a predicate function (`SqmFunctionDescriptor.isPredicate()`, Hibernate 7.0), the mechanism Hibernate uses for its own `regexp_like` and `json_exists`. Hibernate therefore renders it as a condition of its own, not as `(...) = true`, which databases without boolean expressions would reject.
- takes the SQL as a literal, an identifier column of the queried class (the first identifier property for a composite identifier) whose table alias replaces `{alias}`, and the parameters, which replace the `?` placeholders as in Hibernate's own `sql()` function.
Rendering uses Hibernate's SQL AST types, as every custom function renderer does. That package is `@Incubating`, so a Hibernate upgrade may need small changes to `GrailsSqlRestrictionFunction`; the specs below would catch them.
The Hibernate 7 guide now says how the values are bound and documents `{alias}`, with an example of a join that makes it necessary.
### Tests
- New `HibernateCriteriaBuilderSqlRestrictionSpec` (H2): the generated SQL is the condition as written with `{alias}` replaced and the values as `?` (checked with a `StatementInspector`); a condition without values; `{alias}` with a join that adds a column of the same name and inside an association block; `not`, `or` and `count`; an entity with a composite identifier, and one whose composite identifier starts with an association; a subclass mapped to its own table; placeholder count mismatches and a `null` value.
- New `SqlRestrictionHibernate7Spec` (Testcontainers): the same restrictions on H2, PostgreSQL 16, MySQL 8.0, MariaDB 10.11 and Oracle Free, including a paged list's `totalCount`.
- `PredicateGeneratorSpec`: a `SqlRestriction` criterion binds its values and replaces `{alias}`.
All 3495 tests of `:grails-data-hibernate7-core:test` pass, including the Testcontainers spec, and so do `checkstyleMain`, `codenarcMain` and the Hibernate 7 guide build (`:grails-data-hibernate7-docs:asciidoctor`).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty merged PR #16505:
URL: https://github.com/apache/grails-core/pull/16505
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #16505:
URL: https://github.com/apache/grails-core/pull/16505#discussion_r4178509364
##########
grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java:
##########
@@ -1112,6 +1115,46 @@ public Criteria sizeLt(String propertyName, int size) {
return this;
}
+ /**
+ * Builds a junction, whose block {@link DetachedCriteria} resolves its calls against, with
+ * {@code sqlRestriction} added to the detached criteria of the block and of the association blocks inside it.
+ */
+ private void inJunction(Runnable junction) {
+ GroovyCategorySupport.use(SqlRestrictionCategory.class, new Closure<Object>(this) {
Review Comment:
Fixed in c316b7d52c. `SqlRestrictionCategory` is now `HibernateDetachedCriteriaExtension`, registered in `META-INF/services/org.codehaus.groovy.runtime.ExtensionModule`, and the builder's `and`, `or` and `not` call `hibernateQuery` directly again, so no query enters a category scope. The switch point stays valid after an `or` query, and the 300 call sites take about 60 µs after both plain and `or` queries.
`sqlRestriction` now also works on a detached criteria, including one used as a subquery, where `{alias}` stands for the subquery's table. `criteria.adoc` and `detachedCriteria.adoc` document it, and `HibernateCriteriaBuilderSqlRestrictionSpec` covers it, along with a check that no category is active inside a junction. `updateAll` and `deleteAll` on such a criteria throw, because `JpaQueryBuilder` has no handler for `SqlRestriction`. The batch-operations note documents that, and a test covers it.
Verified on JDK 21: `HibernateCriteriaBuilderSqlRestrictionSpec`, `HibernateCriteriaBuilderSpec`, `HibernateCriteriaBuilderDirectSpec`, `CriteriaMethodInvokerSpec`, `PredicateGeneratorSpec` and `JpaCriteriaQueryCreatorSpec` pass on H2, and `SqlRestrictionHibernate7Spec` passes on all five databases.
##########
grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java:
##########
@@ -1112,6 +1113,32 @@ public Criteria sizeLt(String propertyName, int size) {
return this;
}
+ /**
+ * Restricts the results with a native SQL condition. {@code {alias}} in the SQL stands for the table alias of
+ * the queried entity.
+ *
+ * @param sqlRestriction the SQL condition
+ * @return this criteria
+ */
+ public org.grails.datastore.mapping.query.api.Criteria sqlRestriction(String sqlRestriction) {
+ return sqlRestriction(sqlRestriction, Collections.emptyList());
+ }
+
+ /**
+ * Restricts the results with a native SQL condition whose {@code ?} placeholders are bound to the given values.
+ * {@code {alias}} in the SQL stands for the table alias of the queried entity.
+ *
+ * @param sqlRestriction the SQL condition
+ * @param values the values of the {@code ?} placeholders, in order, none of them {@code null}
+ * @return this criteria
+ * @throws IllegalArgumentException if the number of {@code ?} placeholders differs from the number of values,
+ * or a value is {@code null}
+ */
+ public org.grails.datastore.mapping.query.api.Criteria sqlRestriction(String sqlRestriction, List<?> values) {
+ hibernateQuery.add(new SqlRestriction(sqlRestriction, values));
Review Comment:
Follow-up: c316b7d52c replaces the Groovy category scope described above, because entering and leaving a category invalidates the call sites of every thread on each junction. `sqlRestriction` is now a Groovy extension method on `AbstractDetachedCriteria`, so it still lands on whichever criteria is the active delegate: the junction itself, an association block inside it, a junction inside that association, or a nested association. The association-in-junction regressions listed above pass unchanged.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #16505:
URL: https://github.com/apache/grails-core/pull/16505#discussion_r4178099393
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java:
##########
@@ -448,6 +453,38 @@ private Predicate handlePropertyCriterion(
throw new UnsupportedOperationException("Unsupported criterion: " + pc.getClass().getName());
}
+ /**
+ * Returns a column of the entity's own table, whose table alias replaces {@code {alias}} in a SQL restriction.
+ */
+ private static Expression<?> aliasColumn(From<?, ?> root, GrailsHibernatePersistentEntity entity) {
+ PersistentProperty identity = entity.getIdentity();
+ if (identity == null) {
+ HibernatePersistentProperty[] compositeIdentity = entity.getCompositeIdentity();
+ if (compositeIdentity == null || compositeIdentity.length == 0) {
+ throw new ConfigurationException("Cannot use sqlRestriction on class [" + entity.getJavaClass().getName() + "] without an identifier");
+ }
+ identity = compositeIdentity[0];
+ }
+ Path<?> path = root.get(identity.getName());
+ if (identity instanceof Association<?> association && association.getAssociatedEntity() != null) {
+ path = path.get(association.getAssociatedEntity().getIdentity().getName());
Review Comment:
**[P2] Handle composite identifiers on the associated entity too.** `getIdentity()` is null when the associated entity itself has a composite identifier. For example, with `Printing` identified by `['edition', 'code']` and `Edition` identified by `['isbn', 'number']`, a saved row can be created successfully, but `Printing.createCriteria().list { sqlRestriction('{alias}.place = ?', ['City']) }` throws an NPE on this line. I also reproduced the same failure with `sqlRestriction('place = ?', ['City'])`, because alias-column resolution runs unconditionally even without `{alias}`. Please resolve a usable owning-table column for this mapping, including composite association targets, and cover both aliased and unaliased restrictions.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SqlRestriction.java:
##########
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate.query;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.grails.datastore.mapping.query.Query;
+
+/**
+ * Criterion that restricts the results with a native SQL condition, created by {@code sqlRestriction} in a
+ * criteria query. In the SQL, {@code {alias}} stands for the table alias of the queried entity, and each
+ * {@code ?} for one of the values, which are bound as parameters.
+ *
+ * @param sql the SQL condition
+ * @param values the values of the {@code ?} placeholders, in order
+ * @since 8.0.0
+ */
+public record SqlRestriction(String sql, List<?> values) implements Query.Criterion {
+
+ public SqlRestriction {
+ if (sql == null) {
+ throw new IllegalArgumentException("The SQL of a sqlRestriction must not be null");
+ }
+ if (values == null) {
+ values = List.of();
+ }
+ List<Object> copy = new ArrayList<>(values.size());
+ for (Object value : values) {
+ if (value == null) {
+ throw new IllegalArgumentException("The values of a sqlRestriction must not be null: " + sql);
+ }
+ copy.add(value instanceof CharSequence ? value.toString() : value);
+ }
+ long placeholders = sql.chars().filter(c -> c == '?').count();
+ if (placeholders != copy.size()) {
+ throw new IllegalArgumentException("The SQL of a sqlRestriction has " + placeholders +
+ " ? placeholders but " + copy.size() + " values: " + sql);
Review Comment:
**[P2] Distinguish JDBC placeholders from literal question marks.** Counting every `?` rejects valid SQL containing a quoted question mark. I reproduced this through `createCriteria().list { sqlRestriction("{alias}.title like '%?' and {alias}.pages > ?", [50]) }`: it throws `has 2 ? placeholders but 1 values`, although the SQL has only one bind parameter. This prevents existing native restrictions containing such literals from working after migration. Please use the same quote/comment-aware placeholder scan in both this validation and `GrailsSqlRestrictionFunction.render()`; fixing only this count would leave the renderer's `indexOf('?')` consuming the literal as a parameter slot. Add a public-API regression test with a literal `?` before a real placeholder.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16505:
URL: https://github.com/apache/grails-core/pull/16505#issuecomment-5978219612
## ✅ All tests passed ✅
🏷️ Commit: 9fa482ba91a9d72f51e0c681fd28bb5aeb0b77f8
▶️ Tests: 94503 executed
⚪️ Checks: 85/85 completed
---
_Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
Posted by "jamesfredley (via GitHub)" <gi...@apache.org>.
jamesfredley commented on code in PR #16505:
URL: https://github.com/apache/grails-core/pull/16505#discussion_r4178107728
##########
grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java:
##########
@@ -1112,6 +1113,32 @@ public Criteria sizeLt(String propertyName, int size) {
return this;
}
+ /**
+ * Restricts the results with a native SQL condition. {@code {alias}} in the SQL stands for the table alias of
+ * the queried entity.
+ *
+ * @param sqlRestriction the SQL condition
+ * @return this criteria
+ */
+ public org.grails.datastore.mapping.query.api.Criteria sqlRestriction(String sqlRestriction) {
+ return sqlRestriction(sqlRestriction, Collections.emptyList());
+ }
+
+ /**
+ * Restricts the results with a native SQL condition whose {@code ?} placeholders are bound to the given values.
+ * {@code {alias}} in the SQL stands for the table alias of the queried entity.
+ *
+ * @param sqlRestriction the SQL condition
+ * @param values the values of the {@code ?} placeholders, in order, none of them {@code null}
+ * @return this criteria
+ * @throws IllegalArgumentException if the number of {@code ?} placeholders differs from the number of values,
+ * or a value is {@code null}
+ */
+ public org.grails.datastore.mapping.query.api.Criteria sqlRestriction(String sqlRestriction, List<?> values) {
+ hibernateQuery.add(new SqlRestriction(sqlRestriction, values));
Review Comment:
This always adds the restriction to the builder's current `hibernateQuery`. An association nested inside a junction does not update that query. `HibernateQuery.or` / `not` / `and` delegate to `DetachedCriteria`, and that association `methodMissing` changes closure delegates without changing `hibernateQuery.detachedCriteria`. Detached criteria have no `sqlRestriction`, so the call falls back here and lands on the enclosing criteria.
For `or { chapters { sqlRestriction('{alias}.title = ?', ['Epilogue']) }; eq('title', 'Dune') }` the SQL criterion targets the book, and the chapter operand stays empty. Route the restriction to the active association criteria, and add public DSL regressions for association blocks inside `or`, `not`, and nested associations.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java:
##########
@@ -448,6 +453,38 @@ private Predicate handlePropertyCriterion(
throw new UnsupportedOperationException("Unsupported criterion: " + pc.getClass().getName());
}
+ /**
+ * Returns a column of the entity's own table, whose table alias replaces {@code {alias}} in a SQL restriction.
+ */
+ private static Expression<?> aliasColumn(From<?, ?> root, GrailsHibernatePersistentEntity entity) {
+ PersistentProperty identity = entity.getIdentity();
+ if (identity == null) {
+ HibernatePersistentProperty[] compositeIdentity = entity.getCompositeIdentity();
+ if (compositeIdentity == null || compositeIdentity.length == 0) {
+ throw new ConfigurationException("Cannot use sqlRestriction on class [" + entity.getJavaClass().getName() + "] without an identifier");
+ }
+ identity = compositeIdentity[0];
+ }
+ Path<?> path = root.get(identity.getName());
+ if (identity instanceof Association<?> association && association.getAssociatedEntity() != null) {
+ path = path.get(association.getAssociatedEntity().getIdentity().getName());
Review Comment:
`getIdentity()` is null when the associated entity itself has a composite identifier, so this throws. `CompositeIdWithDeepOneToManyMappingSpec` is exactly this mapping: `Child` starts with `parent`, and `Parent` has a composite id. `aliasColumn` is also called when the SQL has no `{alias}`, so a constant restriction fails the same way.
Resolve an owning-table column without assuming the associated identifier is singular. Cover that mapping through the public DSL, with and without `{alias}`.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SqlRestriction.java:
##########
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate.query;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.grails.datastore.mapping.query.Query;
+
+/**
+ * Criterion that restricts the results with a native SQL condition, created by {@code sqlRestriction} in a
+ * criteria query. In the SQL, {@code {alias}} stands for the table alias of the queried entity, and each
+ * {@code ?} for one of the values, which are bound as parameters.
+ *
+ * @param sql the SQL condition
+ * @param values the values of the {@code ?} placeholders, in order
+ * @since 8.0.0
+ */
+public record SqlRestriction(String sql, List<?> values) implements Query.Criterion {
+
+ public SqlRestriction {
+ if (sql == null) {
+ throw new IllegalArgumentException("The SQL of a sqlRestriction must not be null");
+ }
+ if (values == null) {
+ values = List.of();
+ }
+ List<Object> copy = new ArrayList<>(values.size());
+ for (Object value : values) {
+ if (value == null) {
+ throw new IllegalArgumentException("The values of a sqlRestriction must not be null: " + sql);
+ }
+ copy.add(value instanceof CharSequence ? value.toString() : value);
+ }
+ long placeholders = sql.chars().filter(c -> c == '?').count();
Review Comment:
Every `?` is counted, including quoted text and comments. `sqlRestriction("{alias}.title like '%?%'")` is valid SQL with zero parameters and is rejected as missing a value. Supplying a dummy value instead makes the renderer at `GrailsSqlRestrictionFunction` bind inside the quoted literal. Hibernate 5's GORM path passes the SQL and values through to `Restrictions.sqlRestriction` without this character count.
Use one SQL-aware placeholder scan in both validation and rendering, and test a literal question mark next to a real parameter.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsSqlRestrictionFunction.java:
##########
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate.query;
+
+import java.util.List;
+
+import org.hibernate.metamodel.model.domain.ReturnableType;
+import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
+import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
+import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
+import org.hibernate.sql.ast.SqlAstTranslator;
+import org.hibernate.sql.ast.spi.SqlAppender;
+import org.hibernate.sql.ast.tree.SqlAstNode;
+import org.hibernate.sql.ast.tree.expression.ColumnReference;
+import org.hibernate.sql.ast.tree.expression.Expression;
+import org.hibernate.sql.ast.tree.expression.Literal;
+import org.hibernate.sql.ast.tree.expression.SqlTuple;
+import org.hibernate.type.StandardBasicTypes;
+import org.hibernate.type.spi.TypeConfiguration;
+
+/**
+ * Renders a {@link SqlRestriction} as a predicate. The arguments are the SQL condition as a literal, a column of
+ * the queried entity whose table alias replaces {@code {alias}}, and the value of each {@code ?} placeholder.
+ *
+ * @since 8.0.0
+ */
+public class GrailsSqlRestrictionFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
+
+ public static final String NAME = "grails_sql_restriction";
+
+ static final String ALIAS_PLACEHOLDER = "{alias}";
+
+ public GrailsSqlRestrictionFunction(TypeConfiguration typeConfiguration) {
+ super(
+ NAME,
+ StandardArgumentsValidators.min(2),
+ StandardFunctionReturnTypeResolvers.invariant(
+ typeConfiguration.getBasicTypeRegistry().resolve(StandardBasicTypes.BOOLEAN)),
+ null);
+ }
+
+ @Override
+ public boolean isPredicate() {
+ return true;
+ }
+
+ @Override
+ public void render(
+ SqlAppender sqlAppender,
+ List<? extends SqlAstNode> arguments,
+ ReturnableType<?> returnType,
+ SqlAstTranslator<?> walker) {
+ if (!(arguments.get(0) instanceof Literal literal) || !(literal.getLiteralValue() instanceof String sql)) {
+ throw new IllegalArgumentException("The first argument of " + NAME + " must be the SQL as a literal");
+ }
+ if (sql.contains(ALIAS_PLACEHOLDER)) {
+ sql = sql.replace(ALIAS_PLACEHOLDER, tableAlias(arguments.get(1)));
+ }
+ int index = 0;
+ for (int i = 2; i < arguments.size(); i++) {
+ int placeholder = sql.indexOf('?', index);
+ if (placeholder == -1) {
+ throw new IllegalArgumentException("The SQL restriction has fewer ? placeholders than values: " + sql);
+ }
+ sqlAppender.append(sql, index, placeholder);
+ arguments.get(i).accept(walker);
+ index = placeholder + 1;
+ }
+ if (sql.indexOf('?', index) != -1) {
+ throw new IllegalArgumentException("The SQL restriction has more ? placeholders than values: " + sql);
+ }
+ sqlAppender.append(sql, index, sql.length());
Review Comment:
The fragment is appended with no parentheses. Combined with another criterion, `A or B` becomes `A or B and C` rather than `(A or B) and C`. Hibernate treats the function as an opaque predicate and does not add the grouping. Reproduced on Hibernate 7.4.10: a restriction matching either Dune or Emma, combined with an id criterion for Emma, returned both books.
Parenthesize the whole fragment. The `indexOf('?')` scan above has the same quoted-placeholder bug as `SqlRestriction`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #16505:
URL: https://github.com/apache/grails-core/pull/16505#issuecomment-5982313439
I went ahead and fixed these remaining issues. I'm inclined to merge so we can make the 8.0.0 deadline.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
Posted by "codecov[bot] (via GitHub)" <gi...@apache.org>.
codecov[bot] commented on PR #16505:
URL: https://github.com/apache/grails-core/pull/16505#issuecomment-5977827728
## [Codecov](https://app.codecov.io/gh/apache/grails-core/pull/16505?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 59.4020%. Comparing base ([`620d549`](https://app.codecov.io/gh/apache/grails-core/commit/620d549caaf29f053bd2a855583fe8a4d89af74c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`9fa482b`](https://app.codecov.io/gh/apache/grails-core/commit/9fa482ba91a9d72f51e0c681fd28bb5aeb0b77f8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
<details><summary>Additional details and impacted files</summary>
[](https://app.codecov.io/gh/apache/grails-core/pull/16505?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
```diff
@@ Coverage Diff @@
## 8.0.x #16505 +/- ##
==================================================
- Coverage 59.4048% 59.4020% -0.0028%
+ Complexity 24859 24858 -1
==================================================
Files 2171 2171
Lines 108237 108237
Branches 19744 19744
==================================================
- Hits 64298 64295 -3
- Misses 35001 35004 +3
Partials 8938 8938
```
[see 2 files with indirect coverage changes](https://app.codecov.io/gh/apache/grails-core/pull/16505/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
</details>
<details><summary> :rocket: New features to boost your workflow: </summary>
- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #16505:
URL: https://github.com/apache/grails-core/pull/16505#discussion_r4178328800
##########
grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java:
##########
@@ -1112,6 +1115,46 @@ public Criteria sizeLt(String propertyName, int size) {
return this;
}
+ /**
+ * Builds a junction, whose block {@link DetachedCriteria} resolves its calls against, with
+ * {@code sqlRestriction} added to the detached criteria of the block and of the association blocks inside it.
+ */
+ private void inJunction(Runnable junction) {
+ GroovyCategorySupport.use(SqlRestrictionCategory.class, new Closure<Object>(this) {
Review Comment:
**[P1] The category scope invalidates every Groovy call site in the JVM, on every junction.** In Groovy 5.1.3, `GroovyCategorySupport.use` calls `VMPluginFactory.getPlugin().invalidateCallSites()` when it enters the scope and again when it leaves it (`ThreadCategoryInfo.newScope` and `endScope`). That replaces the global `SwitchPoint` that guards every indy call site. Only the method lookup is limited to the current thread. So every Hibernate 7 criteria query with an `and`, `or` or `not` block now makes the call sites of every thread relink, and deoptimizes the compiled code that inlined them, whether or not it uses `sqlRestriction`.
Measured on JDK 21/H2:
- `IndyInterface.switchPoint` before and after `createCriteria().list { or { eq('title', 'Alpha'); eq('title', 'Beta') } }` is invalidated at edc5f687cb and still valid at the base 620d549caa. The same query without the `or` leaves it valid on both.
- A Groovy method with 300 dynamic call sites, timed after each query (median of 1,500 runs), takes about 60 µs after either query at the base. At edc5f687cb it takes about 900 µs after the `or` query, and 150-170 µs after plain queries once `or` queries have run.
The category's methods work unchanged as a Groovy extension module, which Groovy loads once and which needs no scope. I tried registering `SqlRestrictionCategory` in `META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule` and building the junctions directly again. All of `HibernateCriteriaBuilderSqlRestrictionSpec` passed, including `or { and { chapters { ... } } }`, the switch point stayed valid, and the 300 call sites took about 60 µs after both queries. `grails-test-examples/hibernate7/criteria-extension` adds criteria methods the same way.
One difference: `sqlRestriction` is then also callable on any detached criteria, for example `new DetachedCriteria(Book).build { sqlRestriction(...) }`, not only inside the builder's junctions. If you keep that, please document and test it. Otherwise, any approach without a category scope works.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java:
##########
@@ -448,6 +453,38 @@ private Predicate handlePropertyCriterion(
throw new UnsupportedOperationException("Unsupported criterion: " + pc.getClass().getName());
}
+ /**
+ * Returns a column of the entity's own table, whose table alias replaces {@code {alias}} in a SQL restriction.
+ */
+ private static Expression<?> aliasColumn(From<?, ?> root, GrailsHibernatePersistentEntity entity) {
+ PersistentProperty identity = entity.getIdentity();
+ if (identity == null) {
+ HibernatePersistentProperty[] compositeIdentity = entity.getCompositeIdentity();
+ if (compositeIdentity == null || compositeIdentity.length == 0) {
+ throw new ConfigurationException("Cannot use sqlRestriction on class [" + entity.getJavaClass().getName() + "] without an identifier");
+ }
+ identity = compositeIdentity[0];
+ }
+ Path<?> path = root.get(identity.getName());
+ if (identity instanceof Association<?> association && association.getAssociatedEntity() != null) {
+ path = path.get(association.getAssociatedEntity().getIdentity().getName());
Review Comment:
Verified at edc5f687cb: the `Printing`/`Edition` mapping now works with and without `{alias}`.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SqlRestriction.java:
##########
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate.query;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.grails.datastore.mapping.query.Query;
+
+/**
+ * Criterion that restricts the results with a native SQL condition, created by {@code sqlRestriction} in a
+ * criteria query. In the SQL, {@code {alias}} stands for the table alias of the queried entity, and each
+ * {@code ?} for one of the values, which are bound as parameters.
+ *
+ * @param sql the SQL condition
+ * @param values the values of the {@code ?} placeholders, in order
+ * @since 8.0.0
+ */
+public record SqlRestriction(String sql, List<?> values) implements Query.Criterion {
+
+ public SqlRestriction {
+ if (sql == null) {
+ throw new IllegalArgumentException("The SQL of a sqlRestriction must not be null");
+ }
+ if (values == null) {
+ values = List.of();
+ }
+ List<Object> copy = new ArrayList<>(values.size());
+ for (Object value : values) {
+ if (value == null) {
+ throw new IllegalArgumentException("The values of a sqlRestriction must not be null: " + sql);
+ }
+ copy.add(value instanceof CharSequence ? value.toString() : value);
+ }
+ long placeholders = sql.chars().filter(c -> c == '?').count();
+ if (placeholders != copy.size()) {
+ throw new IllegalArgumentException("The SQL of a sqlRestriction has " + placeholders +
+ " ? placeholders but " + copy.size() + " values: " + sql);
Review Comment:
Verified at edc5f687cb: `{alias}.title like '%?' and {alias}.pages > ?` with `[50]` now returns the matching book, through the shared scan in validation and rendering.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #16505:
URL: https://github.com/apache/grails-core/pull/16505#issuecomment-5982369091
@jamesfredley I'm limited on the time I have today so I need to get this merged so i can call the 8.0.0 vote. I believe everything is addressed, but if it's not I can reroll the vote late tonight or we can address in a future PR.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #16505:
URL: https://github.com/apache/grails-core/pull/16505#issuecomment-5981562375
## 🔎 No tests executed 🔎
🏷️ Commit: edc5f687cb1fe35eefae28e89ab62ccdcdedbe66
▶️ Tests: 0 executed
⚪️ Checks: 2/2 completed
---
_Learn more about TestLens at [testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Implement sqlRestriction for Hibernate 7 criteria queries [grails-core]
Posted by "matrei (via GitHub)" <gi...@apache.org>.
matrei commented on code in PR #16505:
URL: https://github.com/apache/grails-core/pull/16505#discussion_r4178241770
##########
grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java:
##########
@@ -1112,6 +1113,32 @@ public Criteria sizeLt(String propertyName, int size) {
return this;
}
+ /**
+ * Restricts the results with a native SQL condition. {@code {alias}} in the SQL stands for the table alias of
+ * the queried entity.
+ *
+ * @param sqlRestriction the SQL condition
+ * @return this criteria
+ */
+ public org.grails.datastore.mapping.query.api.Criteria sqlRestriction(String sqlRestriction) {
+ return sqlRestriction(sqlRestriction, Collections.emptyList());
+ }
+
+ /**
+ * Restricts the results with a native SQL condition whose {@code ?} placeholders are bound to the given values.
+ * {@code {alias}} in the SQL stands for the table alias of the queried entity.
+ *
+ * @param sqlRestriction the SQL condition
+ * @param values the values of the {@code ?} placeholders, in order, none of them {@code null}
+ * @return this criteria
+ * @throws IllegalArgumentException if the number of {@code ?} placeholders differs from the number of values,
+ * or a value is {@code null}
+ */
+ public org.grails.datastore.mapping.query.api.Criteria sqlRestriction(String sqlRestriction, List<?> values) {
+ hibernateQuery.add(new SqlRestriction(sqlRestriction, values));
Review Comment:
Fixed. The builder now builds its `and`, `or` and `not` blocks with a Groovy category that adds `sqlRestriction` to the detached criteria those blocks resolve their calls against. The call therefore lands on whichever criteria is the active delegate: the junction itself, an association block inside it (`DetachedCriteria.methodMissing` makes the association criteria the delegate), a junction inside that association, or a nested association. The category applies only to the current thread and only while the builder is building a junction. Regressions through `createCriteria()` cover an association inside `or` and `not`, an association inside `and` inside `or`, a junction inside an association inside `or`, and nested associations with and without an enclosing junction. The Testcontainers spec runs an association-in-`or` case on all five databases.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java:
##########
@@ -448,6 +453,38 @@ private Predicate handlePropertyCriterion(
throw new UnsupportedOperationException("Unsupported criterion: " + pc.getClass().getName());
}
+ /**
+ * Returns a column of the entity's own table, whose table alias replaces {@code {alias}} in a SQL restriction.
+ */
+ private static Expression<?> aliasColumn(From<?, ?> root, GrailsHibernatePersistentEntity entity) {
+ PersistentProperty identity = entity.getIdentity();
+ if (identity == null) {
+ HibernatePersistentProperty[] compositeIdentity = entity.getCompositeIdentity();
+ if (compositeIdentity == null || compositeIdentity.length == 0) {
+ throw new ConfigurationException("Cannot use sqlRestriction on class [" + entity.getJavaClass().getName() + "] without an identifier");
+ }
+ identity = compositeIdentity[0];
+ }
+ Path<?> path = root.get(identity.getName());
+ if (identity instanceof Association<?> association && association.getAssociatedEntity() != null) {
+ path = path.get(association.getAssociatedEntity().getIdentity().getName());
Review Comment:
Fixed. The `{alias}` column is now resolved by following the identifier, or the first property of a composite identifier, through associations until it reaches a basic property. A composite identifier that starts with an association to an entity with its own composite identifier resolves to the first foreign key column on the owning table. The column is also added only when the SQL contains `{alias}`, so a restriction without it never resolves one. `an entity whose composite identifier starts with an association to an entity with a composite identifier` covers both the `{alias}` and the unaliased form.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java:
##########
@@ -448,6 +453,38 @@ private Predicate handlePropertyCriterion(
throw new UnsupportedOperationException("Unsupported criterion: " + pc.getClass().getName());
}
+ /**
+ * Returns a column of the entity's own table, whose table alias replaces {@code {alias}} in a SQL restriction.
+ */
+ private static Expression<?> aliasColumn(From<?, ?> root, GrailsHibernatePersistentEntity entity) {
+ PersistentProperty identity = entity.getIdentity();
+ if (identity == null) {
+ HibernatePersistentProperty[] compositeIdentity = entity.getCompositeIdentity();
+ if (compositeIdentity == null || compositeIdentity.length == 0) {
+ throw new ConfigurationException("Cannot use sqlRestriction on class [" + entity.getJavaClass().getName() + "] without an identifier");
+ }
+ identity = compositeIdentity[0];
+ }
+ Path<?> path = root.get(identity.getName());
+ if (identity instanceof Association<?> association && association.getAssociatedEntity() != null) {
+ path = path.get(association.getAssociatedEntity().getIdentity().getName());
Review Comment:
Fixed. The `{alias}` column is now resolved by following the identifier, or the first property of a composite identifier, through associations until it reaches a basic property. A composite identifier that starts with an association to an entity with its own composite identifier resolves to the first foreign key column on the owning table. The column is also added only when the SQL contains `{alias}`, so a restriction without it never resolves one. `an entity whose composite identifier starts with an association to an entity with a composite identifier` covers both the `{alias}` and the unaliased form.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SqlRestriction.java:
##########
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate.query;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.grails.datastore.mapping.query.Query;
+
+/**
+ * Criterion that restricts the results with a native SQL condition, created by {@code sqlRestriction} in a
+ * criteria query. In the SQL, {@code {alias}} stands for the table alias of the queried entity, and each
+ * {@code ?} for one of the values, which are bound as parameters.
+ *
+ * @param sql the SQL condition
+ * @param values the values of the {@code ?} placeholders, in order
+ * @since 8.0.0
+ */
+public record SqlRestriction(String sql, List<?> values) implements Query.Criterion {
+
+ public SqlRestriction {
+ if (sql == null) {
+ throw new IllegalArgumentException("The SQL of a sqlRestriction must not be null");
+ }
+ if (values == null) {
+ values = List.of();
+ }
+ List<Object> copy = new ArrayList<>(values.size());
+ for (Object value : values) {
+ if (value == null) {
+ throw new IllegalArgumentException("The values of a sqlRestriction must not be null: " + sql);
+ }
+ copy.add(value instanceof CharSequence ? value.toString() : value);
+ }
+ long placeholders = sql.chars().filter(c -> c == '?').count();
+ if (placeholders != copy.size()) {
+ throw new IllegalArgumentException("The SQL of a sqlRestriction has " + placeholders +
+ " ? placeholders but " + copy.size() + " values: " + sql);
Review Comment:
Fixed. `SqlRestriction` validation and `GrailsSqlRestrictionFunction` rendering now share one scan. It skips a `?` inside a string literal (`'...'`, with `''` as an escaped quote), a quoted identifier (`"..."` or `` `...` ``), a `--` line comment and a `/* */` block comment. Regression tests cover a literal `?` before a real placeholder, a literal `?` with no parameters, an escaped quote, and a `?` in a quoted identifier, a block comment and a line comment. The Testcontainers spec also runs a literal `?` and a trailing comment on all five databases.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SqlRestriction.java:
##########
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate.query;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.grails.datastore.mapping.query.Query;
+
+/**
+ * Criterion that restricts the results with a native SQL condition, created by {@code sqlRestriction} in a
+ * criteria query. In the SQL, {@code {alias}} stands for the table alias of the queried entity, and each
+ * {@code ?} for one of the values, which are bound as parameters.
+ *
+ * @param sql the SQL condition
+ * @param values the values of the {@code ?} placeholders, in order
+ * @since 8.0.0
+ */
+public record SqlRestriction(String sql, List<?> values) implements Query.Criterion {
+
+ public SqlRestriction {
+ if (sql == null) {
+ throw new IllegalArgumentException("The SQL of a sqlRestriction must not be null");
+ }
+ if (values == null) {
+ values = List.of();
+ }
+ List<Object> copy = new ArrayList<>(values.size());
+ for (Object value : values) {
+ if (value == null) {
+ throw new IllegalArgumentException("The values of a sqlRestriction must not be null: " + sql);
+ }
+ copy.add(value instanceof CharSequence ? value.toString() : value);
+ }
+ long placeholders = sql.chars().filter(c -> c == '?').count();
Review Comment:
Fixed. `SqlRestriction` validation and `GrailsSqlRestrictionFunction` rendering now share one scan. It skips a `?` inside a string literal (`'...'`, with `''` as an escaped quote), a quoted identifier (`"..."` or `` `...` ``), a `--` line comment and a `/* */` block comment. Regression tests cover a literal `?` before a real placeholder, a literal `?` with no parameters, an escaped quote, and a `?` in a quoted identifier, a block comment and a line comment. The Testcontainers spec also runs a literal `?` and a trailing comment on all five databases.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/GrailsSqlRestrictionFunction.java:
##########
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate.query;
+
+import java.util.List;
+
+import org.hibernate.metamodel.model.domain.ReturnableType;
+import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
+import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
+import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
+import org.hibernate.sql.ast.SqlAstTranslator;
+import org.hibernate.sql.ast.spi.SqlAppender;
+import org.hibernate.sql.ast.tree.SqlAstNode;
+import org.hibernate.sql.ast.tree.expression.ColumnReference;
+import org.hibernate.sql.ast.tree.expression.Expression;
+import org.hibernate.sql.ast.tree.expression.Literal;
+import org.hibernate.sql.ast.tree.expression.SqlTuple;
+import org.hibernate.type.StandardBasicTypes;
+import org.hibernate.type.spi.TypeConfiguration;
+
+/**
+ * Renders a {@link SqlRestriction} as a predicate. The arguments are the SQL condition as a literal, a column of
+ * the queried entity whose table alias replaces {@code {alias}}, and the value of each {@code ?} placeholder.
+ *
+ * @since 8.0.0
+ */
+public class GrailsSqlRestrictionFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
+
+ public static final String NAME = "grails_sql_restriction";
+
+ static final String ALIAS_PLACEHOLDER = "{alias}";
+
+ public GrailsSqlRestrictionFunction(TypeConfiguration typeConfiguration) {
+ super(
+ NAME,
+ StandardArgumentsValidators.min(2),
+ StandardFunctionReturnTypeResolvers.invariant(
+ typeConfiguration.getBasicTypeRegistry().resolve(StandardBasicTypes.BOOLEAN)),
+ null);
+ }
+
+ @Override
+ public boolean isPredicate() {
+ return true;
+ }
+
+ @Override
+ public void render(
+ SqlAppender sqlAppender,
+ List<? extends SqlAstNode> arguments,
+ ReturnableType<?> returnType,
+ SqlAstTranslator<?> walker) {
+ if (!(arguments.get(0) instanceof Literal literal) || !(literal.getLiteralValue() instanceof String sql)) {
+ throw new IllegalArgumentException("The first argument of " + NAME + " must be the SQL as a literal");
+ }
+ if (sql.contains(ALIAS_PLACEHOLDER)) {
+ sql = sql.replace(ALIAS_PLACEHOLDER, tableAlias(arguments.get(1)));
+ }
+ int index = 0;
+ for (int i = 2; i < arguments.size(); i++) {
+ int placeholder = sql.indexOf('?', index);
+ if (placeholder == -1) {
+ throw new IllegalArgumentException("The SQL restriction has fewer ? placeholders than values: " + sql);
+ }
+ sqlAppender.append(sql, index, placeholder);
+ arguments.get(i).accept(walker);
+ index = placeholder + 1;
+ }
+ if (sql.indexOf('?', index) != -1) {
+ throw new IllegalArgumentException("The SQL restriction has more ? placeholders than values: " + sql);
+ }
+ sqlAppender.append(sql, index, sql.length());
Review Comment:
Fixed. The fragment is now rendered in parentheses, so `A or B` combined with another criterion is `(A or B) and C`. If the SQL ends in a `--` line comment, a line break goes before the closing parenthesis so the comment cannot swallow it. There are tests for the Dune/Emma case and its negation, and the Testcontainers spec runs the grouping on all five databases.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org