diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java index 35352192..6dc50b1d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java @@ -30,6 +30,7 @@ public enum QCriteriaOperator { EQUALS, NOT_EQUALS, + NOT_EQUALS_OR_IS_NULL, IN, NOT_IN, IS_NULL_OR_IN, diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java index e05d0bbd..06d36f64 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java @@ -131,6 +131,7 @@ public class BackendQueryFilterUtils { case EQUALS -> testEquals(criterion, value); case NOT_EQUALS -> !testEquals(criterion, value); + case NOT_EQUALS_OR_IS_NULL -> !testEquals(criterion, value) || testBlank(criterion, value); case IN -> testIn(criterion, value); case NOT_IN -> !testIn(criterion, value); case IS_BLANK -> testBlank(criterion, value); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java index 75c8578c..677fb8d8 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java @@ -113,6 +113,13 @@ class BackendQueryFilterUtilsTest assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, "A", "B"), "f", "B")); assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, List.of()), "f", "A")); assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_IN, ListBuilder.of(null)), "f", "A")); + + /////////////////////////// + // NOT_EQUALS_OR_IS_NULL // + /////////////////////////// + assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, "A"), "f", "A")); + assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, "A"), "f", "B")); + assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, "A"), "f", null)); } diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILogMetaDataProvider.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILogMetaDataProvider.java index ce1d5007..060fb1e7 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILogMetaDataProvider.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/model/OutboundAPILogMetaDataProvider.java @@ -84,6 +84,7 @@ public class OutboundAPILogMetaDataProvider new QPossibleValue<>(401, "401 (Not Authorized)"), new QPossibleValue<>(403, "403 (Forbidden)"), new QPossibleValue<>(404, "404 (Not Found)"), + new QPossibleValue<>(422, "422 (Unprocessable Entity)"), new QPossibleValue<>(429, "429 (Too Many Requests)"), new QPossibleValue<>(500, "500 (Internal Server Error)") ))); @@ -131,6 +132,7 @@ public class OutboundAPILogMetaDataProvider .withValue(AdornmentType.ChipValues.colorValue(401, AdornmentType.ChipValues.COLOR_ERROR)) .withValue(AdornmentType.ChipValues.colorValue(403, AdornmentType.ChipValues.COLOR_ERROR)) .withValue(AdornmentType.ChipValues.colorValue(404, AdornmentType.ChipValues.COLOR_ERROR)) + .withValue(AdornmentType.ChipValues.colorValue(422, AdornmentType.ChipValues.COLOR_ERROR)) .withValue(AdornmentType.ChipValues.colorValue(429, AdornmentType.ChipValues.COLOR_ERROR)) .withValue(AdornmentType.ChipValues.colorValue(500, AdornmentType.ChipValues.COLOR_ERROR))); diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index ec7e7488..6145e8ef 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -534,6 +534,12 @@ public abstract class AbstractRDBMSAction implements QActionInterface expectedNoOfParams = 1; break; } + case NOT_EQUALS_OR_IS_NULL: + { + clause += " != ? OR " + column + " IS NULL "; + expectedNoOfParams = 1; + break; + } case IN: { if(values.isEmpty()) diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index 637601fa..692ea261 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -148,6 +148,39 @@ public class RDBMSQueryActionTest extends RDBMSActionTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testNotEqualsOrIsNullQuery() throws QException + { + ///////////////////////////////////////////////////////////////////////////// + // 5 rows, 1 has a null salary, 1 has 1,000,000. // + // first confirm that query for != returns 3 (the null does NOT come back) // + // then, confirm that != or is null gives the (more humanly expected) 4. // + ///////////////////////////////////////////////////////////////////////////// + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("annualSalary") + .withOperator(QCriteriaOperator.NOT_EQUALS) + .withValues(List.of(1_000_000)))); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(3, queryOutput.getRecords().size(), "Expected # of rows"); + + queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("annualSalary") + .withOperator(QCriteriaOperator.NOT_EQUALS_OR_IS_NULL) + .withValues(List.of(1_000_000)))); + queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); + Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> Objects.equals(1_000_000, r.getValueInteger("annualSalary"))), "Should NOT find expected salary"); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index a0015677..302aa3ca 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -895,12 +895,13 @@ public class QJavalinImplementation queryInput.getFilter().setLimit(limit); } - queryInput.setQueryJoins(processQueryJoinsParam(context)); + List queryJoins = processQueryJoinsParam(context); + queryInput.setQueryJoins(queryJoins); QueryAction queryAction = new QueryAction(); QueryOutput queryOutput = queryAction.execute(queryInput); - QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS)); + QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS), logPairIfSlow("joins", queryJoins, SLOW_LOG_THRESHOLD_MS)); context.result(JsonUtils.toJson(queryOutput)); } catch(Exception e)