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
Expand Up @@ -33,5 +33,6 @@ import groovy.transform.CompileStatic
'org.grails.compiler.WhereQueryTypeCheckingExtension',
'org.grails.compiler.DynamicFinderTypeCheckingExtension',
'org.grails.compiler.DomainMappingTypeCheckingExtension',
'org.grails.compiler.CriteriaTypeCheckingExtension',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To change the default, we need to expand the test coverage so that there are tests in the TCK - this way we know we haven't broken it in hibernate 7.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in b10f36f. The TCK now executes criteria queries that were compiled with @GrailsCompileStatic, covering both dispatch paths the extension produces: statically-resolved Criteria API calls and the dynamic fallback (projections, min/countDistinct/property, maxResults, and the chained count { } terminal, which is not declared on BuildableCriteria). Verified green against the in-memory, Hibernate 5, Hibernate 7, and MongoDB implementations locally.

'org.grails.compiler.RelationshipManagementMethodTypeCheckingExtension'])
@interface GrailsCompileStatic {}
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,28 @@ class CriteriaTypeCheckingExtension extends TypeCheckingDSL {
null
}

private static final List<String> CRITERIA_TERMINALS =
['list', 'listDistinct', 'get', 'count', 'scroll']

protected boolean isCriteriaCall(MethodCall call) {
call instanceof MethodCallExpression &&
call.objectExpression instanceof ClassExpression &&
GrailsASTUtils.isDomainClass(call.objectExpression.type, null) &&
(call.method.value == 'withCriteria' || call.method.value == 'createCriteria')
if (!(call instanceof MethodCallExpression)) {
return false
}
// Domain.withCriteria { } / Domain.createCriteria() — the criteria closure is the direct argument.
if (call.objectExpression instanceof ClassExpression &&
GrailsASTUtils.isDomainClass(call.objectExpression.type, null) &&
(call.method.value == 'withCriteria' || call.method.value == 'createCriteria')) {
return true
}
// Domain.createCriteria().list/get/count/... { } — the closure is the argument to the terminal
// call chained on the builder, so its body must be type-checked in a criteria scope too.
if (call.method.value in CRITERIA_TERMINALS &&
call.objectExpression instanceof MethodCallExpression &&
call.objectExpression.method.value == 'createCriteria' &&
call.objectExpression.objectExpression instanceof ClassExpression &&
GrailsASTUtils.isDomainClass(call.objectExpression.objectExpression.type, null)) {
return true
}
false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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.apache.grails.data.testing.tck.support

import grails.compiler.GrailsCompileStatic

import org.apache.grails.data.testing.tck.domains.TestEntity

/**
* Criteria queries compiled with {@code @GrailsCompileStatic} so that the bytecode produced by
* the {@code CriteriaTypeCheckingExtension} is executed by the TCK against every GORM
* implementation. Calls declared on the {@code Criteria} API (eq, like, or, order) compile to
* static delegate dispatch, while calls that are not on the API (projections, min, countDistinct,
* property, maxResults and the {@code count} terminal chained on {@code createCriteria()}) only
* compile because the extension falls back to dynamic dispatch — executing them here proves that
* dispatch resolves against the criteria builder of the implementation under test.
*/
@GrailsCompileStatic
class StaticCompiledCriteriaQueries {

static List listByNameLike(String pattern) {
(List) TestEntity.createCriteria().list {
like('name', pattern)
}
}

static List listByNameLikeLimited(String pattern, int limit) {
(List) TestEntity.createCriteria().list {
like('name', pattern)
maxResults(limit)
}
}

static List listPaginatedOrderedByAge(int max, int offset) {
(List) TestEntity.createCriteria().list(max: max, offset: offset) {
order('age')
}
}

static Number countByNameLike(String pattern) {
(Number) TestEntity.createCriteria().count {
like('name', pattern)
}
}

static TestEntity getByName(String name) {
(TestEntity) TestEntity.createCriteria().get {
eq('name', name)
}
}

static Number minAge() {
(Number) TestEntity.createCriteria().get {
projections {
min('age')
}
}
}

static Number countDistinctAges() {
(Number) TestEntity.createCriteria().get {
projections {
countDistinct('age')
}
}
}

static List namesMatching(String pattern) {
(List) TestEntity.withCriteria {
like('name', pattern)
projections {
property('name')
}
}
}

static List listByNameLikeOrAge(String pattern, int age) {
(List) TestEntity.withCriteria {
or {
like('name', pattern)
eq('age', age)
}
order('age', 'asc')
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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.apache.grails.data.testing.tck.tests

import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec
import org.apache.grails.data.testing.tck.domains.ChildEntity
import org.apache.grails.data.testing.tck.domains.TestEntity
import org.apache.grails.data.testing.tck.support.StaticCompiledCriteriaQueries

/**
* Executes the criteria queries that {@link StaticCompiledCriteriaQueries} compiled with
* {@code @GrailsCompileStatic} against the GORM implementation under test, verifying that the
* dispatch produced by the {@code CriteriaTypeCheckingExtension} behaves identically to the
* dynamically compiled queries covered by {@code CriteriaBuilderSpec}.
*/
class StaticCompiledCriteriaSpec extends GrailsDataTckSpec {

@Override
void setupSpec() {
manager.registerDomainClasses(TestEntity, ChildEntity)
}

private static void seedPeople() {
def age = 40
['Bob', 'Fred', 'Barney', 'Frank'].each {
new TestEntity(name: it, age: age++, child: new ChildEntity(name: "$it Child")).save()
}
}

void 'Test statically compiled list with a criteria closure on a chained terminal'() {
given:
seedPeople()

when:
def results = StaticCompiledCriteriaQueries.listByNameLike('B%')

then:
2 == results.size()
results.every { it instanceof TestEntity }
}

void 'Test statically compiled list with a dynamically dispatched maxResults call'() {
given:
seedPeople()

when:
def results = StaticCompiledCriteriaQueries.listByNameLikeLimited('B%', 1)

then:
1 == results.size()
}

void 'Test statically compiled paginated list with named arguments and a chained terminal'() {
given:
seedPeople()

when:
def results = StaticCompiledCriteriaQueries.listPaginatedOrderedByAge(2, 1)

then:
2 == results.size()
'Fred' == results[0].name
'Barney' == results[1].name
}

void 'Test statically compiled count terminal chained on createCriteria()'() {
given:
seedPeople()

when:
def result = StaticCompiledCriteriaQueries.countByNameLike('B%')

then:
2 == result
}

void 'Test statically compiled get with an equals criterion'() {
given:
seedPeople()

when:
TestEntity result = StaticCompiledCriteriaQueries.getByName('Bob')

then:
result != null
'Bob' == result.name
40 == result.age
}

void 'Test statically compiled min projection'() {
given:
seedPeople()

when:
def result = StaticCompiledCriteriaQueries.minAge()

then:
40 == result
}

void 'Test statically compiled count distinct projection'() {
given:
seedPeople()
new TestEntity(name: 'Chuck', age: 43, child: new ChildEntity(name: 'Chuckie')).save()

when:
def result = StaticCompiledCriteriaQueries.countDistinctAges()

then:
4 == result
}

void 'Test statically compiled property projection inside withCriteria'() {
given:
seedPeople()

when:
def results = StaticCompiledCriteriaQueries.namesMatching('B%')

then:
['Barney', 'Bob'] == results.sort()
}

void 'Test statically compiled disjunction with ordering'() {
given:
seedPeople()

when:
def results = StaticCompiledCriteriaQueries.listByNameLikeOrAge('B%', 41)

then:
3 == results.size()
'Bob' == results[0].name
'Fred' == results[1].name
'Barney' == results[2].name
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,52 @@ class GrailsCompileStaticCompilationErrorsSpec extends Specification {
then: 'no errors are thrown'
c
}


void 'Test compiling a class which invokes a chained createCriteria() terminal on a domain class'() {
given:
def gcl = new GroovyClassLoader()

when: 'the criteria builder closure is the argument to a terminal chained directly on createCriteria()'
def c = gcl.parseClass('''
package grails.compiler

@GrailsCompileStatic
class SomeClass {

def someMethod() {
Person.createCriteria().list {
cache true
eq 'name', 'Anakin'
}

Person.createCriteria().list(max: 4, offset: 2) {
cache true
eq 'name', 'Anakin'
}

Person.createCriteria().listDistinct {
eq 'name', 'Anakin'
}

Person.createCriteria().get {
eq 'name', 'Anakin'
}

Person.createCriteria().count {
eq 'name', 'Anakin'
}

Person.createCriteria().scroll {
eq 'name', 'Anakin'
}
}
}
'''.stripIndent())

then: 'no errors are thrown'
c
}

void 'Test compiling a domain class with a mapping block'() {
given:
def gcl = new GroovyClassLoader()
Expand Down
Loading