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 f49f35a63487f09e6be5643e684fc44b1e9ab891 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Tue Feb 24 05:49:07 2026 -0600 HibernateMappingBuilder tested --- .../hibernate/HibernateMappingBuilder.groovy | 209 ++++++----- .../mapping/HibernateMappingBuilderSpec.groovy | 402 +++++++++++++++++++++ 2 files changed, 521 insertions(+), 90 deletions(-) diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy index cb991a1439..f037f480a8 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingBuilder.groovy @@ -100,7 +100,7 @@ class HibernateMappingBuilder implements MappingConfigurationBuilder<Mapping, Pr } } - void hibernateCustomUserType(Map args) { + void hibernateCustomUserType(Map<String, Object> args) { if (args.type && (args['class'] instanceof Class)) { mapping.userTypes[(Class)args['class']] = args.type.toString() } @@ -232,32 +232,34 @@ class HibernateMappingBuilder implements MappingConfigurationBuilder<Mapping, Pr mapping.cache = new CacheConfig(enabled: shouldCache) } - void id(Map args) { + void id(Map<String, Object> args) { if (args.composite) { mapping.identity = new CompositeIdentity(propertyNames: (String[]) args.composite) if (args.compositeClass) { (mapping.identity as CompositeIdentity).compositeClass = (Class) args.compositeClass } } else { - if (args?.generator) { - ((Identity) mapping.identity).generator = args.remove('generator').toString() + Object generatorVal = args.remove('generator') + if (generatorVal != null) { + ((Identity) mapping.identity).generator = generatorVal.toString() } - if (args?.name) { - ((Identity) mapping.identity).name = args.remove('name').toString() + Object nameVal = args.remove('name') + if (nameVal != null) { + ((Identity) mapping.identity).name = nameVal.toString() } - if (args?.params) { - Map params = (Map) args.remove('params') + Object paramsVal = args.remove('params') + if (paramsVal instanceof Map) { Map<String, String> stringParams = [:] - params.each { k, v -> stringParams[k.toString()] = v?.toString() } + ((Map<Object, Object>) paramsVal).each { k, v -> stringParams[k.toString()] = v?.toString() } ((Identity) mapping.identity).params = stringParams } } - if (args?.natural) { - Object naturalArgs = args.remove('natural') - Object propertyNames = naturalArgs instanceof Map ? ((Map) naturalArgs).remove('properties') : naturalArgs + Object naturalVal = args.remove('natural') + if (naturalVal != null) { + Object propertyNames = naturalVal instanceof Map ? ((Map<String, Object>) naturalVal).remove('properties') : naturalVal if (propertyNames) { NaturalId ni = new NaturalId() - ni.mutable = (naturalArgs instanceof Map) && ((Map) naturalArgs).mutable ?: false + ni.mutable = (naturalVal instanceof Map) && ((Map<String, Object>) naturalVal).mutable ?: false if (propertyNames instanceof List) { ni.propertyNames = (List<String>) propertyNames } else { @@ -281,7 +283,7 @@ class HibernateMappingBuilder implements MappingConfigurationBuilder<Mapping, Pr /** * Internal logic for building property configurations. */ - protected void handlePropertyInternal(String name, Map namedArgs, Closure subClosure) { + protected void handlePropertyInternal(String name, Map<String, Object> namedArgs, Closure subClosure) { PropertyConfig newConfig = new PropertyConfig() if (defaultConstraints != null && namedArgs.containsKey('shared')) { PropertyConfig sharedConstraints = mapping.columns.get(namedArgs.shared.toString()) @@ -296,39 +298,46 @@ class HibernateMappingBuilder implements MappingConfigurationBuilder<Mapping, Pr } PropertyConfig property = mapping.columns[name] ?: newConfig - property.name = namedArgs.name?.toString() ?: property.name - property.generator = namedArgs.generator?.toString() ?: property.generator - property.formula = namedArgs.formula?.toString() ?: property.formula - property.accessType = namedArgs.accessType instanceof AccessType ? (AccessType)namedArgs.accessType : property.accessType - property.type = namedArgs.type ?: property.type - property.setLazy(namedArgs.lazy instanceof Boolean ? (Boolean)namedArgs.lazy : property.getLazy()) - property.insertable = namedArgs.insertable instanceof Boolean ? (Boolean)namedArgs.insertable : property.insertable - property.updatable = (namedArgs.updateable != null ? namedArgs.updateable : namedArgs.updatable) instanceof Boolean ? (Boolean)(namedArgs.updateable ?: namedArgs.updatable) : property.updatable - property.cascade = namedArgs.cascade?.toString() ?: property.cascade - property.cascadeValidate = namedArgs.cascadeValidate instanceof Boolean ? (Boolean)namedArgs.cascadeValidate : property.cascadeValidate - property.sort = namedArgs.sort?.toString() ?: property.sort - property.order = namedArgs.order?.toString() ?: property.order - property.batchSize = namedArgs.batchSize instanceof Integer ? (Integer)namedArgs.batchSize : property.batchSize - property.ignoreNotFound = namedArgs.ignoreNotFound instanceof Boolean ? (Boolean)namedArgs.ignoreNotFound : property.ignoreNotFound + Object nameVal = namedArgs.name + if (nameVal != null) property.name = nameVal.toString() + Object genVal = namedArgs.generator + if (genVal != null) property.generator = genVal.toString() + Object formulaVal = namedArgs.formula + if (formulaVal != null) property.formula = formulaVal.toString() + if (namedArgs.accessType instanceof AccessType) property.accessType = (AccessType) namedArgs.accessType + Object typeVal = namedArgs.type + if (typeVal != null) property.type = typeVal + if (namedArgs.lazy instanceof Boolean) property.setLazy((Boolean) namedArgs.lazy) + if (namedArgs.insertable instanceof Boolean) property.insertable = (Boolean) namedArgs.insertable + Object updateableVal = namedArgs.updateable != null ? namedArgs.updateable : namedArgs.updatable + if (updateableVal instanceof Boolean) property.updatable = (Boolean) updateableVal + Object cascadeVal = namedArgs.cascade + if (cascadeVal != null) property.cascade = cascadeVal.toString() + if (namedArgs.cascadeValidate instanceof Boolean) property.cascadeValidate = (Boolean) namedArgs.cascadeValidate + Object sortVal = namedArgs.sort + if (sortVal != null) property.sort = sortVal.toString() + Object orderVal = namedArgs.order + if (orderVal != null) property.order = orderVal.toString() + if (namedArgs.batchSize instanceof Integer) property.batchSize = (Integer) namedArgs.batchSize + if (namedArgs.ignoreNotFound instanceof Boolean) property.ignoreNotFound = (Boolean) namedArgs.ignoreNotFound if (namedArgs.params instanceof Map) { Properties typeProps = new Properties() - ((Map<Object, Object>)namedArgs.params).each { k, v -> typeProps.put(k, v) } + ((Map<Object, Object>) namedArgs.params).each { Object k, Object v -> typeProps.put(k, v) } property.typeParams = typeProps } - if (namedArgs.unique instanceof Boolean) property.setUnique((boolean)(Boolean)namedArgs.unique) - else if (namedArgs.unique instanceof String) property.setUnique((String)namedArgs.unique) - else if (namedArgs.unique instanceof List) property.setUnique((List<String>)namedArgs.unique) - property.nullable = namedArgs.nullable instanceof Boolean ? (Boolean)namedArgs.nullable : property.nullable - property.maxSize = namedArgs.maxSize instanceof Number ? (Number)namedArgs.maxSize : property.maxSize - property.minSize = namedArgs.minSize instanceof Number ? (Number)namedArgs.minSize : property.minSize - + Object uniqueVal = namedArgs.unique + if (uniqueVal instanceof Boolean) property.setUnique((boolean)(Boolean) uniqueVal) + else if (uniqueVal instanceof String) property.setUnique((String) uniqueVal) + else if (uniqueVal instanceof List) property.setUnique((List<String>) uniqueVal) + if (namedArgs.nullable instanceof Boolean) property.nullable = (Boolean) namedArgs.nullable + if (namedArgs.maxSize instanceof Number) property.maxSize = (Number) namedArgs.maxSize + if (namedArgs.minSize instanceof Number) property.minSize = (Number) namedArgs.minSize if (namedArgs.size instanceof IntRange) property.size = (IntRange) namedArgs.size - property.max = namedArgs.max instanceof Comparable ? (Comparable) namedArgs.max : property.max - property.min = namedArgs.min instanceof Comparable ? (Comparable) namedArgs.min : property.min - property.range = namedArgs.range instanceof ObjectRange ? (ObjectRange) namedArgs.range : null - property.inList = namedArgs.inList instanceof List ? (List) namedArgs.inList : property.inList - + if (namedArgs.max instanceof Comparable) property.max = (Comparable) namedArgs.max + if (namedArgs.min instanceof Comparable) property.min = (Comparable) namedArgs.min + if (namedArgs.range instanceof ObjectRange) property.range = (ObjectRange) namedArgs.range + if (namedArgs.inList instanceof List) property.inList = (List) namedArgs.inList if (namedArgs.scale instanceof Integer) property.scale = (Integer) namedArgs.scale if (namedArgs.fetch) { @@ -346,34 +355,46 @@ class HibernateMappingBuilder implements MappingConfigurationBuilder<Mapping, Pr ColumnConfig cc = property.columns ? property.columns[0] : new ColumnConfig() if (!property.columns) property.columns << cc - if (namedArgs["column"]) cc.name = namedArgs["column"].toString() - if (namedArgs["sqlType"]) cc.sqlType = namedArgs["sqlType"].toString() - if (namedArgs["enumType"]) cc.enumType = namedArgs["enumType"].toString() - if (namedArgs["index"]) cc.index = namedArgs["index"].toString() - if (namedArgs["unique"]) cc.unique = namedArgs["unique"] - if (namedArgs["read"]) cc.read = namedArgs["read"].toString() - if (namedArgs["write"]) cc.write = namedArgs["write"].toString() - if (namedArgs.defaultValue) cc.defaultValue = namedArgs.defaultValue.toString() - if (namedArgs.comment) cc.comment = namedArgs.comment.toString() - if (namedArgs["length"] instanceof Integer) cc.length = (Integer)namedArgs["length"] - if (namedArgs["precision"] instanceof Integer) cc.precision = (Integer)namedArgs["precision"] - if (namedArgs["scale"] instanceof Integer) cc.scale = (Integer)namedArgs["scale"] - - if (namedArgs.joinTable instanceof String) { - property.joinTable((String)namedArgs.joinTable) - } else if (namedArgs.joinTable instanceof Map) { - property.joinTable((Map)namedArgs.joinTable) + Object colVal = namedArgs["column"] + if (colVal) cc.name = colVal.toString() + Object sqlTypeVal = namedArgs["sqlType"] + if (sqlTypeVal) cc.sqlType = sqlTypeVal.toString() + Object enumTypeVal = namedArgs["enumType"] + if (enumTypeVal) cc.enumType = enumTypeVal.toString() + Object indexVal = namedArgs["index"] + if (indexVal) cc.index = indexVal.toString() + Object ccUniqueVal = namedArgs["unique"] + if (ccUniqueVal) cc.unique = ccUniqueVal instanceof Boolean ? (Boolean) ccUniqueVal : ccUniqueVal + Object readVal = namedArgs["read"] + if (readVal) cc.read = readVal.toString() + Object writeVal = namedArgs["write"] + if (writeVal) cc.write = writeVal.toString() + Object defaultVal = namedArgs.defaultValue + if (defaultVal) cc.defaultValue = defaultVal.toString() + Object commentVal = namedArgs.comment + if (commentVal) cc.comment = commentVal.toString() + if (namedArgs["length"] instanceof Integer) cc.length = (int) (Integer) namedArgs["length"] + if (namedArgs["precision"] instanceof Integer) cc.precision = (int) (Integer) namedArgs["precision"] + if (namedArgs["scale"] instanceof Integer) cc.scale = (int) (Integer) namedArgs["scale"] + + Object joinTableVal = namedArgs.joinTable + if (joinTableVal instanceof String) { + property.joinTable((String) joinTableVal) + } else if (joinTableVal instanceof Map) { + property.joinTable((Map) joinTableVal) } if (namedArgs.indexColumn instanceof Map) { - Map icArgs = (Map)namedArgs.indexColumn + Map<String, Object> icArgs = (Map<String, Object>) namedArgs.indexColumn PropertyConfig ic = new PropertyConfig() ColumnConfig icc = new ColumnConfig() - if (icArgs.name) icc.name = icArgs.name.toString() - if (icArgs.type) icc.sqlType = icArgs.type.toString() - if (icArgs.length instanceof Integer) icc.length = (Integer)icArgs.length + Object icName = icArgs.name + if (icName) icc.name = icName.toString() + Object icType = icArgs.type + if (icType) icc.sqlType = icType.toString() + if (icArgs.length instanceof Integer) icc.length = (int) (Integer) icArgs.length ic.columns << icc - ic.type = icArgs.type + ic.type = icType property.indexColumn = ic } } @@ -381,15 +402,18 @@ class HibernateMappingBuilder implements MappingConfigurationBuilder<Mapping, Pr // Cache association handling if (namedArgs.cache != null) { CacheConfig cc = new CacheConfig() - if (namedArgs.cache instanceof String && CacheConfig.USAGE_OPTIONS.contains(namedArgs.cache)) { - cc.usage = namedArgs.cache.toString() + Object cacheVal = namedArgs.cache + if (cacheVal instanceof String && CacheConfig.USAGE_OPTIONS.contains(cacheVal)) { + cc.usage = (String) cacheVal property.cache = cc - } else if (namedArgs.cache == true) { + } else if (cacheVal == true) { property.cache = cc - } else if (namedArgs.cache instanceof Map) { - Map cacheArgs = (Map) namedArgs.cache - cc.usage = cacheArgs.usage?.toString() - cc.include = cacheArgs.include?.toString() + } else if (cacheVal instanceof Map) { + Map<String, Object> cacheArgs = (Map<String, Object>) cacheVal + Object cacheUsage = cacheArgs.usage + if (cacheUsage != null) cc.usage = cacheUsage.toString() + Object cacheInclude = cacheArgs.include + if (cacheInclude != null) cc.include = cacheInclude.toString() property.cache = cc } } @@ -400,11 +424,13 @@ class HibernateMappingBuilder implements MappingConfigurationBuilder<Mapping, Pr void columns(@DelegatesTo(value = Object, strategy = Closure.DELEGATE_ONLY) Closure callable) { callable.resolveStrategy = Closure.DELEGATE_ONLY callable.delegate = new Object() { - def invokeMethod(String methodName, Object args) { + Object invokeMethod(String methodName, Object args) { Object[] argsArray = (Object[]) args - Map namedArgs = (argsArray.length > 0 && argsArray[0] instanceof Map) ? (Map)argsArray[0] : [:] - Closure sub = (argsArray.length > 0 && argsArray[argsArray.length - 1] instanceof Closure) ? (Closure)argsArray[argsArray.length - 1] : null + int argc = argsArray.length + Map<String, Object> namedArgs = (argc > 0 && argsArray[0] instanceof Map) ? (Map<String, Object>) argsArray[0] : [:] + Closure sub = (argc > 0 && argsArray[argc - 1] instanceof Closure) ? (Closure) argsArray[argc - 1] : null handlePropertyInternal(methodName, namedArgs, sub) + return null } } callable.call() @@ -422,28 +448,31 @@ class HibernateMappingBuilder implements MappingConfigurationBuilder<Mapping, Pr mapping.comment = comment } - def methodMissing(String name, Object args) { + void methodMissing(String name, Object args) { if (methodMissingIncludes != null && !methodMissingIncludes.contains(name)) return if (methodMissingExcludes.contains(name)) return Object[] argsArray = (Object[]) args - boolean hasArgs = argsArray.length > 0 - if (name == 'user-type' && hasArgs && argsArray[0] instanceof Map) { - hibernateCustomUserType((Map) argsArray[0]) - } else if (name == 'importFrom' && hasArgs && argsArray[0] instanceof Class) { + int argc = argsArray.length + boolean hasArgs = argc > 0 + Object firstArg = hasArgs ? argsArray[0] : null + Object lastArg = argc > 0 ? argsArray[argc - 1] : null + + if (name == 'user-type' && hasArgs && firstArg instanceof Map) { + hibernateCustomUserType((Map<String, Object>) firstArg) + } else if (name == 'importFrom' && hasArgs && firstArg instanceof Class) { List<Closure> constraintsToImport = ClassPropertyFetcher.getStaticPropertyValuesFromInheritanceHierarchy( - (Class) argsArray[0], GormProperties.CONSTRAINTS, Closure) + (Class) firstArg, GormProperties.CONSTRAINTS, Closure) if (constraintsToImport) { - List<String> originalIncludes = this.methodMissingIncludes - List<String> originalExcludes = this.methodMissingExcludes + List<String> originalIncludes = methodMissingIncludes + List<String> originalExcludes = methodMissingExcludes try { - Object lastArg = argsArray[argsArray.length - 1] if (lastArg instanceof Map) { - Map argMap = (Map) lastArg + Map<String, Object> argMap = (Map<String, Object>) lastArg Object includes = argMap.get(INCLUDE_PARAM) Object excludes = argMap.get(EXCLUDE_PARAM) - if (includes instanceof List) this.methodMissingIncludes = (List<String>) includes - if (excludes instanceof List) this.methodMissingExcludes = (List<String>) excludes + if (includes instanceof List) methodMissingIncludes = (List<String>) includes + if (excludes instanceof List) methodMissingExcludes = (List<String>) excludes } for (Closure callable in constraintsToImport) { callable.delegate = this @@ -451,13 +480,13 @@ class HibernateMappingBuilder implements MappingConfigurationBuilder<Mapping, Pr callable.call() } } finally { - this.methodMissingIncludes = originalIncludes - this.methodMissingExcludes = originalExcludes + methodMissingIncludes = originalIncludes + methodMissingExcludes = originalExcludes } } - } else if (hasArgs && (argsArray[0] instanceof Map || argsArray[0] instanceof Closure)) { - Map namedArgs = argsArray[0] instanceof Map ? (Map)argsArray[0] : [:] - Closure sub = argsArray[argsArray.length - 1] instanceof Closure ? (Closure)argsArray[argsArray.length - 1] : null + } else if (hasArgs && (firstArg instanceof Map || firstArg instanceof Closure)) { + Map<String, Object> namedArgs = firstArg instanceof Map ? (Map<String, Object>) firstArg : [:] + Closure sub = lastArg instanceof Closure ? (Closure) lastArg : null handlePropertyInternal(name, namedArgs, sub) } } diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderSpec.groovy new file mode 100644 index 0000000000..e72f31557a --- /dev/null +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/hibernate/mapping/HibernateMappingBuilderSpec.groovy @@ -0,0 +1,402 @@ +/* + * 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 grails.gorm.hibernate.mapping + +import jakarta.persistence.AccessType +import org.grails.orm.hibernate.cfg.CacheConfig +import org.grails.orm.hibernate.cfg.Mapping +import org.grails.orm.hibernate.cfg.PropertyConfig +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateMappingBuilder +import org.hibernate.FetchMode +import spock.lang.Specification + +/** + * Covers branches of {@link HibernateMappingBuilder} not exercised by + * {@link HibernateMappingBuilderTests}. + */ +class HibernateMappingBuilderSpec extends Specification { + + private HibernateMappingBuilder builder(String name = 'Foo') { + new HibernateMappingBuilder(new Mapping(), name) + } + + private Mapping evaluate(@DelegatesTo(HibernateMappingBuilder) Closure cl) { + builder().evaluate(cl) + } + + // ------------------------------------------------------------------------- + // autowire / tenantId + // ------------------------------------------------------------------------- + + def "autowire stores the value on the mapping"() { + expect: + evaluate { autowire true }.autowire + !evaluate { autowire false }.autowire + } + + def "tenantId stores the property name"() { + expect: + evaluate { tenantId 'tenantId' }.getPropertyConfig('tenantId') != null + } + + // ------------------------------------------------------------------------- + // cache(String, Map) + // ------------------------------------------------------------------------- + + def "cache(String, Map) sets usage and include"() { + when: + Mapping m = evaluate { cache 'read-write', [include: 'all'] } + + then: + m.cache.usage == 'read-write' + m.cache.include == 'all' + } + + def "cache(String) with invalid usage still creates a CacheConfig with the default usage"() { + when: + Mapping m = evaluate { cache 'INVALID_USAGE' } + + then: + m.cache != null + m.cache.usage == 'read-write' // default preserved; INVALID_USAGE rejected + } + + def "cache(Map) with invalid include still creates a CacheConfig with the default include"() { + when: + Mapping m = evaluate { cache usage: 'read-only', include: 'INVALID_INCLUDE' } + + then: + m.cache != null + m.cache.usage == 'read-only' + m.cache.include == 'all' // default preserved; INVALID_INCLUDE rejected + } + + // ------------------------------------------------------------------------- + // hibernateCustomUserType + // ------------------------------------------------------------------------- + + def "hibernateCustomUserType registers a user type when args are valid"() { + when: + Mapping m = evaluate { 'user-type'(type: 'myType', 'class': String) } + + then: + m.userTypes[String] == 'myType' + } + + def "hibernateCustomUserType is a no-op when class is not a Class"() { + when: + Mapping m = evaluate { 'user-type'(type: 'myType', 'class': 'notAClass') } + + then: + m.userTypes.isEmpty() + } + + def "hibernateCustomUserType is a no-op when type is absent"() { + when: + Mapping m = evaluate { 'user-type'('class': String) } + + then: + m.userTypes.isEmpty() + } + + // ------------------------------------------------------------------------- + // includes() null-safety + // ------------------------------------------------------------------------- + + def "includes() with null closure does not throw"() { + when: + evaluate { includes(null) } + + then: + noExceptionThrown() + } + + // ------------------------------------------------------------------------- + // sort / order null guards + // ------------------------------------------------------------------------- + + def "sort(null) is a no-op"() { + when: + Mapping m = evaluate { sort((String) null) } + + then: + m.sort.name == null + } + + def "order with invalid direction is a no-op"() { + when: + Mapping m = evaluate { order 'invalid' } + + then: + m.sort.direction == null + } + + def "batchSize(null) is a no-op and leaves batchSize as null"() { + when: + Mapping m = evaluate { batchSize null } + + then: + m.batchSize == null + } + + // ------------------------------------------------------------------------- + // evaluate with context argument + // ------------------------------------------------------------------------- + + def "evaluate passes context to the closure"() { + given: + def b = builder() + Object captured = null + + when: + b.evaluate({ Object ctx -> captured = ctx }, 'myContext') + + then: + captured == 'myContext' + } + + // ------------------------------------------------------------------------- + // property(Map, String) — the 2-arg typed method + // ------------------------------------------------------------------------- + + def "property(Map, String) registers the property config"() { + when: + Mapping m = evaluate { property([nullable: true, column: 'my_col'], 'myProp') } + + then: + m.getPropertyConfig('myProp') != null + m.getPropertyConfig('myProp').nullable + m.getPropertyConfig('myProp').column == 'my_col' + } + + // ------------------------------------------------------------------------- + // handlePropertyInternal — uncovered branches + // ------------------------------------------------------------------------- + + def "property with accessType stores it"() { + when: + Mapping m = evaluate { myProp accessType: AccessType.FIELD } + + then: + m.getPropertyConfig('myProp').accessType == AccessType.FIELD + } + + def "property updateable alias is honoured"() { + when: + Mapping m = evaluate { myProp updateable: false } + + then: + !m.getPropertyConfig('myProp').updatable + } + + def "property params map is converted to Properties"() { + when: + Mapping m = evaluate { myProp params: [scale: '4', precision: '10'] } + + then: + m.getPropertyConfig('myProp').typeParams instanceof Properties + m.getPropertyConfig('myProp').typeParams['scale'] == '4' + } + + def "property unique as String creates a named unique constraint"() { + when: + Mapping m = evaluate { myProp unique: 'myGroup' } + + then: + m.getPropertyConfig('myProp').isUniqueWithinGroup() + } + + def "property unique as List creates a composite unique constraint"() { + when: + Mapping m = evaluate { myProp unique: ['a', 'b'] } + + then: + m.getPropertyConfig('myProp').isUniqueWithinGroup() + } + + def "property size as IntRange stores minSize and maxSize"() { + when: + Mapping m = evaluate { myProp size: (1..10) } + + then: + m.getPropertyConfig('myProp').minSize == 1 + m.getPropertyConfig('myProp').maxSize == 10 + } + + def "property range as ObjectRange stores min and max"() { + when: + // ObjectRange is used for non-primitive ranges; 'a'..'e' produces one + Mapping m = evaluate { myProp range: ('a'..'e') } + + then: + m.getPropertyConfig('myProp').min == 'a' + m.getPropertyConfig('myProp').max == 'e' + } + + def "property inList stores the list"() { + when: + Mapping m = evaluate { myProp inList: ['A', 'B', 'C'] } + + then: + m.getPropertyConfig('myProp').inList == ['A', 'B', 'C'] + } + + def "property fetch with join string sets JOIN fetch mode"() { + when: + Mapping m = evaluate { myProp fetch: 'join' } + + then: + m.getPropertyConfig('myProp').getFetchMode() == FetchMode.JOIN + } + + def "property fetch with unknown string falls back to SELECT"() { + when: + Mapping m = evaluate { myProp fetch: 'eager' } + + then: + m.getPropertyConfig('myProp').getFetchMode() == FetchMode.SELECT + } + + def "property with sub-closure delegates to PropertyDefinitionDelegate"() { + when: + Mapping m = evaluate { + myProp { + column name: 'col_one' + } + } + + then: + m.getPropertyConfig('myProp').columns[0].name == 'col_one' + } + + def "property indexColumn map is applied"() { + when: + Mapping m = evaluate { + myProp indexColumn: [name: 'idx', type: 'integer', length: 10] + } + + then: + PropertyConfig ic = m.getPropertyConfig('myProp').indexColumn + ic != null + ic.columns[0].name == 'idx' + ic.columns[0].length == 10 + } + + def "property cache as boolean true enables caching"() { + when: + Mapping m = evaluate { myProp cache: true } + + then: + m.getPropertyConfig('myProp').cache instanceof CacheConfig + } + + def "property cache as boolean false is a no-op"() { + when: + Mapping m = evaluate { myProp cache: false } + + then: + m.getPropertyConfig('myProp').cache == null + } + + def "property cache as Map sets usage and include"() { + when: + Mapping m = evaluate { myProp cache: [usage: 'read-only', include: 'all'] } + + then: + m.getPropertyConfig('myProp').cache.usage == 'read-only' + m.getPropertyConfig('myProp').cache.include == 'all' + } + + def "property column sqlType is set"() { + when: + Mapping m = evaluate { myProp sqlType: 'text' } + + then: + m.getPropertyConfig('myProp').sqlType == 'text' + } + + def "property column read/write formulas are set"() { + when: + Mapping m = evaluate { myProp read: 'lower(col)', write: 'upper(?)' } + + then: + m.getPropertyConfig('myProp').columns[0].read == 'lower(col)' + m.getPropertyConfig('myProp').columns[0].write == 'upper(?)' + } + + def "property column defaultValue and comment are set"() { + when: + Mapping m = evaluate { myProp defaultValue: 'N/A', comment: 'a test column' } + + then: + m.getPropertyConfig('myProp').columns[0].defaultValue == 'N/A' + m.getPropertyConfig('myProp').columns[0].comment == 'a test column' + } + + // ------------------------------------------------------------------------- + // methodMissing — filtering branches + // ------------------------------------------------------------------------- + + def "methodMissing skips properties in methodMissingExcludes via importFrom"() { + given: "a class whose constraints closure maps 'foos' and 'bars'" + def cl = new GroovyClassLoader().parseClass(''' + class ImportSource { + static constraints = { + foos(lazy: false) + bars(lazy: true) + } + } + ''') + + when: "importFrom with exclude:[bars]" + Mapping m = evaluate { importFrom(cl, [exclude: ['bars']]) } + + then: "foos is mapped, bars is not" + m.getPropertyConfig('foos') != null + m.getPropertyConfig('bars') == null + } + + def "methodMissing skips properties not in methodMissingIncludes via importFrom"() { + given: + def cl = new GroovyClassLoader().parseClass(''' + class ImportSource2 { + static constraints = { + foos(lazy: false) + bars(lazy: true) + } + } + ''') + + when: "importFrom with include:[bars]" + Mapping m = evaluate { importFrom(cl, [include: ['bars']]) } + + then: "bars is mapped, foos is not" + m.getPropertyConfig('bars') != null + m.getPropertyConfig('foos') == null + } + + def "methodMissing with no matching args signature is silently ignored"() { + when: "call with a plain String arg (no Map, no Closure)" + Mapping m = evaluate { myProp 'justAString' } + + then: + noExceptionThrown() + m.getPropertyConfig('myProp') == null + } +}
