This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7 in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 195f87baf41197e84938ca09aba5eb6159c090b8 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Tue Feb 24 13:57:18 2026 -0600 update tests --- .../groovy/grails/orm/CriteriaMethodInvoker.java | 4 +- .../grails/orm/CriteriaMethodInvokerSpec.groovy | 285 +++++++++++++ .../orm/HibernateCriteriaBuilderDirectSpec.groovy | 378 +++++++++++++++++ .../grails/orm/hibernate/cfg/MappingSpec.groovy | 85 ++++ .../orm/hibernate/cfg/PropertyConfigSpec.groovy | 457 +++++++++++++++++++++ 5 files changed, 1207 insertions(+), 2 deletions(-) diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java index ae696b3d64..ef68e6fb66 100644 --- a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java +++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java @@ -200,7 +200,7 @@ public class CriteriaMethodInvoker { return UNHANDLED; } - private Object trySimpleCriteria(String name, CriteriaMethods method, Object[] args) { + protected Object trySimpleCriteria(String name, CriteriaMethods method, Object[] args) { if (args.length != 1 || args[0] == null) { return UNHANDLED; } @@ -230,7 +230,7 @@ public class CriteriaMethodInvoker { return UNHANDLED; } - private Object tryPropertyCriteria(CriteriaMethods method, Object[] args) { + protected Object tryPropertyCriteria(CriteriaMethods method, Object[] args) { if (method == null || args.length < 2 || !(args[0] instanceof String)) { return UNHANDLED; } diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy index 4cc10bcfdf..3f9c34f5da 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy @@ -167,6 +167,291 @@ class CriteriaMethodInvokerSpec extends Specification { _ * builder.getMetaClass() >> GroovySystem.metaClassRegistry.getMetaClass(HibernateCriteriaBuilder) thrown(MissingMethodException) } + + // ─── trySimpleCriteria ───────────────────────────────────────────────── + // Both methods are protected, so the same-package spec calls them directly. + + void "trySimpleCriteria: idEq delegates to builder.eq('id', value)"() { + when: + invoker.trySimpleCriteria('idEq', CriteriaMethods.ID_EQUALS, [42L] as Object[]) + + then: + 1 * builder.eq('id', 42L) + } + + void "trySimpleCriteria: isNull with String delegates to hibernateQuery.isNull"() { + when: + invoker.trySimpleCriteria('isNull', CriteriaMethods.IS_NULL, ['branch'] as Object[]) + + then: + 1 * builder.calculatePropertyName('branch') >> 'branch' + 1 * query.isNull('branch') + } + + void "trySimpleCriteria: isNotNull with String delegates to hibernateQuery.isNotNull"() { + when: + invoker.trySimpleCriteria('isNotNull', CriteriaMethods.IS_NOT_NULL, ['branch'] as Object[]) + + then: + 1 * builder.calculatePropertyName('branch') >> 'branch' + 1 * query.isNotNull('branch') + } + + void "trySimpleCriteria: isEmpty with String delegates to hibernateQuery.isEmpty"() { + when: + invoker.trySimpleCriteria('isEmpty', CriteriaMethods.IS_EMPTY, ['transactions'] as Object[]) + + then: + 1 * builder.calculatePropertyName('transactions') >> 'transactions' + 1 * query.isEmpty('transactions') + } + + void "trySimpleCriteria: isNotEmpty with String delegates to hibernateQuery.isNotEmpty"() { + when: + invoker.trySimpleCriteria('isNotEmpty', CriteriaMethods.IS_NOT_EMPTY, ['transactions'] as Object[]) + + then: + 1 * builder.calculatePropertyName('transactions') >> 'transactions' + 1 * query.isNotEmpty('transactions') + } + + void "trySimpleCriteria: non-String arg to isNull calls throwRuntimeException"() { + given: + builder.throwRuntimeException(_ as RuntimeException) >> { throw it[0] } + + when: + invoker.trySimpleCriteria('isNull', CriteriaMethods.IS_NULL, [42] as Object[]) + + then: + thrown(IllegalArgumentException) + } + + void "trySimpleCriteria: null value returns UNHANDLED without touching builder"() { + when: + def result = invoker.trySimpleCriteria('isNull', CriteriaMethods.IS_NULL, [null] as Object[]) + + then: + result != null // UNHANDLED sentinel object + 0 * builder.calculatePropertyName(_) + } + + void "trySimpleCriteria: null method returns UNHANDLED"() { + when: + def result = invoker.trySimpleCriteria('unknown', null, ['x'] as Object[]) + + then: + result != null // UNHANDLED sentinel + 0 * builder._ + } + + // ─── tryPropertyCriteria ─────────────────────────────────────────────── + + void "tryPropertyCriteria: rlike delegates to builder.rlike"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.RLIKE, ['firstName', '^F.*'] as Object[]) + + then: + 1 * builder.calculatePropertyName('firstName') >> 'firstName' + 1 * builder.rlike('firstName', '^F.*') + } + + void "tryPropertyCriteria: between delegates to builder.between"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.BETWEEN, ['balance', 10, 100] as Object[]) + + then: + 1 * builder.calculatePropertyName('balance') >> 'balance' + 1 * builder.between('balance', 10, 100) + } + + void "tryPropertyCriteria: eq delegates to builder.eq"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.EQUALS, ['firstName', 'Fred'] as Object[]) + + then: + 1 * builder.calculatePropertyName('firstName') >> 'firstName' + 1 * builder.eq('firstName', 'Fred') + } + + void "tryPropertyCriteria: eq with Map params delegates to builder.eq(prop, val, map)"() { + given: + def params = [ignoreCase: true] + + when: + invoker.tryPropertyCriteria(CriteriaMethods.EQUALS, ['firstName', 'Fred', params] as Object[]) + + then: + 1 * builder.calculatePropertyName('firstName') >> 'firstName' + 1 * builder.eq('firstName', 'Fred', params) + } + + void "tryPropertyCriteria: eqProperty delegates to builder.eqProperty"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.EQUALS_PROPERTY, ['firstName', 'lastName'] as Object[]) + + then: + 1 * builder.calculatePropertyName('firstName') >> 'firstName' + 1 * builder.eqProperty('firstName', 'lastName') + } + + void "tryPropertyCriteria: gt delegates to builder.gt"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.GREATER_THAN, ['balance', 100] as Object[]) + + then: + 1 * builder.calculatePropertyName('balance') >> 'balance' + 1 * builder.gt('balance', 100) + } + + void "tryPropertyCriteria: gtProperty delegates to builder.gtProperty"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.GREATER_THAN_PROPERTY, ['balance', 'balance'] as Object[]) + + then: + 1 * builder.calculatePropertyName('balance') >> 'balance' + 1 * builder.gtProperty('balance', 'balance') + } + + void "tryPropertyCriteria: ge delegates to builder.ge"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.GREATER_THAN_OR_EQUAL, ['balance', 100] as Object[]) + + then: + 1 * builder.calculatePropertyName('balance') >> 'balance' + 1 * builder.ge('balance', 100) + } + + void "tryPropertyCriteria: geProperty delegates to builder.geProperty"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.GREATER_THAN_OR_EQUAL_PROPERTY, ['balance', 'balance'] as Object[]) + + then: + 1 * builder.calculatePropertyName('balance') >> 'balance' + 1 * builder.geProperty('balance', 'balance') + } + + void "tryPropertyCriteria: ilike delegates to builder.ilike"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.ILIKE, ['firstName', 'fr%'] as Object[]) + + then: + 1 * builder.calculatePropertyName('firstName') >> 'firstName' + 1 * builder.ilike('firstName', 'fr%') + } + + void "tryPropertyCriteria: in with Collection delegates to builder.in"() { + given: + def names = ['Fred', 'Barney'] + + when: + invoker.tryPropertyCriteria(CriteriaMethods.IN, ['firstName', names] as Object[]) + + then: + 1 * builder.calculatePropertyName('firstName') >> 'firstName' + 1 * builder.in('firstName', names) + } + + void "tryPropertyCriteria: in with Object[] delegates to builder.in"() { + given: + def names = ['Fred', 'Barney'] as Object[] + + when: + invoker.tryPropertyCriteria(CriteriaMethods.IN, ['firstName', names] as Object[]) + + then: + 1 * builder.calculatePropertyName('firstName') >> 'firstName' + 1 * builder.in('firstName', names) + } + + void "tryPropertyCriteria: lt delegates to builder.lt"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.LESS_THAN, ['balance', 500] as Object[]) + + then: + 1 * builder.calculatePropertyName('balance') >> 'balance' + 1 * builder.lt('balance', 500) + } + + void "tryPropertyCriteria: ltProperty delegates to builder.ltProperty"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.LESS_THAN_PROPERTY, ['balance', 'balance'] as Object[]) + + then: + 1 * builder.calculatePropertyName('balance') >> 'balance' + 1 * builder.ltProperty('balance', 'balance') + } + + void "tryPropertyCriteria: le delegates to builder.le"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.LESS_THAN_OR_EQUAL, ['balance', 500] as Object[]) + + then: + 1 * builder.calculatePropertyName('balance') >> 'balance' + 1 * builder.le('balance', 500) + } + + void "tryPropertyCriteria: leProperty delegates to builder.leProperty"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.LESS_THAN_OR_EQUAL_PROPERTY, ['balance', 'balance'] as Object[]) + + then: + 1 * builder.calculatePropertyName('balance') >> 'balance' + 1 * builder.leProperty('balance', 'balance') + } + + void "tryPropertyCriteria: like delegates to builder.like"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.LIKE, ['firstName', 'Fr%'] as Object[]) + + then: + 1 * builder.calculatePropertyName('firstName') >> 'firstName' + 1 * builder.like('firstName', 'Fr%') + } + + void "tryPropertyCriteria: ne delegates to builder.ne"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.NOT_EQUAL, ['firstName', 'Fred'] as Object[]) + + then: + 1 * builder.calculatePropertyName('firstName') >> 'firstName' + 1 * builder.ne('firstName', 'Fred') + } + + void "tryPropertyCriteria: neProperty delegates to builder.neProperty"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.NOT_EQUAL_PROPERTY, ['firstName', 'lastName'] as Object[]) + + then: + 1 * builder.calculatePropertyName('firstName') >> 'firstName' + 1 * builder.neProperty('firstName', 'lastName') + } + + void "tryPropertyCriteria: sizeEq delegates to builder.sizeEq"() { + when: + invoker.tryPropertyCriteria(CriteriaMethods.SIZE_EQUALS, ['transactions', 2] as Object[]) + + then: + 1 * builder.calculatePropertyName('transactions') >> 'transactions' + 1 * builder.sizeEq('transactions', 2) + } + + void "tryPropertyCriteria: null method returns UNHANDLED"() { + when: + def result = invoker.tryPropertyCriteria(null, ['x', 'y'] as Object[]) + + then: + result != null // UNHANDLED sentinel + 0 * builder._ + } + + void "tryPropertyCriteria: non-String first arg returns UNHANDLED"() { + when: + def result = invoker.tryPropertyCriteria(CriteriaMethods.EQUALS, [42, 'Fred'] as Object[]) + + then: + result != null // UNHANDLED sentinel + 0 * builder._ + } } class InvokerAccount { diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy new file mode 100644 index 0000000000..d7a747b915 --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy @@ -0,0 +1,378 @@ +package grails.orm + +import grails.gorm.annotation.Entity +import grails.gorm.specs.HibernateGormDatastoreSpec +import jakarta.persistence.criteria.JoinType +import org.grails.datastore.mapping.query.Query +import spock.lang.Shared + +/** + * Direct (non-DSL) tests for the thin-wrapper methods of {@link HibernateCriteriaBuilder}. + * <p> + * The existing {@link HibernateCriteriaBuilderSpec} exercises the same methods through the + * Groovy DSL (closures routed via {@code invokeMethod} → {@code CriteriaMethodInvoker}). + * JaCoCo cannot trace method-body coverage through that dynamic dispatch path. + * These tests call every wrapper method as a direct Java-style invocation so JaCoCo + * sees the actual method bodies executed. + */ +class HibernateCriteriaBuilderDirectSpec extends HibernateGormDatastoreSpec { + + @Shared HibernateCriteriaBuilder builder + + def setupSpec() { + manager.addAllDomainClasses([DirectAccount, DirectTransaction]) + } + + def setup() { + builder = new HibernateCriteriaBuilder( + DirectAccount, + manager.hibernateDatastore.sessionFactory, + manager.hibernateDatastore) + } + + // ─── Comparison predicates ───────────────────────────────────────────── + + def "eq(String, Object) delegates and returns this"() { + expect: builder.eq("firstName", "Fred").is(builder) + } + + def "eq(String, Object, Map) delegates and returns this"() { + expect: builder.eq("firstName", "Fred", Collections.emptyMap()).is(builder) + } + + def "eq with ignoreCase=true calls like on hibernateQuery"() { + expect: builder.eq("firstName", "Fred", [ignoreCase: true]).is(builder) + } + + def "eq(Map, String, Object) groovy-map-first form delegates and returns this"() { + expect: builder.eq(Collections.emptyMap(), "firstName", "Fred").is(builder) + } + + def "ne(String, Object) delegates and returns this"() { + expect: builder.ne("firstName", "Fred").is(builder) + } + + def "gt(String, Object) delegates and returns this"() { + expect: builder.gt("balance", BigDecimal.ONE).is(builder) + } + + def "ge(String, Object) delegates and returns this"() { + expect: builder.ge("balance", BigDecimal.ONE).is(builder) + } + + def "lt(String, Object) delegates and returns this"() { + expect: builder.lt("balance", BigDecimal.TEN).is(builder) + } + + def "le(String, Object) delegates and returns this"() { + expect: builder.le("balance", BigDecimal.TEN).is(builder) + } + + def "gte(String, Object) aliases ge and returns this"() { + expect: builder.gte("balance", BigDecimal.ONE).is(builder) + } + + def "lte(String, Object) aliases le and returns this"() { + expect: builder.lte("balance", BigDecimal.TEN).is(builder) + } + + def "like(String, Object) delegates and returns this"() { + expect: builder.like("firstName", "Fr%").is(builder) + } + + def "ilike(String, Object) delegates and returns this"() { + expect: builder.ilike("firstName", "fr%").is(builder) + } + + def "rlike(String, Object) delegates and returns this"() { + expect: builder.rlike("firstName", "^Fr.*").is(builder) + } + + def "between(String, Object, Object) delegates and returns this"() { + expect: builder.between("balance", BigDecimal.ONE, BigDecimal.TEN).is(builder) + } + + // ─── Null / empty ────────────────────────────────────────────────────── + + def "isEmpty(String) delegates and returns this"() { + expect: builder.isEmpty("transactions").is(builder) + } + + def "isNotEmpty(String) delegates and returns this"() { + expect: builder.isNotEmpty("transactions").is(builder) + } + + def "isNull(String) delegates and returns this"() { + expect: builder.isNull("branch").is(builder) + } + + def "isNotNull(String) delegates and returns this"() { + expect: builder.isNotNull("branch").is(builder) + } + + // ─── Identity equality ───────────────────────────────────────────────── + + def "idEq(Object) aliases eq('id', o) and returns this"() { + given: def fred = new DirectAccount(balance: 100, firstName: "Fred", lastName: "F").save(failOnError: true, flush: true) + expect: builder.idEq(fred.id).is(builder) + } + + def "idEquals(Object) aliases idEq and returns this"() { + given: def fred = DirectAccount.first() + expect: builder.idEquals(fred?.id ?: 1L).is(builder) + } + + // ─── 'in' variants ──────────────────────────────────────────────────── + + def "in(String, Collection) delegates and returns this"() { + expect: builder.in("firstName", ["Fred", "Barney"]).is(builder) + } + + def "in(String, Object[]) delegates and returns this"() { + expect: builder.in("firstName", ["Fred", "Barney"] as Object[]).is(builder) + } + + def "inList(String, Collection) delegates to in and returns this"() { + expect: builder.inList("firstName", ["Fred", "Barney"]).is(builder) + } + + def "inList(String, Object[]) delegates to in and returns this"() { + expect: builder.inList("firstName", ["Fred", "Barney"] as Object[]).is(builder) + } + + // ─── allEq ──────────────────────────────────────────────────────────── + + def "allEq(Map) delegates and returns this"() { + expect: builder.allEq([firstName: "Fred", lastName: "Flintstone"]).is(builder) + } + + // ─── Property comparisons ────────────────────────────────────────────── + + def "eqProperty(String, String) delegates and returns this"() { + expect: builder.eqProperty("firstName", "firstName").is(builder) + } + + def "neProperty(String, String) delegates and returns this"() { + expect: builder.neProperty("firstName", "lastName").is(builder) + } + + def "gtProperty(String, String) delegates and returns this"() { + expect: builder.gtProperty("balance", "balance").is(builder) + } + + def "geProperty(String, String) delegates and returns this"() { + expect: builder.geProperty("balance", "balance").is(builder) + } + + def "ltProperty(String, String) delegates and returns this"() { + expect: builder.ltProperty("balance", "balance").is(builder) + } + + def "leProperty(String, String) delegates and returns this"() { + expect: builder.leProperty("balance", "balance").is(builder) + } + + // ─── Collection-size constraints ─────────────────────────────────────── + + def "sizeEq(String, int) delegates and returns this"() { + expect: builder.sizeEq("transactions", 2).is(builder) + } + + def "sizeGt(String, int) delegates and returns this"() { + expect: builder.sizeGt("transactions", 0).is(builder) + } + + def "sizeGe(String, int) delegates and returns this"() { + expect: builder.sizeGe("transactions", 1).is(builder) + } + + def "sizeLe(String, int) delegates and returns this"() { + expect: builder.sizeLe("transactions", 5).is(builder) + } + + def "sizeLt(String, int) delegates and returns this"() { + expect: builder.sizeLt("transactions", 3).is(builder) + } + + def "sizeNe(String, int) delegates and returns this"() { + expect: builder.sizeNe("transactions", 0).is(builder) + } + + // ─── Ordering ────────────────────────────────────────────────────────── + + def "order(String) delegates via Order and returns this"() { + expect: builder.order("firstName").is(builder) + } + + def "order(String, 'asc') sets ascending order and returns this"() { + expect: builder.order("firstName", "asc").is(builder) + } + + def "order(String, 'desc') sets descending order and returns this"() { + expect: builder.order("balance", "desc").is(builder) + } + + def "order(String, unrecognised) defaults to ascending and returns this"() { + expect: builder.order("balance", "unknown").is(builder) + } + + def "order(Query.Order) delegates and returns this"() { + expect: builder.order(new Query.Order("firstName")).is(builder) + } + + // ─── Pagination / fetch ──────────────────────────────────────────────── + + def "firstResult(int) delegates and returns this"() { + expect: builder.firstResult(5).is(builder) + } + + def "maxResults(int) delegates and returns this"() { + expect: builder.maxResults(10).is(builder) + } + + // ─── Join / select ───────────────────────────────────────────────────── + + def "join(String) delegates with INNER join and returns this"() { + expect: builder.join("transactions").is(builder) + } + + def "join(String, JoinType) delegates and returns this"() { + expect: builder.join("transactions", JoinType.LEFT).is(builder) + } + + def "select(String) delegates and returns this"() { + expect: builder.select("balance").is(builder) + } + + // ─── Cache / readOnly / lock ─────────────────────────────────────────── + + def "cache(boolean) sets flag and returns this"() { + expect: builder.cache(true).is(builder) + } + + def "readOnly(boolean) sets flag and returns this"() { + expect: builder.readOnly(true).is(builder) + } + + def "lock(boolean) sets flag without throwing"() { + when: builder.lock(true) + then: noExceptionThrown() + } + + // ─── Projection wrappers ─────────────────────────────────────────────── + + def "property(String) adds property projection and returns this"() { + expect: builder.property("firstName").is(builder) + } + + def "avg(String) adds avg projection and returns this"() { + expect: builder.avg("balance").is(builder) + } + + def "distinct(String) adds distinct projection and returns this"() { + expect: builder.distinct("firstName").is(builder) + } + + def "count() adds count projection and returns non-null ProjectionList"() { + expect: builder.count() != null + } + + def "countDistinct(String) adds countDistinct projection and returns this"() { + expect: builder.countDistinct("firstName").is(builder) + } + + def "groupProperty(String) adds groupProperty projection and returns this"() { + expect: builder.groupProperty("lastName").is(builder) + } + + def "min(String) adds min projection and returns this"() { + expect: builder.min("balance").is(builder) + } + + def "max(String) adds max projection and returns this"() { + expect: builder.max("balance").is(builder) + } + + def "sum(String) adds sum projection and returns this"() { + expect: builder.sum("balance").is(builder) + } + + def "rowCount() delegates to count and returns non-null ProjectionList"() { + expect: builder.rowCount() != null + } + + def "id() adds id projection and returns this"() { + expect: builder.id().is(builder) + } + + // ─── State flags ─────────────────────────────────────────────────────── + + def "setUniqueResult and isUniqueResult are symmetric"() { + when: builder.setUniqueResult(true) + then: builder.isUniqueResult() + } + + def "setPaginationEnabledList and isPaginationEnabledList are symmetric"() { + when: builder.setPaginationEnabledList(true) + then: builder.isPaginationEnabledList() + } + + def "setScroll(boolean) sets scroll flag"() { + when: builder.setScroll(true) + then: noExceptionThrown() + } + + def "setCount(boolean) sets count flag"() { + when: builder.setCount(true) + then: builder.isCount() + } + + def "setDistinct(boolean) sets distinct flag"() { + when: builder.setDistinct(true) + then: builder.isDistinct() + } + + def "setDefaultFlushMode and getDefaultFlushMode are symmetric"() { + when: builder.setDefaultFlushMode(2) + then: builder.getDefaultFlushMode() == 2 + } + + def "getTargetClass returns the entity class"() { + expect: builder.targetClass == DirectAccount + } + + def "getHibernateQuery returns non-null"() { + expect: builder.hibernateQuery != null + } + + def "getSessionFactory returns non-null"() { + expect: builder.sessionFactory != null + } + + def "getCriteriaBuilder returns non-null"() { + expect: builder.criteriaBuilder != null + } + + def "setTargetClass updates the target class"() { + when: builder.targetClass = DirectTransaction + then: builder.targetClass == DirectTransaction + } +} + +@Entity +class DirectAccount { + String firstName + String lastName + BigDecimal balance + String branch + Set<DirectTransaction> transactions + + static hasMany = [transactions: DirectTransaction] + static constraints = { branch nullable: true } +} + +@Entity +class DirectTransaction { + BigDecimal amount + static belongsTo = [account: DirectAccount] +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/MappingSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/MappingSpec.groovy index 3e1cbc07b6..91f286da1f 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/MappingSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/MappingSpec.groovy @@ -117,6 +117,91 @@ class MappingSpec extends HibernateGormDatastoreSpec { thrown(MissingMethodException) } + // --- getOrInitializePropertyConfig (protected, same-package access) --- + + void "getOrInitializePropertyConfig creates a new PropertyConfig when none exists"() { + given: + Mapping mapping = new Mapping() + + when: + PropertyConfig pc = mapping.getOrInitializePropertyConfig('age') + + then: + pc != null + mapping.columns['age'].is(pc) + } + + void "getOrInitializePropertyConfig returns existing PropertyConfig when already set"() { + given: + Mapping mapping = new Mapping() + PropertyConfig existing = new PropertyConfig() + mapping.columns['age'] = existing + + when: + PropertyConfig pc = mapping.getOrInitializePropertyConfig('age') + + then: + pc.is(existing) + } + + void "getOrInitializePropertyConfig clones global constraint when present"() { + given: + Mapping mapping = new Mapping() + PropertyConfig global = new PropertyConfig() + global.column('default_col') + mapping.columns['*'] = global + + when: + PropertyConfig pc = mapping.getOrInitializePropertyConfig('someField') + + then: + pc != null + !pc.is(global) // cloned, not the same instance + pc.firstColumnIsColumnCopy // single-column clone sets the flag + } + + // --- cloneGlobalConstraint (protected, same-package access) --- + + void "cloneGlobalConstraint returns a clone with firstColumnIsColumnCopy set for single column"() { + given: + Mapping mapping = new Mapping() + PropertyConfig global = new PropertyConfig() + global.column('shared_col') + mapping.columns['*'] = global + + when: + PropertyConfig cloned = mapping.cloneGlobalConstraint() + + then: + cloned != null + !cloned.is(global) + cloned.firstColumnIsColumnCopy + } + + // --- PropertyConfig.checkHasSingleColumn (protected, same-package access) --- + + void "checkHasSingleColumn does not throw when only one column is configured"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.column('my_col') + + expect: + pc.checkHasSingleColumn() // no exception + } + + void "checkHasSingleColumn throws when multiple columns are configured"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.columns << new ColumnConfig(name: 'col_a') + pc.columns << new ColumnConfig(name: 'col_b') + + when: + pc.checkHasSingleColumn() + + then: + thrown(RuntimeException) + } + } // --- Test Domain Classes --- diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/PropertyConfigSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/PropertyConfigSpec.groovy new file mode 100644 index 0000000000..38687e271f --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/PropertyConfigSpec.groovy @@ -0,0 +1,457 @@ +package org.grails.orm.hibernate.cfg + +import org.hibernate.FetchMode +import spock.lang.Specification + +/** + * Unit spec for {@link PropertyConfig}. + * Placed in the same package to access protected methods directly. + */ +class PropertyConfigSpec extends Specification { + + // ─── column(String) ────────────────────────────────────────────────────── + + void "column(String) adds a new ColumnConfig with the given name"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.column('my_col') + + then: + pc.columns.size() == 1 + pc.columns[0].name == 'my_col' + } + + void "column(String) adds a second ColumnConfig when called twice normally"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.column('col_a') + pc.column('col_b') + + then: + pc.columns.size() == 2 + } + + void "column(String) replaces name in-place when firstColumnIsColumnCopy is true"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.column('original') + pc.firstColumnIsColumnCopy = true + + when: + pc.column('replaced') + + then: + pc.columns.size() == 1 + pc.columns[0].name == 'replaced' + !pc.firstColumnIsColumnCopy + } + + // ─── column(Map) ───────────────────────────────────────────────────────── + + void "column(Map) adds a ColumnConfig with the given name"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.column(name: 'map_col') + + then: + pc.columns.size() == 1 + pc.columns[0].name == 'map_col' + } + + void "column(Map) configures existing column in-place when firstColumnIsColumnCopy is true"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.column('original') + pc.firstColumnIsColumnCopy = true + + when: + pc.column(name: 'updated') + + then: + pc.columns.size() == 1 + pc.columns[0].name == 'updated' + !pc.firstColumnIsColumnCopy + } + + // ─── column(Closure) ───────────────────────────────────────────────────── + + void "column(Closure) adds a ColumnConfig configured by the closure"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.column { name = 'closure_col' } + + then: + pc.columns.size() == 1 + pc.columns[0].name == 'closure_col' + } + + // ─── getColumn / single-column shortcuts ───────────────────────────────── + + void "getColumn returns null when no columns are configured"() { + expect: + new PropertyConfig().column == null + } + + void "getColumn returns the column name when one column is configured"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.column('the_col') + + expect: + pc.column == 'the_col' + } + + void "getColumn throws when multiple columns are configured"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.columns << new ColumnConfig(name: 'a') + pc.columns << new ColumnConfig(name: 'b') + + when: + pc.column + + then: + thrown(RuntimeException) + } + + void "getSqlType returns null when no columns are configured"() { + expect: + new PropertyConfig().sqlType == null + } + + void "getIndexName returns null when no columns are configured"() { + expect: + new PropertyConfig().indexName == null + } + + void "getEnumType returns 'default' when no columns are configured"() { + expect: + new PropertyConfig().enumType == 'default' + } + + void "getLength returns -1 when no columns are configured"() { + expect: + new PropertyConfig().length == -1 + } + + void "getPrecision returns -1 when no columns are configured"() { + expect: + new PropertyConfig().precision == -1 + } + + // ─── setUnique / isUnique ──────────────────────────────────────────────── + + void "setUnique propagates to the single column when one column exists"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.column('u_col') + + when: + pc.setUnique(true) + + then: + pc.columns[0].unique + pc.unique + } + + void "isUnique delegates to super when no columns exist"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.setUnique(true) + + expect: + pc.unique + } + + void "isUnique delegates to super when multiple columns exist"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.columns << new ColumnConfig(name: 'a') + pc.columns << new ColumnConfig(name: 'b') + pc.setUnique(true) + + expect: + pc.unique // falls through to super.isUnique() + } + + // ─── FetchMode ─────────────────────────────────────────────────────────── + + void "setFetch(JOIN) maps to EAGER strategy"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.fetch = FetchMode.JOIN + + then: + pc.fetchMode == FetchMode.JOIN + } + + void "setFetch(SELECT) maps to LAZY strategy"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.fetch = FetchMode.SELECT + + then: + pc.fetchMode == FetchMode.SELECT + } + + void "getFetchMode returns DEFAULT when no strategy is set"() { + expect: + new PropertyConfig().fetchMode == FetchMode.DEFAULT + } + + // ─── cache ─────────────────────────────────────────────────────────────── + + void "cache(Closure) creates and configures a CacheConfig"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.cache { usage = 'read-only' } + + then: + pc.cache != null + pc.cache.usage == 'read-only' + } + + void "cache(Map) creates and configures a CacheConfig"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.cache(usage: 'read-write') + + then: + pc.cache != null + pc.cache.usage == 'read-write' + } + + // ─── joinTable ─────────────────────────────────────────────────────────── + + void "joinTable(String) sets the join table name"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.joinTable('book_authors') + + then: + pc.joinTable.name == 'book_authors' + } + + void "joinTable(Closure) configures the JoinTable"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.joinTable { name = 'jt_table' } + + then: + pc.joinTable.name == 'jt_table' + } + + void "joinTable(Map) sets table name and key column via map"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.joinTable(name: 'book_tag', key: 'book_id', column: 'tag_id') + + then: + pc.joinTable.name == 'book_tag' + pc.joinTable.key?.name == 'book_id' + pc.joinTable.column?.name == 'tag_id' + } + + void "hasJoinKeyMapping returns false when no join table key is set"() { + expect: + !new PropertyConfig().hasJoinKeyMapping() + } + + void "hasJoinKeyMapping returns true when a join table key is set"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.joinTable { key 'author_id' } + + expect: + pc.hasJoinKeyMapping() + } + + // ─── indexColumn ───────────────────────────────────────────────────────── + + void "indexColumn(Closure) creates and configures the index column PropertyConfig"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.indexColumn { column('idx_col') } + + then: + pc.indexColumn != null + pc.indexColumn.column == 'idx_col' + } + + // ─── scale ─────────────────────────────────────────────────────────────── + + void "setScale sets scale on the existing column"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.column('s_col') + + when: + pc.scale = 4 + + then: + pc.scale == 4 + pc.columns[0].scale == 4 + } + + void "setScale delegates to super when no columns are configured"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.scale = 3 + + then: + pc.scale == 3 + } + + // ─── checkHasSingleColumn (protected, same-package access) ─────────────── + + void "checkHasSingleColumn passes silently for zero columns"() { + expect: + new PropertyConfig().checkHasSingleColumn() + } + + void "checkHasSingleColumn passes silently for exactly one column"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.column('one') + + expect: + pc.checkHasSingleColumn() + } + + void "checkHasSingleColumn throws for two or more columns"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.columns << new ColumnConfig(name: 'x') + pc.columns << new ColumnConfig(name: 'y') + + when: + pc.checkHasSingleColumn() + + then: + thrown(RuntimeException) + } + + // ─── clone ─────────────────────────────────────────────────────────────── + + void "clone produces an independent deep copy of columns"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.column('orig') + + when: + PropertyConfig cloned = pc.clone() + cloned.columns[0].name = 'changed' + + then: + pc.columns[0].name == 'orig' + } + + void "clone copies cache independently"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.cache { usage = 'read-only' } + + when: + PropertyConfig cloned = pc.clone() + cloned.cache.usage = 'read-write' + + then: + pc.cache.usage == 'read-only' + } + + void "clone copies indexColumn independently"() { + given: + PropertyConfig pc = new PropertyConfig() + pc.indexColumn { column('idx') } + + when: + PropertyConfig cloned = pc.clone() + + then: + cloned.indexColumn != null + !cloned.indexColumn.is(pc.indexColumn) + } + + // ─── static factories ───────────────────────────────────────────────────── + + void "configureNew(Closure) creates a PropertyConfig configured by the closure"() { + when: + PropertyConfig pc = PropertyConfig.configureNew { type = 'string' } + + then: + pc != null + pc.type == 'string' + } + + void "configureNew(Map) creates a PropertyConfig from a map"() { + when: + PropertyConfig pc = PropertyConfig.configureNew([column: 'map_col']) + + then: + pc != null + pc.column == 'map_col' + } + + void "configureExisting(Map) updates an existing PropertyConfig"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + PropertyConfig result = PropertyConfig.configureExisting(pc, [column: 'updated_col']) + + then: + result.is(pc) + result.column == 'updated_col' + } + + void "configureExisting(Closure) delegates the closure to the PropertyConfig"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + PropertyConfig result = PropertyConfig.configureExisting(pc) { type = 'integer' } + + then: + result.is(pc) + result.type == 'integer' + } + + // ─── deprecated updateable ─────────────────────────────────────────────── + + void "setUpdatable and getUpdateable are symmetric"() { + given: + PropertyConfig pc = new PropertyConfig() + + when: + pc.updatable = false + + then: + !pc.updateable + } +}
