Add filterExpressions as a concept

This commit is contained in:
2023-02-08 17:06:05 -06:00
parent eae01bb8c4
commit 07e6c7019d
6 changed files with 298 additions and 0 deletions

View File

@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -44,6 +45,7 @@ public class QFilterCriteria implements Serializable, Cloneable
private List<Serializable> values; private List<Serializable> values;
private String otherFieldName; private String otherFieldName;
private AbstractFilterExpression<?> expression;
@ -302,4 +304,35 @@ public class QFilterCriteria implements Serializable, Cloneable
return (rs.toString()); return (rs.toString());
} }
/*******************************************************************************
** Getter for expression
*******************************************************************************/
public AbstractFilterExpression<?> getExpression()
{
return (this.expression);
}
/*******************************************************************************
** Setter for expression
*******************************************************************************/
public void setExpression(AbstractFilterExpression<?> expression)
{
this.expression = expression;
}
/*******************************************************************************
** Fluent setter for expression
*******************************************************************************/
public QFilterCriteria withExpression(AbstractFilterExpression<?> expression)
{
this.expression = expression;
return (this);
}
} }

View File

@ -0,0 +1,37 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.io.Serializable;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractFilterExpression<T extends Serializable>
{
/*******************************************************************************
**
*******************************************************************************/
public abstract T evaluate();
}

View File

@ -0,0 +1,43 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.time.Instant;
/*******************************************************************************
**
*******************************************************************************/
public class Now extends AbstractFilterExpression<Instant>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public Instant evaluate()
{
return (Instant.now());
}
}

View File

@ -0,0 +1,128 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
/*******************************************************************************
**
*******************************************************************************/
public class NowWithOffset extends AbstractFilterExpression<Instant>
{
private final Operator operator;
private final int amount;
private final TimeUnit timeUnit;
public enum Operator
{PLUS, MINUS}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
private NowWithOffset(Operator operator, int amount, TimeUnit timeUnit)
{
this.operator = operator;
this.amount = amount;
this.timeUnit = timeUnit;
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static NowWithOffset minus(int amount, TimeUnit timeUnit)
{
return (new NowWithOffset(Operator.MINUS, amount, timeUnit));
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static NowWithOffset plus(int amount, TimeUnit timeUnit)
{
return (new NowWithOffset(Operator.PLUS, amount, timeUnit));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Instant evaluate()
{
if(operator.equals(Operator.PLUS))
{
return (Instant.now().plus(amount, timeUnit.toChronoUnit()));
}
else
{
return (Instant.now().minus(amount, timeUnit.toChronoUnit()));
}
}
/*******************************************************************************
** Getter for operator
**
*******************************************************************************/
public Operator getOperator()
{
return operator;
}
/*******************************************************************************
** Getter for amount
**
*******************************************************************************/
public int getAmount()
{
return amount;
}
/*******************************************************************************
** Getter for timeUnit
**
*******************************************************************************/
public TimeUnit getTimeUnit()
{
return timeUnit;
}
}

View File

@ -703,6 +703,10 @@ public abstract class AbstractRDBMSAction implements QActionInterface
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
values = Collections.emptyList(); values = Collections.emptyList();
} }
else if(expectedNoOfParams.equals(1) && criterion.getExpression() != null)
{
values = List.of(criterion.getExpression().evaluate());
}
else if(!expectedNoOfParams.equals(values.size())) else if(!expectedNoOfParams.equals(values.size()))
{ {
throw new IllegalArgumentException("Incorrect number of values given for criteria [" + field.getName() + "]"); throw new IllegalArgumentException("Incorrect number of values given for criteria [" + field.getName() + "]");

View File

@ -22,9 +22,12 @@
package com.kingsrook.qqq.backend.module.rdbms.actions; package com.kingsrook.qqq.backend.module.rdbms.actions;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate; import java.util.function.Predicate;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
@ -40,6 +43,8 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.Now;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
@ -446,6 +451,54 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testFilterExpressions() throws QException
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON);
insertInput.setRecords(List.of(
new QRecord().withValue("email", "-").withValue("firstName", "past").withValue("lastName", "ExpressionTest").withValue("birthDate", Instant.now().minus(3, ChronoUnit.DAYS)),
new QRecord().withValue("email", "-").withValue("firstName", "future").withValue("lastName", "ExpressionTest").withValue("birthDate", Instant.now().plus(3, ChronoUnit.DAYS))
));
new InsertAction().execute(insertInput);
{
QueryInput queryInput = initQueryRequest();
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest")))
.withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withExpression(new Now())));
QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows");
Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row");
}
{
QueryInput queryInput = initQueryRequest();
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest")))
.withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withExpression(NowWithOffset.plus(2, TimeUnit.DAYS))));
QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows");
Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row");
}
{
QueryInput queryInput = initQueryRequest();
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest")))
.withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.GREATER_THAN).withExpression(NowWithOffset.minus(5, TimeUnit.DAYS))));
QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows");
Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row");
Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("future")), "Should find expected row");
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/