diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/DistinctFilteringRecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/DistinctFilteringRecordPipe.java
new file mode 100644
index 00000000..ad2aa3fd
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/DistinctFilteringRecordPipe.java
@@ -0,0 +1,146 @@
+/*
+ * 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.Serializable;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
+
+
+/*******************************************************************************
+ ** Subclass of record pipe that ony allows through distinct records, based on
+ ** the set of fields specified in the constructor as a uniqueKey.
+ *******************************************************************************/
+public class DistinctFilteringRecordPipe extends RecordPipe
+{
+ private UniqueKey uniqueKey;
+ private Set seenValues = new HashSet<>();
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public DistinctFilteringRecordPipe(UniqueKey uniqueKey)
+ {
+ this.uniqueKey = uniqueKey;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor that accepts pipe's overrideCapacity (allowed to be null)
+ **
+ *******************************************************************************/
+ public DistinctFilteringRecordPipe(UniqueKey uniqueKey, Integer overrideCapacity)
+ {
+ super(overrideCapacity);
+ this.uniqueKey = uniqueKey;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void addRecords(List records) throws QException
+ {
+ List recordsToAdd = new ArrayList<>();
+ for(QRecord record : records)
+ {
+ if(!seenBefore(record))
+ {
+ recordsToAdd.add(record);
+ }
+ }
+
+ if(recordsToAdd.isEmpty())
+ {
+ return;
+ }
+
+ super.addRecords(recordsToAdd);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void addRecord(QRecord record) throws QException
+ {
+ if(seenBefore(record))
+ {
+ return;
+ }
+
+ super.addRecord(record);
+ }
+
+
+
+ /*******************************************************************************
+ ** return true if we've seen this record before (based on the unique key) -
+ ** also - update the set of seen values!
+ *******************************************************************************/
+ private boolean seenBefore(QRecord record)
+ {
+ Serializable ukValues = extractUKValues(record);
+ if(seenValues.contains(ukValues))
+ {
+ return true;
+ }
+ seenValues.add(ukValues);
+ return false;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private Serializable extractUKValues(QRecord record)
+ {
+ if(uniqueKey.getFieldNames().size() == 1)
+ {
+ return (record.getValue(uniqueKey.getFieldNames().get(0)));
+ }
+ else
+ {
+ ArrayList rs = new ArrayList<>();
+ for(String fieldName : uniqueKey.getFieldNames())
+ {
+ rs.add(record.getValue(fieldName));
+ }
+ return (rs);
+ }
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java
index 6bd4b83d..51241382 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java
@@ -189,6 +189,9 @@ public class ExportAction
Set addedJoinNames = new HashSet<>();
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
{
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // make sure that any tables being selected from are included as (LEFT) joins in the query //
+ /////////////////////////////////////////////////////////////////////////////////////////////
for(String fieldName : exportInput.getFieldNames())
{
if(fieldName.contains("."))
@@ -197,27 +200,7 @@ public class ExportAction
String joinTableName = parts[0];
if(!addedJoinNames.contains(joinTableName))
{
- QueryJoin queryJoin = new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true);
- queryJoins.add(queryJoin);
-
- /////////////////////////////////////////////////////////////////////////////////////////////
- // in at least some cases, we need to let the queryJoin know what join-meta-data to use... //
- // This code basically mirrors what QFMD is doing right now, so it's better - //
- // but shouldn't all of this just be in JoinsContext? it does some of this... //
- /////////////////////////////////////////////////////////////////////////////////////////////
- QTableMetaData table = exportInput.getTable();
- Optional exposedJoinOptional = CollectionUtils.nonNullList(table.getExposedJoins()).stream().filter(ej -> ej.getJoinTable().equals(joinTableName)).findFirst();
- if(exposedJoinOptional.isEmpty())
- {
- throw (new QException("Could not find exposed join between base table " + table.getName() + " and requested join table " + joinTableName));
- }
- ExposedJoin exposedJoin = exposedJoinOptional.get();
-
- if(exposedJoin.getJoinPath().size() == 1)
- {
- queryJoin.setJoinMetaData(QContext.getQInstance().getJoin(exposedJoin.getJoinPath().get(exposedJoin.getJoinPath().size() - 1)));
- }
-
+ queryJoins.add(new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true));
addedJoinNames.add(joinTableName);
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java
index dff2c4de..9625d4e8 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@@ -43,8 +44,9 @@ public class RecordPipe
private static final long BLOCKING_SLEEP_MILLIS = 100;
private static final long MAX_SLEEP_LOOP_MILLIS = 300_000; // 5 minutes
+ private static final int DEFAULT_CAPACITY = 1_000;
- private ArrayBlockingQueue queue = new ArrayBlockingQueue<>(1_000);
+ private ArrayBlockingQueue queue = new ArrayBlockingQueue<>(DEFAULT_CAPACITY);
private boolean isTerminated = false;
@@ -69,10 +71,12 @@ public class RecordPipe
/*******************************************************************************
** Construct a record pipe, with an alternative capacity for the internal queue.
+ **
+ ** overrideCapacity is allowed to be null - in which case, DEFAULT_CAPACITY is used.
*******************************************************************************/
public RecordPipe(Integer overrideCapacity)
{
- queue = new ArrayBlockingQueue<>(overrideCapacity);
+ queue = new ArrayBlockingQueue<>(Objects.requireNonNullElse(overrideCapacity, DEFAULT_CAPACITY));
}
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..70aa2ccb 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,16 @@ 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.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 +65,17 @@ public class JoinsContext
private final String mainTableName;
private final List queryJoins;
+ private final QQueryFilter securityFilter;
+
////////////////////////////////////////////////////////////////
// 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;
@@ -74,54 +85,182 @@ 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();
+
+ // 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);
+
+ ///////////////////////////////////////////////////////////////////////////////////////
+ // ensure that any record locks on the main table, which require a join, are present //
+ ///////////////////////////////////////////////////////////////////////////////////////
+ for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())))
+ {
+ ensureRecordSecurityLockIsRepresented(tableName, tableName, recordSecurityLock, null);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////
+ // 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();
+
///////////////////////////////////////////////////////////////
// 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);
- 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);
+ }
+
+ for(QQueryFilter subFilter : securityFilter.getSubFilters())
+ {
+ filter.addSubFilter(subFilter);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** 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 //
+ /////////////////////////////////////////////////
+ if(processedQueryJoins.contains(queryJoin))
+ {
+ 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());
+ for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()))
+ {
+ List addedQueryJoins = ensureRecordSecurityLockIsRepresented(joinTable.getName(), queryJoin.getJoinTableOrItsAlias(), recordSecurityLock, 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<>();
+
///////////////////////////////////////////////////////////////////////////////////////////////////
- // 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 +268,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 +299,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 +340,193 @@ 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);
+
+ 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);
+
+ 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());
+ if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
+ {
+ ///////////////////////////////////////////////////////////////////////////////
+ // if we have all-access on this key, then we don't need a criterion for it. //
+ ///////////////////////////////////////////////////////////////////////////////
+ if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
+ {
+ 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<>());
+ }
+
+ 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);
+ }
+
+ 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 - 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(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 //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(sourceQueryJoin != null)
+ {
+ 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)
+ {
+ 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.securityFilter.addSubFilter(lockFilter);
}
}
@@ -197,9 +538,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 +549,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 +618,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 +628,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 +648,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 +659,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 +688,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 +724,7 @@ public class JoinsContext
}
while(addedJoin);
+ log("Done adding missing join meta data");
}
@@ -370,12 +734,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 +759,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 +804,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 +815,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 +870,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 +889,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 +937,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 +968,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 +1020,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 +1067,63 @@ public class JoinsContext
LOG.log(logLevel, message, null, logPairs);
}
+
+
+ /*******************************************************************************
+ ** 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);
+ 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/QQueryFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
index 6ce122bb..36933402 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
@@ -136,7 +136,7 @@ public class QQueryFilter implements Serializable, Cloneable
/*******************************************************************************
- **
+ ** recursively look at both this filter, and any sub-filters it may have.
*******************************************************************************/
public boolean hasAnyCriteria()
{
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/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 f1d6827b..009a6981 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
@@ -226,16 +226,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/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 1f3e776d..c6038ae9 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());
@@ -135,6 +146,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);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -144,6 +194,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();
@@ -243,4 +294,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/StreamedETLExecuteStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java
index d842cf03..2fb6c34f 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java
@@ -34,7 +34,6 @@ import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
-import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
@@ -77,17 +76,19 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
loadStep.setTransformStep(transformStep);
+ extractStep.preRun(runBackendStepInput, runBackendStepOutput);
+ transformStep.preRun(runBackendStepInput, runBackendStepOutput);
+ loadStep.preRun(runBackendStepInput, runBackendStepOutput);
+
/////////////////////////////////////////////////////////////////////////////
// let the load step override the capacity for the record pipe. //
// this is useful for slower load steps - so that the extract step doesn't //
// fill the pipe, then timeout waiting for all the records to be consumed, //
// before it can put more records in. //
/////////////////////////////////////////////////////////////////////////////
- RecordPipe recordPipe;
- Integer overrideRecordPipeCapacity = loadStep.getOverrideRecordPipeCapacity(runBackendStepInput);
+ Integer overrideRecordPipeCapacity = loadStep.getOverrideRecordPipeCapacity(runBackendStepInput);
if(overrideRecordPipeCapacity != null)
{
- recordPipe = new RecordPipe(overrideRecordPipeCapacity);
LOG.debug("per " + loadStep.getClass().getName() + ", we are overriding record pipe capacity to: " + overrideRecordPipeCapacity);
}
else
@@ -95,20 +96,12 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
overrideRecordPipeCapacity = transformStep.getOverrideRecordPipeCapacity(runBackendStepInput);
if(overrideRecordPipeCapacity != null)
{
- recordPipe = new RecordPipe(overrideRecordPipeCapacity);
LOG.debug("per " + transformStep.getClass().getName() + ", we are overriding record pipe capacity to: " + overrideRecordPipeCapacity);
}
- else
- {
- recordPipe = new RecordPipe();
- }
}
+ RecordPipe recordPipe = extractStep.createRecordPipe(runBackendStepInput, overrideRecordPipeCapacity);
extractStep.setRecordPipe(recordPipe);
- extractStep.preRun(runBackendStepInput, runBackendStepOutput);
-
- transformStep.preRun(runBackendStepInput, runBackendStepOutput);
- loadStep.preRun(runBackendStepInput, runBackendStepOutput);
/////////////////////////////////////////////////////////////////////////////
// open a transaction for the whole process, if that's the requested level //
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 4921c9ce..f8edde2f 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 63d858c1..d9b6dd34 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,17 +77,29 @@ 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");
- RecordPipe recordPipe = new RecordPipe();
- AbstractExtractStep extractStep = getExtractStep(runBackendStepInput);
extractStep.setLimit(null);
- extractStep.setRecordPipe(recordPipe);
extractStep.preRun(runBackendStepInput, runBackendStepOutput);
- AbstractTransformStep transformStep = getTransformStep(runBackendStepInput);
+ //////////////////////////////////////////
+ // set up a record pipe for the process //
+ //////////////////////////////////////////
+ Integer overrideRecordPipeCapacity = transformStep.getOverrideRecordPipeCapacity(runBackendStepInput);
+ if(overrideRecordPipeCapacity != null)
+ {
+ LOG.debug("per " + transformStep.getClass().getName() + ", we are overriding record pipe capacity to: " + overrideRecordPipeCapacity);
+ }
+
+ RecordPipe recordPipe = extractStep.createRecordPipe(runBackendStepInput, null);
+ extractStep.setRecordPipe(recordPipe);
+
transformStep.preRun(runBackendStepInput, runBackendStepOutput);
List previewRecordList = new ArrayList<>();
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 8d223435..a8ea14f3 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
@@ -52,9 +52,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;
@@ -68,12 +66,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.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;
@@ -212,7 +208,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
/*******************************************************************************
**
*******************************************************************************/
- 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));
@@ -229,17 +225,9 @@ public abstract class AbstractRDBMSAction implements QActionInterface
////////////////////////////////////////////////////////////
// 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())
{
@@ -270,6 +258,14 @@ public abstract class AbstractRDBMSAction implements QActionInterface
+ " = " + escapeIdentifier(joinTableOrAlias)
+ "." + escapeIdentifier(getColumnName((rightTable.getField(joinOn.getRightField())))));
}
+
+ if(CollectionUtils.nullSafeHasContents(queryJoin.getSecurityCriteria()))
+ {
+ String securityOnClause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(joinsContext, queryJoin.getSecurityCriteria(), QQueryFilter.BooleanOperator.AND, params);
+ LOG.debug("Wrote securityOnClause", logPair("clause", securityOnClause));
+ joinClauseList.add(securityOnClause);
+ }
+
rs.append(" ON ").append(StringUtils.join(" AND ", joinClauseList));
}
@@ -285,34 +281,66 @@ public abstract class AbstractRDBMSAction implements QActionInterface
*******************************************************************************/
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);
@@ -321,35 +349,19 @@ public abstract class AbstractRDBMSAction implements QActionInterface
/*******************************************************************************
- ** 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);
+ String clause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(joinsContext, filter.getCriteria(), filter.getBooleanOperator(), params);
if(!CollectionUtils.nullSafeHasContents(filter.getSubFilters()))
{
///////////////////////////////////////////////////////////////
@@ -368,7 +380,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
}
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 + ")");
@@ -379,146 +391,10 @@ public abstract class AbstractRDBMSAction implements QActionInterface
- /*******************************************************************************
- ** 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);
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
- 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(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 - 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(recordSecurityLock.getNullValueBehavior()))
- {
- 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 String getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(JoinsContext joinsContext, List criteria, QQueryFilter.BooleanOperator booleanOperator, List params) throws IllegalArgumentException
{
List clauses = new ArrayList<>();
for(QFilterCriteria criterion : criteria)
@@ -1123,4 +999,20 @@ public abstract class AbstractRDBMSAction implements QActionInterface
}
}
+
+
+ /*******************************************************************************
+ ** 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 ce720120..c7ebc8ba 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 fc5dfa79..1242bd24 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
@@ -79,13 +79,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..8100c4c1
--- /dev/null
+++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportActionWithinRDBMSTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.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.setReportFormat(ReportFormat.CSV);
+ exportInput.setReportOutputStream(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/TestUtils.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java
index d882807a..ccf5dd86 100644
--- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java
+++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java
@@ -243,6 +243,7 @@ public class TestUtils
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId"))
.withAssociation(new Association().withName("orderLine").withAssociatedTableName(TABLE_NAME_ORDER_LINE).withJoinName("orderJoinOrderLine"))
.withExposedJoin(new ExposedJoin().withJoinTable(TABLE_NAME_ITEM).withJoinPath(List.of("orderJoinOrderLine", "orderLineJoinItem")))
+ .withExposedJoin(new ExposedJoin().withJoinTable(TABLE_NAME_ORDER_INSTRUCTIONS).withJoinPath(List.of("orderJoinCurrentOrderInstructions")))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
.withField(new QFieldMetaData("billToPersonId", QFieldType.INTEGER).withBackendName("bill_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
.withField(new QFieldMetaData("shipToPersonId", QFieldType.INTEGER).withBackendName("ship_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
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 f1bc1763..224b229a 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..d096b1cb
--- /dev/null
+++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionJoinsTest.java
@@ -0,0 +1,987 @@
+/*
+ * 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.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.security.RecordSecurityLock;
+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.setLogSQLOutput("system.out");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @AfterEach
+ void afterEach()
+ {
+ AbstractRDBMSAction.setLogSQL(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().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, null));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, Collections.emptyList()));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 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(TestUtils.TABLE_NAME_STORE, null));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, Collections.emptyList()));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ QContext.setQSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 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(TestUtils.TABLE_NAME_STORE, null));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ //////////////////////////////////////////////////////
+ // with empty-list security key value, 0 rows found //
+ //////////////////////////////////////////////////////
+ QContext.setQSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, Collections.emptyList()));
+ assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
+
+ ////////////////////////////////
+ // with 2 values, find 2 rows //
+ ////////////////////////////////
+ QContext.setQSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 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().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 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().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 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));
+ }
+
+}
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 01c2c65c..249c5495 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;
@@ -783,675 +775,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().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, null));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, Collections.emptyList()));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 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(TestUtils.TABLE_NAME_STORE, null));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, Collections.emptyList()));
- assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
-
- QContext.setQSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 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().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 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);
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
@@ -1606,68 +929,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().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 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);
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
@@ -1697,51 +958,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());
- }
-
- }
-
}