diff --git a/project/gradle.properties b/project/gradle.properties index 215e32a508..b8c8e66e1c 100644 --- a/project/gradle.properties +++ b/project/gradle.properties @@ -1,2 +1,2 @@ group=org.babyfish.jimmer -version=0.9.4 +version=0.9.5 diff --git a/project/jimmer-apt/src/main/java/org/babyfish/jimmer/apt/dto/DtoGenerator.java b/project/jimmer-apt/src/main/java/org/babyfish/jimmer/apt/dto/DtoGenerator.java index 3e1225c150..f45ab074d5 100644 --- a/project/jimmer-apt/src/main/java/org/babyfish/jimmer/apt/dto/DtoGenerator.java +++ b/project/jimmer-apt/src/main/java/org/babyfish/jimmer/apt/dto/DtoGenerator.java @@ -1004,6 +1004,10 @@ private void addSpecificationConverter(DtoProp pro ); } break; + case "null": + case "notNull": + baseTypeName = TypeName.BOOLEAN; + break; case "valueIn": case "valueNotIn": baseTypeName = ParameterizedTypeName.get( @@ -1027,10 +1031,11 @@ private void addSpecificationConverter(DtoProp pro baseTypeName = baseProp.getTypeName(); } baseTypeName = baseTypeName.box(); + TypeName dtoPropTypeName = getPropTypeName(prop); MethodSpec.Builder builder = MethodSpec .methodBuilder(StringUtil.identifier("__convert", prop.getName())) .addModifiers(Modifier.PRIVATE) - .addParameter(getPropTypeName(prop), "value") + .addParameter(dtoPropTypeName, "value") .returns(baseTypeName); CodeBlock.Builder cb = CodeBlock.builder(); cb.beginControlFlow("if ($L == null)", prop.getName()); @@ -1524,12 +1529,15 @@ private boolean isSpecificationConverterRequired(DtoProp prop) { + String funcName = prop.getFuncName(); + if ("null".equals(funcName) || "notNull".equals(funcName)) { + return null; + } ImmutableProp baseProp = prop.toTailProp().getBaseProp(); ConverterMetadata metadata = baseProp.getConverterMetadata(); if (metadata != null) { return metadata; } - String funcName = prop.getFuncName(); if ("id".equals(funcName)) { metadata = baseProp.getTargetType().getIdProp().getConverterMetadata(); if (metadata != null && baseProp.isList() && !dtoType.getModifiers().contains(DtoModifier.SPECIFICATION)) { diff --git a/project/jimmer-dto-compiler/src/main/java/org/babyfish/jimmer/dto/compiler/DtoPropBuilder.java b/project/jimmer-dto-compiler/src/main/java/org/babyfish/jimmer/dto/compiler/DtoPropBuilder.java index 4336e6e0a5..2d7aae7ccd 100644 --- a/project/jimmer-dto-compiler/src/main/java/org/babyfish/jimmer/dto/compiler/DtoPropBuilder.java +++ b/project/jimmer-dto-compiler/src/main/java/org/babyfish/jimmer/dto/compiler/DtoPropBuilder.java @@ -323,6 +323,16 @@ class DtoPropBuilder implements DtoPropI } break; case "null": + if (!baseProp.isNullable()) { + throw ctx.exception( + prop.func.getLine(), + prop.func.getCharPositionInLine(), + "Cannot call the function \"" + funcName + "\" because the current prop \"" + + baseProp + + "\" is non-null" + ); + } + // Don't break case "notNull": if (baseProp.isList() && baseProp.isAssociation(true)) { throw ctx.exception( diff --git a/project/jimmer-dto-compiler/src/main/java/org/babyfish/jimmer/dto/compiler/DtoTypeBuilder.java b/project/jimmer-dto-compiler/src/main/java/org/babyfish/jimmer/dto/compiler/DtoTypeBuilder.java index fa454a93d9..47d1ccf094 100644 --- a/project/jimmer-dto-compiler/src/main/java/org/babyfish/jimmer/dto/compiler/DtoTypeBuilder.java +++ b/project/jimmer-dto-compiler/src/main/java/org/babyfish/jimmer/dto/compiler/DtoTypeBuilder.java @@ -328,8 +328,8 @@ private void handlePositiveProp0(DtoPropBuilder propBuilder, P baseProp) { String oldFuncName = builders.get(0).getFuncName(); String newFuncName = propBuilder.getFuncName(); if (!Objects.equals(oldFuncName, newFuncName) && - Constants.QBE_FUNC_NAMES.contains(oldFuncName) && - (Constants.QBE_FUNC_NAMES.contains(newFuncName))) { + ("flat".equals(oldFuncName) || Constants.QBE_FUNC_NAMES.contains(oldFuncName)) && + ("flat".equals(newFuncName) || Constants.QBE_FUNC_NAMES.contains(newFuncName))) { valid = true; } } diff --git a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/expression/impl/NullityExpression.kt b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/expression/impl/NullityExpression.kt index 99d31b2f1f..bdc3fcd325 100644 --- a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/expression/impl/NullityExpression.kt +++ b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/expression/impl/NullityExpression.kt @@ -4,7 +4,7 @@ import org.babyfish.jimmer.sql.ast.impl.Ast import org.babyfish.jimmer.sql.ast.impl.AstContext import org.babyfish.jimmer.sql.ast.impl.AstVisitor import org.babyfish.jimmer.sql.ast.impl.render.AbstractSqlBuilder -import org.babyfish.jimmer.sql.ast.impl.table.JoinUtils +import org.babyfish.jimmer.sql.ast.impl.table.IsNullUtils import org.babyfish.jimmer.sql.ast.impl.table.TableImplementor import org.babyfish.jimmer.sql.ast.table.spi.PropExpressionImplementor import org.babyfish.jimmer.sql.kt.ast.expression.KExpression @@ -59,22 +59,8 @@ internal class IsNullPredicate( ) : NullityPredicate(expression) { init { - if (expression is PropExpressionImplementor<*>) { - if (!expression.prop.isNullable && !JoinUtils.hasLeftJoin(expression.table)) { - throw IllegalArgumentException( - "Unable to instantiate `is null` predicate which attempts to check if a " + - "non-null property of root table or inner joined table is null " + - "(eg: `table.parent.isNull()`). " + - "There are two solutions: " + - "1. Use associated id property " + - "(eg: `table.parentId.isNull()`), " + - "2. This non-property must belong to a join table " + - "and table join path needs to have at least one left join " + - "(eg: `table.`parent?`.isNull()`)." + - "The non-null property is `${expression.prop.name}` " + - "of table `${expression.table.immutableType.javaClass.name}`." - ) - } + if (!isNegative() && expression is PropExpressionImplementor<*>) { + IsNullUtils.isValidIsNullExpression(expression) } } diff --git a/project/jimmer-sql-kotlin/src/test/dto/Employee.dto b/project/jimmer-sql-kotlin/src/test/dto/Employee.dto new file mode 100644 index 0000000000..abe283cf39 --- /dev/null +++ b/project/jimmer-sql-kotlin/src/test/dto/Employee.dto @@ -0,0 +1,10 @@ +export org.babyfish.jimmer.sql.kt.model.hr.Employee + -> package org.babyfish.jimmer.sql.kt.model.hr.dto + +specification EmployeeSpecificationForIssue735 { + like/i(name) + null(department) + flat(department) { + name as departmentName + } +} \ No newline at end of file diff --git a/project/jimmer-sql-kotlin/src/test/kotlin/org/babyfish/jimmer/sql/kt/dto/EmployeeSpecificationTest.kt b/project/jimmer-sql-kotlin/src/test/kotlin/org/babyfish/jimmer/sql/kt/dto/EmployeeSpecificationTest.kt new file mode 100644 index 0000000000..3604fe1adc --- /dev/null +++ b/project/jimmer-sql-kotlin/src/test/kotlin/org/babyfish/jimmer/sql/kt/dto/EmployeeSpecificationTest.kt @@ -0,0 +1,69 @@ +package org.babyfish.jimmer.sql.kt.dto + +import org.babyfish.jimmer.sql.kt.common.AbstractQueryTest +import org.babyfish.jimmer.sql.kt.model.hr.Employee +import org.babyfish.jimmer.sql.kt.model.hr.dto.EmployeeSpecificationForIssue735 +import org.junit.Test + +class EmployeeSpecificationTest : AbstractQueryTest() { + + @Test + fun testParentEmpty() { + val spec = EmployeeSpecificationForIssue735() + executeAndExpect( + sqlClient.createQuery(Employee::class) { + where(spec) + select(table) + } + ) { + sql( + """select tb_1_.ID, tb_1_.NAME, tb_1_.DEPARTMENT_ID, tb_1_.DELETED_UUID + |from EMPLOYEE tb_1_ + |where tb_1_.DELETED_UUID is null""".trimMargin() + ) + rows { } + } + } + + @Test + fun testParentIsNull() { + val spec = EmployeeSpecificationForIssue735(isDepartmentNull = true) + executeAndExpect( + sqlClient.createQuery(Employee::class) { + where(spec) + select(table) + } + ) { + sql( + """select tb_1_.ID, tb_1_.NAME, tb_1_.DEPARTMENT_ID, tb_1_.DELETED_UUID + |from EMPLOYEE tb_1_ + |where tb_1_.DEPARTMENT_ID is null and + |tb_1_.DELETED_UUID is null""".trimMargin() + ) + } + } + + @Test + fun testParentIsNullAndDepartmentName() { + val spec = EmployeeSpecificationForIssue735( + isDepartmentNull = true, + departmentName = "ABC" + ) + executeAndExpect( + sqlClient.createQuery(Employee::class) { + where(spec) + select(table) + } + ) { + sql( + """select tb_1_.ID, tb_1_.NAME, tb_1_.DEPARTMENT_ID, tb_1_.DELETED_UUID + |from EMPLOYEE tb_1_ + |inner join DEPARTMENT tb_2_ on tb_1_.DEPARTMENT_ID = tb_2_.ID + |where tb_1_.DEPARTMENT_ID is null and + |tb_2_.NAME = ? + |and tb_1_.DELETED_UUID is null + |and tb_2_.DELETED_TIME is null""".trimMargin() + ) + } + } +} \ No newline at end of file diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/NullityPredicate.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/NullityPredicate.java index 0c48291a44..cd05bcaa02 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/NullityPredicate.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/NullityPredicate.java @@ -5,7 +5,7 @@ import org.babyfish.jimmer.sql.ast.Predicate; import org.babyfish.jimmer.sql.ast.PropExpression; import org.babyfish.jimmer.sql.ast.impl.render.AbstractSqlBuilder; -import org.babyfish.jimmer.sql.ast.impl.table.JoinUtils; +import org.babyfish.jimmer.sql.ast.impl.table.IsNullUtils; import org.babyfish.jimmer.sql.ast.impl.table.TableImplementor; import org.babyfish.jimmer.sql.ast.impl.table.TableProxies; import org.babyfish.jimmer.sql.ast.table.spi.PropExpressionImplementor; @@ -23,25 +23,9 @@ class NullityPredicate extends AbstractPredicate { private final boolean negative; public NullityPredicate(Expression expression, boolean negative) { - if (!negative) { - if (expression instanceof PropExpression) { - PropExpressionImplementor propExpr = (PropExpressionImplementor) expression; - if (!propExpr.getProp().isNullable() && !JoinUtils.hasLeftJoin(propExpr.getTable())) { - throw new IllegalArgumentException( - "Unable to instantiate `is null` predicate which attempts to check if a " + - "non-null property of root table or inner joined table is null " + - "(eg: `table.parent().isNull()`). " + - "There are two solutions: " + - "1. Use associated id property " + - "(eg: `table.parentId().isNull()`), " + - "2. This non-property must belong to a join table " + - "and table join path needs to have at least one left join " + - "(eg: `table.parent(JoinType.LEFT).isNull()`). " + - "The non-null property is `" + propExpr.getProp().getName() + - "` of table `" + propExpr.getTable().getImmutableType().getClass().getName() + "`." - ); - } - } + if (!negative && expression instanceof PropExpression) { + PropExpressionImplementor propExpr = (PropExpressionImplementor) expression; + IsNullUtils.isValidIsNullExpression(propExpr); } this.expression = expression; this.negative = negative; diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/table/IsNullUtils.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/table/IsNullUtils.java new file mode 100644 index 0000000000..3bf5a07153 --- /dev/null +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/table/IsNullUtils.java @@ -0,0 +1,114 @@ +package org.babyfish.jimmer.sql.ast.impl.table; + +import org.babyfish.jimmer.meta.ImmutableProp; +import org.babyfish.jimmer.sql.JoinType; +import org.babyfish.jimmer.sql.ast.table.Table; +import org.babyfish.jimmer.sql.ast.table.spi.PropExpressionImplementor; +import org.babyfish.jimmer.sql.ast.table.spi.TableProxy; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +public class IsNullUtils { + + private IsNullUtils() {} + + public static void isValidIsNullExpression(@NotNull PropExpressionImplementor propExpression) { + for (PropExpressionImplementor pe = propExpression; pe != null; pe = pe.getBase()) { + if (pe.getProp().isNullable()) { + return; + } + } + for (Table table = propExpression.getTable(); table != null; ) { + if (table instanceof TableProxy) { + TableProxy proxy = (TableProxy) table; + ImmutableProp prop = proxy.__prop(); + if (proxy.__isInverse()) { + prop = prop.getOpposite(); + } + if (prop != null) { + JoinType currentJoinType = proxy.__joinType(); + if (currentJoinType == JoinType.LEFT || currentJoinType == JoinType.FULL) { + return; + } + } + table = proxy.__parent(); + } else { + TableImplementor impl = (TableImplementor) table; + ImmutableProp prop = impl.getJoinProp(); + if (impl.isInverse()) { + prop = prop.getOpposite(); + } + if (prop != null) { + JoinType currentJoinType = impl.getJoinType(); + if (currentJoinType == JoinType.LEFT || currentJoinType == JoinType.FULL) { + return; + } + } + table = impl.getParent(); + } + } + + List pathNames = new LinkedList<>(); + for (PropExpressionImplementor pe = propExpression; pe != null; pe = pe.getBase()) { + if (pe.getProp().isNullable()) { + pathNames.add(0, pe.getProp().getName()); + } + } + boolean joined = false; + for (Table table = propExpression.getTable(); table != null; ) { + if (table instanceof TableProxy) { + TableProxy proxy = (TableProxy) table; + ImmutableProp prop = proxy.__prop(); + if (proxy.__isInverse()) { + prop = prop.getOpposite(); + } + if (prop != null) { + pathNames.add(0, prop.getName()); + } else if (proxy.__weakJoinHandle() != null) { + pathNames.add( + 0, + "weakJoin<" + proxy.__weakJoinHandle().getWeakJoinType().getSimpleName() + ">" + ); + } else { + pathNames.add(0, table.getImmutableType().getJavaClass().getSimpleName()); + } + table = proxy.__parent(); + } else { + TableImplementor impl = (TableImplementor) table; + ImmutableProp prop = impl.getJoinProp(); + if (impl.isInverse()) { + prop = prop.getOpposite(); + } + if (prop != null) { + pathNames.add(0, prop.getName()); + } if (impl.getWeakJoinHandle() != null) { + pathNames.add( + 0, + "weakJoin<" + impl.getWeakJoinHandle().getWeakJoinType().getSimpleName() + ">" + ); + } else { + pathNames.add(0, table.getImmutableType().getJavaClass().getSimpleName()); + } + table = impl.getParent(); + } + } + String path = String.join(".", pathNames); + if (!joined) { + throw new IllegalArgumentException( + "Unable to instantiate the \"is null\" predicate, the path \"" + + path + + "\" is non-null expression" + ); + } + throw new IllegalArgumentException( + "Unable to instantiate the \"is null\" predicate, the path \"" + + path + + "\" is neither non-null expression " + + "nor path with left or full table join" + ); + } +} diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/table/JoinUtils.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/table/JoinUtils.java deleted file mode 100644 index 9839640030..0000000000 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/table/JoinUtils.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.babyfish.jimmer.sql.ast.impl.table; - -import org.babyfish.jimmer.meta.ImmutableProp; -import org.babyfish.jimmer.sql.JoinType; -import org.babyfish.jimmer.sql.ast.table.Table; -import org.babyfish.jimmer.sql.ast.table.spi.TableProxy; - -public class JoinUtils { - - private JoinUtils() {} - - public static boolean hasLeftJoin(Table table) { - while (table != null) { - if (table instanceof TableProxy) { - TableProxy proxy = (TableProxy) table; - ImmutableProp prop = proxy.__prop(); - if (proxy.__isInverse()) { - prop = prop.getOpposite(); - } - if (prop != null && prop.isNullable()) { - JoinType currentJoinType = proxy.__joinType(); - if (currentJoinType == JoinType.LEFT || currentJoinType == JoinType.FULL) { - return true; - } - } - table = proxy.__parent(); - } else { - TableImplementor impl = (TableImplementor) table; - ImmutableProp prop = impl.getJoinProp(); - if (impl.isInverse()) { - prop = prop.getOpposite(); - } - if (prop != null && prop.isNullable()) { - JoinType currentJoinType = impl.getJoinType(); - if (currentJoinType == JoinType.LEFT || currentJoinType == JoinType.FULL) { - return true; - } - } - table = impl.getParent(); - } - } - return false; - } -} diff --git a/project/jimmer-sql/src/test/dto/Department.dto b/project/jimmer-sql/src/test/dto/Department.dto index 2ba2c29829..1f3b1d0d6c 100644 --- a/project/jimmer-sql/src/test/dto/Department.dto +++ b/project/jimmer-sql/src/test/dto/Department.dto @@ -43,4 +43,4 @@ specification DepartmentSpecification2 { like/i(name) } } -} \ No newline at end of file +} diff --git a/project/jimmer-sql/src/test/dto/Employee.dto b/project/jimmer-sql/src/test/dto/Employee.dto new file mode 100644 index 0000000000..ea2f5fff56 --- /dev/null +++ b/project/jimmer-sql/src/test/dto/Employee.dto @@ -0,0 +1,10 @@ +export org.babyfish.jimmer.sql.model.hr.Employee + -> package org.babyfish.jimmer.sql.model.hr.dto + +specification EmployeeSpecificationForIssue735 { + like/i(name) + null(department) + flat(department) { + name as departmentName + } +} \ No newline at end of file diff --git a/project/jimmer-sql/src/test/java/org/babyfish/jimmer/sql/dto/EmployeeSpecificationTest.java b/project/jimmer-sql/src/test/java/org/babyfish/jimmer/sql/dto/EmployeeSpecificationTest.java new file mode 100644 index 0000000000..2eff1804ab --- /dev/null +++ b/project/jimmer-sql/src/test/java/org/babyfish/jimmer/sql/dto/EmployeeSpecificationTest.java @@ -0,0 +1,75 @@ +package org.babyfish.jimmer.sql.dto; + +import org.babyfish.jimmer.sql.common.AbstractQueryTest; +import org.babyfish.jimmer.sql.model.hr.EmployeeTable; +import org.babyfish.jimmer.sql.model.hr.dto.EmployeeSpecificationForIssue735; +import org.junit.jupiter.api.Test; + +public class EmployeeSpecificationTest extends AbstractQueryTest { + + @Test + public void testParentEmpty() { + EmployeeTable table = EmployeeTable.$; + EmployeeSpecificationForIssue735 spec = new EmployeeSpecificationForIssue735(); + executeAndExpect( + getSqlClient() + .createQuery(table) + .where(spec) + .select(table), + ctx -> { + ctx.sql( + "select tb_1_.ID, tb_1_.NAME, tb_1_.DELETED_MILLIS, tb_1_.DEPARTMENT_ID " + + "from EMPLOYEE tb_1_ " + + "where tb_1_.DELETED_MILLIS = ?" + ); + ctx.rows(it -> {}); + } + ); + } + + @Test + public void testParentIsNull() { + EmployeeTable table = EmployeeTable.$; + EmployeeSpecificationForIssue735 spec = new EmployeeSpecificationForIssue735(); + spec.setDepartmentNull(true); + executeAndExpect( + getSqlClient() + .createQuery(table) + .where(spec) + .select(table), + ctx -> { + ctx.sql( + "select tb_1_.ID, tb_1_.NAME, tb_1_.DELETED_MILLIS, tb_1_.DEPARTMENT_ID " + + "from EMPLOYEE tb_1_ " + + "where tb_1_.DEPARTMENT_ID is null and tb_1_.DELETED_MILLIS = ?" + ); + ctx.rows(it -> {}); + } + ); + } + + @Test + public void testParentIsNullAndDepartmentName() { + EmployeeTable table = EmployeeTable.$; + EmployeeSpecificationForIssue735 spec = new EmployeeSpecificationForIssue735(); + spec.setDepartmentNull(true); + spec.setDepartmentName("X"); + executeAndExpect( + getSqlClient() + .createQuery(table) + .where(spec) + .select(table), + ctx -> { + ctx.sql( + "select tb_1_.ID, tb_1_.NAME, tb_1_.DELETED_MILLIS, tb_1_.DEPARTMENT_ID " + + "from EMPLOYEE tb_1_ " + + "inner join DEPARTMENT tb_2_ on tb_1_.DEPARTMENT_ID = tb_2_.ID " + + "where tb_1_.DEPARTMENT_ID is null and " + + "tb_2_.NAME = ? and " + + "tb_1_.DELETED_MILLIS = ? and tb_2_.DELETED_MILLIS = ?" + ); + ctx.rows(it -> {}); + } + ); + } +}