validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
{
if(recordSecurityValue == null)
{
@@ -302,7 +452,7 @@ public class ValidateRecordSecurityLockHelper
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel();
- record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel));
+ return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel)));
}
}
else
@@ -314,15 +464,305 @@ public class ValidateRecordSecurityLockHelper
///////////////////////////////////////////////////////////////////////////////////////////////
// avoid telling the user a value from a foreign record that they didn't pass in themselves. //
///////////////////////////////////////////////////////////////////////////////////////////////
- record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record."));
+ return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record.")));
}
else
{
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
- record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel()));
+ return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel())));
}
}
}
+ return (Collections.emptyList());
+ }
+
+
+
+ /*******************************************************************************
+ ** Class to track errors that we're associating with a record.
+ **
+ ** More complex than it first seems to be needed, because as we're evaluating
+ ** locks, we might find some, but based on the boolean condition associated with
+ ** them, they might not actually be record-level errors.
+ **
+ ** e.g., two locks with an OR relationship - as long as one passes, the record
+ ** should have no errors. And so-on through the tree of locks/multi-locks.
+ **
+ ** Stores the errors in a tree of ErrorTreeNode objects.
+ **
+ ** References into that tree are achieved via a List of Integer called "tree positions"
+ ** where each entry in the list denotes the index of the tree node at that level.
+ **
+ ** e.g., given this tree:
+ **
+ ** A B
+ ** / \ /|\
+ ** C D E F G
+ ** |
+ ** H
+ **
+ **
+ ** The positions of each node would be:
+ **
+ ** A: [0]
+ ** B: [1]
+ ** C: [0,0]
+ ** D: [0,1]
+ ** E: [1,0]
+ ** F: [1,1]
+ ** G: [1,2]
+ ** H: [0,1,0]
+ **
+ *******************************************************************************/
+ static class RecordWithErrors
+ {
+ private QRecord record;
+ private ErrorTreeNode errorTree;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public RecordWithErrors(QRecord record)
+ {
+ this.record = record;
+ }
+
+
+
+ /*******************************************************************************
+ ** add a list of errors, for a given list of tree positions
+ *******************************************************************************/
+ public void addAll(List recordErrors, List treePositions)
+ {
+ if(errorTree == null)
+ {
+ errorTree = new ErrorTreeNode();
+ }
+
+ ErrorTreeNode node = errorTree;
+ for(Integer treePosition : treePositions)
+ {
+ if(node.children == null)
+ {
+ node.children = new ArrayList<>(treePosition);
+ }
+
+ while(treePosition >= node.children.size())
+ {
+ node.children.add(null);
+ }
+
+ if(node.children.get(treePosition) == null)
+ {
+ node.children.set(treePosition, new ErrorTreeNode());
+ }
+
+ node = node.children.get(treePosition);
+ }
+
+ if(node.errors == null)
+ {
+ node.errors = new ArrayList<>();
+ }
+ node.errors.addAll(recordErrors);
+ }
+
+
+
+ /*******************************************************************************
+ ** add a single error to a given tree-position
+ *******************************************************************************/
+ public void add(QErrorMessage error, List treePositions)
+ {
+ addAll(List.of(error), treePositions);
+ }
+
+
+
+ /*******************************************************************************
+ ** after the tree of errors has been built - walk a lock-tree (locksToCheck)
+ ** and resolve boolean operations, to get a final list of errors (possibly empty)
+ ** to put on the record.
+ *******************************************************************************/
+ public void propagateErrorsToRecord(MultiRecordSecurityLock locksToCheck)
+ {
+ List errors = recursivePropagation(locksToCheck, new ArrayList<>());
+
+ if(CollectionUtils.nullSafeHasContents(errors))
+ {
+ errors.forEach(e -> record.addError(e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** recursive implementation of the propagation method - e.g., walk tree applying
+ ** boolean logic.
+ *******************************************************************************/
+ private List recursivePropagation(MultiRecordSecurityLock locksToCheck, List treePositions)
+ {
+ //////////////////////////////////////////////////////////////////
+ // build a list of errors at this level (and deeper levels too) //
+ //////////////////////////////////////////////////////////////////
+ List errorsFromThisLevel = new ArrayList<>();
+
+ int i = 0;
+ for(RecordSecurityLock lock : locksToCheck.getLocks())
+ {
+ List errorsFromThisLock;
+
+ treePositions.add(i);
+ if(lock instanceof MultiRecordSecurityLock childMultiLock)
+ {
+ errorsFromThisLock = recursivePropagation(childMultiLock, treePositions);
+ }
+ else
+ {
+ errorsFromThisLock = getErrorsFromTree(treePositions);
+ }
+
+ errorsFromThisLevel.addAll(errorsFromThisLock);
+
+ treePositions.remove(treePositions.size() - 1);
+ i++;
+ }
+
+ if(MultiRecordSecurityLock.BooleanOperator.AND.equals(locksToCheck.getOperator()))
+ {
+ //////////////////////////////////////////////////////////////
+ // for an AND - if there were ANY errors, then return them. //
+ //////////////////////////////////////////////////////////////
+ if(!errorsFromThisLevel.isEmpty())
+ {
+ return (errorsFromThisLevel);
+ }
+ }
+ else // OR
+ {
+ //////////////////////////////////////////////////////////
+ // for an OR - only return if ALL conditions had errors //
+ //////////////////////////////////////////////////////////
+ if(errorsFromThisLevel.size() == locksToCheck.getLocks().size())
+ {
+ return (errorsFromThisLevel); // todo something smarter?
+ }
+ }
+
+ ///////////////////////////////////
+ // else - no errors - empty list //
+ ///////////////////////////////////
+ return Collections.emptyList();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private List getErrorsFromTree(List treePositions)
+ {
+ ErrorTreeNode node = errorTree;
+
+ for(Integer treePosition : treePositions)
+ {
+ if(node.children == null)
+ {
+ return Collections.emptyList();
+ }
+
+ if(treePosition >= node.children.size())
+ {
+ return Collections.emptyList();
+ }
+
+ if(node.children.get(treePosition) == null)
+ {
+ return Collections.emptyList();
+ }
+
+ node = node.children.get(treePosition);
+ }
+
+ if(node.errors == null)
+ {
+ return Collections.emptyList();
+ }
+
+ return node.errors;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ try
+ {
+ return JsonUtils.toPrettyJson(this);
+ }
+ catch(Exception e)
+ {
+ return "error in toString";
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** tree node used by RecordWithErrors
+ *******************************************************************************/
+ static class ErrorTreeNode
+ {
+ private List errors;
+ private ArrayList children;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ try
+ {
+ return JsonUtils.toPrettyJson(this);
+ }
+ catch(Exception e)
+ {
+ return "error in toString";
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for errors - only here for Jackson/toString
+ **
+ *******************************************************************************/
+ public List getErrors()
+ {
+ return errors;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for children - only here for Jackson/toString
+ **
+ *******************************************************************************/
+ public ArrayList getChildren()
+ {
+ return children;
+ }
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
index 3697108b..2df4c4fd 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
@@ -86,6 +86,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
+import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
@@ -716,7 +717,6 @@ public class QInstanceValidator
{
String prefix = "Table " + table.getName() + " ";
- RECORD_SECURITY_LOCKS_LOOP:
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
if(!assertCondition(recordSecurityLock != null, prefix + "has a null recordSecurityLock (did you mean to give it a null list of locks?)"))
@@ -724,82 +724,115 @@ public class QInstanceValidator
continue;
}
- String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
- if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
+ if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
- assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
+ validateMultiRecordSecurityLock(qInstance, table, multiRecordSecurityLock, prefix);
}
-
- prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
-
- assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
-
- boolean hasAnyBadJoins = false;
- for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
+ else
{
- if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName))
- {
- hasAnyBadJoins = true;
- }
+ validateRecordSecurityLock(qInstance, table, recordSecurityLock, prefix);
}
+ }
+ }
- String fieldName = recordSecurityLock.getFieldName();
- ////////////////////////////////////////////////////////////////////////////////
- // don't bother trying to validate field names if we know we have a bad join. //
- ////////////////////////////////////////////////////////////////////////////////
- if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void validateMultiRecordSecurityLock(QInstance qInstance, QTableMetaData table, MultiRecordSecurityLock multiRecordSecurityLock, String prefix)
+ {
+ assertCondition(multiRecordSecurityLock.getOperator() != null, prefix + "has a MultiRecordSecurityLock that is missing an operator");
+
+ for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
+ {
+ validateRecordSecurityLock(qInstance, table, lock, prefix);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void validateRecordSecurityLock(QInstance qInstance, QTableMetaData table, RecordSecurityLock recordSecurityLock, String prefix)
+ {
+ String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
+ if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
+ {
+ assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
+ }
+
+ prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
+
+ assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
+
+ boolean hasAnyBadJoins = false;
+ for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
+ {
+ if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName))
{
- if(fieldName.contains("."))
+ hasAnyBadJoins = true;
+ }
+ }
+
+ String fieldName = recordSecurityLock.getFieldName();
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // don't bother trying to validate field names if we know we have a bad join. //
+ ////////////////////////////////////////////////////////////////////////////////
+ if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
+ {
+ if(fieldName.contains("."))
+ {
+ if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
{
- if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
- {
- String[] split = fieldName.split("\\.");
+ String[] split = fieldName.split("\\.");
String joinTableName = split[0];
String joinFieldName = split[1];
List joins = new ArrayList<>();
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- // ok - so - the join name chain is going to be like this: //
- // for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
- // - securityFieldName = order.clientId //
- // - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
- // so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
- // and step (via tmpTable variable) back to the securityField //
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- ArrayList joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
- Collections.reverse(joinNameChain);
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ // ok - so - the join name chain is going to be like this: //
+ // for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
+ // - securityFieldName = order.clientId //
+ // - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
+ // so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
+ // and step (via tmpTable variable) back to the securityField //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ ArrayList joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
+ Collections.reverse(joinNameChain);
- QTableMetaData tmpTable = table;
+ QTableMetaData tmpTable = table;
- for(String joinName : joinNameChain)
+ for(String joinName : joinNameChain)
+ {
+ QJoinMetaData join = qInstance.getJoin(joinName);
+ if(join == null)
{
- QJoinMetaData join = qInstance.getJoin(joinName);
- if(join == null)
- {
- errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
- continue RECORD_SECURITY_LOCKS_LOOP;
- }
-
- if(join.getLeftTable().equals(tmpTable.getName()))
- {
- joins.add(new QueryJoin(join));
- tmpTable = qInstance.getTable(join.getRightTable());
- }
- else if(join.getRightTable().equals(tmpTable.getName()))
- {
- joins.add(new QueryJoin(join.flip()));
- tmpTable = qInstance.getTable(join.getLeftTable());
- }
- else
- {
- errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
- continue RECORD_SECURITY_LOCKS_LOOP;
- }
+ errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
+ return;
}
- assertCondition(Objects.equals(tmpTable.getName(), joinTableName), prefix + "has a joinNameChain doesn't end in the expected table [" + joinTableName + "]");
+ if(join.getLeftTable().equals(tmpTable.getName()))
+ {
+ joins.add(new QueryJoin(join));
+ tmpTable = qInstance.getTable(join.getRightTable());
+ }
+ else if(join.getRightTable().equals(tmpTable.getName()))
+ {
+ joins.add(new QueryJoin(join.flip()));
+ tmpTable = qInstance.getTable(join.getLeftTable());
+ }
+ else
+ {
+ errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
+ return;
+ }
+ }
+
+ assertCondition(Objects.equals(tmpTable.getName(), joinTableName), prefix + "has a joinNameChain doesn't end in the expected table [" + joinTableName + "]");
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
@@ -813,12 +846,10 @@ public class QInstanceValidator
}
}
- assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
- }
+ assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
}
-
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java
index 3945246e..c7a92518 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java
@@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete;
import java.io.Serializable;
+import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
@@ -139,6 +140,24 @@ public class DeleteInput extends AbstractTableActionInput
+ /*******************************************************************************
+ ** Fluently add 1 primary key to the delete input
+ **
+ *******************************************************************************/
+ public DeleteInput withPrimaryKey(Serializable primaryKey)
+ {
+ if(primaryKeys == null)
+ {
+ primaryKeys = new ArrayList<>();
+ }
+
+ primaryKeys.add(primaryKey);
+
+ return (this);
+ }
+
+
+
/*******************************************************************************
** Setter for ids
**
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java
index 8fa6f5ec..8252e84f 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -37,12 +38,17 @@ import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
+import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
import org.apache.logging.log4j.Level;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@@ -60,11 +66,23 @@ public class JoinsContext
private final String mainTableName;
private final List queryJoins;
+ private final QQueryFilter securityFilter;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // pointer either at securityFilter, or at a sub-filter within it, for when we're doing a recursive build-out of multi-locks //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ private QQueryFilter securityFilterCursor;
+
////////////////////////////////////////////////////////////////
// note - will have entries for all tables, not just aliases. //
////////////////////////////////////////////////////////////////
private final Map aliasToTableNameMap = new HashMap<>();
- private Level logLevel = Level.OFF;
+
+ /////////////////////////////////////////////////////////////////////////////
+ // we will get a TON of more output if this gets turned up, so be cautious //
+ /////////////////////////////////////////////////////////////////////////////
+ private Level logLevel = Level.OFF;
+ private Level logLevelForFilter = Level.OFF;
@@ -74,54 +92,225 @@ public class JoinsContext
*******************************************************************************/
public JoinsContext(QInstance instance, String tableName, List queryJoins, QQueryFilter filter) throws QException
{
- log("--- START ----------------------------------------------------------------------", logPair("mainTable", tableName));
this.instance = instance;
this.mainTableName = tableName;
this.queryJoins = new MutableList<>(queryJoins);
+ this.securityFilter = new QQueryFilter();
+ this.securityFilterCursor = this.securityFilter;
+
+ // log("--- START ----------------------------------------------------------------------", logPair("mainTable", tableName));
+ dumpDebug(true, false);
for(QueryJoin queryJoin : this.queryJoins)
{
- log("Processing input query join", logPair("joinTable", queryJoin.getJoinTable()), logPair("alias", queryJoin.getAlias()), logPair("baseTableOrAlias", queryJoin.getBaseTableOrAlias()), logPair("joinMetaDataName", () -> queryJoin.getJoinMetaData().getName()));
processQueryJoin(queryJoin);
}
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // make sure that all tables specified in filter columns are being brought into the query as joins //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ ensureFilterIsRepresented(filter);
+ logFilter("After ensureFilterIsRepresented:", securityFilter);
+
+ ///////////////////////////////////////////////////////////////////////////////////////
+ // ensure that any record locks on the main table, which require a join, are present //
+ ///////////////////////////////////////////////////////////////////////////////////////
+ MultiRecordSecurityLock multiRecordSecurityLock = RecordSecurityLockFilters.filterForReadLockTree(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()));
+ for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
+ {
+ ensureRecordSecurityLockIsRepresented(tableName, tableName, lock, null);
+ logFilter("After ensureRecordSecurityLockIsRepresented[fieldName=" + lock.getFieldName() + "]:", securityFilter);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////
+ // make sure that all joins in the query have meta data specified //
+ // e.g., a user-added join may just specify the join-table //
+ // or a join implicitly added from a filter may also not have its join meta data //
+ ///////////////////////////////////////////////////////////////////////////////////
+ fillInMissingJoinMetaData();
+ logFilter("After fillInMissingJoinMetaData:", securityFilter);
+
///////////////////////////////////////////////////////////////
// ensure any joins that contribute a recordLock are present //
///////////////////////////////////////////////////////////////
- for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())))
- {
- ensureRecordSecurityLockIsRepresented(instance, tableName, recordSecurityLock);
- }
+ ensureAllJoinRecordSecurityLocksAreRepresented(instance);
+ logFilter("After ensureAllJoinRecordSecurityLocksAreRepresented:", securityFilter);
- ensureFilterIsRepresented(filter);
-
- addJoinsFromExposedJoinPaths();
-
- /* todo!!
- for(QueryJoin queryJoin : queryJoins)
- {
- QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
- for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()))
- {
- // addCriteriaForRecordSecurityLock(instance, session, joinTable, securityCriteria, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias());
- }
- }
- */
+ ////////////////////////////////////////////////////////////////////////////////////
+ // if there were any security filters built, then put those into the input filter //
+ ////////////////////////////////////////////////////////////////////////////////////
+ addSecurityFiltersToInputFilter(filter);
log("Constructed JoinsContext", logPair("mainTableName", this.mainTableName), logPair("queryJoins", this.queryJoins.stream().map(qj -> qj.getJoinTable()).collect(Collectors.joining(","))));
- log("--- END ------------------------------------------------------------------------");
+ log("", logPair("securityFilter", securityFilter));
+ log("", logPair("fullFilter", filter));
+ dumpDebug(false, true);
+ // log("--- END ------------------------------------------------------------------------");
}
/*******************************************************************************
- **
+ ** Update the input filter with any security filters that were built.
*******************************************************************************/
- private void ensureRecordSecurityLockIsRepresented(QInstance instance, String tableName, RecordSecurityLock recordSecurityLock) throws QException
+ private void addSecurityFiltersToInputFilter(QQueryFilter filter)
{
+ ////////////////////////////////////////////////////////////////////////////////////
+ // if there's no security filter criteria (including sub-filters), return w/ noop //
+ ////////////////////////////////////////////////////////////////////////////////////
+ if(CollectionUtils.nullSafeIsEmpty(securityFilter.getSubFilters()))
+ {
+ return;
+ }
+
+ ///////////////////////////////////////////////////////////////////////
+ // if the input filter is an OR we need to replace it with a new AND //
+ ///////////////////////////////////////////////////////////////////////
+ if(filter.getBooleanOperator().equals(QQueryFilter.BooleanOperator.OR))
+ {
+ List originalCriteria = filter.getCriteria();
+ List originalSubFilters = filter.getSubFilters();
+
+ QQueryFilter replacementFilter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
+ replacementFilter.setCriteria(originalCriteria);
+ replacementFilter.setSubFilters(originalSubFilters);
+
+ filter.setCriteria(new ArrayList<>());
+ filter.setSubFilters(new ArrayList<>());
+ filter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
+ filter.addSubFilter(replacementFilter);
+ }
+
+ filter.addSubFilter(securityFilter);
+ }
+
+
+
+ /*******************************************************************************
+ ** In case we've added any joins to the query that have security locks which
+ ** weren't previously added to the query, add them now. basically, this is
+ ** calling ensureRecordSecurityLockIsRepresented for each queryJoin.
+ *******************************************************************************/
+ private void ensureAllJoinRecordSecurityLocksAreRepresented(QInstance instance) throws QException
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // avoid concurrent modification exceptions by doing a double-loop and breaking the inner any time anything gets added //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ Set processedQueryJoins = new HashSet<>();
+ boolean addedAnyThisIteration = true;
+ while(addedAnyThisIteration)
+ {
+ addedAnyThisIteration = false;
+
+ for(QueryJoin queryJoin : this.queryJoins)
+ {
+ boolean addedAnyForThisJoin = false;
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // avoid double-processing the same query join //
+ // or adding security filters for a join who was only added to the query so that we could add locks (an ImplicitQueryJoinForSecurityLock) //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(processedQueryJoins.contains(queryJoin) || queryJoin instanceof ImplicitQueryJoinForSecurityLock)
+ {
+ continue;
+ }
+ processedQueryJoins.add(queryJoin);
+
+ //////////////////////////////////////////////////////////////////////////////////////////
+ // process all locks on this join's join-table. keep track if any new joins were added //
+ //////////////////////////////////////////////////////////////////////////////////////////
+ QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
+
+ MultiRecordSecurityLock multiRecordSecurityLock = RecordSecurityLockFilters.filterForReadLockTree(CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()));
+ for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
+ {
+ List addedQueryJoins = ensureRecordSecurityLockIsRepresented(joinTable.getName(), queryJoin.getJoinTableOrItsAlias(), lock, queryJoin);
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if any joins were added by this call, add them to the set of processed ones, so they don't get re-processed. //
+ // also mark the flag that any were added for this join, to manage the double-looping //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(CollectionUtils.nullSafeHasContents(addedQueryJoins))
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // make all new joins added in that method be of the same type (inner/left/etc) as the query join they are connected to //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ for(QueryJoin addedQueryJoin : addedQueryJoins)
+ {
+ addedQueryJoin.setType(queryJoin.getType());
+ }
+
+ processedQueryJoins.addAll(addedQueryJoins);
+ addedAnyForThisJoin = true;
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // if any new joins were added, we need to break the inner-loop, and continue the outer loop //
+ // e.g., to process the next query join (but we can't just go back to the foreach queryJoin, //
+ // because it would fail with concurrent modification) //
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ if(addedAnyForThisJoin)
+ {
+ addedAnyThisIteration = true;
+ break;
+ }
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** For a given recordSecurityLock on a given table (with a possible alias),
+ ** make sure that if any joins are needed to get to the lock, that they are in the query.
+ **
+ ** returns the list of query joins that were added, if any were added
+ *******************************************************************************/
+ private List ensureRecordSecurityLockIsRepresented(String tableName, String tableNameOrAlias, RecordSecurityLock recordSecurityLock, QueryJoin sourceQueryJoin) throws QException
+ {
+ List addedQueryJoins = new ArrayList<>();
+
+ ////////////////////////////////////////////////////////////////////////////
+ // if this lock is a multi-lock, then recursively process its child-locks //
+ ////////////////////////////////////////////////////////////////////////////
+ if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
+ {
+ log("Processing MultiRecordSecurityLock...");
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // make a new level in the filter-tree - storing old cursor, and updating cursor to point at new level //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////
+ QQueryFilter oldSecurityFilterCursor = this.securityFilterCursor;
+ QQueryFilter nextLevelSecurityFilter = new QQueryFilter();
+ this.securityFilterCursor.addSubFilter(nextLevelSecurityFilter);
+ this.securityFilterCursor = nextLevelSecurityFilter;
+
+ ///////////////////////////////////////
+ // set the boolean operator to match //
+ ///////////////////////////////////////
+ nextLevelSecurityFilter.setBooleanOperator(multiRecordSecurityLock.getOperator().toFilterOperator());
+
+ //////////////////////
+ // process children //
+ //////////////////////
+ for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
+ {
+ log(" - Recursive call for childLock: " + childLock);
+ addedQueryJoins.addAll(ensureRecordSecurityLockIsRepresented(tableName, tableNameOrAlias, childLock, sourceQueryJoin));
+ }
+
+ ////////////////////
+ // restore cursor //
+ ////////////////////
+ this.securityFilterCursor = oldSecurityFilterCursor;
+
+ return addedQueryJoins;
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////////////
- // ok - so - the join name chain is going to be like this: //
- // for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
+ // A join name chain is going to look like this: //
+ // for a table: orderLineItemExtrinsic (that's 2 away from order, where its security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
@@ -129,30 +318,30 @@ public class JoinsContext
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
- log("Evaluating recordSecurityLock", logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
+ log("Evaluating recordSecurityLock. Join name chain is of length: " + joinNameChain.size(), logPair("tableNameOrAlias", tableNameOrAlias), logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
- QTableMetaData tmpTable = instance.getTable(mainTableName);
+ QTableMetaData tmpTable = instance.getTable(tableName);
+ String securityFieldTableAlias = tableNameOrAlias;
+ String baseTableOrAlias = tableNameOrAlias;
+
+ boolean chainIsInner = true;
+ if(sourceQueryJoin != null && QueryJoin.Type.isOuter(sourceQueryJoin.getType()))
+ {
+ chainIsInner = false;
+ }
for(String joinName : joinNameChain)
{
- ///////////////////////////////////////////////////////////////////////////////////////////////////////
- // check the joins currently in the query - if any are for this table, then we don't need to add one //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////
- List matchingJoins = this.queryJoins.stream().filter(queryJoin ->
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // check the joins currently in the query - if any are THIS join, then we don't need to add one //
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ List matchingQueryJoins = this.queryJoins.stream().filter(queryJoin ->
{
- QJoinMetaData joinMetaData = null;
- if(queryJoin.getJoinMetaData() != null)
- {
- joinMetaData = queryJoin.getJoinMetaData();
- }
- else
- {
- joinMetaData = findJoinMetaData(instance, tableName, queryJoin.getJoinTable());
- }
+ QJoinMetaData joinMetaData = queryJoin.getJoinMetaData();
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
}).toList();
- if(CollectionUtils.nullSafeHasContents(matchingJoins))
+ if(CollectionUtils.nullSafeHasContents(matchingQueryJoins))
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - if a user added a join as an outer type, we need to change it to be inner, for the security purpose. //
@@ -160,11 +349,40 @@ public class JoinsContext
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
log("- skipping join already in the query", logPair("joinName", joinName));
- if(matchingJoins.get(0).getType().equals(QueryJoin.Type.LEFT) || matchingJoins.get(0).getType().equals(QueryJoin.Type.RIGHT))
+ QueryJoin matchedQueryJoin = matchingQueryJoins.get(0);
+
+ if(matchedQueryJoin.getType().equals(QueryJoin.Type.LEFT) || matchedQueryJoin.getType().equals(QueryJoin.Type.RIGHT))
+ {
+ chainIsInner = false;
+ }
+
+ /* ?? todo ??
+ if(matchedQueryJoin.getType().equals(QueryJoin.Type.LEFT) || matchedQueryJoin.getType().equals(QueryJoin.Type.RIGHT))
{
log("- - although... it was here as an outer - so switching it to INNER", logPair("joinName", joinName));
- matchingJoins.get(0).setType(QueryJoin.Type.INNER);
+ matchedQueryJoin.setType(QueryJoin.Type.INNER);
}
+ */
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // as we're walking from tmpTable to the table which ultimately has the security key field, //
+ // if the queryJoin we just found is joining out to tmpTable, then we need to advance tmpTable back //
+ // to the queryJoin's base table - else, tmpTable advances to the matched queryJoin's joinTable //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(tmpTable.getName().equals(matchedQueryJoin.getJoinTable()))
+ {
+ securityFieldTableAlias = Objects.requireNonNullElse(matchedQueryJoin.getBaseTableOrAlias(), mainTableName);
+ }
+ else
+ {
+ securityFieldTableAlias = matchedQueryJoin.getJoinTableOrItsAlias();
+ }
+ tmpTable = instance.getTable(securityFieldTableAlias);
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // set the baseTableOrAlias for the next iteration to be this join's joinTableOrAlias //
+ ////////////////////////////////////////////////////////////////////////////////////////
+ baseTableOrAlias = securityFieldTableAlias;
continue;
}
@@ -172,20 +390,233 @@ public class JoinsContext
QJoinMetaData join = instance.getJoin(joinName);
if(join.getLeftTable().equals(tmpTable.getName()))
{
- QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
- this.addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)");
+ securityFieldTableAlias = join.getRightTable() + "_forSecurityJoin_" + join.getName();
+ QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock()
+ .withJoinMetaData(join)
+ .withType(chainIsInner ? QueryJoin.Type.INNER : QueryJoin.Type.LEFT)
+ .withBaseTableOrAlias(baseTableOrAlias)
+ .withAlias(securityFieldTableAlias);
+
+ if(securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR)
+ {
+ queryJoin.withType(QueryJoin.Type.LEFT);
+ chainIsInner = false;
+ }
+
+ addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)", "- ");
+ addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
- QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
- this.addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)");
+ securityFieldTableAlias = join.getLeftTable() + "_forSecurityJoin_" + join.getName();
+ QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock()
+ .withJoinMetaData(join.flip())
+ .withType(chainIsInner ? QueryJoin.Type.INNER : QueryJoin.Type.LEFT)
+ .withBaseTableOrAlias(baseTableOrAlias)
+ .withAlias(securityFieldTableAlias);
+
+ if(securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR)
+ {
+ queryJoin.withType(QueryJoin.Type.LEFT);
+ chainIsInner = false;
+ }
+
+ addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)", "- ");
+ addedQueryJoins.add(queryJoin);
tmpTable = instance.getTable(join.getLeftTable());
}
else
{
+ dumpDebug(false, true);
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
}
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // for the next iteration of the loop, set the next join's baseTableOrAlias to be the alias we just created //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ baseTableOrAlias = securityFieldTableAlias;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////
+ // now that we know the joins/tables are in the query, add to the security filter //
+ ////////////////////////////////////////////////////////////////////////////////////
+ QueryJoin lastAddedQueryJoin = addedQueryJoins.isEmpty() ? null : addedQueryJoins.get(addedQueryJoins.size() - 1);
+ if(sourceQueryJoin != null && lastAddedQueryJoin == null)
+ {
+ lastAddedQueryJoin = sourceQueryJoin;
+ }
+ addSubFilterForRecordSecurityLock(recordSecurityLock, tmpTable, securityFieldTableAlias, !chainIsInner, lastAddedQueryJoin);
+
+ log("Finished evaluating recordSecurityLock");
+
+ return (addedQueryJoins);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void addSubFilterForRecordSecurityLock(RecordSecurityLock recordSecurityLock, QTableMetaData table, String tableNameOrAlias, boolean isOuter, QueryJoin sourceQueryJoin)
+ {
+ QSession session = QContext.getQSession();
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // check if the key type has an all-access key, and if so, if it's set to true for the current user/session //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ QSecurityKeyType securityKeyType = instance.getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
+ boolean haveAllAccessKey = false;
+ if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if we have all-access on this key, then we don't need a criterion for it (as long as we're in an AND filter) //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
+ {
+ haveAllAccessKey = true;
+
+ if(sourceQueryJoin != null)
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // in case the queryJoin object is re-used between queries, and its security criteria need to be different (!!), reset it //
+ // this can be exposed in tests - maybe not entirely expected in real-world, but seems safe enough //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ sourceQueryJoin.withSecurityCriteria(new ArrayList<>());
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // if we're in an AND filter, then we don't need a criteria for this lock, so return. //
+ ////////////////////////////////////////////////////////////////////////////////////////
+ boolean inAnAndFilter = securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.AND;
+ if(inAnAndFilter)
+ {
+ return;
+ }
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // for locks w/o a join chain, the lock fieldName will simply be a field on the table. //
+ // so just prepend that with the tableNameOrAlias. //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName();
+ if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
+ {
+ /////////////////////////////////////////////////////////////////////////////////
+ // else, expect a "table.field" in the lock fieldName - but we want to replace //
+ // the table name part with a possible alias that we took in. //
+ /////////////////////////////////////////////////////////////////////////////////
+ String[] parts = recordSecurityLock.getFieldName().split("\\.");
+ if(parts.length != 2)
+ {
+ dumpDebug(false, true);
+ throw new IllegalArgumentException("Mal-formatted recordSecurityLock fieldName for lock with joinNameChain in query: " + fieldName);
+ }
+ fieldName = tableNameOrAlias + "." + parts[1];
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // else - get the key values from the session and decide what kind of criterion to build //
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ QQueryFilter lockFilter = new QQueryFilter();
+ List lockCriteria = new ArrayList<>();
+ lockFilter.setCriteria(lockCriteria);
+
+ QFieldType type = QFieldType.INTEGER;
+ try
+ {
+ JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = getFieldAndTableNameOrAlias(fieldName);
+ type = fieldAndTableNameOrAlias.field().getType();
+ }
+ catch(Exception e)
+ {
+ LOG.debug("Error getting field type... Trying Integer", e);
+ }
+
+ if(haveAllAccessKey)
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // if we have an all access key (but we got here because we're part of an OR query), then //
+ // write a criterion that will always be true - e.g., field=field //
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.TRUE));
+ }
+ else
+ {
+ List securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
+ if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // handle user with no values -- they can only see null values, and only iff the lock's null-value behavior is ALLOW //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
+ {
+ lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
+ }
+ else
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // else, if no user/session values, and null-value behavior is deny, then setup a FALSE condition, to allow no rows. //
+ // todo - maybe avoid running the whole query - as you're not allowed ANY records (based on boolean tree down to this point) //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.FALSE));
+ }
+ }
+ else
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // else, if user/session has some values, build an IN rule - //
+ // noting that if the lock's null-value behavior is ALLOW, then we actually want IS_NULL_OR_IN, not just IN //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
+ {
+ lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues));
+ }
+ else
+ {
+ lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, securityKeyValues));
+ }
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if there's a sourceQueryJoin, then set the lockCriteria on that join - so it gets written into the JOIN ... ON clause //
+ // ... unless we're writing an OR filter. then we need the condition in the WHERE clause //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ boolean doNotPutCriteriaInJoinOn = securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR;
+ if(sourceQueryJoin != null && !doNotPutCriteriaInJoinOn)
+ {
+ sourceQueryJoin.withSecurityCriteria(lockCriteria);
+ }
+ else
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // we used to add an OR IS NULL for cases of an outer-join - but instead, this is now handled by putting the lockCriteria //
+ // into the join (see above) - so this check is probably deprecated. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ /*
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if this field is on the outer side of an outer join, then if we do a straight filter on it, then we're basically //
+ // nullifying the outer join... so for an outer join use-case, OR the security field criteria with a primary-key IS NULL //
+ // which will make missing rows from the join be found. //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(isOuter)
+ {
+ if(table == null)
+ {
+ table = QContext.getQInstance().getTable(aliasToTableNameMap.get(tableNameOrAlias));
+ }
+
+ lockFilter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
+ lockFilter.addCriteria(new QFilterCriteria(tableNameOrAlias + "." + table.getPrimaryKeyField(), QCriteriaOperator.IS_BLANK));
+ }
+ */
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // If this filter isn't for a queryJoin, then just add it to the main list of security sub-filters //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ this.securityFilterCursor.addSubFilter(lockFilter);
}
}
@@ -197,9 +628,9 @@ public class JoinsContext
** use this method to add to the list, instead of ever adding directly, as it's
** important do to that process step (and we've had bugs when it wasn't done).
*******************************************************************************/
- private void addQueryJoin(QueryJoin queryJoin, String reason) throws QException
+ private void addQueryJoin(QueryJoin queryJoin, String reason, String logPrefix) throws QException
{
- log("Adding query join to context",
+ log(Objects.requireNonNullElse(logPrefix, "") + "Adding query join to context",
logPair("reason", reason),
logPair("joinTable", queryJoin.getJoinTable()),
logPair("joinMetaData.name", () -> queryJoin.getJoinMetaData().getName()),
@@ -208,34 +639,46 @@ public class JoinsContext
);
this.queryJoins.add(queryJoin);
processQueryJoin(queryJoin);
+ dumpDebug(false, false);
}
/*******************************************************************************
** If there are any joins in the context that don't have a join meta data, see
- ** if we can find the JoinMetaData to use for them by looking at the main table's
- ** exposed joins, and using their join paths.
+ ** if we can find the JoinMetaData to use for them by looking at all joins in the
+ ** instance, or at the main table's exposed joins, and using their join paths.
*******************************************************************************/
- private void addJoinsFromExposedJoinPaths() throws QException
+ private void fillInMissingJoinMetaData() throws QException
{
+ log("Begin adding missing join meta data");
+
////////////////////////////////////////////////////////////////////////////////
// do a double-loop, to avoid concurrent modification on the queryJoins list. //
// that is to say, we'll loop over that list, but possibly add things to it, //
// in which case we'll set this flag, and break the inner loop, to go again. //
////////////////////////////////////////////////////////////////////////////////
- boolean addedJoin;
+ Set processedQueryJoins = new HashSet<>();
+ boolean addedJoin;
do
{
addedJoin = false;
for(QueryJoin queryJoin : queryJoins)
{
+ if(processedQueryJoins.contains(queryJoin))
+ {
+ continue;
+ }
+ processedQueryJoins.add(queryJoin);
+
///////////////////////////////////////////////////////////////////////////////////////////////
// if the join has joinMetaData, then we don't need to process it... unless it needs flipped //
///////////////////////////////////////////////////////////////////////////////////////////////
QJoinMetaData joinMetaData = queryJoin.getJoinMetaData();
if(joinMetaData != null)
{
+ log("- QueryJoin already has joinMetaData", logPair("joinMetaDataName", joinMetaData.getName()));
+
boolean isJoinLeftTableInQuery = false;
String joinMetaDataLeftTable = joinMetaData.getLeftTable();
if(joinMetaDataLeftTable.equals(mainTableName))
@@ -265,7 +708,7 @@ public class JoinsContext
/////////////////////////////////////////////////////////////////////////////////
if(!isJoinLeftTableInQuery)
{
- log("Flipping queryJoin because its leftTable wasn't found in the query", logPair("joinMetaDataName", joinMetaData.getName()), logPair("leftTable", joinMetaDataLeftTable));
+ log("- - Flipping queryJoin because its leftTable wasn't found in the query", logPair("joinMetaDataName", joinMetaData.getName()), logPair("leftTable", joinMetaDataLeftTable));
queryJoin.setJoinMetaData(joinMetaData.flip());
}
}
@@ -275,11 +718,13 @@ public class JoinsContext
// try to find a direct join between the main table and this table. //
// if one is found, then put it (the meta data) on the query join. //
//////////////////////////////////////////////////////////////////////
+ log("- QueryJoin doesn't have metaData - looking for it", logPair("joinTableOrItsAlias", queryJoin.getJoinTableOrItsAlias()));
+
String baseTableName = Objects.requireNonNullElse(resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), mainTableName);
- QJoinMetaData found = findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
+ QJoinMetaData found = findJoinMetaData(baseTableName, queryJoin.getJoinTable(), true);
if(found != null)
{
- log("Found joinMetaData - setting it in queryJoin", logPair("joinMetaDataName", found.getName()), logPair("baseTableName", baseTableName), logPair("joinTable", queryJoin.getJoinTable()));
+ log("- - Found joinMetaData - setting it in queryJoin", logPair("joinMetaDataName", found.getName()), logPair("baseTableName", baseTableName), logPair("joinTable", queryJoin.getJoinTable()));
queryJoin.setJoinMetaData(found);
}
else
@@ -293,7 +738,7 @@ public class JoinsContext
{
if(queryJoin.getJoinTable().equals(exposedJoin.getJoinTable()))
{
- log("Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
+ log("- - Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
/////////////////////////////////////////////////////////////////////////////////////
// loop backward through the join path (from the joinTable back to the main table) //
@@ -304,6 +749,7 @@ public class JoinsContext
{
String joinName = exposedJoin.getJoinPath().get(i);
QJoinMetaData joinToAdd = instance.getJoin(joinName);
+ log("- - - evaluating joinPath element", logPair("i", i), logPair("joinName", joinName));
/////////////////////////////////////////////////////////////////////////////
// get the name from the opposite side of the join (flipping it if needed) //
@@ -332,15 +778,22 @@ public class JoinsContext
queryJoin.setBaseTableOrAlias(nextTable);
}
queryJoin.setJoinMetaData(joinToAdd);
+ log("- - - - this is the last element in the join path, so setting this joinMetaData on the original queryJoin");
}
else
{
QueryJoin queryJoinToAdd = makeQueryJoinFromJoinAndTableNames(nextTable, tmpTable, joinToAdd);
queryJoinToAdd.setType(queryJoin.getType());
addedAnyQueryJoins = true;
- this.addQueryJoin(queryJoinToAdd, "forExposedJoin");
+ log("- - - - this is not the last element in the join path, so adding a new query join:");
+ addQueryJoin(queryJoinToAdd, "forExposedJoin", "- - - - - - ");
+ dumpDebug(false, false);
}
}
+ else
+ {
+ log("- - - - join doesn't need added to the query");
+ }
tmpTable = nextTable;
}
@@ -361,6 +814,7 @@ public class JoinsContext
}
while(addedJoin);
+ log("Done adding missing join meta data");
}
@@ -370,12 +824,12 @@ public class JoinsContext
*******************************************************************************/
private boolean doesJoinNeedAddedToQuery(String joinName)
{
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // look at all queryJoins already in context - if any have this join's name, then we don't need this join... //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // look at all queryJoins already in context - if any have this join's name, and aren't implicit-security joins, then we don't need this join... //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QueryJoin queryJoin : queryJoins)
{
- if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName))
+ if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName) && !(queryJoin instanceof ImplicitQueryJoinForSecurityLock))
{
return (false);
}
@@ -395,6 +849,7 @@ public class JoinsContext
String tableNameOrAlias = queryJoin.getJoinTableOrItsAlias();
if(aliasToTableNameMap.containsKey(tableNameOrAlias))
{
+ dumpDebug(false, true);
throw (new QException("Duplicate table name or alias: " + tableNameOrAlias));
}
aliasToTableNameMap.put(tableNameOrAlias, joinTable.getName());
@@ -439,6 +894,7 @@ public class JoinsContext
String[] parts = fieldName.split("\\.");
if(parts.length != 2)
{
+ dumpDebug(false, true);
throw new IllegalArgumentException("Mal-formatted field name in query: " + fieldName);
}
@@ -449,6 +905,7 @@ public class JoinsContext
QTableMetaData table = instance.getTable(tableName);
if(table == null)
{
+ dumpDebug(false, true);
throw new IllegalArgumentException("Could not find table [" + tableName + "] in instance for query");
}
return new FieldAndTableNameOrAlias(table.getField(baseFieldName), tableOrAlias);
@@ -503,17 +960,17 @@ public class JoinsContext
for(String filterTable : filterTables)
{
- log("Evaluating filterTable", logPair("filterTable", filterTable));
+ log("Evaluating filter", logPair("filterTable", filterTable));
if(!aliasToTableNameMap.containsKey(filterTable) && !Objects.equals(mainTableName, filterTable))
{
- log("- table not in query - adding it", logPair("filterTable", filterTable));
+ log("- table not in query - adding a join for it", logPair("filterTable", filterTable));
boolean found = false;
for(QJoinMetaData join : CollectionUtils.nonNullMap(QContext.getQInstance().getJoins()).values())
{
QueryJoin queryJoin = makeQueryJoinFromJoinAndTableNames(mainTableName, filterTable, join);
if(queryJoin != null)
{
- this.addQueryJoin(queryJoin, "forFilter (join found in instance)");
+ addQueryJoin(queryJoin, "forFilter (join found in instance)", "- - ");
found = true;
break;
}
@@ -522,9 +979,13 @@ public class JoinsContext
if(!found)
{
QueryJoin queryJoin = new QueryJoin().withJoinTable(filterTable).withType(QueryJoin.Type.INNER);
- this.addQueryJoin(queryJoin, "forFilter (join not found in instance)");
+ addQueryJoin(queryJoin, "forFilter (join not found in instance)", "- - ");
}
}
+ else
+ {
+ log("- table is already in query - not adding any joins", logPair("filterTable", filterTable));
+ }
}
}
@@ -566,6 +1027,11 @@ public class JoinsContext
getTableNameFromFieldNameAndAddToSet(criteria.getOtherFieldName(), filterTables);
}
+ for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(filter.getOrderBys()))
+ {
+ getTableNameFromFieldNameAndAddToSet(orderBy.getFieldName(), filterTables);
+ }
+
for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters()))
{
populateFilterTablesSet(subFilter, filterTables);
@@ -592,7 +1058,7 @@ public class JoinsContext
/*******************************************************************************
**
*******************************************************************************/
- public QJoinMetaData findJoinMetaData(QInstance instance, String baseTableName, String joinTableName)
+ public QJoinMetaData findJoinMetaData(String baseTableName, String joinTableName, boolean useExposedJoins)
{
List matches = new ArrayList<>();
if(baseTableName != null)
@@ -644,7 +1110,29 @@ public class JoinsContext
}
else if(matches.size() > 1)
{
- throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "]. Specify which one in your QueryJoin."));
+ ////////////////////////////////////////////////////////////////////////////////
+ // if we found more than one join, but we're allowed to useExposedJoins, then //
+ // see if we can tell which match to used based on the table's exposed joins //
+ ////////////////////////////////////////////////////////////////////////////////
+ if(useExposedJoins)
+ {
+ QTableMetaData mainTable = QContext.getQInstance().getTable(mainTableName);
+ for(ExposedJoin exposedJoin : mainTable.getExposedJoins())
+ {
+ if(exposedJoin.getJoinTable().equals(joinTableName))
+ {
+ // todo ... is it wrong to always use 0??
+ return instance.getJoin(exposedJoin.getJoinPath().get(0));
+ }
+ }
+ }
+
+ ///////////////////////////////////////////////
+ // if we couldn't figure it out, then throw. //
+ ///////////////////////////////////////////////
+ dumpDebug(false, true);
+ throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "] "
+ + (useExposedJoins ? "(and exposed joins didn't clarify which one to use). " : "") + "Specify which one in your QueryJoin."));
}
return (null);
@@ -669,4 +1157,79 @@ public class JoinsContext
LOG.log(logLevel, message, null, logPairs);
}
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void logFilter(String message, QQueryFilter filter)
+ {
+ if(logLevelForFilter.equals(Level.OFF))
+ {
+ return;
+ }
+ System.out.println(message + "\n" + filter);
+ }
+
+
+
+ /*******************************************************************************
+ ** Print (to stdout, for easier reading) the object in a big table format for
+ ** debugging. Happens any time logLevel is > OFF. Not meant for loggly.
+ *******************************************************************************/
+ private void dumpDebug(boolean isStart, boolean isEnd)
+ {
+ if(logLevel.equals(Level.OFF))
+ {
+ return;
+ }
+
+ int sm = 8;
+ int md = 30;
+ int lg = 50;
+ int overhead = 14;
+ int full = sm + 3 * md + lg + overhead;
+
+ if(isStart)
+ {
+ System.out.println("\n" + StringUtils.safeTruncate("--- Start [main table: " + this.mainTableName + "] " + "-".repeat(full), full));
+ }
+
+ StringBuilder rs = new StringBuilder();
+ String formatString = "| %-" + md + "s | %-" + md + "s %-" + md + "s | %-" + lg + "s | %-" + sm + "s |\n";
+ rs.append(String.format(formatString, "Base Table", "Join Table", "(Alias)", "Join Meta Data", "Type"));
+ String dashesLg = "-".repeat(lg);
+ String dashesMd = "-".repeat(md);
+ String dashesSm = "-".repeat(sm);
+ rs.append(String.format(formatString, dashesMd, dashesMd, dashesMd, dashesLg, dashesSm));
+ if(CollectionUtils.nullSafeHasContents(queryJoins))
+ {
+ for(QueryJoin queryJoin : queryJoins)
+ {
+ rs.append(String.format(
+ formatString,
+ StringUtils.hasContent(queryJoin.getBaseTableOrAlias()) ? StringUtils.safeTruncate(queryJoin.getBaseTableOrAlias(), md) : "--",
+ StringUtils.safeTruncate(queryJoin.getJoinTable(), md),
+ (StringUtils.hasContent(queryJoin.getAlias()) ? "(" + StringUtils.safeTruncate(queryJoin.getAlias(), md - 2) + ")" : ""),
+ queryJoin.getJoinMetaData() == null ? "--" : StringUtils.safeTruncate(queryJoin.getJoinMetaData().getName(), lg),
+ queryJoin.getType()));
+ }
+ }
+ else
+ {
+ rs.append(String.format(formatString, "-empty-", "", "", "", ""));
+ }
+
+ System.out.print(rs);
+
+ System.out.println(securityFilter);
+
+ if(isEnd)
+ {
+ System.out.println(StringUtils.safeTruncate("--- End " + "-".repeat(full), full) + "\n");
+ }
+ else
+ {
+ System.out.println(StringUtils.safeTruncate("-".repeat(full), full));
+ }
+ }
}
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 6dc50b1d..99498007 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
@@ -49,5 +49,7 @@ public enum QCriteriaOperator
IS_BLANK,
IS_NOT_BLANK,
BETWEEN,
- NOT_BETWEEN
+ NOT_BETWEEN,
+ TRUE,
+ FALSE
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
index bf14f05f..118aacbf 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
@@ -306,6 +306,11 @@ public class QFilterCriteria implements Serializable, Cloneable
@Override
public String toString()
{
+ if(fieldName == null)
+ {
+ return ("");
+ }
+
StringBuilder rs = new StringBuilder(fieldName);
try
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
index fffd3c9e..0ed4544d 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
@@ -138,7 +138,7 @@ public class QQueryFilter implements Serializable, Cloneable
/*******************************************************************************
- **
+ ** recursively look at both this filter, and any sub-filters it may have.
*******************************************************************************/
public boolean hasAnyCriteria()
{
@@ -151,7 +151,7 @@ public class QQueryFilter implements Serializable, Cloneable
{
for(QQueryFilter subFilter : subFilters)
{
- if(subFilter.hasAnyCriteria())
+ if(subFilter != null && subFilter.hasAnyCriteria())
{
return (true);
}
@@ -361,23 +361,44 @@ public class QQueryFilter implements Serializable, Cloneable
StringBuilder rs = new StringBuilder("(");
try
{
+ int criteriaIndex = 0;
for(QFilterCriteria criterion : CollectionUtils.nonNullList(criteria))
{
- rs.append(criterion).append(" ").append(getBooleanOperator()).append(" ");
+ if(criteriaIndex > 0)
+ {
+ rs.append(" ").append(getBooleanOperator()).append(" ");
+ }
+ rs.append(criterion);
+ criteriaIndex++;
}
- for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
+ if(CollectionUtils.nullSafeHasContents(subFilters))
{
- rs.append(subFilter);
+ rs.append("Sub:{");
+ int subIndex = 0;
+ for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
+ {
+ if(subIndex > 0)
+ {
+ rs.append(" ").append(getBooleanOperator()).append(" ");
+ }
+ rs.append(subFilter);
+ subIndex++;
+ }
+ rs.append("}");
}
+
rs.append(")");
- rs.append("OrderBy[");
- for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
+ if(CollectionUtils.nullSafeHasContents(orderBys))
{
- rs.append(orderBy).append(",");
+ rs.append("OrderBy[");
+ for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
+ {
+ rs.append(orderBy).append(",");
+ }
+ rs.append("]");
}
- rs.append("]");
}
catch(Exception e)
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java
index c1e103e3..a2ef66ad 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java
@@ -22,6 +22,8 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -49,6 +51,10 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
** specific joinMetaData to use must be set. The joinMetaData field can also be
** used instead of specify joinTable and baseTableOrAlias, but only for cases
** where the baseTable is not an alias.
+ **
+ ** The securityCriteria member, in general, is meant to be populated when a
+ ** JoinsContext is constructed before executing a query, and not meant to be set
+ ** by users.
*******************************************************************************/
public class QueryJoin
{
@@ -59,13 +65,30 @@ public class QueryJoin
private boolean select = false;
private Type type = Type.INNER;
+ private List securityCriteria = new ArrayList<>();
+
/*******************************************************************************
- **
+ ** define the types of joins - INNER, LEFT, RIGHT, or FULL.
*******************************************************************************/
public enum Type
- {INNER, LEFT, RIGHT, FULL}
+ {
+ INNER,
+ LEFT,
+ RIGHT,
+ FULL;
+
+
+
+ /*******************************************************************************
+ ** check if a join is an OUTER type (LEFT or RIGHT).
+ *******************************************************************************/
+ public static boolean isOuter(Type type)
+ {
+ return (LEFT == type || RIGHT == type);
+ }
+ }
@@ -348,4 +371,50 @@ public class QueryJoin
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for securityCriteria
+ *******************************************************************************/
+ public List getSecurityCriteria()
+ {
+ return (this.securityCriteria);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for securityCriteria
+ *******************************************************************************/
+ public void setSecurityCriteria(List securityCriteria)
+ {
+ this.securityCriteria = securityCriteria;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for securityCriteria
+ *******************************************************************************/
+ public QueryJoin withSecurityCriteria(List securityCriteria)
+ {
+ this.securityCriteria = securityCriteria;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for securityCriteria
+ *******************************************************************************/
+ public QueryJoin withSecurityCriteria(QFilterCriteria securityCriteria)
+ {
+ if(this.securityCriteria == null)
+ {
+ this.securityCriteria = new ArrayList<>();
+ }
+ this.securityCriteria.add(securityCriteria);
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/MultiRecordSecurityLock.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/MultiRecordSecurityLock.java
new file mode 100644
index 00000000..04cb945b
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/MultiRecordSecurityLock.java
@@ -0,0 +1,198 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.security;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+
+
+/*******************************************************************************
+ ** Subclass of RecordSecurityLock, for combining multiple locks using a boolean
+ ** (AND/OR) condition. Note that the combined locks can themselves also be
+ ** Multi-locks, thus creating a tree of locks.
+ *******************************************************************************/
+public class MultiRecordSecurityLock extends RecordSecurityLock implements Cloneable
+{
+ private List locks = new ArrayList<>();
+ private BooleanOperator operator;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ protected MultiRecordSecurityLock clone() throws CloneNotSupportedException
+ {
+ MultiRecordSecurityLock clone = (MultiRecordSecurityLock) super.clone();
+
+ /////////////////////////
+ // deep-clone the list //
+ /////////////////////////
+ if(locks != null)
+ {
+ clone.locks = new ArrayList<>();
+ for(RecordSecurityLock lock : locks)
+ {
+ clone.locks.add(lock.clone());
+ }
+ }
+
+ return (clone);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public enum BooleanOperator
+ {
+ AND,
+ OR;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QQueryFilter.BooleanOperator toFilterOperator()
+ {
+ return switch(this)
+ {
+ case AND -> QQueryFilter.BooleanOperator.AND;
+ case OR -> QQueryFilter.BooleanOperator.OR;
+ };
+ }
+ }
+
+
+
+ ////////////////////////////////
+ // todo - remove, this is POC //
+ ////////////////////////////////
+ static
+ {
+ new QTableMetaData()
+ .withName("savedReport")
+ .withRecordSecurityLock(new MultiRecordSecurityLock()
+ .withLocks(List.of(
+ new RecordSecurityLock()
+ .withFieldName("userId")
+ .withSecurityKeyType("user")
+ .withNullValueBehavior(NullValueBehavior.DENY)
+ .withLockScope(LockScope.READ_AND_WRITE),
+ new RecordSecurityLock()
+ .withFieldName("sharedReport.userId")
+ .withJoinNameChain(List.of("reportJoinSharedReport"))
+ .withSecurityKeyType("user")
+ .withNullValueBehavior(NullValueBehavior.DENY)
+ .withLockScope(LockScope.READ_AND_WRITE), // dynamic, from a value...
+ new RecordSecurityLock()
+ .withFieldName("sharedReport.groupId")
+ .withJoinNameChain(List.of("reportJoinSharedReport"))
+ .withSecurityKeyType("group")
+ .withNullValueBehavior(NullValueBehavior.DENY)
+ .withLockScope(LockScope.READ_AND_WRITE) // dynamic, from a value...
+ )));
+
+ }
+
+ /*******************************************************************************
+ ** Getter for locks
+ *******************************************************************************/
+ public List getLocks()
+ {
+ return (this.locks);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for locks
+ *******************************************************************************/
+ public void setLocks(List locks)
+ {
+ this.locks = locks;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for locks
+ *******************************************************************************/
+ public MultiRecordSecurityLock withLocks(List locks)
+ {
+ this.locks = locks;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluently add one lock
+ *******************************************************************************/
+ public MultiRecordSecurityLock withLock(RecordSecurityLock lock)
+ {
+ if(this.locks == null)
+ {
+ this.locks = new ArrayList<>();
+ }
+ this.locks.add(lock);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for operator
+ *******************************************************************************/
+ public BooleanOperator getOperator()
+ {
+ return (this.operator);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for operator
+ *******************************************************************************/
+ public void setOperator(BooleanOperator operator)
+ {
+ this.operator = operator;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for operator
+ *******************************************************************************/
+ public MultiRecordSecurityLock withOperator(BooleanOperator operator)
+ {
+ this.operator = operator;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java
index b16deac0..9902307a 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.security;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -41,7 +42,7 @@ import java.util.Map;
** - READ_AND_WRITE means that users cannot read or write records without a valid key.
** - WRITE means that users cannot write records without a valid key (but they can read them).
*******************************************************************************/
-public class RecordSecurityLock
+public class RecordSecurityLock implements Cloneable
{
private String securityKeyType;
private String fieldName;
@@ -52,6 +53,28 @@ public class RecordSecurityLock
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ protected RecordSecurityLock clone() throws CloneNotSupportedException
+ {
+ RecordSecurityLock clone = (RecordSecurityLock) super.clone();
+
+ /////////////////////////
+ // deep-clone the list //
+ /////////////////////////
+ if(joinNameChain != null)
+ {
+ clone.joinNameChain = new ArrayList<>();
+ clone.joinNameChain.addAll(joinNameChain);
+ }
+
+ return (clone);
+ }
+
+
+
/*******************************************************************************
** Constructor
**
@@ -265,4 +288,22 @@ public class RecordSecurityLock
return (this);
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ return "RecordSecurityLock{"
+ + "securityKeyType='" + securityKeyType + '\''
+ + ", fieldName='" + fieldName + '\''
+ + ", joinNameChain=" + joinNameChain
+ + ", nullValueBehavior=" + nullValueBehavior
+ + ", lockScope=" + lockScope
+ + '}';
+ }
+
}
+
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLockFilters.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLockFilters.java
index c8c7e9dc..4d5ad71b 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLockFilters.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLockFilters.java
@@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.security;
import java.util.List;
+import java.util.Set;
/*******************************************************************************
@@ -46,6 +47,65 @@ public class RecordSecurityLockFilters
+ /*******************************************************************************
+ ** filter a list of locks so that we only see the ones that apply to reads.
+ *******************************************************************************/
+ public static MultiRecordSecurityLock filterForReadLockTree(List recordSecurityLocks)
+ {
+ return filterForLockTree(recordSecurityLocks, Set.of(RecordSecurityLock.LockScope.READ_AND_WRITE));
+ }
+
+
+
+ /*******************************************************************************
+ ** filter a list of locks so that we only see the ones that apply to writes.
+ *******************************************************************************/
+ public static MultiRecordSecurityLock filterForWriteLockTree(List recordSecurityLocks)
+ {
+ return filterForLockTree(recordSecurityLocks, Set.of(RecordSecurityLock.LockScope.READ_AND_WRITE, RecordSecurityLock.LockScope.WRITE));
+ }
+
+
+
+ /*******************************************************************************
+ ** filter a list of locks so that we only see the ones that apply to any of the
+ ** input set of scopes.
+ *******************************************************************************/
+ private static MultiRecordSecurityLock filterForLockTree(List recordSecurityLocks, Set allowedScopes)
+ {
+ if(recordSecurityLocks == null)
+ {
+ return (null);
+ }
+
+ //////////////////////////////////////////////////////////////
+ // at the top-level we build a multi-lock with AND operator //
+ //////////////////////////////////////////////////////////////
+ MultiRecordSecurityLock result = new MultiRecordSecurityLock();
+ result.setOperator(MultiRecordSecurityLock.BooleanOperator.AND);
+
+ for(RecordSecurityLock recordSecurityLock : recordSecurityLocks)
+ {
+ if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
+ {
+ MultiRecordSecurityLock filteredSubLock = filterForReadLockTree(multiRecordSecurityLock.getLocks());
+ filteredSubLock.setOperator(multiRecordSecurityLock.getOperator());
+ result.withLock(filteredSubLock);
+ }
+ else
+ {
+ if(allowedScopes.contains(recordSecurityLock.getLockScope()))
+ {
+ result.withLock(recordSecurityLock);
+ }
+ }
+ }
+
+ return (result);
+ }
+
+
+
/*******************************************************************************
** filter a list of locks so that we only see the ones that apply to writes.
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
index 7909963a..d7d395fc 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
@@ -177,6 +177,14 @@ public class MemoryRecordStore
for(QRecord qRecord : tableData)
{
+ if(qRecord.getTableName() == null)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // internally, doesRecordMatch likes to know table names on records, so, set if missing. //
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ qRecord.setTableName(input.getTableName());
+ }
+
boolean recordMatches = BackendQueryFilterUtils.doesRecordMatch(input.getFilter(), qRecord);
if(recordMatches)
@@ -232,16 +240,7 @@ public class MemoryRecordStore
{
QTableMetaData nextTable = qInstance.getTable(queryJoin.getJoinTable());
Collection nextTableRecords = getTableData(nextTable).values();
-
- QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () ->
- {
- QJoinMetaData found = joinsContext.findJoinMetaData(qInstance, input.getTableName(), queryJoin.getJoinTable());
- if(found == null)
- {
- throw (new RuntimeException("Could not find a join between tables [" + input.getTableName() + "][" + queryJoin.getJoinTable() + "]"));
- }
- return (found);
- });
+ QJoinMetaData joinMetaData = Objects.requireNonNull(queryJoin.getJoinMetaData(), () -> "Could not find a join between tables [" + leftTable + "][" + queryJoin.getJoinTable() + "]");
List nextLevelProduct = new ArrayList<>();
for(QRecord productRecord : crossProduct)
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 86fa2191..e8b09615 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
@@ -78,14 +78,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]);
}
}
}
@@ -177,6 +179,8 @@ public class BackendQueryFilterUtils
boolean between = (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
yield !between;
}
+ case TRUE -> true;
+ case FALSE -> false;
};
return criterionMatches;
}
@@ -203,12 +207,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);
@@ -217,6 +222,7 @@ public class BackendQueryFilterUtils
else
{
accumulatorValue |= newValue;
+ accumulator.set(accumulatorValue);
if(accumulatorValue)
{
return (true);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java
index ce227961..516f7d6a 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/AbstractExtractStep.java
@@ -112,4 +112,17 @@ public abstract class AbstractExtractStep implements BackendStep
this.limit = limit;
}
+
+
+ /*******************************************************************************
+ ** Create the record pipe to be used for this process step.
+ **
+ ** Here in case a subclass needs a different type of pipe - for example, a
+ ** DistinctFilteringRecordPipe.
+ *******************************************************************************/
+ public RecordPipe createRecordPipe(RunBackendStepInput runBackendStepInput, Integer overrideCapacity)
+ {
+ return (new RecordPipe(overrideCapacity));
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java
index 22ab5f17..777d3f72 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ExtractViaQueryStep.java
@@ -24,8 +24,14 @@ package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwit
import java.io.IOException;
import java.io.Serializable;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.actions.reporting.DistinctFilteringRecordPipe;
+import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@@ -36,9 +42,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
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.QFilterOrderBy;
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.QueryJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -105,6 +115,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep
QueryInput queryInput = new QueryInput();
queryInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE));
queryInput.setFilter(filterClone);
+ getQueryJoinsForOrderByIfNeeded(queryFilter).forEach(queryJoin -> queryInput.withQueryJoin(queryJoin));
queryInput.setSelectDistinct(true);
queryInput.setRecordPipe(getRecordPipe());
queryInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback());
@@ -139,6 +150,45 @@ public class ExtractViaQueryStep extends AbstractExtractStep
+ /*******************************************************************************
+ ** If the queryFilter has order-by fields from a joinTable, then create QueryJoins
+ ** for each such table - marked as LEFT, and select=true.
+ **
+ ** This is under the rationale that, the filter would have come from the frontend,
+ ** which would be doing outer-join semantics for a column being shown (but not filtered by).
+ ** If the table IS filtered by, it's still OK to do a LEFT, as we'll only get rows
+ ** that match.
+ **
+ ** Also, they are being select=true'ed so that the DISTINCT clause works (since
+ ** process queries always try to be DISTINCT).
+ *******************************************************************************/
+ private List getQueryJoinsForOrderByIfNeeded(QQueryFilter queryFilter)
+ {
+ if(queryFilter == null)
+ {
+ return (Collections.emptyList());
+ }
+
+ List rs = new ArrayList<>();
+ Set addedTables = new HashSet<>();
+ for(QFilterOrderBy filterOrderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys()))
+ {
+ if(filterOrderBy.getFieldName().contains("."))
+ {
+ String tableName = filterOrderBy.getFieldName().split("\\.")[0];
+ if(!addedTables.contains(tableName))
+ {
+ rs.add(new QueryJoin(tableName).withType(QueryJoin.Type.LEFT).withSelect(true));
+ }
+ addedTables.add(tableName);
+ }
+ }
+
+ return (rs);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -148,6 +198,7 @@ public class ExtractViaQueryStep extends AbstractExtractStep
CountInput countInput = new CountInput();
countInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE));
countInput.setFilter(queryFilter);
+ getQueryJoinsForOrderByIfNeeded(queryFilter).forEach(queryJoin -> countInput.withQueryJoin(queryJoin));
countInput.setIncludeDistinctCount(true);
CountOutput countOutput = new CountAction().execute(countInput);
Integer count = countOutput.getDistinctCount();
@@ -247,4 +298,33 @@ public class ExtractViaQueryStep extends AbstractExtractStep
}
}
+
+
+ /*******************************************************************************
+ ** Create the record pipe to be used for this process step.
+ **
+ *******************************************************************************/
+ @Override
+ public RecordPipe createRecordPipe(RunBackendStepInput runBackendStepInput, Integer overrideCapacity)
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if the filter has order-bys from a join-table, then we have to include that join-table in the SELECT clause, //
+ // which means we need to do distinct "manually", e.g., via a DistinctFilteringRecordPipe //
+ // todo - really, wouldn't this only be if it's a many-join? but that's not completely trivial to detect, given join-chains... //
+ // as it is, we may end up using DistinctPipe in some cases that we need it - which isn't an error, just slightly sub-optimal. //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ List queryJoinsForOrderByIfNeeded = getQueryJoinsForOrderByIfNeeded(queryFilter);
+ boolean needDistinctPipe = CollectionUtils.nullSafeHasContents(queryJoinsForOrderByIfNeeded);
+
+ if(needDistinctPipe)
+ {
+ String sourceTableName = runBackendStepInput.getValueString(StreamedETLWithFrontendProcess.FIELD_SOURCE_TABLE);
+ QTableMetaData sourceTable = runBackendStepInput.getInstance().getTable(sourceTableName);
+ return (new DistinctFilteringRecordPipe(new UniqueKey(sourceTable.getPrimaryKeyField()), overrideCapacity));
+ }
+ else
+ {
+ return (super.createRecordPipe(runBackendStepInput, overrideCapacity));
+ }
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java
index e24e617a..46383ca8 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLPreviewStep.java
@@ -72,15 +72,6 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
return;
}
- //////////////////////////////
- // set up the extract steps //
- //////////////////////////////
- AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
- RecordPipe recordPipe = new RecordPipe();
- extractStep.setLimit(limit);
- extractStep.setRecordPipe(recordPipe);
- extractStep.preRun(runBackendStepInput, runBackendStepOutput);
-
/////////////////////////////////////////////////////////////////
// if we're running inside an automation, then skip this step. //
/////////////////////////////////////////////////////////////////
@@ -90,6 +81,19 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
return;
}
+ /////////////////////////////
+ // set up the extract step //
+ /////////////////////////////
+ AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
+ extractStep.setLimit(limit);
+ extractStep.preRun(runBackendStepInput, runBackendStepOutput);
+
+ //////////////////////////////////////////
+ // set up a record pipe for the process //
+ //////////////////////////////////////////
+ RecordPipe recordPipe = extractStep.createRecordPipe(runBackendStepInput, null);
+ extractStep.setRecordPipe(recordPipe);
+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if skipping frontend steps, skip this action - //
// but, if inside an (ideally, only async) API call, at least do the count, so status calls can get x of y status //
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java
index 12f584e2..1b30c4e1 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java
@@ -77,14 +77,16 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
moveReviewStepAfterValidateStep(runBackendStepOutput);
+ AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
+ AbstractTransformStep transformStep = getTransformStep(runBackendStepInput);
+
+ runBackendStepInput.getAsyncJobCallback().updateStatus("Validating Records");
+
//////////////////////////////////////////////////////////
// basically repeat the preview step, but with no limit //
//////////////////////////////////////////////////////////
runBackendStepInput.getAsyncJobCallback().updateStatus("Validating Records");
- AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
- AbstractTransformStep transformStep = getTransformStep(runBackendStepInput);
-
//////////////////////////////////////////////////////////////////////
// let the transform step override the capacity for the record pipe //
//////////////////////////////////////////////////////////////////////
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelperTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelperTest.java
new file mode 100644
index 00000000..6186ad4e
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelperTest.java
@@ -0,0 +1,109 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.tables.helpers;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper.RecordWithErrors;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
+import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
+import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
+import org.junit.jupiter.api.Test;
+import static com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock.BooleanOperator.AND;
+
+
+/*******************************************************************************
+ ** Unit test for ValidateRecordSecurityLockHelper
+ *******************************************************************************/
+class ValidateRecordSecurityLockHelperTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testRecordWithErrors()
+ {
+ {
+ RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
+ recordWithErrors.add(new BadInputStatusMessage("0"), List.of(0));
+ System.out.println(recordWithErrors);
+ recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withOperator(AND).withLocks(List.of(new RecordSecurityLock())));
+ System.out.println("----------------------------------------------------------------------------");
+ }
+
+ {
+ RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
+ recordWithErrors.add(new BadInputStatusMessage("1"), List.of(1));
+ System.out.println(recordWithErrors);
+ recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock())));
+ System.out.println("----------------------------------------------------------------------------");
+ }
+
+ {
+ RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
+ recordWithErrors.add(new BadInputStatusMessage("0"), List.of(0));
+ recordWithErrors.add(new BadInputStatusMessage("1"), List.of(1));
+ System.out.println(recordWithErrors);
+ recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock())));
+ System.out.println("----------------------------------------------------------------------------");
+ }
+
+ {
+ RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
+ recordWithErrors.add(new BadInputStatusMessage("1,1"), List.of(1, 1));
+ System.out.println(recordWithErrors);
+ recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withLocks(List.of(
+ new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock())),
+ new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock()))
+ )));
+ System.out.println("----------------------------------------------------------------------------");
+ }
+
+ {
+ RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
+ recordWithErrors.add(new BadInputStatusMessage("0,0"), List.of(0, 0));
+ recordWithErrors.add(new BadInputStatusMessage("1,1"), List.of(1, 1));
+ System.out.println(recordWithErrors);
+ recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withLocks(List.of(
+ new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock())),
+ new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock()))
+ )));
+ System.out.println("----------------------------------------------------------------------------");
+ }
+
+ {
+ RecordWithErrors recordWithErrors = new RecordWithErrors(new QRecord());
+ recordWithErrors.add(new BadInputStatusMessage("0"), List.of(0));
+ recordWithErrors.add(new BadInputStatusMessage("1,1"), List.of(1, 1));
+ System.out.println(recordWithErrors);
+ recordWithErrors.propagateErrorsToRecord(new MultiRecordSecurityLock().withLocks(List.of(
+ new RecordSecurityLock(),
+ new MultiRecordSecurityLock().withLocks(List.of(new RecordSecurityLock(), new RecordSecurityLock()))
+ )));
+ System.out.println("----------------------------------------------------------------------------");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLockFiltersTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLockFiltersTest.java
new file mode 100644
index 00000000..b5c360b1
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLockFiltersTest.java
@@ -0,0 +1,160 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.security;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import org.junit.jupiter.api.Test;
+import static com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock.BooleanOperator.AND;
+import static com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock.BooleanOperator.OR;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+
+/*******************************************************************************
+ ** Unit test for RecordSecurityLockFilters
+ *******************************************************************************/
+class RecordSecurityLockFiltersTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ MultiRecordSecurityLock nullBecauseNull = RecordSecurityLockFilters.filterForReadLockTree(null);
+ assertNull(nullBecauseNull);
+
+ MultiRecordSecurityLock emptyBecauseEmptyList = RecordSecurityLockFilters.filterForReadLockTree(List.of());
+ assertEquals(0, emptyBecauseEmptyList.getLocks().size());
+
+ MultiRecordSecurityLock emptyBecauseAllWrite = RecordSecurityLockFilters.filterForReadLockTree(List.of(
+ new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.WRITE),
+ new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE)
+ ));
+ assertEquals(0, emptyBecauseAllWrite.getLocks().size());
+
+ MultiRecordSecurityLock onlyA = RecordSecurityLockFilters.filterForReadLockTree(List.of(
+ new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
+ new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE)
+ ));
+ assertMultiRecordSecurityLock(onlyA, AND, "A");
+
+ MultiRecordSecurityLock twoOutOfThreeTopLevel = RecordSecurityLockFilters.filterForReadLockTree(List.of(
+ new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
+ new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE),
+ new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
+ ));
+ assertMultiRecordSecurityLock(twoOutOfThreeTopLevel, AND, "A", "C");
+
+ MultiRecordSecurityLock treeOfAllReads = RecordSecurityLockFilters.filterForReadLockTree(List.of(
+ new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
+ new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
+ new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
+ )),
+ new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
+ new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
+ new RecordSecurityLock().withFieldName("D").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
+ ))
+ ));
+ assertEquals(2, treeOfAllReads.getLocks().size());
+ assertEquals(AND, treeOfAllReads.getOperator());
+ assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeOfAllReads.getLocks().get(0), OR, "A", "B");
+ assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeOfAllReads.getLocks().get(1), OR, "C", "D");
+
+ MultiRecordSecurityLock treeWithOneBranchReadsOneBranchWrites = RecordSecurityLockFilters.filterForReadLockTree(List.of(
+ new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
+ new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
+ new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
+ )),
+ new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
+ new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.WRITE),
+ new RecordSecurityLock().withFieldName("D").withLockScope(RecordSecurityLock.LockScope.WRITE)
+ ))
+ ));
+ assertEquals(2, treeWithOneBranchReadsOneBranchWrites.getLocks().size());
+ assertEquals(AND, treeWithOneBranchReadsOneBranchWrites.getOperator());
+ assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeWithOneBranchReadsOneBranchWrites.getLocks().get(0), OR, "A", "B");
+ assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeWithOneBranchReadsOneBranchWrites.getLocks().get(1), OR);
+
+ MultiRecordSecurityLock deepSparseTree = RecordSecurityLockFilters.filterForReadLockTree(List.of(
+ new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
+ new MultiRecordSecurityLock().withOperator(AND).withLocks(List.of(
+ new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
+ new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE)
+ )),
+ new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
+ new RecordSecurityLock().withFieldName("D").withLockScope(RecordSecurityLock.LockScope.WRITE)
+ )),
+ new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
+ new MultiRecordSecurityLock().withOperator(AND).withLocks(List.of(
+ new RecordSecurityLock().withFieldName("E").withLockScope(RecordSecurityLock.LockScope.WRITE),
+ new RecordSecurityLock().withFieldName("F").withLockScope(RecordSecurityLock.LockScope.WRITE)
+ )),
+ new MultiRecordSecurityLock().withOperator(AND).withLocks(List.of(
+ new RecordSecurityLock().withFieldName("G").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
+ new RecordSecurityLock().withFieldName("H").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
+ ))
+ )),
+ new RecordSecurityLock().withFieldName("I").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
+ new RecordSecurityLock().withFieldName("J").withLockScope(RecordSecurityLock.LockScope.WRITE)
+ ));
+
+ assertEquals(3, deepSparseTree.getLocks().size());
+ assertEquals(AND, deepSparseTree.getOperator());
+ MultiRecordSecurityLock deepChild0 = (MultiRecordSecurityLock) deepSparseTree.getLocks().get(0);
+ assertEquals(2, deepChild0.getLocks().size());
+ assertEquals(OR, deepChild0.getOperator());
+ MultiRecordSecurityLock deepGrandChild0 = (MultiRecordSecurityLock) deepChild0.getLocks().get(0);
+ assertMultiRecordSecurityLock(deepGrandChild0, AND, "A");
+ assertEquals("C", deepChild0.getLocks().get(1).getFieldName());
+
+ MultiRecordSecurityLock deepChild1 = (MultiRecordSecurityLock) deepSparseTree.getLocks().get(1);
+ assertEquals(2, deepChild1.getLocks().size());
+ assertEquals(OR, deepChild1.getOperator());
+ MultiRecordSecurityLock deepGrandChild1 = (MultiRecordSecurityLock) deepChild1.getLocks().get(0);
+ assertMultiRecordSecurityLock(deepGrandChild1, AND);
+ MultiRecordSecurityLock deepGrandChild2 = (MultiRecordSecurityLock) deepChild1.getLocks().get(1);
+ assertMultiRecordSecurityLock(deepGrandChild2, AND, "G", "H");
+
+ assertEquals("I", deepSparseTree.getLocks().get(2).getFieldName());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void assertMultiRecordSecurityLock(MultiRecordSecurityLock lock, MultiRecordSecurityLock.BooleanOperator operator, String... lockFieldNames)
+ {
+ assertEquals(lockFieldNames.length, lock.getLocks().size());
+ assertEquals(operator, lock.getOperator());
+
+ for(int i = 0; i < lockFieldNames.length; i++)
+ {
+ assertEquals(lockFieldNames[i], lock.getLocks().get(i).getFieldName());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
index fe06c6b1..d9f6df9c 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
@@ -224,6 +224,9 @@ class MemoryBackendModuleTest extends BaseTest
));
new InsertAction().execute(insertInput);
+ assertEquals(3, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.TRUE)).size());
+ assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.FALSE)).size());
+
assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.IN, List.of(1, 2))).size());
assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.IN, List.of(3, 4))).size());
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
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManagerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManagerTest.java
index fb6ff602..b33249e1 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManagerTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/scheduler/QScheduleManagerTest.java
@@ -157,4 +157,4 @@ class QScheduleManagerTest extends BaseTest
.anyMatch(l -> l.getMessage().matches(".*Scheduled new job.*TABLE_AUTOMATIONS.scheduledJob:4.*"));
}
-}
\ No newline at end of file
+}
diff --git a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java
index fb7d2156..3f5f58ab 100644
--- a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java
+++ b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java
@@ -626,6 +626,8 @@ public class AbstractMongoDBAction
case IS_NOT_BLANK -> Filters.nor(filterIsBlank(fieldBackendName));
case BETWEEN -> filterBetween(fieldBackendName, values);
case NOT_BETWEEN -> Filters.nor(filterBetween(fieldBackendName, values));
+ case TRUE -> Filters.or(Filters.eq(fieldBackendName, "true"), Filters.ne(fieldBackendName, "true"), Filters.eq(fieldBackendName, null)); // todo test!!
+ case FALSE -> Filters.and(Filters.eq(fieldBackendName, "true"), Filters.ne(fieldBackendName, "true"), Filters.eq(fieldBackendName, null));
});
}
diff --git a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java
index f9bc56db..3e51e7c2 100644
--- a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java
+++ b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java
@@ -213,6 +213,33 @@ class MongoDBQueryActionTest extends BaseTest
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void testTrueQuery() throws QException
+ {
+ QueryInput queryInput = initQueryRequest();
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.TRUE)));
+ QueryOutput queryOutput = new MongoDBQueryAction().execute(queryInput);
+ assertEquals(5, queryOutput.getRecords().size(), "'TRUE' query should find all rows");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void testFalseQuery() throws QException
+ {
+ QueryInput queryInput = initQueryRequest();
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.FALSE)));
+ QueryOutput queryOutput = new MongoDBQueryAction().execute(queryInput);
+ assertEquals(0, queryOutput.getRecords().size(), "'FALSE' query should find no rows");
+ }
+
+
/*******************************************************************************
**
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 ac62386f..64519687 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
@@ -42,7 +42,6 @@ import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
-import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
@@ -50,9 +49,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy;
-import com.kingsrook.qqq.backend.core.model.actions.tables.query.ImplicitQueryJoinForSecurityLock;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
-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.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@@ -66,13 +63,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
-import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
-import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
-import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@@ -210,7 +204,7 @@ public abstract class AbstractRDBMSAction
/*******************************************************************************
**
*******************************************************************************/
- protected String makeFromClause(QInstance instance, String tableName, JoinsContext joinsContext) throws QException
+ protected String makeFromClause(QInstance instance, String tableName, JoinsContext joinsContext, List params)
{
StringBuilder rs = new StringBuilder(escapeIdentifier(getTableName(instance.getTable(tableName))) + " AS " + escapeIdentifier(tableName));
@@ -227,17 +221,9 @@ public abstract class AbstractRDBMSAction
////////////////////////////////////////////////////////////
// find the join in the instance, to set the 'on' clause //
////////////////////////////////////////////////////////////
- List joinClauseList = new ArrayList<>();
- String baseTableName = Objects.requireNonNullElse(joinsContext.resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), tableName);
- QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () ->
- {
- QJoinMetaData found = joinsContext.findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
- if(found == null)
- {
- throw (new RuntimeException("Could not find a join between tables [" + baseTableName + "][" + queryJoin.getJoinTable() + "]"));
- }
- return (found);
- });
+ List joinClauseList = new ArrayList<>();
+ String baseTableName = Objects.requireNonNullElse(joinsContext.resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), tableName);
+ QJoinMetaData joinMetaData = Objects.requireNonNull(queryJoin.getJoinMetaData(), () -> "Could not find a join between tables [" + baseTableName + "][" + queryJoin.getJoinTable() + "]");
for(JoinOn joinOn : joinMetaData.getJoinOns())
{
@@ -268,6 +254,17 @@ public abstract class AbstractRDBMSAction
+ " = " + escapeIdentifier(joinTableOrAlias)
+ "." + escapeIdentifier(getColumnName((rightTable.getField(joinOn.getRightField())))));
}
+
+ if(CollectionUtils.nullSafeHasContents(queryJoin.getSecurityCriteria()))
+ {
+ Optional securityOnClause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(joinsContext, queryJoin.getSecurityCriteria(), QQueryFilter.BooleanOperator.AND, params);
+ if(securityOnClause.isPresent())
+ {
+ LOG.debug("Wrote securityOnClause", logPair("clause", securityOnClause));
+ joinClauseList.add(securityOnClause.get());
+ }
+ }
+
rs.append(" ON ").append(StringUtils.join(" AND ", joinClauseList));
}
@@ -283,34 +280,66 @@ public abstract class AbstractRDBMSAction
*******************************************************************************/
private List sortQueryJoinsForFromClause(String mainTableName, List queryJoins)
{
+ List rs = new ArrayList<>();
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // make a copy of the input list that we can feel safe removing elements from //
+ ////////////////////////////////////////////////////////////////////////////////
List inputListCopy = new ArrayList<>(queryJoins);
- List rs = new ArrayList<>();
- Set seenTables = new HashSet<>();
- seenTables.add(mainTableName);
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ // keep track of the tables (or aliases) that we've seen - that's what we'll "grow" outward from //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ Set seenTablesOrAliases = new HashSet<>();
+ seenTablesOrAliases.add(mainTableName);
+ ////////////////////////////////////////////////////////////////////////////////////
+ // loop as long as there are more tables in the inputList, and the keepGoing flag //
+ // is set (e.g., indicating that we added something in the last iteration) //
+ ////////////////////////////////////////////////////////////////////////////////////
boolean keepGoing = true;
while(!inputListCopy.isEmpty() && keepGoing)
{
keepGoing = false;
+
Iterator iterator = inputListCopy.iterator();
while(iterator.hasNext())
{
- QueryJoin next = iterator.next();
- if((StringUtils.hasContent(next.getBaseTableOrAlias()) && seenTables.contains(next.getBaseTableOrAlias())) || seenTables.contains(next.getJoinTable()))
+ QueryJoin nextQueryJoin = iterator.next();
+
+ //////////////////////////////////////////////////////////////////////////
+ // get the baseTableOrAlias from this join - and if it isn't set in the //
+ // QueryJoin, then get it from the left-side of the join's metaData //
+ //////////////////////////////////////////////////////////////////////////
+ String baseTableOrAlias = nextQueryJoin.getBaseTableOrAlias();
+ if(baseTableOrAlias == null && nextQueryJoin.getJoinMetaData() != null)
{
- rs.add(next);
- if(StringUtils.hasContent(next.getBaseTableOrAlias()))
+ baseTableOrAlias = nextQueryJoin.getJoinMetaData().getLeftTable();
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if we have a baseTableOrAlias (would we ever not?), and we've seen it before - OR - we've seen this query join's joinTableOrAlias, //
+ // then we can add this pair of namesOrAliases to our seen-set, remove this queryJoin from the inputListCopy (iterator), and keep going //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if((StringUtils.hasContent(baseTableOrAlias) && seenTablesOrAliases.contains(baseTableOrAlias)) || seenTablesOrAliases.contains(nextQueryJoin.getJoinTableOrItsAlias()))
+ {
+ rs.add(nextQueryJoin);
+ if(StringUtils.hasContent(baseTableOrAlias))
{
- seenTables.add(next.getBaseTableOrAlias());
+ seenTablesOrAliases.add(baseTableOrAlias);
}
- seenTables.add(next.getJoinTable());
+
+ seenTablesOrAliases.add(nextQueryJoin.getJoinTableOrItsAlias());
iterator.remove();
keepGoing = true;
}
}
}
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // in case any are left, add them all here - does this ever happen? //
+ // the only time a conditional breakpoint here fires in the RDBMS test suite, is in query designed to throw. //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
rs.addAll(inputListCopy);
return (rs);
@@ -319,208 +348,72 @@ public abstract class AbstractRDBMSAction
/*******************************************************************************
- ** method that sub-classes should call to make a full WHERE clause, including
- ** security clauses.
+ ** Method to make a full WHERE clause.
+ **
+ ** Note that criteria for security are assumed to have been added to the filter
+ ** during the construction of the JoinsContext.
*******************************************************************************/
- protected String makeWhereClause(QInstance instance, QSession session, QTableMetaData table, JoinsContext joinsContext, QQueryFilter filter, List params) throws IllegalArgumentException, QException
- {
- String whereClauseWithoutSecurity = makeWhereClauseWithoutSecurity(instance, table, joinsContext, filter, params);
- QQueryFilter securityFilter = getSecurityFilter(instance, session, table, joinsContext);
- if(!securityFilter.hasAnyCriteria())
- {
- return (whereClauseWithoutSecurity);
- }
- String securityWhereClause = makeWhereClauseWithoutSecurity(instance, table, joinsContext, securityFilter, params);
- return ("(" + whereClauseWithoutSecurity + ") AND (" + securityWhereClause + ")");
- }
-
-
-
- /*******************************************************************************
- ** private method for making the part of a where clause that gets AND'ed to the
- ** security clause. Recursively handles sub-clauses.
- *******************************************************************************/
- private String makeWhereClauseWithoutSecurity(QInstance instance, QTableMetaData table, JoinsContext joinsContext, QQueryFilter filter, List params) throws IllegalArgumentException, QException
+ protected String makeWhereClause(JoinsContext joinsContext, QQueryFilter filter, List params) throws IllegalArgumentException
{
if(filter == null || !filter.hasAnyCriteria())
{
return ("1 = 1");
}
- String clause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(instance, table, joinsContext, filter.getCriteria(), filter.getBooleanOperator(), params);
+ Optional clause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(joinsContext, filter.getCriteria(), filter.getBooleanOperator(), params);
if(!CollectionUtils.nullSafeHasContents(filter.getSubFilters()))
{
///////////////////////////////////////////////////////////////
// if there are no sub-clauses, then just return this clause //
+ // and if there's no clause, use the default 1 = 1 //
///////////////////////////////////////////////////////////////
- return (clause);
+ return (clause.orElse("1 = 1"));
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, build a list of clauses - recursively expanding the sub-filters into clauses, then return them joined with our operator //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
List clauses = new ArrayList<>();
- if(StringUtils.hasContent(clause))
+ if(clause.isPresent() && StringUtils.hasContent(clause.get()))
{
- clauses.add("(" + clause + ")");
+ clauses.add("(" + clause.get() + ")");
}
+
for(QQueryFilter subFilter : filter.getSubFilters())
{
- String subClause = makeWhereClauseWithoutSecurity(instance, table, joinsContext, subFilter, params);
+ String subClause = makeWhereClause(joinsContext, subFilter, params);
if(StringUtils.hasContent(subClause))
{
clauses.add("(" + subClause + ")");
}
}
+
return (String.join(" " + filter.getBooleanOperator().toString() + " ", clauses));
}
- /*******************************************************************************
- ** Build a QQueryFilter to apply record-level security to the query.
- ** Note, it may be empty, if there are no lock fields, or all are all-access.
- *******************************************************************************/
- private QQueryFilter getSecurityFilter(QInstance instance, QSession session, QTableMetaData table, JoinsContext joinsContext)
- {
- QQueryFilter securityFilter = new QQueryFilter();
- securityFilter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
-
- for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
- {
- // todo - uh, if it's a RIGHT (or FULL) join, then, this should be isOuter = true, right?
- boolean isOuter = false;
- addSubFilterForRecordSecurityLock(instance, session, table, securityFilter, recordSecurityLock, joinsContext, table.getName(), isOuter);
- }
-
- for(QueryJoin queryJoin : CollectionUtils.nonNullList(joinsContext.getQueryJoins()))
- {
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // for user-added joins, we want to add their security-locks to the query //
- // but if a join was implicitly added because it's needed to find a security lock on table being queried, //
- // don't add additional layers of locks for each join table. that's the idea here at least. //
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
- if(queryJoin instanceof ImplicitQueryJoinForSecurityLock)
- {
- continue;
- }
-
- QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
- for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks())))
- {
- boolean isOuter = queryJoin.getType().equals(QueryJoin.Type.LEFT); // todo full?
- addSubFilterForRecordSecurityLock(instance, session, joinTable, securityFilter, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias(), isOuter);
- }
- }
-
- return (securityFilter);
- }
-
-
-
/*******************************************************************************
**
+ ** @return optional sql where sub-clause, as in "x AND y"
*******************************************************************************/
- private static void addSubFilterForRecordSecurityLock(QInstance instance, QSession session, QTableMetaData table, QQueryFilter securityFilter, RecordSecurityLock recordSecurityLock, JoinsContext joinsContext, String tableNameOrAlias, boolean isOuter)
- {
- //////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // check if the key type has an all-access key, and if so, if it's set to true for the current user/session //
- //////////////////////////////////////////////////////////////////////////////////////////////////////////////
- QSecurityKeyType securityKeyType = instance.getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
- if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
- {
- if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
- {
- ///////////////////////////////////////////////////////////////////////////////
- // if we have all-access on this key, then we don't need a criterion for it. //
- ///////////////////////////////////////////////////////////////////////////////
- return;
- }
- }
-
- String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName();
- if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
- {
- fieldName = recordSecurityLock.getFieldName();
- }
-
- ///////////////////////////////////////////////////////////////////////////////////////////
- // else - get the key values from the session and decide what kind of criterion to build //
- ///////////////////////////////////////////////////////////////////////////////////////////
- QQueryFilter lockFilter = new QQueryFilter();
- List lockCriteria = new ArrayList<>();
- lockFilter.setCriteria(lockCriteria);
-
- QFieldType type = QFieldType.INTEGER;
- try
- {
- JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(fieldName);
- type = fieldAndTableNameOrAlias.field().getType();
- }
- catch(Exception e)
- {
- LOG.debug("Error getting field type... Trying Integer", e);
- }
-
- List securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
- if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
- {
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // handle user with no values -- they can only see null values, and only iff the lock's null-value behavior is ALLOW //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
- {
- lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
- }
- else
- {
- /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // else, if no user/session values, and null-value behavior is deny, then setup a FALSE condition, to allow no rows. //
- // todo - make some explicit contradiction here - maybe even avoid running the whole query - as you're not allowed ANY records //
- /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, Collections.emptyList()));
- }
- }
- else
- {
- //////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // else, if user/session has some values, build an IN rule - //
- // noting that if the lock's null-value behavior is ALLOW, then we actually want IS_NULL_OR_IN, not just IN //
- //////////////////////////////////////////////////////////////////////////////////////////////////////////////
- if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
- {
- lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues));
- }
- else
- {
- lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, securityKeyValues));
- }
- }
-
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // if this field is on the outer side of an outer join, then if we do a straight filter on it, then we're basically //
- // nullifying the outer join... so for an outer join use-case, OR the security field criteria with a primary-key IS NULL //
- // which will make missing rows from the join be found. //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- if(isOuter)
- {
- lockFilter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
- lockFilter.addCriteria(new QFilterCriteria(tableNameOrAlias + "." + table.getPrimaryKeyField(), QCriteriaOperator.IS_BLANK));
- }
-
- securityFilter.addSubFilter(lockFilter);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private String getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(QInstance instance, QTableMetaData table, JoinsContext joinsContext, List criteria, QQueryFilter.BooleanOperator booleanOperator, List params) throws IllegalArgumentException
+ private Optional getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(JoinsContext joinsContext, List criteria, QQueryFilter.BooleanOperator booleanOperator, List params) throws IllegalArgumentException
{
List clauses = new ArrayList<>();
for(QFilterCriteria criterion : criteria)
{
+ if(criterion.getFieldName() == null)
+ {
+ LOG.info("QFilter criteria is missing a fieldName - will not be included in query.");
+ continue;
+ }
+
+ if(criterion.getOperator() == null)
+ {
+ LOG.info("QFilter criteria is missing a operator - will not be included in query.", logPair("fieldName", criterion.getFieldName()));
+ continue;
+ }
+
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(criterion.getFieldName());
List values = criterion.getValues() == null ? new ArrayList<>() : new ArrayList<>(criterion.getValues());
@@ -530,25 +423,22 @@ public abstract class AbstractRDBMSAction
Integer expectedNoOfParams = null;
switch(criterion.getOperator())
{
- case EQUALS:
+ case EQUALS ->
{
clause += " = ?";
expectedNoOfParams = 1;
- break;
}
- case NOT_EQUALS:
+ case NOT_EQUALS ->
{
clause += " != ?";
expectedNoOfParams = 1;
- break;
}
- case NOT_EQUALS_OR_IS_NULL:
+ case NOT_EQUALS_OR_IS_NULL ->
{
clause += " != ? OR " + column + " IS NULL ";
expectedNoOfParams = 1;
- break;
}
- case IN:
+ case IN ->
{
if(values.isEmpty())
{
@@ -561,9 +451,8 @@ public abstract class AbstractRDBMSAction
{
clause += " IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ")";
}
- break;
}
- case IS_NULL_OR_IN:
+ case IS_NULL_OR_IN ->
{
clause += " IS NULL ";
@@ -571,9 +460,8 @@ public abstract class AbstractRDBMSAction
{
clause += " OR " + column + " IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ")";
}
- break;
}
- case NOT_IN:
+ case NOT_IN ->
{
if(values.isEmpty())
{
@@ -586,87 +474,74 @@ public abstract class AbstractRDBMSAction
{
clause += " NOT IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ")";
}
- break;
}
- case LIKE:
+ case LIKE ->
{
clause += " LIKE ?";
expectedNoOfParams = 1;
- break;
}
- case NOT_LIKE:
+ case NOT_LIKE ->
{
clause += " NOT LIKE ?";
expectedNoOfParams = 1;
- break;
}
- case STARTS_WITH:
+ case STARTS_WITH ->
{
clause += " LIKE ?";
ActionHelper.editFirstValue(values, (s -> s + "%"));
expectedNoOfParams = 1;
- break;
}
- case ENDS_WITH:
+ case ENDS_WITH ->
{
clause += " LIKE ?";
ActionHelper.editFirstValue(values, (s -> "%" + s));
expectedNoOfParams = 1;
- break;
}
- case CONTAINS:
+ case CONTAINS ->
{
clause += " LIKE ?";
ActionHelper.editFirstValue(values, (s -> "%" + s + "%"));
expectedNoOfParams = 1;
- break;
}
- case NOT_STARTS_WITH:
+ case NOT_STARTS_WITH ->
{
clause += " NOT LIKE ?";
ActionHelper.editFirstValue(values, (s -> s + "%"));
expectedNoOfParams = 1;
- break;
}
- case NOT_ENDS_WITH:
+ case NOT_ENDS_WITH ->
{
clause += " NOT LIKE ?";
ActionHelper.editFirstValue(values, (s -> "%" + s));
expectedNoOfParams = 1;
- break;
}
- case NOT_CONTAINS:
+ case NOT_CONTAINS ->
{
clause += " NOT LIKE ?";
ActionHelper.editFirstValue(values, (s -> "%" + s + "%"));
expectedNoOfParams = 1;
- break;
}
- case LESS_THAN:
+ case LESS_THAN ->
{
clause += " < ?";
expectedNoOfParams = 1;
- break;
}
- case LESS_THAN_OR_EQUALS:
+ case LESS_THAN_OR_EQUALS ->
{
clause += " <= ?";
expectedNoOfParams = 1;
- break;
}
- case GREATER_THAN:
+ case GREATER_THAN ->
{
clause += " > ?";
expectedNoOfParams = 1;
- break;
}
- case GREATER_THAN_OR_EQUALS:
+ case GREATER_THAN_OR_EQUALS ->
{
clause += " >= ?";
expectedNoOfParams = 1;
- break;
}
- case IS_BLANK:
+ case IS_BLANK ->
{
clause += " IS NULL";
if(field.getType().isStringLike())
@@ -674,9 +549,8 @@ public abstract class AbstractRDBMSAction
clause += " OR " + column + " = ''";
}
expectedNoOfParams = 0;
- break;
}
- case IS_NOT_BLANK:
+ case IS_NOT_BLANK ->
{
clause += " IS NOT NULL";
if(field.getType().isStringLike())
@@ -684,24 +558,28 @@ public abstract class AbstractRDBMSAction
clause += " AND " + column + " != ''";
}
expectedNoOfParams = 0;
- break;
}
- case BETWEEN:
+ case BETWEEN ->
{
clause += " BETWEEN ? AND ?";
expectedNoOfParams = 2;
- break;
}
- case NOT_BETWEEN:
+ case NOT_BETWEEN ->
{
clause += " NOT BETWEEN ? AND ?";
expectedNoOfParams = 2;
- break;
}
- default:
+ case TRUE ->
{
- throw new IllegalArgumentException("Unexpected operator: " + criterion.getOperator());
+ clause = " 1 = 1 ";
+ expectedNoOfParams = 0;
}
+ case FALSE ->
+ {
+ clause = " 0 = 1 ";
+ expectedNoOfParams = 0;
+ }
+ default -> throw new IllegalStateException("Unexpected operator: " + criterion.getOperator());
}
if(expectedNoOfParams != null)
@@ -756,7 +634,16 @@ public abstract class AbstractRDBMSAction
params.addAll(values);
}
- return (String.join(" " + booleanOperator.toString() + " ", clauses));
+ //////////////////////////////////////////////////////////////////////////////
+ // since we're skipping criteria w/o a field or operator in the loop - //
+ // we can get to the end here without any clauses... so, return a null here //
+ //////////////////////////////////////////////////////////////////////////////
+ if(clauses.isEmpty())
+ {
+ return (Optional.empty());
+ }
+
+ return (Optional.of(String.join(" " + booleanOperator.toString() + " ", clauses)));
}
@@ -1144,4 +1031,20 @@ public abstract class AbstractRDBMSAction
}
}
+
+
+ /*******************************************************************************
+ ** Either clone the input filter (so we can change it safely), or return a new blank filter.
+ *******************************************************************************/
+ protected QQueryFilter clonedOrNewFilter(QQueryFilter filter)
+ {
+ if(filter == null)
+ {
+ return (new QQueryFilter());
+ }
+ else
+ {
+ return (filter.clone());
+ }
+ }
}
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java
index 4e0d0fad..21a2052f 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java
@@ -59,6 +59,7 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega
private ActionTimeoutHelper actionTimeoutHelper;
+
/*******************************************************************************
**
*******************************************************************************/
@@ -68,16 +69,17 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega
{
QTableMetaData table = aggregateInput.getTable();
- JoinsContext joinsContext = new JoinsContext(aggregateInput.getInstance(), table.getName(), aggregateInput.getQueryJoins(), aggregateInput.getFilter());
- String fromClause = makeFromClause(aggregateInput.getInstance(), table.getName(), joinsContext);
+ QQueryFilter filter = clonedOrNewFilter(aggregateInput.getFilter());
+ JoinsContext joinsContext = new JoinsContext(aggregateInput.getInstance(), table.getName(), aggregateInput.getQueryJoins(), filter);
+
+ List params = new ArrayList<>();
+
+ String fromClause = makeFromClause(aggregateInput.getInstance(), table.getName(), joinsContext, params);
List selectClauses = buildSelectClauses(aggregateInput, joinsContext);
String sql = "SELECT " + StringUtils.join(", ", selectClauses)
- + " FROM " + fromClause;
-
- QQueryFilter filter = aggregateInput.getFilter();
- List params = new ArrayList<>();
- sql += " WHERE " + makeWhereClause(aggregateInput.getInstance(), aggregateInput.getSession(), table, joinsContext, filter, params);
+ + " FROM " + fromClause
+ + " WHERE " + makeWhereClause(joinsContext, filter, params);
if(CollectionUtils.nullSafeHasContents(aggregateInput.getGroupBys()))
{
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java
index 7177a4a1..ba167674 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java
@@ -62,7 +62,8 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf
{
QTableMetaData table = countInput.getTable();
- JoinsContext joinsContext = new JoinsContext(countInput.getInstance(), countInput.getTableName(), countInput.getQueryJoins(), countInput.getFilter());
+ QQueryFilter filter = clonedOrNewFilter(countInput.getFilter());
+ JoinsContext joinsContext = new JoinsContext(countInput.getInstance(), countInput.getTableName(), countInput.getQueryJoins(), filter);
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(table.getPrimaryKeyField());
boolean requiresDistinct = doesSelectClauseRequireDistinct(table);
@@ -74,12 +75,10 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf
clausePrefix = "SELECT COUNT(DISTINCT (" + primaryKeyColumn + ")) AS distinct_count, COUNT(*)";
}
- String sql = clausePrefix + " AS record_count FROM "
- + makeFromClause(countInput.getInstance(), table.getName(), joinsContext);
-
- QQueryFilter filter = countInput.getFilter();
List params = new ArrayList<>();
- sql += " WHERE " + makeWhereClause(countInput.getInstance(), countInput.getSession(), table, joinsContext, filter, params);
+ String sql = clausePrefix + " AS record_count "
+ + " FROM " + makeFromClause(countInput.getInstance(), table.getName(), joinsContext, params)
+ + " WHERE " + makeWhereClause(joinsContext, filter, params);
// todo sql customization - can edit sql and/or param list
setSqlAndJoinsInQueryStat(sql, joinsContext);
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
index 734c3202..baec4f0a 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
@@ -268,7 +268,7 @@ public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInte
String tableName = getTableName(table);
JoinsContext joinsContext = new JoinsContext(deleteInput.getInstance(), table.getName(), new ArrayList<>(), deleteInput.getQueryFilter());
- String whereClause = makeWhereClause(deleteInput.getInstance(), deleteInput.getSession(), table, joinsContext, filter, params);
+ String whereClause = makeWhereClause(joinsContext, filter, params);
// todo sql customization - can edit sql and/or param list?
String sql = "DELETE FROM "
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java
index 7e7dbd2c..86f119fb 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java
@@ -93,13 +93,12 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
StringBuilder sql = new StringBuilder(makeSelectClause(queryInput));
- JoinsContext joinsContext = new JoinsContext(queryInput.getInstance(), tableName, queryInput.getQueryJoins(), queryInput.getFilter());
- sql.append(" FROM ").append(makeFromClause(queryInput.getInstance(), tableName, joinsContext));
+ QQueryFilter filter = clonedOrNewFilter(queryInput.getFilter());
+ JoinsContext joinsContext = new JoinsContext(queryInput.getInstance(), tableName, queryInput.getQueryJoins(), filter);
- QQueryFilter filter = queryInput.getFilter();
List params = new ArrayList<>();
-
- sql.append(" WHERE ").append(makeWhereClause(queryInput.getInstance(), queryInput.getSession(), table, joinsContext, filter, params));
+ sql.append(" FROM ").append(makeFromClause(queryInput.getInstance(), tableName, joinsContext, params));
+ sql.append(" WHERE ").append(makeWhereClause(joinsContext, filter, params));
if(filter != null && CollectionUtils.nullSafeHasContents(filter.getOrderBys()))
{
diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportActionWithinRDBMSTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportActionWithinRDBMSTest.java
new file mode 100644
index 00000000..1b9671cc
--- /dev/null
+++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportActionWithinRDBMSTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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 .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting;
+
+
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
+import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+
+/*******************************************************************************
+ ** Test some harder exports, using RDBMS backend.
+ *******************************************************************************/
+public class ExportActionWithinRDBMSTest extends RDBMSActionTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ public void beforeEach() throws Exception
+ {
+ super.primeTestDatabase();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testIncludingFieldsFromExposedJoinTableWithTwoJoinsToMainTable() throws QException
+ {
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ExportInput exportInput = new ExportInput();
+ exportInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+
+ exportInput.setReportDestination(new ReportDestination()
+ .withReportFormat(ReportFormat.CSV)
+ .withReportOutputStream(baos));
+ exportInput.setQueryFilter(new QQueryFilter());
+ exportInput.setFieldNames(List.of("id", "storeId", "billToPersonId", "currentOrderInstructionsId", TestUtils.TABLE_NAME_ORDER_INSTRUCTIONS + ".id", TestUtils.TABLE_NAME_ORDER_INSTRUCTIONS + ".instructions"));
+ ExportOutput exportOutput = new ExportAction().execute(exportInput);
+
+ assertNotNull(exportOutput);
+
+ ///////////////////////////////////////////////////////////////////////////
+ // if there was an exception running the query, we get back 0 records... //
+ ///////////////////////////////////////////////////////////////////////////
+ assertEquals(3, exportOutput.getRecordCount());
+ }
+
+}
diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertActionTest.java
index 6a3cb3a8..74d8804e 100644
--- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertActionTest.java
+++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertActionTest.java
@@ -161,10 +161,10 @@ public class RDBMSInsertActionTest extends RDBMSActionTest
insertInput.setRecords(List.of(
new QRecord().withValue("storeId", 1).withValue("billToPersonId", 100).withValue("shipToPersonId", 200)
- .withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC1").withValue("quantity", 1)
+ .withAssociatedRecord("orderLine", new QRecord().withValue("storeId", 1).withValue("sku", "BASIC1").withValue("quantity", 1)
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-1.1").withValue("value", "LINE-VAL-1")))
- .withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC2").withValue("quantity", 2)
+ .withAssociatedRecord("orderLine", new QRecord().withValue("storeId", 1).withValue("sku", "BASIC2").withValue("quantity", 2)
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.1").withValue("value", "LINE-VAL-2"))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.2").withValue("value", "LINE-VAL-3")))
));
diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionJoinsTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionJoinsTest.java
new file mode 100644
index 00000000..12993de3
--- /dev/null
+++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionJoinsTest.java
@@ -0,0 +1,1032 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. 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 .
+ */
+
+package com.kingsrook.qqq.backend.module.rdbms.actions;
+
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
+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.QFilterOrderBy;
+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.QueryJoin;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+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.joins.QJoinMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
+import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
+import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Tests on RDBMS - specifically dealing with Joins.
+ *******************************************************************************/
+public class RDBMSQueryActionJoinsTest extends RDBMSActionTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ public void beforeEach() throws Exception
+ {
+ super.primeTestDatabase();
+
+ AbstractRDBMSAction.setLogSQL(true);
+ AbstractRDBMSAction.setLogSQLReformat(true);
+ AbstractRDBMSAction.setLogSQLOutput("system.out");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @AfterEach
+ void afterEach()
+ {
+ AbstractRDBMSAction.setLogSQL(false);
+ AbstractRDBMSAction.setLogSQLReformat(false);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QueryInput initQueryRequest()
+ {
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_PERSON);
+ return queryInput;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testFilterFromJoinTableImplicitly() throws QException
+ {
+ QueryInput queryInput = initQueryRequest();
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("personalIdCard.idNumber", QCriteriaOperator.EQUALS, "19800531")));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(1, queryOutput.getRecords().size(), "Query should find 1 rows");
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testOneToOneInnerJoinWithoutWhere() throws QException
+ {
+ QueryInput queryInput = initQueryRequest();
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(3, queryOutput.getRecords().size(), "Join query should find 3 rows");
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531"));
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515"));
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("personalIdCard.idNumber").equals("19760528"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testOneToOneLeftJoinWithoutWhere() throws QException
+ {
+ QueryInput queryInput = initQueryRequest();
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.LEFT).withSelect(true));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(5, queryOutput.getRecords().size(), "Left Join query should find 5 rows");
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531"));
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515"));
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("personalIdCard.idNumber").equals("19760528"));
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Garret") && r.getValue("personalIdCard.idNumber") == null);
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tyler") && r.getValue("personalIdCard.idNumber") == null);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testOneToOneRightJoinWithoutWhere() throws QException
+ {
+ QueryInput queryInput = initQueryRequest();
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.RIGHT).withSelect(true));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(6, queryOutput.getRecords().size(), "Right Join query should find 6 rows");
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531"));
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515"));
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("personalIdCard.idNumber").equals("19760528"));
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValue("firstName") == null && r.getValueString("personalIdCard.idNumber").equals("123123123"));
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValue("firstName") == null && r.getValueString("personalIdCard.idNumber").equals("987987987"));
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValue("firstName") == null && r.getValueString("personalIdCard.idNumber").equals("456456456"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testOneToOneInnerJoinWithWhere() throws QException
+ {
+ QueryInput queryInput = initQueryRequest();
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true));
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber", QCriteriaOperator.STARTS_WITH, "1980")));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(2, queryOutput.getRecords().size(), "Join query should find 2 rows");
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531"));
+ assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testOneToOneInnerJoinWithOrderBy() throws QException
+ {
+ QInstance qInstance = TestUtils.defineInstance();
+ QueryInput queryInput = initQueryRequest();
+ queryInput.withQueryJoin(new QueryJoin(qInstance.getJoin(TestUtils.TABLE_NAME_PERSON + "Join" + StringUtils.ucFirst(TestUtils.TABLE_NAME_PERSONAL_ID_CARD))).withSelect(true));
+ queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(3, queryOutput.getRecords().size(), "Join query should find 3 rows");
+ List idNumberListFromQuery = queryOutput.getRecords().stream().map(r -> r.getValueString(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")).toList();
+ assertEquals(List.of("19760528", "19800515", "19800531"), idNumberListFromQuery);
+
+ /////////////////////////
+ // repeat, sorted desc //
+ /////////////////////////
+ queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber", false)));
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(3, queryOutput.getRecords().size(), "Join query should find 3 rows");
+ idNumberListFromQuery = queryOutput.getRecords().stream().map(r -> r.getValueString(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")).toList();
+ assertEquals(List.of("19800531", "19800515", "19760528"), idNumberListFromQuery);
+ }
+
+
+
+ /*******************************************************************************
+ ** In the prime data, we've got 1 order line set up with an item from a different
+ ** store than its order. Write a query to find such a case.
+ *******************************************************************************/
+ @Test
+ void testFiveTableOmsJoinFindMismatchedStoreId() throws Exception
+ {
+ QueryInput queryInput = new QueryInput();
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_STORE).withAlias("orderStore").withSelect(true));
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_ORDER_LINE).withSelect(true));
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_LINE, TestUtils.TABLE_NAME_ITEM).withSelect(true));
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ITEM, TestUtils.TABLE_NAME_STORE).withAlias("itemStore").withSelect(true));
+
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria().withFieldName("orderStore.id").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("item.storeId")));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query");
+ QRecord qRecord = queryOutput.getRecords().get(0);
+ assertEquals(2, qRecord.getValueInteger("id"));
+ assertEquals(1, qRecord.getValueInteger("orderStore.id"));
+ assertEquals(2, qRecord.getValueInteger("itemStore.id"));
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // run the same setup, but this time, use the other-field-name as itemStore.id, instead of item.storeId //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria().withFieldName("orderStore.id").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("itemStore.id")));
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query");
+ qRecord = queryOutput.getRecords().get(0);
+ assertEquals(2, qRecord.getValueInteger("id"));
+ assertEquals(1, qRecord.getValueInteger("orderStore.id"));
+ assertEquals(2, qRecord.getValueInteger("itemStore.id"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testOmsQueryByOrderLines() throws Exception
+ {
+ AtomicInteger orderLineCount = new AtomicInteger();
+ runTestSql("SELECT COUNT(*) from order_line", (rs) ->
+ {
+ rs.next();
+ orderLineCount.set(rs.getInt(1));
+ });
+
+ QueryInput queryInput = new QueryInput();
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER_LINE);
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER).withSelect(true));
+
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(orderLineCount.get(), queryOutput.getRecords().size(), "# of rows found by query");
+ assertEquals(3, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("order.id").equals(1)).count());
+ assertEquals(1, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("order.id").equals(2)).count());
+ assertEquals(1, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("orderId").equals(3)).count());
+ assertEquals(2, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("orderId").equals(4)).count());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testOmsQueryByPersons() throws Exception
+ {
+ QInstance instance = TestUtils.defineInstance();
+ QueryInput queryInput = new QueryInput();
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+
+ /////////////////////////////////////////////////////
+ // inner join on bill-to person should find 6 rows //
+ /////////////////////////////////////////////////////
+ queryInput.withQueryJoins(List.of(new QueryJoin(TestUtils.TABLE_NAME_PERSON).withJoinMetaData(instance.getJoin("orderJoinBillToPerson")).withSelect(true)));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(6, queryOutput.getRecords().size(), "# of rows found by query");
+
+ /////////////////////////////////////////////////////
+ // inner join on ship-to person should find 7 rows //
+ /////////////////////////////////////////////////////
+ queryInput.withQueryJoins(List.of(new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withSelect(true)));
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(7, queryOutput.getRecords().size(), "# of rows found by query");
+
+ /////////////////////////////////////////////////////////////////////////////
+ // inner join on both bill-to person and ship-to person should find 5 rows //
+ /////////////////////////////////////////////////////////////////////////////
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true),
+ new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true)
+ ));
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(5, queryOutput.getRecords().size(), "# of rows found by query");
+
+ /////////////////////////////////////////////////////////////////////////////
+ // left join on both bill-to person and ship-to person should find 8 rows //
+ /////////////////////////////////////////////////////////////////////////////
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withType(QueryJoin.Type.LEFT).withAlias("shipToPerson").withSelect(true),
+ new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withType(QueryJoin.Type.LEFT).withAlias("billToPerson").withSelect(true)
+ ));
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(8, queryOutput.getRecords().size(), "# of rows found by query");
+
+ //////////////////////////////////////////////////
+ // now join through to personalIdCard table too //
+ //////////////////////////////////////////////////
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true),
+ new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true),
+ new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true),
+ new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true)
+ ));
+ queryInput.setFilter(new QQueryFilter()
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // look for billToPersons w/ idNumber starting with 1980 - should only be James and Darin (assert on that below). //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ .withCriteria(new QFilterCriteria("billToIdCard.idNumber", QCriteriaOperator.STARTS_WITH, "1980"))
+ );
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(3, queryOutput.getRecords().size(), "# of rows found by query");
+ assertThat(queryOutput.getRecords().stream().map(r -> r.getValueString("billToPerson.firstName")).toList()).allMatch(p -> p.equals("Darin") || p.equals("James"));
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // ensure we throw if either of the ambiguous joins from person to id-card doesn't specify its left-table //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true),
+ new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true),
+ new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true),
+ new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true)
+ ));
+ assertThatThrownBy(() -> new QueryAction().execute(queryInput))
+ .rootCause()
+ .hasMessageContaining("Could not find a join between tables [order][personalIdCard]");
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // ensure we throw if either of the ambiguous joins from person to id-card doesn't specify its left-table //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true),
+ new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true),
+ new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true),
+ new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true)
+ ));
+ assertThatThrownBy(() -> new QueryAction().execute(queryInput))
+ .rootCause()
+ .hasMessageContaining("Could not find a join between tables [order][personalIdCard]");
+
+ ////////////////////////////////////////////////////////////////////////
+ // ensure we throw if we have a bogus alias name given as a left-side //
+ ////////////////////////////////////////////////////////////////////////
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true),
+ new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true),
+ new QueryJoin("notATable", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true),
+ new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true)
+ ));
+ assertThatThrownBy(() -> new QueryAction().execute(queryInput))
+ .hasRootCauseMessage("Could not find a join between tables [notATable][personalIdCard]");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testOmsQueryByPersonsExtraKelkhoffOrder() throws Exception
+ {
+ QInstance instance = TestUtils.defineInstance();
+ QueryInput queryInput = new QueryInput();
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // insert a second person w/ last name Kelkhoff, then an order for Darin Kelkhoff and this new Kelkhoff - //
+ // then query for orders w/ bill to person & ship to person both lastname = Kelkhoff, but different ids. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ Integer specialOrderId = 1701;
+ runTestSql("INSERT INTO person (id, first_name, last_name, email) VALUES (6, 'Jimmy', 'Kelkhoff', 'dk@gmail.com')", null);
+ runTestSql("INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (" + specialOrderId + ", 1, 1, 6)", null);
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withType(QueryJoin.Type.LEFT).withAlias("shipToPerson").withSelect(true),
+ new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withType(QueryJoin.Type.LEFT).withAlias("billToPerson").withSelect(true)
+ ));
+ queryInput.setFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.lastName").withOperator(QCriteriaOperator.EQUALS).withOtherFieldName("billToPerson.lastName"))
+ .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.id").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("billToPerson.id"))
+ );
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query");
+ assertEquals(specialOrderId, queryOutput.getRecords().get(0).getValueInteger("id"));
+
+ ////////////////////////////////////////////////////////////
+ // re-run that query using personIds from the order table //
+ ////////////////////////////////////////////////////////////
+ queryInput.setFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.lastName").withOperator(QCriteriaOperator.EQUALS).withOtherFieldName("billToPerson.lastName"))
+ .withCriteria(new QFilterCriteria().withFieldName("order.shipToPersonId").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("order.billToPersonId"))
+ );
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query");
+ assertEquals(specialOrderId, queryOutput.getRecords().get(0).getValueInteger("id"));
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // re-run that query using personIds from the order table, but not specifying the table name //
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ queryInput.setFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.lastName").withOperator(QCriteriaOperator.EQUALS).withOtherFieldName("billToPerson.lastName"))
+ .withCriteria(new QFilterCriteria().withFieldName("shipToPersonId").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("billToPersonId"))
+ );
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query");
+ assertEquals(specialOrderId, queryOutput.getRecords().get(0).getValueInteger("id"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testDuplicateAliases()
+ {
+ QInstance instance = TestUtils.defineInstance();
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson"),
+ new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson"),
+ new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true),
+ new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true) // w/o alias, should get exception here - dupe table.
+ ));
+ assertThatThrownBy(() -> new QueryAction().execute(queryInput))
+ .hasRootCauseMessage("Duplicate table name or alias: personalIdCard");
+
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson"),
+ new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson"),
+ new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToPerson").withSelect(true), // dupe alias, should get exception here
+ new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToPerson").withSelect(true)
+ ));
+ assertThatThrownBy(() -> new QueryAction().execute(queryInput))
+ .hasRootCauseMessage("Duplicate table name or alias: shipToPerson");
+ }
+
+
+
+ /*******************************************************************************
+ ** Given tables:
+ ** order - orderLine - item
+ ** with exposedJoin on order to item
+ ** do a query on order, also selecting item.
+ *******************************************************************************/
+ @Test
+ void testTwoTableAwayExposedJoin() throws QException
+ {
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+
+ QInstance instance = TestUtils.defineInstance();
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(TestUtils.TABLE_NAME_ITEM).withType(QueryJoin.Type.INNER).withSelect(true)
+ ));
+
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ List records = queryOutput.getRecords();
+ assertThat(records).hasSize(11); // one per line item
+ assertThat(records).allMatch(r -> r.getValue("id") != null);
+ assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ITEM + ".description") != null);
+ }
+
+
+
+ /*******************************************************************************
+ ** Given tables:
+ ** order - orderLine - item
+ ** with exposedJoin on item to order
+ ** do a query on item, also selecting order.
+ ** This is a reverse of the above, to make sure join flipping, etc, is good.
+ *******************************************************************************/
+ @Test
+ void testTwoTableAwayExposedJoinReversed() throws QException
+ {
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+
+ QInstance instance = TestUtils.defineInstance();
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ITEM);
+
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(TestUtils.TABLE_NAME_ORDER).withType(QueryJoin.Type.INNER).withSelect(true)
+ ));
+
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ List records = queryOutput.getRecords();
+ assertThat(records).hasSize(11); // one per line item
+ assertThat(records).allMatch(r -> r.getValue("description") != null);
+ assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ORDER + ".id") != null);
+ }
+
+
+
+ /*******************************************************************************
+ ** Given tables:
+ ** order - orderLine - item
+ ** with exposedJoin on order to item
+ ** do a query on order, also selecting item, and also selecting orderLine...
+ *******************************************************************************/
+ @Test
+ void testTwoTableAwayExposedJoinAlsoSelectingInBetweenTable() throws QException
+ {
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+
+ QInstance instance = TestUtils.defineInstance();
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+
+ queryInput.withQueryJoins(List.of(
+ new QueryJoin(TestUtils.TABLE_NAME_ORDER_LINE).withType(QueryJoin.Type.INNER).withSelect(true),
+ new QueryJoin(TestUtils.TABLE_NAME_ITEM).withType(QueryJoin.Type.INNER).withSelect(true)
+ ));
+
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ List records = queryOutput.getRecords();
+ assertThat(records).hasSize(11); // one per line item
+ assertThat(records).allMatch(r -> r.getValue("id") != null);
+ assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ORDER_LINE + ".quantity") != null);
+ assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ITEM + ".description") != null);
+ }
+
+
+
+ /*******************************************************************************
+ ** Given tables:
+ ** order - orderLine - item
+ ** with exposedJoin on order to item
+ ** do a query on order, filtered by item
+ *******************************************************************************/
+ @Test
+ void testTwoTableAwayExposedJoinWhereClauseOnly() throws QException
+ {
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+
+ QInstance instance = TestUtils.defineInstance();
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_ITEM + ".description", QCriteriaOperator.STARTS_WITH, "Q-Mart")));
+
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ List records = queryOutput.getRecords();
+ assertThat(records).hasSize(4);
+ assertThat(records).allMatch(r -> r.getValue("id") != null);
+ }
+
+
+
+ /*******************************************************************************
+ ** Given tables:
+ ** order - orderLine - item
+ ** with exposedJoin on order to item
+ ** do a query on order, filtered by item
+ *******************************************************************************/
+ @Test
+ void testTwoTableAwayExposedJoinWhereClauseBothJoinTables() throws QException
+ {
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+
+ QInstance instance = TestUtils.defineInstance();
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+ queryInput.setFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria(TestUtils.TABLE_NAME_ITEM + ".description", QCriteriaOperator.STARTS_WITH, "Q-Mart"))
+ .withCriteria(new QFilterCriteria(TestUtils.TABLE_NAME_ORDER_LINE + ".quantity", QCriteriaOperator.IS_NOT_BLANK))
+ );
+
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ List records = queryOutput.getRecords();
+ assertThat(records).hasSize(4);
+ assertThat(records).allMatch(r -> r.getValue("id") != null);
+ }
+
+
+
+ /*******************************************************************************
+ ** queries on the store table, where the primary key (id) is the security field
+ *******************************************************************************/
+ @Test
+ void testRecordSecurityPrimaryKeyFieldNoFilters() throws QException
+ {
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_STORE);
+
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(3);
+
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(1)
+ .anyMatch(r -> r.getValueInteger("id").equals(1));
+
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(1)
+ .anyMatch(r -> r.getValueInteger("id").equals(2));
+
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession());
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, null));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession().withSecurityKeyValues(Collections.emptyMap()));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1).withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(2)
+ .anyMatch(r -> r.getValueInteger("id").equals(1))
+ .anyMatch(r -> r.getValueInteger("id").equals(3));
+ }
+
+
+
+ /*******************************************************************************
+ ** not really expected to be any different from where we filter on the primary key,
+ ** but just good to make sure
+ *******************************************************************************/
+ @Test
+ void testRecordSecurityForeignKeyFieldNoFilters() throws QException
+ {
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(8);
+
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(3)
+ .allMatch(r -> r.getValueInteger("storeId").equals(1));
+
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(2)
+ .allMatch(r -> r.getValueInteger("storeId").equals(2));
+
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession());
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession().withSecurityKeyValues(Collections.emptyMap()));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession().withSecurityKeyValues(Map.of(TestUtils.TABLE_NAME_STORE, Collections.emptyList())));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1).withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(6)
+ .allMatch(r -> r.getValueInteger("storeId").equals(1) || r.getValueInteger("storeId").equals(3));
+ }
+
+
+
+ /*******************************************************************************
+ ** Error seen in CTLive - query for order join lineItem, where lineItem's security
+ ** key is in order.
+ **
+ ** Note - in this test-db setup, there happens to be a storeId in both order &
+ ** orderLine tables, so we can't quite reproduce the error we saw in CTL - so
+ ** query on different tables with the structure that'll produce the error.
+ *******************************************************************************/
+ @Test
+ void testRequestedJoinWithTableWhoseSecurityFieldIsInMainTable() throws QException
+ {
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_WAREHOUSE_STORE_INT);
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_WAREHOUSE).withSelect(true));
+
+ //////////////////////////////////////////////
+ // with the all-access key, find all 3 rows //
+ //////////////////////////////////////////////
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(3);
+
+ ///////////////////////////////////////////
+ // with 1 security key value, find 1 row //
+ ///////////////////////////////////////////
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(1)
+ .allMatch(r -> r.getValueInteger("storeId").equals(1));
+
+ ///////////////////////////////////////////
+ // with 1 security key value, find 1 row //
+ ///////////////////////////////////////////
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(1)
+ .allMatch(r -> r.getValueInteger("storeId").equals(2));
+
+ //////////////////////////////////////////////////////////
+ // with a mis-matching security key value, 0 rows found //
+ //////////////////////////////////////////////////////////
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ ///////////////////////////////////////////////
+ // with no security key values, 0 rows found //
+ ///////////////////////////////////////////////
+ QContext.setQSession(new QSession());
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ ////////////////////////////////////////////////
+ // with null security key value, 0 rows found //
+ ////////////////////////////////////////////////
+ QContext.setQSession(new QSession().withSecurityKeyValues(Collections.emptyMap()));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ //////////////////////////////////////////////////////
+ // with empty-list security key value, 0 rows found //
+ //////////////////////////////////////////////////////
+ QContext.setQSession(new QSession().withSecurityKeyValues(Map.of(TestUtils.TABLE_NAME_STORE, Collections.emptyList())));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ ////////////////////////////////
+ // with 2 values, find 2 rows //
+ ////////////////////////////////
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1).withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(2)
+ .allMatch(r -> r.getValueInteger("storeId").equals(1) || r.getValueInteger("storeId").equals(3));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testRecordSecurityWithFilters() throws QException
+ {
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(6);
+
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(2)
+ .allMatch(r -> r.getValueInteger("storeId").equals(1));
+
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
+ QContext.setQSession(new QSession());
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("storeId", QCriteriaOperator.IN, List.of(1, 2))));
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1).withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(3)
+ .allMatch(r -> r.getValueInteger("storeId").equals(1));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testRecordSecurityFromJoinTableAlsoImplicitlyInQuery() throws QException
+ {
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER_LINE);
+
+ ///////////////////////////////////////////////////////////////////////////////////////
+ // orders 1, 2, and 3 are from store 1, so their lines (5 in total) should be found. //
+ // note, order 2 has the line with mis-matched store id - but, that shouldn't apply //
+ // here, because the line table's security comes from the order table. //
+ ///////////////////////////////////////////////////////////////////////////////////////
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("order.id", QCriteriaOperator.IN, List.of(1, 2, 3, 4))));
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(5);
+
+ ///////////////////////////////////////////////////////////////////
+ // order 4 should be the only one found this time (with 2 lines) //
+ ///////////////////////////////////////////////////////////////////
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("order.id", QCriteriaOperator.IN, List.of(1, 2, 3, 4))));
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(2);
+
+ ////////////////////////////////////////////////////////////////
+ // make sure we're also good if we explicitly join this table //
+ ////////////////////////////////////////////////////////////////
+ queryInput.withQueryJoin(new QueryJoin().withJoinTable(TestUtils.TABLE_NAME_ORDER).withSelect(true));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(2);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testRecordSecurityWithLockFromJoinTable() throws QException
+ {
+ QInstance qInstance = TestUtils.defineInstance();
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // remove the normal lock on the order table - replace it with one from the joined store table //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().clear();
+ qInstance.getTable(TestUtils.TABLE_NAME_ORDER).withRecordSecurityLock(new RecordSecurityLock()
+ .withSecurityKeyType(TestUtils.TABLE_NAME_STORE)
+ .withJoinNameChain(List.of("orderJoinStore"))
+ .withFieldName("store.id"));
+
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(6);
+
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(2)
+ .allMatch(r -> r.getValueInteger("storeId").equals(1));
+
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
+ QContext.setQSession(new QSession());
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("storeId", QCriteriaOperator.IN, List.of(1, 2))));
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1).withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3));
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(3)
+ .allMatch(r -> r.getValueInteger("storeId").equals(1));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testRecordSecurityWithLockFromJoinTableWhereTheKeyIsOnTheManySide() throws QException
+ {
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_WAREHOUSE);
+
+ assertThat(new QueryAction().execute(queryInput).getRecords())
+ .hasSize(1);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testMultipleReversedDirectionJoinsBetweenSameTables() throws QException
+ {
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
+
+ Integer noOfOrders = new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_ORDER)).getCount();
+ Integer noOfOrderInstructions = new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_ORDER_INSTRUCTIONS)).getCount();
+
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // make sure we can join on order.current_order_instruction_id = order_instruction.id -- and that we get back 1 row per order //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_INSTRUCTIONS).withJoinMetaData(QContext.getQInstance().getJoin("orderJoinCurrentOrderInstructions")));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(noOfOrders, queryOutput.getRecords().size());
+ }
+
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // assert that the query succeeds (based on exposed join) if the joinMetaData isn't specified //
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_INSTRUCTIONS));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(noOfOrders, queryOutput.getRecords().size());
+ }
+
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // make sure we can join on order.id = order_instruction.order_id (e.g., not the exposed one used above) -- and that we get back 1 row per order instruction //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_INSTRUCTIONS).withJoinMetaData(QContext.getQInstance().getJoin("orderInstructionsJoinOrder")));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(noOfOrderInstructions, queryOutput.getRecords().size());
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testSecurityJoinForJoinedTableFromImplicitlyJoinedTable() throws QException
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // in this test: //
+ // query on Order, joined with OrderLine. //
+ // Order has its own security field (storeId), that's always worked fine. //
+ // We want to change OrderLine's security field to be item.storeId - not order.storeId //
+ // so that item has to be brought into the query to secure the items. //
+ // this was originally broken, as it would generate a WHERE clause for item.storeId, //
+ // but it wouldn't put item in the FROM cluase.
+ /////////////////////////////////////////////////////////////////////////////////////////
+ QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
+ QContext.getQInstance().getTable(TestUtils.TABLE_NAME_ORDER_LINE)
+ .setRecordSecurityLocks(ListBuilder.of(
+ new RecordSecurityLock()
+ .withSecurityKeyType(TestUtils.TABLE_NAME_STORE)
+ .withFieldName("item.storeId")
+ .withJoinNameChain(List.of("orderLineJoinItem"))));
+
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
+ queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_LINE).withSelect(true));
+ queryInput.withFilter(new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_ORDER_LINE + ".sku", QCriteriaOperator.IS_NOT_BLANK)));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ List records = queryOutput.getRecords();
+ assertEquals(3, records.size(), "expected no of records");
+
+ ///////////////////////////////////////////////////////////////////////
+ // we should get the orderLines for orders 4 and 5 - but not the one //
+ // for order 2, as it has an item from a different store //
+ ///////////////////////////////////////////////////////////////////////
+ assertThat(records).allMatch(r -> r.getValueInteger("id").equals(4) || r.getValueInteger("id").equals(5));
+ }
+
+
+
+ /*******************************************************************************
+ ** Addressing a regression where a table was brought into a query for its
+ ** security field, but it was a write-scope lock, so, it shouldn't have been.
+ *******************************************************************************/
+ @Test
+ void testWriteLockOnJoinTableDoesntLimitQuery() throws Exception
+ {
+ ///////////////////////////////////////////////////////////////////////
+ // add a security key type for "idNumber" //
+ // then set up the person table with a read-write lock on that field //
+ ///////////////////////////////////////////////////////////////////////
+ QContext.getQInstance().addSecurityKeyType(new QSecurityKeyType().withName("idNumber"));
+ QTableMetaData personTable = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON);
+ personTable.withRecordSecurityLock(new RecordSecurityLock()
+ .withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
+ .withSecurityKeyType("idNumber")
+ .withFieldName(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")
+ .withJoinNameChain(List.of(QJoinMetaData.makeInferredJoinName(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD))));
+
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // first, with no idNumber security key in session, query on person should find 0 rows //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ assertEquals(0, new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON)).getRecords().size());
+
+ ///////////////////////////////////////////////////////////////////
+ // put an idNumber in the session - query and find just that one //
+ ///////////////////////////////////////////////////////////////////
+ QContext.setQSession(new QSession().withSecurityKeyValue("idNumber", "19800531"));
+ assertEquals(1, new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON)).getRecords().size());
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // change the lock to be scope=WRITE - and now, we should be able to see all of the records //
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ personTable.getRecordSecurityLocks().get(0).setLockScope(RecordSecurityLock.LockScope.WRITE);
+ assertEquals(5, new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON)).getRecords().size());
+ }
+
+}
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 223fa5f4..fa27d4ea 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
@@ -25,26 +25,20 @@ package com.kingsrook.qqq.backend.module.rdbms.actions;
import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
-import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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.QFilterOrderBy;
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.QueryJoin;
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;
@@ -52,14 +46,12 @@ 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.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.session.QSession;
-import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -107,6 +99,34 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void testTrueQuery() throws QException
+ {
+ QueryInput queryInput = initQueryRequest();
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.TRUE)));
+ QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
+ assertEquals(5, queryOutput.getRecords().size(), "'TRUE' query should find all rows");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void testFalseQuery() throws QException
+ {
+ QueryInput queryInput = initQueryRequest();
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.FALSE)));
+ QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
+ assertEquals(0, queryOutput.getRecords().size(), "'FALSE' query should find no rows");
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -783,675 +803,6 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testFilterFromJoinTableImplicitly() throws QException
- {
- QueryInput queryInput = initQueryRequest();
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("personalIdCard.idNumber", QCriteriaOperator.EQUALS, "19800531")));
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(1, queryOutput.getRecords().size(), "Query should find 1 rows");
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin"));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testOneToOneInnerJoinWithoutWhere() throws QException
- {
- QueryInput queryInput = initQueryRequest();
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true));
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(3, queryOutput.getRecords().size(), "Join query should find 3 rows");
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531"));
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515"));
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("personalIdCard.idNumber").equals("19760528"));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testOneToOneLeftJoinWithoutWhere() throws QException
- {
- QueryInput queryInput = initQueryRequest();
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.LEFT).withSelect(true));
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(5, queryOutput.getRecords().size(), "Left Join query should find 5 rows");
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531"));
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515"));
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("personalIdCard.idNumber").equals("19760528"));
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Garret") && r.getValue("personalIdCard.idNumber") == null);
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tyler") && r.getValue("personalIdCard.idNumber") == null);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testOneToOneRightJoinWithoutWhere() throws QException
- {
- QueryInput queryInput = initQueryRequest();
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.RIGHT).withSelect(true));
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(6, queryOutput.getRecords().size(), "Right Join query should find 6 rows");
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531"));
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515"));
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("personalIdCard.idNumber").equals("19760528"));
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValue("firstName") == null && r.getValueString("personalIdCard.idNumber").equals("123123123"));
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValue("firstName") == null && r.getValueString("personalIdCard.idNumber").equals("987987987"));
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValue("firstName") == null && r.getValueString("personalIdCard.idNumber").equals("456456456"));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testOneToOneInnerJoinWithWhere() throws QException
- {
- QueryInput queryInput = initQueryRequest();
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true));
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber", QCriteriaOperator.STARTS_WITH, "1980")));
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(2, queryOutput.getRecords().size(), "Join query should find 2 rows");
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531"));
- assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515"));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testOneToOneInnerJoinWithOrderBy() throws QException
- {
- QInstance qInstance = TestUtils.defineInstance();
- QueryInput queryInput = initQueryRequest();
- queryInput.withQueryJoin(new QueryJoin(qInstance.getJoin(TestUtils.TABLE_NAME_PERSON + "Join" + StringUtils.ucFirst(TestUtils.TABLE_NAME_PERSONAL_ID_CARD))).withSelect(true));
- queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")));
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(3, queryOutput.getRecords().size(), "Join query should find 3 rows");
- List idNumberListFromQuery = queryOutput.getRecords().stream().map(r -> r.getValueString(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")).toList();
- assertEquals(List.of("19760528", "19800515", "19800531"), idNumberListFromQuery);
-
- /////////////////////////
- // repeat, sorted desc //
- /////////////////////////
- queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber", false)));
- queryOutput = new QueryAction().execute(queryInput);
- assertEquals(3, queryOutput.getRecords().size(), "Join query should find 3 rows");
- idNumberListFromQuery = queryOutput.getRecords().stream().map(r -> r.getValueString(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")).toList();
- assertEquals(List.of("19800531", "19800515", "19760528"), idNumberListFromQuery);
- }
-
-
-
- /*******************************************************************************
- ** In the prime data, we've got 1 order line set up with an item from a different
- ** store than its order. Write a query to find such a case.
- *******************************************************************************/
- @Test
- void testFiveTableOmsJoinFindMismatchedStoreId() throws Exception
- {
- QueryInput queryInput = new QueryInput();
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_STORE).withAlias("orderStore").withSelect(true));
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_ORDER_LINE).withSelect(true));
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_LINE, TestUtils.TABLE_NAME_ITEM).withSelect(true));
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ITEM, TestUtils.TABLE_NAME_STORE).withAlias("itemStore").withSelect(true));
-
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria().withFieldName("orderStore.id").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("item.storeId")));
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query");
- QRecord qRecord = queryOutput.getRecords().get(0);
- assertEquals(2, qRecord.getValueInteger("id"));
- assertEquals(1, qRecord.getValueInteger("orderStore.id"));
- assertEquals(2, qRecord.getValueInteger("itemStore.id"));
-
- //////////////////////////////////////////////////////////////////////////////////////////////////////////
- // run the same setup, but this time, use the other-field-name as itemStore.id, instead of item.storeId //
- //////////////////////////////////////////////////////////////////////////////////////////////////////////
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria().withFieldName("orderStore.id").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("itemStore.id")));
- queryOutput = new QueryAction().execute(queryInput);
- assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query");
- qRecord = queryOutput.getRecords().get(0);
- assertEquals(2, qRecord.getValueInteger("id"));
- assertEquals(1, qRecord.getValueInteger("orderStore.id"));
- assertEquals(2, qRecord.getValueInteger("itemStore.id"));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testOmsQueryByOrderLines() throws Exception
- {
- AtomicInteger orderLineCount = new AtomicInteger();
- runTestSql("SELECT COUNT(*) from order_line", (rs) ->
- {
- rs.next();
- orderLineCount.set(rs.getInt(1));
- });
-
- QueryInput queryInput = new QueryInput();
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER_LINE);
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER).withSelect(true));
-
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(orderLineCount.get(), queryOutput.getRecords().size(), "# of rows found by query");
- assertEquals(3, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("order.id").equals(1)).count());
- assertEquals(1, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("order.id").equals(2)).count());
- assertEquals(1, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("orderId").equals(3)).count());
- assertEquals(2, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("orderId").equals(4)).count());
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testOmsQueryByPersons() throws Exception
- {
- QInstance instance = TestUtils.defineInstance();
- QueryInput queryInput = new QueryInput();
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
-
- /////////////////////////////////////////////////////
- // inner join on bill-to person should find 6 rows //
- /////////////////////////////////////////////////////
- queryInput.withQueryJoins(List.of(new QueryJoin(TestUtils.TABLE_NAME_PERSON).withJoinMetaData(instance.getJoin("orderJoinBillToPerson")).withSelect(true)));
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(6, queryOutput.getRecords().size(), "# of rows found by query");
-
- /////////////////////////////////////////////////////
- // inner join on ship-to person should find 7 rows //
- /////////////////////////////////////////////////////
- queryInput.withQueryJoins(List.of(new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withSelect(true)));
- queryOutput = new QueryAction().execute(queryInput);
- assertEquals(7, queryOutput.getRecords().size(), "# of rows found by query");
-
- /////////////////////////////////////////////////////////////////////////////
- // inner join on both bill-to person and ship-to person should find 5 rows //
- /////////////////////////////////////////////////////////////////////////////
- queryInput.withQueryJoins(List.of(
- new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true),
- new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true)
- ));
- queryOutput = new QueryAction().execute(queryInput);
- assertEquals(5, queryOutput.getRecords().size(), "# of rows found by query");
-
- /////////////////////////////////////////////////////////////////////////////
- // left join on both bill-to person and ship-to person should find 8 rows //
- /////////////////////////////////////////////////////////////////////////////
- queryInput.withQueryJoins(List.of(
- new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withType(QueryJoin.Type.LEFT).withAlias("shipToPerson").withSelect(true),
- new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withType(QueryJoin.Type.LEFT).withAlias("billToPerson").withSelect(true)
- ));
- queryOutput = new QueryAction().execute(queryInput);
- assertEquals(8, queryOutput.getRecords().size(), "# of rows found by query");
-
- //////////////////////////////////////////////////
- // now join through to personalIdCard table too //
- //////////////////////////////////////////////////
- queryInput.withQueryJoins(List.of(
- new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true),
- new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true),
- new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true),
- new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true)
- ));
- queryInput.setFilter(new QQueryFilter()
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // look for billToPersons w/ idNumber starting with 1980 - should only be James and Darin (assert on that below). //
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- .withCriteria(new QFilterCriteria("billToIdCard.idNumber", QCriteriaOperator.STARTS_WITH, "1980"))
- );
- queryOutput = new QueryAction().execute(queryInput);
- assertEquals(3, queryOutput.getRecords().size(), "# of rows found by query");
- assertThat(queryOutput.getRecords().stream().map(r -> r.getValueString("billToPerson.firstName")).toList()).allMatch(p -> p.equals("Darin") || p.equals("James"));
-
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // ensure we throw if either of the ambiguous joins from person to id-card doesn't specify its left-table //
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
- queryInput.withQueryJoins(List.of(
- new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true),
- new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true),
- new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true),
- new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true)
- ));
- assertThatThrownBy(() -> new QueryAction().execute(queryInput))
- .rootCause()
- .hasMessageContaining("Could not find a join between tables [order][personalIdCard]");
-
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // ensure we throw if either of the ambiguous joins from person to id-card doesn't specify its left-table //
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
- queryInput.withQueryJoins(List.of(
- new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true),
- new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true),
- new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true),
- new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true)
- ));
- assertThatThrownBy(() -> new QueryAction().execute(queryInput))
- .rootCause()
- .hasMessageContaining("Could not find a join between tables [order][personalIdCard]");
-
- ////////////////////////////////////////////////////////////////////////
- // ensure we throw if we have a bogus alias name given as a left-side //
- ////////////////////////////////////////////////////////////////////////
- queryInput.withQueryJoins(List.of(
- new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true),
- new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true),
- new QueryJoin("notATable", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true),
- new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true)
- ));
- assertThatThrownBy(() -> new QueryAction().execute(queryInput))
- .hasRootCauseMessage("Could not find a join between tables [notATable][personalIdCard]");
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testOmsQueryByPersonsExtraKelkhoffOrder() throws Exception
- {
- QInstance instance = TestUtils.defineInstance();
- QueryInput queryInput = new QueryInput();
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
-
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // insert a second person w/ last name Kelkhoff, then an order for Darin Kelkhoff and this new Kelkhoff - //
- // then query for orders w/ bill to person & ship to person both lastname = Kelkhoff, but different ids. //
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
- Integer specialOrderId = 1701;
- runTestSql("INSERT INTO person (id, first_name, last_name, email) VALUES (6, 'Jimmy', 'Kelkhoff', 'dk@gmail.com')", null);
- runTestSql("INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (" + specialOrderId + ", 1, 1, 6)", null);
- queryInput.withQueryJoins(List.of(
- new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withType(QueryJoin.Type.LEFT).withAlias("shipToPerson").withSelect(true),
- new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withType(QueryJoin.Type.LEFT).withAlias("billToPerson").withSelect(true)
- ));
- queryInput.setFilter(new QQueryFilter()
- .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.lastName").withOperator(QCriteriaOperator.EQUALS).withOtherFieldName("billToPerson.lastName"))
- .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.id").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("billToPerson.id"))
- );
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query");
- assertEquals(specialOrderId, queryOutput.getRecords().get(0).getValueInteger("id"));
-
- ////////////////////////////////////////////////////////////
- // re-run that query using personIds from the order table //
- ////////////////////////////////////////////////////////////
- queryInput.setFilter(new QQueryFilter()
- .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.lastName").withOperator(QCriteriaOperator.EQUALS).withOtherFieldName("billToPerson.lastName"))
- .withCriteria(new QFilterCriteria().withFieldName("order.shipToPersonId").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("order.billToPersonId"))
- );
- queryOutput = new QueryAction().execute(queryInput);
- assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query");
- assertEquals(specialOrderId, queryOutput.getRecords().get(0).getValueInteger("id"));
-
- ///////////////////////////////////////////////////////////////////////////////////////////////
- // re-run that query using personIds from the order table, but not specifying the table name //
- ///////////////////////////////////////////////////////////////////////////////////////////////
- queryInput.setFilter(new QQueryFilter()
- .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.lastName").withOperator(QCriteriaOperator.EQUALS).withOtherFieldName("billToPerson.lastName"))
- .withCriteria(new QFilterCriteria().withFieldName("shipToPersonId").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("billToPersonId"))
- );
- queryOutput = new QueryAction().execute(queryInput);
- assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query");
- assertEquals(specialOrderId, queryOutput.getRecords().get(0).getValueInteger("id"));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testDuplicateAliases()
- {
- QInstance instance = TestUtils.defineInstance();
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
-
- queryInput.withQueryJoins(List.of(
- new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson"),
- new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson"),
- new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true),
- new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true) // w/o alias, should get exception here - dupe table.
- ));
- assertThatThrownBy(() -> new QueryAction().execute(queryInput))
- .hasRootCauseMessage("Duplicate table name or alias: personalIdCard");
-
- queryInput.withQueryJoins(List.of(
- new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson"),
- new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson"),
- new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToPerson").withSelect(true), // dupe alias, should get exception here
- new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToPerson").withSelect(true)
- ));
- assertThatThrownBy(() -> new QueryAction().execute(queryInput))
- .hasRootCauseMessage("Duplicate table name or alias: shipToPerson");
- }
-
-
-
- /*******************************************************************************
- ** Given tables:
- ** order - orderLine - item
- ** with exposedJoin on order to item
- ** do a query on order, also selecting item.
- *******************************************************************************/
- @Test
- void testTwoTableAwayExposedJoin() throws QException
- {
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
-
- QInstance instance = TestUtils.defineInstance();
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
-
- queryInput.withQueryJoins(List.of(
- new QueryJoin(TestUtils.TABLE_NAME_ITEM).withType(QueryJoin.Type.INNER).withSelect(true)
- ));
-
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
-
- List records = queryOutput.getRecords();
- assertThat(records).hasSize(11); // one per line item
- assertThat(records).allMatch(r -> r.getValue("id") != null);
- assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ITEM + ".description") != null);
- }
-
-
-
- /*******************************************************************************
- ** Given tables:
- ** order - orderLine - item
- ** with exposedJoin on item to order
- ** do a query on item, also selecting order.
- ** This is a reverse of the above, to make sure join flipping, etc, is good.
- *******************************************************************************/
- @Test
- void testTwoTableAwayExposedJoinReversed() throws QException
- {
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
-
- QInstance instance = TestUtils.defineInstance();
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ITEM);
-
- queryInput.withQueryJoins(List.of(
- new QueryJoin(TestUtils.TABLE_NAME_ORDER).withType(QueryJoin.Type.INNER).withSelect(true)
- ));
-
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
-
- List records = queryOutput.getRecords();
- assertThat(records).hasSize(11); // one per line item
- assertThat(records).allMatch(r -> r.getValue("description") != null);
- assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ORDER + ".id") != null);
- }
-
-
-
- /*******************************************************************************
- ** Given tables:
- ** order - orderLine - item
- ** with exposedJoin on order to item
- ** do a query on order, also selecting item, and also selecting orderLine...
- *******************************************************************************/
- @Test
- void testTwoTableAwayExposedJoinAlsoSelectingInBetweenTable() throws QException
- {
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
-
- QInstance instance = TestUtils.defineInstance();
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
-
- queryInput.withQueryJoins(List.of(
- new QueryJoin(TestUtils.TABLE_NAME_ORDER_LINE).withType(QueryJoin.Type.INNER).withSelect(true),
- new QueryJoin(TestUtils.TABLE_NAME_ITEM).withType(QueryJoin.Type.INNER).withSelect(true)
- ));
-
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
-
- List records = queryOutput.getRecords();
- assertThat(records).hasSize(11); // one per line item
- assertThat(records).allMatch(r -> r.getValue("id") != null);
- assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ORDER_LINE + ".quantity") != null);
- assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ITEM + ".description") != null);
- }
-
-
-
- /*******************************************************************************
- ** Given tables:
- ** order - orderLine - item
- ** with exposedJoin on order to item
- ** do a query on order, filtered by item
- *******************************************************************************/
- @Test
- void testTwoTableAwayExposedJoinWhereClauseOnly() throws QException
- {
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
-
- QInstance instance = TestUtils.defineInstance();
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_ITEM + ".description", QCriteriaOperator.STARTS_WITH, "Q-Mart")));
-
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
-
- List records = queryOutput.getRecords();
- assertThat(records).hasSize(4);
- assertThat(records).allMatch(r -> r.getValue("id") != null);
- }
-
-
-
- /*******************************************************************************
- ** Given tables:
- ** order - orderLine - item
- ** with exposedJoin on order to item
- ** do a query on order, filtered by item
- *******************************************************************************/
- @Test
- void testTwoTableAwayExposedJoinWhereClauseBothJoinTables() throws QException
- {
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
-
- QInstance instance = TestUtils.defineInstance();
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
- queryInput.setFilter(new QQueryFilter()
- .withCriteria(new QFilterCriteria(TestUtils.TABLE_NAME_ITEM + ".description", QCriteriaOperator.STARTS_WITH, "Q-Mart"))
- .withCriteria(new QFilterCriteria(TestUtils.TABLE_NAME_ORDER_LINE + ".quantity", QCriteriaOperator.IS_NOT_BLANK))
- );
-
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
-
- List records = queryOutput.getRecords();
- assertThat(records).hasSize(4);
- assertThat(records).allMatch(r -> r.getValue("id") != null);
- }
-
-
-
- /*******************************************************************************
- ** queries on the store table, where the primary key (id) is the security field
- *******************************************************************************/
- @Test
- void testRecordSecurityPrimaryKeyFieldNoFilters() throws QException
- {
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_STORE);
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
- assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(3);
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
- assertThat(new QueryAction().execute(queryInput).getRecords())
- .hasSize(1)
- .anyMatch(r -> r.getValueInteger("id").equals(1));
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
- assertThat(new QueryAction().execute(queryInput).getRecords())
- .hasSize(1)
- .anyMatch(r -> r.getValueInteger("id").equals(2));
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession());
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, null));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession().withSecurityKeyValues(Map.of(TestUtils.TABLE_NAME_STORE, Collections.emptyList())));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1).withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3));
- assertThat(new QueryAction().execute(queryInput).getRecords())
- .hasSize(2)
- .anyMatch(r -> r.getValueInteger("id").equals(1))
- .anyMatch(r -> r.getValueInteger("id").equals(3));
- }
-
-
-
- /*******************************************************************************
- ** not really expected to be any different from where we filter on the primary key,
- ** but just good to make sure
- *******************************************************************************/
- @Test
- void testRecordSecurityForeignKeyFieldNoFilters() throws QException
- {
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
- assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(8);
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
- assertThat(new QueryAction().execute(queryInput).getRecords())
- .hasSize(3)
- .allMatch(r -> r.getValueInteger("storeId").equals(1));
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
- assertThat(new QueryAction().execute(queryInput).getRecords())
- .hasSize(2)
- .allMatch(r -> r.getValueInteger("storeId").equals(2));
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession());
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, null));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession().withSecurityKeyValues(Map.of(TestUtils.TABLE_NAME_STORE, Collections.emptyList())));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1).withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3));
- assertThat(new QueryAction().execute(queryInput).getRecords())
- .hasSize(6)
- .allMatch(r -> r.getValueInteger("storeId").equals(1) || r.getValueInteger("storeId").equals(3));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testRecordSecurityWithFilters() throws QException
- {
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
-
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
- assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(6);
-
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
- assertThat(new QueryAction().execute(queryInput).getRecords())
- .hasSize(2)
- .allMatch(r -> r.getValueInteger("storeId").equals(1));
-
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
- QContext.setQSession(new QSession());
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("storeId", QCriteriaOperator.IN, List.of(1, 2))));
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1).withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3));
- assertThat(new QueryAction().execute(queryInput).getRecords())
- .hasSize(3)
- .allMatch(r -> r.getValueInteger("storeId").equals(1));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testRecordSecurityFromJoinTableAlsoImplicitlyInQuery() throws QException
- {
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER_LINE);
-
- ///////////////////////////////////////////////////////////////////////////////////////////
- // orders 1, 2, and 3 are from store 1, so their lines (5 in total) should be found. //
- // note, order 2 has the line with mis-matched store id - but, that shouldn't apply here //
- ///////////////////////////////////////////////////////////////////////////////////////////
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("order.id", QCriteriaOperator.IN, List.of(1, 2, 3, 4))));
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
- assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(5);
-
- ///////////////////////////////////////////////////////////////////
- // order 4 should be the only one found this time (with 2 lines) //
- ///////////////////////////////////////////////////////////////////
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("order.id", QCriteriaOperator.IN, List.of(1, 2, 3, 4))));
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
- assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(2);
-
- ////////////////////////////////////////////////////////////////
- // make sure we're also good if we explicitly join this table //
- ////////////////////////////////////////////////////////////////
- queryInput.withQueryJoin(new QueryJoin().withJoinTable(TestUtils.TABLE_NAME_ORDER).withSelect(true));
- assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(2);
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
@@ -1627,68 +978,6 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testRecordSecurityWithLockFromJoinTable() throws QException
- {
- QInstance qInstance = TestUtils.defineInstance();
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
-
- /////////////////////////////////////////////////////////////////////////////////////////////////
- // remove the normal lock on the order table - replace it with one from the joined store table //
- /////////////////////////////////////////////////////////////////////////////////////////////////
- qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().clear();
- qInstance.getTable(TestUtils.TABLE_NAME_ORDER).withRecordSecurityLock(new RecordSecurityLock()
- .withSecurityKeyType(TestUtils.TABLE_NAME_STORE)
- .withJoinNameChain(List.of("orderJoinStore"))
- .withFieldName("store.id"));
-
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
- assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(6);
-
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
- assertThat(new QueryAction().execute(queryInput).getRecords())
- .hasSize(2)
- .allMatch(r -> r.getValueInteger("storeId").equals(1));
-
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
- QContext.setQSession(new QSession());
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- queryInput.setFilter(new QQueryFilter(new QFilterCriteria("storeId", QCriteriaOperator.IN, List.of(1, 2))));
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1).withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3));
- assertThat(new QueryAction().execute(queryInput).getRecords())
- .hasSize(3)
- .allMatch(r -> r.getValueInteger("storeId").equals(1));
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testRecordSecurityWithLockFromJoinTableWhereTheKeyIsOnTheManySide() throws QException
- {
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_WAREHOUSE);
-
- assertThat(new QueryAction().execute(queryInput).getRecords())
- .hasSize(1);
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
@@ -1718,51 +1007,4 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
}
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Test
- void testMultipleReversedDirectionJoinsBetweenSameTables() throws QException
- {
- QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
-
- {
- /////////////////////////////////////////////////////////
- // assert a failure if the join to use isn't specified //
- /////////////////////////////////////////////////////////
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_INSTRUCTIONS));
- assertThatThrownBy(() -> new QueryAction().execute(queryInput)).rootCause().hasMessageContaining("More than 1 join was found");
- }
-
- Integer noOfOrders = new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_ORDER)).getCount();
- Integer noOfOrderInstructions = new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_ORDER_INSTRUCTIONS)).getCount();
-
- {
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // make sure we can join on order.current_order_instruction_id = order_instruction.id -- and that we get back 1 row per order //
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_INSTRUCTIONS).withJoinMetaData(QContext.getQInstance().getJoin("orderJoinCurrentOrderInstructions")));
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(noOfOrders, queryOutput.getRecords().size());
- }
-
- {
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // make sure we can join on order.id = order_instruction.order_id -- and that we get back 1 row per order instruction //
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- QueryInput queryInput = new QueryInput();
- queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
- queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_INSTRUCTIONS).withJoinMetaData(QContext.getQInstance().getJoin("orderInstructionsJoinOrder")));
- QueryOutput queryOutput = new QueryAction().execute(queryInput);
- assertEquals(noOfOrderInstructions, queryOutput.getRecords().size());
- }
-
- }
-
}
diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/sharing/SharingMetaDataProvider.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/sharing/SharingMetaDataProvider.java
new file mode 100644
index 00000000..98f822c6
--- /dev/null
+++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/sharing/SharingMetaDataProvider.java
@@ -0,0 +1,168 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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 .
+ */
+
+package com.kingsrook.qqq.backend.module.rdbms.sharing;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
+import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
+import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
+import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
+import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
+import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
+import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
+import com.kingsrook.qqq.backend.module.rdbms.sharing.model.Asset;
+import com.kingsrook.qqq.backend.module.rdbms.sharing.model.Client;
+import com.kingsrook.qqq.backend.module.rdbms.sharing.model.Group;
+import com.kingsrook.qqq.backend.module.rdbms.sharing.model.SharedAsset;
+import com.kingsrook.qqq.backend.module.rdbms.sharing.model.User;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class SharingMetaDataProvider
+{
+ public static final String USER_ID_KEY_TYPE = "userIdKey";
+ public static final String USER_ID_ALL_ACCESS_KEY_TYPE = "userIdAllAccessKey";
+
+ public static final String GROUP_ID_KEY_TYPE = "groupIdKey";
+ public static final String GROUP_ID_ALL_ACCESS_KEY_TYPE = "groupIdAllAccessKey";
+
+ private static final String ASSET_JOIN_SHARED_ASSET = "assetJoinSharedAsset";
+ private static final String SHARED_ASSET_JOIN_ASSET = "sharedAssetJoinAsset";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void defineAll(QInstance qInstance) throws QException
+ {
+ qInstance.addSecurityKeyType(new QSecurityKeyType()
+ .withName(USER_ID_KEY_TYPE)
+ .withAllAccessKeyName(USER_ID_ALL_ACCESS_KEY_TYPE));
+
+ qInstance.addSecurityKeyType(new QSecurityKeyType()
+ .withName(GROUP_ID_KEY_TYPE)
+ .withAllAccessKeyName(GROUP_ID_ALL_ACCESS_KEY_TYPE));
+
+ qInstance.addTable(new QTableMetaData()
+ .withName(Asset.TABLE_NAME)
+ .withPrimaryKeyField("id")
+ .withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
+ .withBackendDetails(new RDBMSTableBackendDetails().withTableName("asset"))
+ .withFieldsFromEntity(Asset.class)
+
+ ////////////////////////////////////////
+ // This is original - just owner/user //
+ ////////////////////////////////////////
+ // .withRecordSecurityLock(new RecordSecurityLock()
+ // .withSecurityKeyType(USER_ID_KEY_TYPE)
+ // .withFieldName("userId")));
+
+ .withRecordSecurityLock(new MultiRecordSecurityLock()
+ .withOperator(MultiRecordSecurityLock.BooleanOperator.OR)
+ .withLock(new RecordSecurityLock()
+ .withSecurityKeyType(USER_ID_KEY_TYPE)
+ .withFieldName("userId"))
+ .withLock(new RecordSecurityLock()
+ .withSecurityKeyType(USER_ID_KEY_TYPE)
+ .withFieldName("sharedAsset.userId")
+ .withJoinNameChain(List.of(SHARED_ASSET_JOIN_ASSET)))
+ .withLock(new RecordSecurityLock()
+ .withSecurityKeyType(GROUP_ID_KEY_TYPE)
+ .withFieldName("sharedAsset.groupId")
+ .withJoinNameChain(List.of(SHARED_ASSET_JOIN_ASSET)))
+ ));
+ QInstanceEnricher.setInferredFieldBackendNames(qInstance.getTable(Asset.TABLE_NAME));
+
+ qInstance.addTable(new QTableMetaData()
+ .withName(SharedAsset.TABLE_NAME)
+ .withBackendDetails(new RDBMSTableBackendDetails().withTableName("shared_asset"))
+ .withPrimaryKeyField("id")
+ .withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
+ .withFieldsFromEntity(SharedAsset.class)
+ .withRecordSecurityLock(new MultiRecordSecurityLock()
+ .withOperator(MultiRecordSecurityLock.BooleanOperator.OR)
+ .withLock(new RecordSecurityLock()
+ .withSecurityKeyType(USER_ID_KEY_TYPE)
+ .withFieldName("userId"))
+ .withLock(new RecordSecurityLock()
+ .withSecurityKeyType(GROUP_ID_KEY_TYPE)
+ .withFieldName("groupId"))
+ ));
+ QInstanceEnricher.setInferredFieldBackendNames(qInstance.getTable(SharedAsset.TABLE_NAME));
+
+ qInstance.addTable(new QTableMetaData()
+ .withName(User.TABLE_NAME)
+ .withPrimaryKeyField("id")
+ .withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
+ .withFieldsFromEntity(User.class)
+ .withRecordSecurityLock(new RecordSecurityLock()
+ .withSecurityKeyType(USER_ID_KEY_TYPE)
+ .withFieldName("id")));
+ QInstanceEnricher.setInferredFieldBackendNames(qInstance.getTable(User.TABLE_NAME));
+
+ qInstance.addTable(new QTableMetaData()
+ .withName(Group.TABLE_NAME)
+ .withPrimaryKeyField("id")
+ .withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
+ .withFieldsFromEntity(Group.class));
+ QInstanceEnricher.setInferredFieldBackendNames(qInstance.getTable(Group.TABLE_NAME));
+
+ qInstance.addTable(new QTableMetaData()
+ .withName(Client.TABLE_NAME)
+ .withPrimaryKeyField("id")
+ .withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
+ .withFieldsFromEntity(Client.class));
+ QInstanceEnricher.setInferredFieldBackendNames(qInstance.getTable(Client.TABLE_NAME));
+
+ qInstance.addPossibleValueSource(QPossibleValueSource.newForTable(User.TABLE_NAME));
+ qInstance.addPossibleValueSource(QPossibleValueSource.newForTable(Group.TABLE_NAME));
+ qInstance.addPossibleValueSource(QPossibleValueSource.newForTable(Client.TABLE_NAME));
+ qInstance.addPossibleValueSource(QPossibleValueSource.newForTable(Asset.TABLE_NAME));
+
+ qInstance.addJoin(new QJoinMetaData()
+ .withName(ASSET_JOIN_SHARED_ASSET)
+ .withLeftTable(Asset.TABLE_NAME)
+ .withRightTable(SharedAsset.TABLE_NAME)
+ .withType(JoinType.ONE_TO_MANY)
+ .withJoinOn(new JoinOn("id", "assetId"))
+ );
+
+ qInstance.addJoin(new QJoinMetaData()
+ .withName(SHARED_ASSET_JOIN_ASSET)
+ .withLeftTable(SharedAsset.TABLE_NAME)
+ .withRightTable(Asset.TABLE_NAME)
+ .withType(JoinType.MANY_TO_ONE)
+ .withJoinOn(new JoinOn("assetId", "id"))
+ );
+ }
+
+}
diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/sharing/SharingTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/sharing/SharingTest.java
new file mode 100644
index 00000000..c8b7fa20
--- /dev/null
+++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/sharing/SharingTest.java
@@ -0,0 +1,514 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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 .
+ */
+
+package com.kingsrook.qqq.backend.module.rdbms.sharing;
+
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
+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.session.QSession;
+import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
+import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
+import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
+import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
+import com.kingsrook.qqq.backend.module.rdbms.sharing.model.Asset;
+import com.kingsrook.qqq.backend.module.rdbms.sharing.model.Group;
+import com.kingsrook.qqq.backend.module.rdbms.sharing.model.SharedAsset;
+import com.kingsrook.qqq.backend.module.rdbms.sharing.model.User;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import static com.kingsrook.qqq.backend.module.rdbms.sharing.SharingMetaDataProvider.GROUP_ID_ALL_ACCESS_KEY_TYPE;
+import static com.kingsrook.qqq.backend.module.rdbms.sharing.SharingMetaDataProvider.GROUP_ID_KEY_TYPE;
+import static com.kingsrook.qqq.backend.module.rdbms.sharing.SharingMetaDataProvider.USER_ID_ALL_ACCESS_KEY_TYPE;
+import static com.kingsrook.qqq.backend.module.rdbms.sharing.SharingMetaDataProvider.USER_ID_KEY_TYPE;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class SharingTest
+{
+ //////////////
+ // user ids //
+ //////////////
+ public static final int HOMER_ID = 1;
+ public static final int MARGE_ID = 2;
+ public static final int BART_ID = 3;
+ public static final int LISA_ID = 4;
+ public static final int BURNS_ID = 5;
+
+ ///////////////
+ // group ids //
+ ///////////////
+ public static final int SIMPSONS_ID = 1;
+ public static final int POWER_PLANT_ID = 2;
+ public static final int BOGUS_GROUP_ID = Integer.MAX_VALUE;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ void beforeEach() throws Exception
+ {
+ TestUtils.primeTestDatabase("prime-test-database-sharing-test.sql");
+
+ QInstance qInstance = TestUtils.defineInstance();
+ SharingMetaDataProvider.defineAll(qInstance);
+
+ QContext.init(qInstance, new QSession());
+
+ loadData();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void loadData() throws QException
+ {
+ QContext.getQSession().withSecurityKeyValue(SharingMetaDataProvider.USER_ID_ALL_ACCESS_KEY_TYPE, true);
+
+ List userList = List.of(
+ new User().withId(HOMER_ID).withUsername("homer"),
+ new User().withId(MARGE_ID).withUsername("marge"),
+ new User().withId(BART_ID).withUsername("bart"),
+ new User().withId(LISA_ID).withUsername("lisa"),
+ new User().withId(BURNS_ID).withUsername("burns"));
+ new InsertAction().execute(new InsertInput(User.TABLE_NAME).withRecordEntities(userList));
+
+ List groupList = List.of(
+ new Group().withId(SIMPSONS_ID).withName("simpsons"),
+ new Group().withId(POWER_PLANT_ID).withName("powerplant"));
+ new InsertAction().execute(new InsertInput(Group.TABLE_NAME).withRecordEntities(groupList));
+
+ List assetList = List.of(
+ new Asset().withId(1).withName("742evergreen").withUserId(HOMER_ID),
+ new Asset().withId(2).withName("beer").withUserId(HOMER_ID),
+ new Asset().withId(3).withName("car").withUserId(MARGE_ID),
+ new Asset().withId(4).withName("skateboard").withUserId(BART_ID),
+ new Asset().withId(5).withName("santaslittlehelper").withUserId(BART_ID),
+ new Asset().withId(6).withName("saxamaphone").withUserId(LISA_ID),
+ new Asset().withId(7).withName("radiation").withUserId(BURNS_ID));
+ new InsertAction().execute(new InsertInput(Asset.TABLE_NAME).withRecordEntities(assetList));
+
+ List sharedAssetList = List.of(
+ new SharedAsset().withAssetId(1).withGroupId(SIMPSONS_ID), // homer shares his house with the simpson family (group)
+ new SharedAsset().withAssetId(3).withUserId(HOMER_ID), // marge shares a car with homer
+ new SharedAsset().withAssetId(5).withGroupId(SIMPSONS_ID), // bart shares santa's little helper with the whole family
+ new SharedAsset().withAssetId(7).withGroupId(POWER_PLANT_ID) // burns shares radiation with the power plant
+ );
+ new InsertAction().execute(new InsertInput(SharedAsset.TABLE_NAME).withRecordEntities(sharedAssetList));
+
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testQueryAssetWithUserIdOnlySecurityKey() throws QException
+ {
+ ////////////////////////////////////////////////////////////////////
+ // update the asset table to change its lock to only be on userId //
+ ////////////////////////////////////////////////////////////////////
+ QContext.getQInstance().getTable(Asset.TABLE_NAME)
+ .withRecordSecurityLocks(List.of(new RecordSecurityLock()
+ .withSecurityKeyType(USER_ID_KEY_TYPE)
+ .withFieldName("userId")));
+
+ ////////////////////////////////////////////////////////
+ // with nothing in session, make sure we find nothing //
+ ////////////////////////////////////////////////////////
+ assertEquals(0, new QueryAction().execute(new QueryInput(Asset.TABLE_NAME)).getRecords().size());
+
+ ////////////////////////////////////
+ // marge direct owner only of car //
+ ////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, MARGE_ID);
+ assertEquals(1, new QueryAction().execute(new QueryInput(Asset.TABLE_NAME)).getRecords().size());
+
+ /////////////////////////////////////////////////
+ // homer direct owner of 742evergreen and beer //
+ /////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, HOMER_ID);
+ assertEquals(2, new QueryAction().execute(new QueryInput(Asset.TABLE_NAME)).getRecords().size());
+
+ /////////////////////////////////////////////////////
+ // marge & homer - own car, 742evergreen, and beer //
+ /////////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, HOMER_ID);
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, MARGE_ID);
+ assertEquals(3, new QueryAction().execute(new QueryInput(Asset.TABLE_NAME)).getRecords().size());
+ }
+
+
+
+ /*******************************************************************************
+ ** normally (?) maybe we wouldn't query sharedAsset directly (we'd instead query
+ ** for asset, and understand that there's a security lock coming from sharedAsset),
+ ** but this test is here as we build up making a more complex lock like that.
+ *******************************************************************************/
+ @Test
+ void testQuerySharedAssetDirectly() throws QException
+ {
+ ////////////////////////////////////////////////////////
+ // with nothing in session, make sure we find nothing //
+ ////////////////////////////////////////////////////////
+ assertEquals(0, new QueryAction().execute(new QueryInput(SharedAsset.TABLE_NAME)).getRecords().size());
+
+ /////////////////////////////////////
+ // homer has a car shared with him //
+ /////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, HOMER_ID);
+ assertEquals(1, new QueryAction().execute(new QueryInput(SharedAsset.TABLE_NAME)).getRecords().size());
+
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // now put homer's groups in the session as well - and we should find 742evergreen too //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, HOMER_ID);
+ QContext.getQSession().withSecurityKeyValue(GROUP_ID_KEY_TYPE, SIMPSONS_ID);
+ QContext.getQSession().withSecurityKeyValue(GROUP_ID_KEY_TYPE, POWER_PLANT_ID);
+ List records = new QueryAction().execute(new QueryInput(SharedAsset.TABLE_NAME)).getRecords();
+ assertEquals(4, records.size());
+ }
+
+
+
+ /*******************************************************************************
+ ** real-world use-case (e.g., why sharing concept exists) - query the asset table
+ **
+ *******************************************************************************/
+ @Test
+ void testQueryAssetsWithLockThroughSharing() throws QException, SQLException
+ {
+ ////////////////////////////////////////////////////////
+ // with nothing in session, make sure we find nothing //
+ ////////////////////////////////////////////////////////
+ assertEquals(0, new QueryAction().execute(new QueryInput(Asset.TABLE_NAME)).getRecords().size());
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // homer has a car shared with him and 2 things he owns himself - so w/ only his userId in session (and no groups), should find those 3 //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, HOMER_ID);
+ assertEquals(3, new QueryAction().execute(new QueryInput(Asset.TABLE_NAME)).getRecords().size());
+
+ //////////////////////////////////////////////////////////////////////
+ // add a group that matches nothing now, just to ensure same result //
+ //////////////////////////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValue(GROUP_ID_KEY_TYPE, BOGUS_GROUP_ID);
+ assertEquals(3, new QueryAction().execute(new QueryInput(Asset.TABLE_NAME)).getRecords().size());
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // now put homer's groups in the session as well - and we should find the 3 from above, plus a shared family asset and shared power-plant asset //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, HOMER_ID);
+ QContext.getQSession().withSecurityKeyValue(GROUP_ID_KEY_TYPE, SIMPSONS_ID);
+ QContext.getQSession().withSecurityKeyValue(GROUP_ID_KEY_TYPE, POWER_PLANT_ID);
+ assertEquals(5, new QueryAction().execute(new QueryInput(Asset.TABLE_NAME)).getRecords().size());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testQueryAllAccessKeys() throws QException
+ {
+ ///////////////////////////////////////////////////////////////
+ // with user-id all access key, should get all asset records //
+ ///////////////////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ QContext.getQSession().withSecurityKeyValue(USER_ID_ALL_ACCESS_KEY_TYPE, true);
+ assertEquals(7, new QueryAction().execute(new QueryInput(Asset.TABLE_NAME)).getRecords().size());
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // with group-id all access key... //
+ // the original thought was, that we should get all assets which are shared to any group //
+ // but the code that we first wrote generates SQL w/ an OR (1=1) clause, meaning we get all //
+ // assets, which makes some sense too, so we'll go with that for now... //
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ QContext.getQSession().withSecurityKeyValue(GROUP_ID_ALL_ACCESS_KEY_TYPE, true);
+ assertEquals(7, new QueryAction().execute(new QueryInput(Asset.TABLE_NAME)).getRecords().size());
+ }
+
+
+
+ /*******************************************************************************
+ ** if I'm only able to access user 1 and 2, I shouldn't be able to share to user 3
+ *******************************************************************************/
+ @Test
+ void testInsertUpdateDeleteShareUserIdKey() throws QException, SQLException
+ {
+ SharedAsset recordToInsert = new SharedAsset().withUserId(3).withAssetId(6);
+
+ /////////////////////////////////////////
+ // empty set of keys should give error //
+ /////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ InsertOutput insertOutput = new InsertAction().execute(new InsertInput(SharedAsset.TABLE_NAME).withRecordEntity(recordToInsert));
+ assertThat(insertOutput.getRecords().get(0).getErrors()).isNotEmpty();
+
+ /////////////////////////////////////////////
+ // mis-matched keys should give same error //
+ /////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, 1);
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, 2);
+ insertOutput = new InsertAction().execute(new InsertInput(SharedAsset.TABLE_NAME).withRecordEntity(recordToInsert));
+ assertThat(insertOutput.getRecords().get(0).getErrors()).isNotEmpty();
+
+ /////////////////////////////////////////////////////////
+ // then if I get user 3, I can insert the share for it //
+ /////////////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, 3);
+ insertOutput = new InsertAction().execute(new InsertInput(SharedAsset.TABLE_NAME).withRecordEntity(recordToInsert));
+ assertThat(insertOutput.getRecords().get(0).getErrors()).isEmpty();
+
+ /////////////////////////////////////////
+ // get ready for a sequence of updates //
+ /////////////////////////////////////////
+ Integer shareId = insertOutput.getRecords().get(0).getValueInteger("id");
+ Supplier makeRecordToUpdate = () -> new QRecord().withValue("id", shareId).withValue("modifyDate", Instant.now());
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // now w/o user 3 in my session, I shouldn't be allowed to update that share //
+ // start w/ empty security keys //
+ ///////////////////////////////////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(SharedAsset.TABLE_NAME).withRecord(makeRecordToUpdate.get()));
+ assertThat(updateOutput.getRecords().get(0).getErrors())
+ .anyMatch(e -> e.getMessage().contains("No record was found")); // because w/o the key, you can't even see it.
+
+ /////////////////////////////////////////////
+ // mis-matched keys should give same error //
+ /////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, 1);
+ updateOutput = new UpdateAction().execute(new UpdateInput(SharedAsset.TABLE_NAME).withRecord(makeRecordToUpdate.get()));
+ assertThat(updateOutput.getRecords().get(0).getErrors())
+ .anyMatch(e -> e.getMessage().contains("No record was found")); // because w/o the key, you can't even see it.
+
+ //////////////////////////////////////////////////
+ // now with user id 3, should be able to update //
+ //////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, 3);
+ updateOutput = new UpdateAction().execute(new UpdateInput(SharedAsset.TABLE_NAME).withRecord(makeRecordToUpdate.get()));
+ assertThat(updateOutput.getRecords().get(0).getErrors()).isEmpty();
+
+ //////////////////////////////////////////////////////////////////////////
+ // now see if you can update to a user that you don't have (you can't!) //
+ //////////////////////////////////////////////////////////////////////////
+ updateOutput = new UpdateAction().execute(new UpdateInput(SharedAsset.TABLE_NAME).withRecord(makeRecordToUpdate.get().withValue("userId", 2)));
+ assertThat(updateOutput.getRecords().get(0).getErrors()).isNotEmpty();
+
+ ///////////////////////////////////////////////////////////////////////
+ // Add that user (2) to the session - then the update should succeed //
+ ///////////////////////////////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, 2);
+ updateOutput = new UpdateAction().execute(new UpdateInput(SharedAsset.TABLE_NAME).withRecord(makeRecordToUpdate.get().withValue("userId", 2)));
+ assertThat(updateOutput.getRecords().get(0).getErrors()).isEmpty();
+
+ ///////////////////////////////////////////////
+ // now move on to deletes - first empty keys //
+ ///////////////////////////////////////////////
+ QContext.getQSession().withSecurityKeyValues(new HashMap<>());
+ DeleteOutput deleteOutput = new DeleteAction().execute(new DeleteInput(SharedAsset.TABLE_NAME).withPrimaryKey(shareId));
+ assertEquals(0, deleteOutput.getDeletedRecordCount()); // can't even find it, so no error to be reported.
+
+ ///////////////////////
+ // next mismatch key //
+ ///////////////////////
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, 1);
+ deleteOutput = new DeleteAction().execute(new DeleteInput(SharedAsset.TABLE_NAME).withPrimaryKey(shareId));
+ assertEquals(0, deleteOutput.getDeletedRecordCount()); // can't even find it, so no error to be reported.
+
+ ///////////////////
+ // next success! //
+ ///////////////////
+ QContext.getQSession().withSecurityKeyValue(USER_ID_KEY_TYPE, 2);
+ deleteOutput = new DeleteAction().execute(new DeleteInput(SharedAsset.TABLE_NAME).withPrimaryKey(shareId));
+ assertEquals(1, deleteOutput.getDeletedRecordCount());
+ }
+
+
+
+ /*******************************************************************************
+ ** useful to debug (e.g., to see inside h2). add calls as needed.
+ *******************************************************************************/
+ private void printSQL(String sql) throws SQLException
+ {
+ Connection connection = new ConnectionManager().getConnection((RDBMSBackendMetaData) QContext.getQInstance().getBackend(TestUtils.DEFAULT_BACKEND_NAME));
+ List