From 6c7621a2f7888dd90eaba24df551495f7bf43da7 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 8 Sep 2023 10:30:39 -0500 Subject: [PATCH] Update handling of criteria in format "table.field" when the "table" portion equals the record's tableName; fix applyBooleanOperator to always update the accumulator; --- .../utils/BackendQueryFilterUtils.java | 12 +- .../utils/BackendQueryFilterUtilsTest.java | 271 ++++++++++++++++++ 2 files changed, 279 insertions(+), 4 deletions(-) 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 06d36f64..5356ebe6 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 @@ -75,14 +75,16 @@ public class BackendQueryFilterUtils { /////////////////////////////////////////////////////////////////////////////////////////////////// // if the value isn't in the record - check, if it looks like a table.fieldName, but none of the // - // field names in the record are fully qualified, then just use the field-name portion... // + // field names in the record are fully qualified - OR - the table name portion of the field name // + // matches the record's field name, then just use the field-name portion... // /////////////////////////////////////////////////////////////////////////////////////////////////// if(fieldName.contains(".")) { + String[] parts = fieldName.split("\\."); Map values = qRecord.getValues(); - if(values.keySet().stream().noneMatch(n -> n.contains("."))) + if(values.keySet().stream().noneMatch(n -> n.contains(".")) || parts[0].equals(qRecord.getTableName())) { - value = qRecord.getValue(fieldName.substring(fieldName.indexOf(".") + 1)); + value = qRecord.getValue(parts[1]); } } } @@ -190,12 +192,13 @@ public class BackendQueryFilterUtils ** operator, update the accumulator, and if we can then short-circuit remaining ** operations, return a true or false. Returning null means to keep going. *******************************************************************************/ - private static Boolean applyBooleanOperator(AtomicBoolean accumulator, boolean newValue, QQueryFilter.BooleanOperator booleanOperator) + static Boolean applyBooleanOperator(AtomicBoolean accumulator, boolean newValue, QQueryFilter.BooleanOperator booleanOperator) { boolean accumulatorValue = accumulator.getPlain(); if(booleanOperator.equals(QQueryFilter.BooleanOperator.AND)) { accumulatorValue &= newValue; + accumulator.set(accumulatorValue); if(!accumulatorValue) { return (false); @@ -204,6 +207,7 @@ public class BackendQueryFilterUtils else { accumulatorValue |= newValue; + accumulator.set(accumulatorValue); if(accumulatorValue) { return (true); 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 677fb8d8..afc3ad1f 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 @@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.utils; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -37,6 +42,182 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class BackendQueryFilterUtilsTest { + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDoesRecordMatch_emptyFilters() + { + assertTrue(BackendQueryFilterUtils.doesRecordMatch(null, new QRecord().withValue("a", 1))); + assertTrue(BackendQueryFilterUtils.doesRecordMatch(new QQueryFilter(), new QRecord().withValue("a", 1))); + assertTrue(BackendQueryFilterUtils.doesRecordMatch(new QQueryFilter().withSubFilters(ListBuilder.of(null)), new QRecord().withValue("a", 1))); + assertTrue(BackendQueryFilterUtils.doesRecordMatch(new QQueryFilter().withSubFilters(List.of(new QQueryFilter())), new QRecord().withValue("a", 1))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDoesRecordMatch_singleAnd() + { + QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)); + + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDoesRecordMatch_singleOr() + { + QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)); + + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2))); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Test + void testDoesRecordMatch_multipleAnd() + { + QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)) + .withCriteria(new QFilterCriteria("b", QCriteriaOperator.EQUALS, 2)); + + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 2))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2).withValue("b", 2))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 1))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Test + void testDoesRecordMatch_multipleOr() + { + QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)) + .withCriteria(new QFilterCriteria("b", QCriteriaOperator.EQUALS, 2)); + + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 2))); + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2).withValue("b", 2))); + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 1))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 3).withValue("b", 4))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Test + void testDoesRecordMatch_subFilterAnd() + { + QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withSubFilters(List.of( + new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)), + new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("b", QCriteriaOperator.EQUALS, 2)) + )); + + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 2))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2).withValue("b", 2))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 1))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord())); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Test + void testDoesRecordMatch_subFilterOr() + { + QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withSubFilters(List.of( + new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)), + new QQueryFilter() + .withCriteria(new QFilterCriteria("b", QCriteriaOperator.EQUALS, 2)) + )); + + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 2))); + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 2).withValue("b", 2))); + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("b", 1))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 3).withValue("b", 4))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDoesRecordMatch_criteriaHasTableNameNoFieldsDo() + { + QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("t.a", QCriteriaOperator.EQUALS, 1)); + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDoesRecordMatch_criteriaHasTableNameSomeFieldsDo() + { + QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("t.a", QCriteriaOperator.EQUALS, 1)); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // shouldn't find the "a", because "some" fields in here have a prefix (e.g., 's' was a join table, selected with 't' as the main table, which didn't prefix) // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("a", 1).withValue("s.b", 2))); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // but this case (contrasted with above) set the record's tableName to "t", so criteria on "t.a" should find field "a" // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withTableName("t").withValue("a", 1).withValue("s.b", 2))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDoesRecordMatch_criteriaHasTableNameMatchingField() + { + QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("t.a", QCriteriaOperator.EQUALS, 1)); + assertTrue(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("t.a", 1))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("t.b", 1))); + assertFalse(BackendQueryFilterUtils.doesRecordMatch(filter, new QRecord().withValue("s.a", 1))); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -184,4 +365,94 @@ class BackendQueryFilterUtilsTest assertFalse("Not Darin".matches(pattern)); assertFalse("David".matches(pattern)); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testApplyBooleanOperator() + { + ///////////////////////////// + // tests for operator: AND // + ///////////////////////////// + { + ///////////////////////////////////////////////////////////////////////////////////// + // old value was true; new value is true. // + // result should be true, and we should not be short-circuited (return value null) // + ///////////////////////////////////////////////////////////////////////////////////// + AtomicBoolean accumulator = new AtomicBoolean(true); + assertNull(BackendQueryFilterUtils.applyBooleanOperator(accumulator, true, QQueryFilter.BooleanOperator.AND)); + assertTrue(accumulator.getPlain()); + } + { + ////////////////////////////////////////////////////////////////////////////////////// + // old value was true; new value is false. // + // result should be false, and we should be short-circuited (return value not-null) // + ////////////////////////////////////////////////////////////////////////////////////// + AtomicBoolean accumulator = new AtomicBoolean(true); + assertEquals(Boolean.FALSE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, false, QQueryFilter.BooleanOperator.AND)); + assertFalse(accumulator.getPlain()); + } + { + ////////////////////////////////////////////////////////////////////////////////////// + // old value was false; new value is true. // + // result should be false, and we should be short-circuited (return value not-null) // + ////////////////////////////////////////////////////////////////////////////////////// + AtomicBoolean accumulator = new AtomicBoolean(false); + assertEquals(Boolean.FALSE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, true, QQueryFilter.BooleanOperator.AND)); + assertFalse(accumulator.getPlain()); + } + { + ////////////////////////////////////////////////////////////////////////////////////// + // old value was false; new value is false. // + // result should be false, and we should be short-circuited (return value not-null) // + ////////////////////////////////////////////////////////////////////////////////////// + AtomicBoolean accumulator = new AtomicBoolean(false); + assertEquals(Boolean.FALSE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, false, QQueryFilter.BooleanOperator.AND)); + assertFalse(accumulator.getPlain()); + } + + //////////////////////////// + // tests for operator: OR // + //////////////////////////// + { + ///////////////////////////////////////////////////////////////////////////////////// + // old value was true; new value is true. // + // result should be true, and we should be short-circuited (return value not-null) // + ///////////////////////////////////////////////////////////////////////////////////// + AtomicBoolean accumulator = new AtomicBoolean(true); + assertEquals(Boolean.TRUE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, true, QQueryFilter.BooleanOperator.OR)); + assertTrue(accumulator.getPlain()); + } + { + ////////////////////////////////////////////////////////////////////////////////////// + // old value was true; new value is false. // + // result should be true, and we should be short-circuited (return value not-null) // + ////////////////////////////////////////////////////////////////////////////////////// + AtomicBoolean accumulator = new AtomicBoolean(true); + assertEquals(Boolean.TRUE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, false, QQueryFilter.BooleanOperator.OR)); + assertTrue(accumulator.getPlain()); + } + { + ////////////////////////////////////////////////////////////////////////////////////// + // old value was false; new value is true. // + // result should be false, and we should be short-circuited (return value not-null) // + ////////////////////////////////////////////////////////////////////////////////////// + AtomicBoolean accumulator = new AtomicBoolean(false); + assertEquals(Boolean.TRUE, BackendQueryFilterUtils.applyBooleanOperator(accumulator, true, QQueryFilter.BooleanOperator.OR)); + assertTrue(accumulator.getPlain()); + } + { + ////////////////////////////////////////////////////////////////////////////////////// + // old value was false; new value is false. // + // result should be false, and we should not be short-circuited (return value null) // + ////////////////////////////////////////////////////////////////////////////////////// + AtomicBoolean accumulator = new AtomicBoolean(false); + assertNull(BackendQueryFilterUtils.applyBooleanOperator(accumulator, false, QQueryFilter.BooleanOperator.OR)); + assertFalse(accumulator.getPlain()); + } + } + } \ No newline at end of file