diff --git a/.circleci/config.yml b/.circleci/config.yml
index da983b05..5981d2fa 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -26,6 +26,7 @@ commands:
sudo rm /etc/alternatives/java
sudo ln -s /usr/lib/jvm/java-17-openjdk-amd64/bin/java /etc/alternatives/java
- run:
+ ## used by jacoco uncovered class reporting in pom.xml
name: Install html2text
command: |
sudo apt-get update
@@ -73,11 +74,6 @@ commands:
when: always
- store_test_results:
path: ~/test-results
- - run:
- name: Find Un-tested Classes
- command: |
- set +o pipefail && for i in */target/site/jacoco/*/index.html; do html2text -width 500 -nobs $i | sed '1,/^Total/d;' | grep -v Created | sed 's/ \+/ /g' | sed 's/ [[:digit:]]$//' | grep -v 0$ | cut -d' ' -f1; done
- when: always
- save_cache:
paths:
- ~/.m2
diff --git a/pom.xml b/pom.xml
index 2bc0ecf0..4623b96d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -206,6 +206,63 @@
true
+
+ exec-maven-plugin
+ org.codehaus.mojo
+ 3.0.0
+
+
+ test-coverage-summary
+ verify
+
+ exec
+
+
+ sh
+
+ -c
+
+ /dev/null 2>&1
+if [ "$?" == "0" ]; then
+ echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
+ xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
+ paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
+ rm /tmp/$$.headers /tmp/$$.values
+else
+ echo "xpath is not installed. Jacoco coverage summary will not be produced here...";
+fi
+
+which xpath > /dev/null 2>&1
+if [ "$?" == "0" ]; then
+ echo "Untested classes, per Jacoco:"
+ echo "-----------------------------"
+ for i in target/site/jacoco/*/index.html; do
+ html2text -width 500 -nobs $i | sed '1,/^Total/d;' | grep -v Created | sed 's/ \+/ /g' | sed 's/ [[:digit:]]$//' | grep -v 0$ | cut -d' ' -f1;
+ done;
+ echo
+else
+ echo "html2text is not installed. Untested classes from Jacoco will not be printed here...";
+fi
+
+ ]]>
+
+
+
+
+
+
org.jacoco
jacoco-maven-plugin
@@ -249,56 +306,14 @@
post-unit-test
- verify
+
+ post-integration-test
report
-
- exec-maven-plugin
- org.codehaus.mojo
- 3.0.0
-
-
- test-coverage-summary
- verify
-
- exec
-
-
- sh
-
- -c
-
- /dev/null 2>&1
-if [ "$?" == "0" ]; then
- echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
- xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
- paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
- rm /tmp/$$.headers /tmp/$$.values
-else
- echo "xpath is not installed. Jacoco coverage summary will not be produced here..";
-fi
- ]]>
-
-
-
-
-
-
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java
index c103cc73..8c2f747e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java
@@ -239,7 +239,7 @@ public class DMLAuditAction extends AbstractQActionFunction.
+ */
+
+package com.kingsrook.qqq.backend.core.actions.metadata;
+
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ ** Object to represent the graph of joins in a QQQ Instance. e.g., all of the
+ ** connections among tables through joins.
+ *******************************************************************************/
+public class JoinGraph
+{
+
+ private Set edges = new HashSet<>();
+
+
+
+ /*******************************************************************************
+ ** Graph edge (no graph nodes needed in here)
+ *******************************************************************************/
+ private record Edge(String joinName, String leftTable, String rightTable)
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** In this class, we are treating joins as non-directional graph edges - so -
+ ** use this class to "normalize" what may otherwise be duplicated joins in the
+ ** qInstance (e.g., A -> B and B -> A -- in the instance, those are valid, but
+ ** in our graph here, we want to consider those the same).
+ *******************************************************************************/
+ private static class NormalizedJoin
+ {
+ private String tableA;
+ private String tableB;
+ private String joinFieldA;
+ private String joinFieldB;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public NormalizedJoin(QJoinMetaData joinMetaData)
+ {
+ boolean needFlip = false;
+ int tableCompare = joinMetaData.getLeftTable().compareTo(joinMetaData.getRightTable());
+ if(tableCompare < 0)
+ {
+ needFlip = true;
+ }
+ else if(tableCompare == 0)
+ {
+ int fieldCompare = joinMetaData.getJoinOns().get(0).getLeftField().compareTo(joinMetaData.getJoinOns().get(0).getRightField());
+ if(fieldCompare < 0)
+ {
+ needFlip = true;
+ }
+ }
+
+ if(needFlip)
+ {
+ joinMetaData = joinMetaData.flip();
+ }
+
+ tableA = joinMetaData.getLeftTable();
+ tableB = joinMetaData.getRightTable();
+ joinFieldA = joinMetaData.getJoinOns().get(0).getLeftField();
+ joinFieldB = joinMetaData.getJoinOns().get(0).getRightField();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public boolean equals(Object o)
+ {
+ if(this == o)
+ {
+ return true;
+ }
+ if(o == null || getClass() != o.getClass())
+ {
+ return false;
+ }
+ NormalizedJoin that = (NormalizedJoin) o;
+ return Objects.equals(tableA, that.tableA) && Objects.equals(tableB, that.tableB) && Objects.equals(joinFieldA, that.joinFieldA) && Objects.equals(joinFieldB, that.joinFieldB);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(tableA, tableB, joinFieldA, joinFieldB);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public JoinGraph(QInstance qInstance)
+ {
+ Set usedJoins = new HashSet<>();
+ for(QJoinMetaData join : CollectionUtils.nonNullMap(qInstance.getJoins()).values())
+ {
+ NormalizedJoin normalizedJoin = new NormalizedJoin(join);
+ if(usedJoins.contains(normalizedJoin))
+ {
+ continue;
+ }
+
+ usedJoins.add(normalizedJoin);
+ edges.add(new Edge(join.getName(), join.getLeftTable(), join.getRightTable()));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public record JoinConnection(String joinTable, String viaJoinName) implements Comparable
+ {
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public int compareTo(JoinConnection that)
+ {
+ Comparator comparator = Comparator.comparing((JoinConnection jc) -> jc.joinTable())
+ .thenComparing((JoinConnection jc) -> jc.viaJoinName());
+ return (comparator.compare(this, that));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public record JoinConnectionList(List list) implements Comparable
+ {
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public JoinConnectionList copy()
+ {
+ return new JoinConnectionList(new ArrayList<>(list));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public int compareTo(JoinConnectionList that)
+ {
+ if(this.equals(that))
+ {
+ return (0);
+ }
+
+ for(int i = 0; i < Math.min(this.list.size(), that.list.size()); i++)
+ {
+ int comp = this.list.get(i).compareTo(that.list.get(i));
+ if(comp != 0)
+ {
+ return (comp);
+ }
+ }
+
+ return (this.list.size() - that.list.size());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public boolean matchesJoinPath(List joinPath)
+ {
+ if(list.size() != joinPath.size())
+ {
+ return (false);
+ }
+
+ for(int i = 0; i < list.size(); i++)
+ {
+ if(!list.get(i).viaJoinName().equals(joinPath.get(i)))
+ {
+ return (false);
+ }
+ }
+
+ return (true);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public String getJoinNamesAsString()
+ {
+ return (StringUtils.join(", ", list().stream().map(jc -> jc.viaJoinName()).toList()));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public List getJoinNamesAsList()
+ {
+ return (list().stream().map(jc -> jc.viaJoinName()).toList());
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Set getJoinConnections(String tableName)
+ {
+ Set rs = new TreeSet<>();
+ doGetJoinConnections(rs, tableName, new ArrayList<>(), new JoinConnectionList(new ArrayList<>()));
+ return (rs);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void doGetJoinConnections(Set joinConnections, String tableName, List path, JoinConnectionList connectionList)
+ {
+ for(Edge edge : edges)
+ {
+ if(edge.leftTable.equals(tableName) || edge.rightTable.equals(tableName))
+ {
+ if(path.contains(edge.joinName))
+ {
+ continue;
+ }
+
+ List newPath = new ArrayList<>(path);
+ newPath.add(edge.joinName);
+ if(!joinConnectionsContain(joinConnections, newPath))
+ {
+ String otherTableName = null;
+ if(!edge.leftTable.equals(tableName))
+ {
+ otherTableName = edge.leftTable;
+ }
+ else if(!edge.rightTable.equals(tableName))
+ {
+ otherTableName = edge.rightTable;
+ }
+
+ if(otherTableName != null)
+ {
+
+ JoinConnectionList newConnectionList = connectionList.copy();
+ JoinConnection joinConnection = new JoinConnection(otherTableName, edge.joinName);
+ newConnectionList.list.add(joinConnection);
+ joinConnections.add(newConnectionList);
+ doGetJoinConnections(joinConnections, otherTableName, new ArrayList<>(newPath), newConnectionList);
+ }
+ }
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private boolean joinConnectionsContain(Set joinPaths, List newPath)
+ {
+ for(JoinConnectionList joinConnections : joinPaths)
+ {
+ List joinConnectionJoins = joinConnections.list.stream().map(jc -> jc.viaJoinName).toList();
+ if(joinConnectionJoins.equals(newPath))
+ {
+ return (true);
+ }
+ }
+ return (false);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java
index 04dbc905..ef571e45 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java
@@ -83,11 +83,14 @@ public class MetaDataAction
}
QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName);
- tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false));
+ tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false, false));
treeNodes.put(tableName, new AppTreeNode(table));
}
metaDataOutput.setTables(tables);
+ // addJoinsToTables(tables);
+ // addJoinedTablesToTables(tables);
+
////////////////////////////////////////
// map processes to frontend metadata //
////////////////////////////////////////
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java
index 223eb475..46c4f243 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java
@@ -54,7 +54,7 @@ public class TableMetaDataAction
throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found."));
}
QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName());
- tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true));
+ tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true, true));
// todo post-customization - can do whatever w/ the result if you want
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 ce0a0920..2ca95347 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
@@ -23,8 +23,12 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
@@ -32,6 +36,7 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
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.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
@@ -41,10 +46,13 @@ 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.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
+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.data.QRecord;
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.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@@ -95,15 +103,25 @@ public class ExportAction
///////////////////////////////////
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
{
- QTableMetaData table = exportInput.getTable();
- List badFieldNames = new ArrayList<>();
+ QTableMetaData table = exportInput.getTable();
+ Map joinTableMap = getJoinTableMap(table);
+
+ List badFieldNames = new ArrayList<>();
for(String fieldName : exportInput.getFieldNames())
{
try
{
- table.getField(fieldName);
+ if(fieldName.contains("."))
+ {
+ String[] parts = fieldName.split("\\.", 2);
+ joinTableMap.get(parts[0]).getField(parts[1]);
+ }
+ else
+ {
+ table.getField(fieldName);
+ }
}
- catch(IllegalArgumentException iae)
+ catch(Exception e)
{
badFieldNames.add(fieldName);
}
@@ -128,6 +146,21 @@ public class ExportAction
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static Map getJoinTableMap(QTableMetaData table)
+ {
+ Map joinTableMap = new HashMap<>();
+ for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins()))
+ {
+ joinTableMap.put(exposedJoin.getJoinTable(), QContext.getQInstance().getTable(exposedJoin.getJoinTable()));
+ }
+ return joinTableMap;
+ }
+
+
+
/*******************************************************************************
** Run the report.
*******************************************************************************/
@@ -151,7 +184,33 @@ public class ExportAction
QueryInput queryInput = new QueryInput();
queryInput.setTableName(exportInput.getTableName());
queryInput.setFilter(exportInput.getQueryFilter());
- queryInput.setLimit(exportInput.getLimit());
+
+ List queryJoins = new ArrayList<>();
+ Set addedJoinNames = new HashSet<>();
+ if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
+ {
+ for(String fieldName : exportInput.getFieldNames())
+ {
+ if(fieldName.contains("."))
+ {
+ String[] parts = fieldName.split("\\.", 2);
+ String joinTableName = parts[0];
+ if(!addedJoinNames.contains(joinTableName))
+ {
+ queryJoins.add(new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true));
+ addedJoinNames.add(joinTableName);
+ }
+ }
+ }
+ }
+
+ queryInput.setQueryJoins(queryJoins);
+
+ if(queryInput.getFilter() == null)
+ {
+ queryInput.setFilter(new QQueryFilter());
+ }
+ queryInput.getFilter().setLimit(exportInput.getLimit());
queryInput.setShouldTranslatePossibleValues(true);
/////////////////////////////////////////////////////////////////
@@ -298,11 +357,29 @@ public class ExportAction
*******************************************************************************/
private List getFields(ExportInput exportInput)
{
+ QTableMetaData table = exportInput.getTable();
+ Map joinTableMap = getJoinTableMap(table);
+
List fieldList;
- QTableMetaData table = exportInput.getTable();
if(exportInput.getFieldNames() != null)
{
- fieldList = exportInput.getFieldNames().stream().map(table::getField).toList();
+ fieldList = new ArrayList<>();
+ for(String fieldName : exportInput.getFieldNames())
+ {
+ if(fieldName.contains("."))
+ {
+ String[] parts = fieldName.split("\\.", 2);
+ QTableMetaData joinTable = joinTableMap.get(parts[0]);
+ QFieldMetaData field = joinTable.getField(parts[1]).clone();
+ field.setName(fieldName);
+ field.setLabel(joinTable.getLabel() + ": " + field.getLabel());
+ fieldList.add(field);
+ }
+ else
+ {
+ fieldList.add(table.getField(fieldName));
+ }
+ }
}
else
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java
index 04b1d51f..5014caa2 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/StoreAssociatedScriptAction.java
@@ -156,8 +156,7 @@ public class StoreAssociatedScriptAction
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
- );
- queryInput.setLimit(1);
+ .withLimit(1));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(!queryOutput.getRecords().isEmpty())
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java
index 9d7ed1a7..d797e393 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java
@@ -123,7 +123,7 @@ public class QPossibleValueTranslator
return;
}
- LOG.debug("Translating possible values in [" + records.size() + "] records from the [" + table.getName() + "] table.");
+ LOG.trace("Translating possible values in [" + records.size() + "] records from the [" + table.getName() + "] table.");
primePvsCache(table, records, queryJoins, limitedToFieldNames);
for(QRecord record : records)
@@ -378,11 +378,11 @@ public class QPossibleValueTranslator
for(String valueField : valueFields)
{
Object value = switch(valueField)
- {
- case "id" -> id;
- case "label" -> label;
- default -> throw new IllegalArgumentException("Unexpected value field: " + valueField);
- };
+ {
+ case "id" -> id;
+ case "label" -> label;
+ default -> throw new IllegalArgumentException("Unexpected value field: " + valueField);
+ };
values.add(Objects.requireNonNullElse(value, ""));
}
}
@@ -427,7 +427,7 @@ public class QPossibleValueTranslator
int size = entry.getValue().size();
if(size > 50_000)
{
- LOG.debug("Found a big PVS cache - clearing it.", logPair("name", entry.getKey()), logPair("size", size));
+ LOG.info("Found a big PVS cache - clearing it.", logPair("name", entry.getKey()), logPair("size", size));
}
}
@@ -483,7 +483,7 @@ public class QPossibleValueTranslator
{
if(limitedToFieldNames != null && !limitedToFieldNames.contains(fieldNamePrefix + field.getName()))
{
- LOG.debug("Skipping cache priming for translation of possible value field [" + fieldNamePrefix + field.getName() + "] - it's not in the limitedToFieldNames set.");
+ LOG.trace("Skipping cache priming for translation of possible value field [" + fieldNamePrefix + field.getName() + "] - it's not in the limitedToFieldNames set.");
continue;
}
@@ -556,7 +556,7 @@ public class QPossibleValueTranslator
queryInput.setFieldsToTranslatePossibleValues(possibleValueFieldsToTranslate);
}
- LOG.debug("Priming PVS cache for [" + page.size() + "] ids from [" + tableName + "] table.");
+ LOG.trace("Priming PVS cache for [" + page.size() + "] ids from [" + tableName + "] table.");
QueryOutput queryOutput = new QueryAction().execute(queryInput);
///////////////////////////////////////////////////////////////////////////////////
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java
index c034e1f7..8a25136c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/SearchPossibleValueSourceAction.java
@@ -247,7 +247,7 @@ public class SearchPossibleValueSourceAction
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
// todo - skip & limit as params
- queryInput.setLimit(250);
+ queryFilter.setLimit(250);
///////////////////////////////////////////////////////////////////////////////////////////////////////
// if given a default filter, make it the 'top level' filter and the one we just created a subfilter //
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
index df6fca5a..38213836 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
@@ -32,7 +32,9 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
+import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
import com.kingsrook.qqq.backend.core.actions.permissions.BulkTableActionProcessPermissionChecker;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@@ -59,6 +61,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@@ -87,6 +90,8 @@ public class QInstanceEnricher
private final QInstance qInstance;
+ private JoinGraph joinGraph;
+
//////////////////////////////////////////////////////////
// todo - come up w/ a way for app devs to set configs! //
//////////////////////////////////////////////////////////
@@ -144,6 +149,81 @@ public class QInstanceEnricher
{
qInstance.getWidgets().values().forEach(this::enrichWidget);
}
+
+ enrichJoins();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void enrichJoins()
+ {
+ try
+ {
+ joinGraph = new JoinGraph(qInstance);
+
+ for(QTableMetaData table : CollectionUtils.nonNullMap(qInstance.getTables()).values())
+ {
+ Set joinConnections = joinGraph.getJoinConnections(table.getName());
+ for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins()))
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // proceed with caution - remember, validator will fail the instance if things are missing/invalid //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(exposedJoin.getJoinTable() != null)
+ {
+ QTableMetaData joinTable = qInstance.getTable(exposedJoin.getJoinTable());
+ if(joinTable != null)
+ {
+ //////////////////////////////////////////////////////////////////////////////////
+ // default the exposed join's label to the join table's label, if it wasn't set //
+ //////////////////////////////////////////////////////////////////////////////////
+ if(!StringUtils.hasContent(exposedJoin.getLabel()))
+ {
+ exposedJoin.setLabel(joinTable.getLabel());
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // default the exposed join's join-path from the joinGraph, if it wasn't set //
+ ///////////////////////////////////////////////////////////////////////////////
+ if(CollectionUtils.nullSafeIsEmpty(exposedJoin.getJoinPath()))
+ {
+ List eligibleJoinConnections = new ArrayList<>();
+ for(JoinGraph.JoinConnectionList joinConnection : joinConnections)
+ {
+ if(joinTable.getName().equals(joinConnection.list().get(joinConnection.list().size() - 1).joinTable()))
+ {
+ eligibleJoinConnections.add(joinConnection);
+ }
+ }
+
+ if(eligibleJoinConnections.isEmpty())
+ {
+ throw (new QException("Could not infer a joinPath for table [" + table.getName() + "], exposedJoin to [" + exposedJoin.getJoinTable() + "]: No join connections between these tables exist in this instance."));
+ }
+ else if(eligibleJoinConnections.size() > 1)
+ {
+ throw (new QException("Could not infer a joinPath for table [" + table.getName() + "], exposedJoin to [" + exposedJoin.getJoinTable() + "]: "
+ + eligibleJoinConnections.size() + " join connections exist between these tables. You need to specify one:\n"
+ + StringUtils.join("\n", eligibleJoinConnections.stream().map(jcl -> jcl.getJoinNamesAsString()).toList()) + "."
+ ));
+ }
+ else
+ {
+ exposedJoin.setJoinPath(eligibleJoinConnections.get(0).getJoinNamesAsList());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ throw (new RuntimeException("Error enriching instance joins", e));
+ }
}
@@ -1016,4 +1096,13 @@ public class QInstanceEnricher
}
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public JoinGraph getJoinGraph()
+ {
+ return (this.joinGraph);
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
index 1a211992..64f85dde 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
@@ -36,6 +36,7 @@ import java.util.function.Supplier;
import java.util.stream.Stream;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
+import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
@@ -69,6 +70,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
@@ -115,17 +117,26 @@ public class QInstanceValidator
return;
}
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // the enricher will build a join graph (if there are any joins). we'd like to only do that //
+ // once, during the enrichment/validation work, so, capture it, and store it back in the instance. //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ JoinGraph joinGraph = null;
try
{
/////////////////////////////////////////////////////////////////////////////////////////////////
// before validation, enrich the object (e.g., to fill in values that the user doesn't have to //
/////////////////////////////////////////////////////////////////////////////////////////////////
// TODO - possible point of customization (use a different enricher, or none, or pass it options).
- new QInstanceEnricher(qInstance).enrich();
+ QInstanceEnricher qInstanceEnricher = new QInstanceEnricher(qInstance);
+ qInstanceEnricher.enrich();
+ joinGraph = qInstanceEnricher.getJoinGraph();
}
catch(Exception e)
{
+ System.out.println();
LOG.error("Error enriching instance prior to validation", e);
+ System.out.println();
throw (new QInstanceValidationException("Error enriching qInstance prior to validation.", e));
}
@@ -136,7 +147,7 @@ public class QInstanceValidator
{
validateBackends(qInstance);
validateAutomationProviders(qInstance);
- validateTables(qInstance);
+ validateTables(qInstance, joinGraph);
validateProcesses(qInstance);
validateReports(qInstance);
validateApps(qInstance);
@@ -158,7 +169,9 @@ public class QInstanceValidator
throw (new QInstanceValidationException(errors));
}
- qInstance.setHasBeenValidated(new QInstanceValidationKey());
+ QInstanceValidationKey validationKey = new QInstanceValidationKey();
+ qInstance.setHasBeenValidated(validationKey);
+ qInstance.setJoinGraph(validationKey, joinGraph);
}
@@ -366,7 +379,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
- private void validateTables(QInstance qInstance)
+ private void validateTables(QInstance qInstance, JoinGraph joinGraph)
{
if(assertCondition(CollectionUtils.nullSafeHasContents(qInstance.getTables()), "At least 1 table must be defined."))
{
@@ -466,12 +479,61 @@ public class QInstanceValidator
validateTableCacheOf(qInstance, table);
validateTableRecordSecurityLocks(qInstance, table);
validateTableAssociations(qInstance, table);
+ validateExposedJoins(qInstance, joinGraph, table);
});
}
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void validateExposedJoins(QInstance qInstance, JoinGraph joinGraph, QTableMetaData table)
+ {
+ Set joinConnectionsForTable = null;
+ Set usedLabels = new HashSet<>();
+ Set> usedJoinPaths = new HashSet<>();
+
+ String tablePrefix = "Table " + table.getName() + " ";
+ for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins()))
+ {
+ String joinPrefix = tablePrefix + "exposedJoin [missingJoinTableName] ";
+ if(assertCondition(StringUtils.hasContent(exposedJoin.getJoinTable()), tablePrefix + "has an exposedJoin that is missing a joinTable name."))
+ {
+ joinPrefix = tablePrefix + "exposedJoin " + exposedJoin.getJoinTable() + " ";
+ if(assertCondition(qInstance.getTable(exposedJoin.getJoinTable()) != null, joinPrefix + "is referencing an unrecognized table"))
+ {
+ if(assertCondition(CollectionUtils.nullSafeHasContents(exposedJoin.getJoinPath()), joinPrefix + "is missing a joinPath."))
+ {
+ joinConnectionsForTable = Objects.requireNonNullElseGet(joinConnectionsForTable, () -> joinGraph.getJoinConnections(table.getName()));
+
+ boolean foundJoinConnection = false;
+ for(JoinGraph.JoinConnectionList joinConnectionList : joinConnectionsForTable)
+ {
+ if(joinConnectionList.matchesJoinPath(exposedJoin.getJoinPath()))
+ {
+ foundJoinConnection = true;
+ }
+ }
+ assertCondition(foundJoinConnection, joinPrefix + "specified a joinPath [" + exposedJoin.getJoinPath() + "] which does not match a valid join connection in the instance.");
+
+ assertCondition(!usedJoinPaths.contains(exposedJoin.getJoinPath()), tablePrefix + "has more than one join with the joinPath: " + exposedJoin.getJoinPath());
+ usedJoinPaths.add(exposedJoin.getJoinPath());
+ }
+ }
+ }
+
+ if(assertCondition(StringUtils.hasContent(exposedJoin.getLabel()), joinPrefix + "is missing a label."))
+ {
+ assertCondition(!usedLabels.contains(exposedJoin.getLabel()), tablePrefix + "has more than one join labeled: " + exposedJoin.getLabel());
+ usedLabels.add(exposedJoin.getLabel());
+ }
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java
index 09ff116c..1a4863e5 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java
@@ -37,7 +37,8 @@ public class CountInput extends AbstractTableActionInput
{
private QQueryFilter filter;
- private List queryJoins = null;
+ private List queryJoins = null;
+ private Boolean includeDistinctCount = false;
@@ -120,4 +121,35 @@ public class CountInput extends AbstractTableActionInput
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for includeDistinctCount
+ *******************************************************************************/
+ public Boolean getIncludeDistinctCount()
+ {
+ return (this.includeDistinctCount);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for includeDistinctCount
+ *******************************************************************************/
+ public void setIncludeDistinctCount(Boolean includeDistinctCount)
+ {
+ this.includeDistinctCount = includeDistinctCount;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for includeDistinctCount
+ *******************************************************************************/
+ public CountInput withIncludeDistinctCount(Boolean includeDistinctCount)
+ {
+ this.includeDistinctCount = includeDistinctCount;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountOutput.java
index d3703cfa..0484a175 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountOutput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountOutput.java
@@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
public class CountOutput extends AbstractActionOutput
{
private Integer count;
+ private Integer distinctCount;
@@ -52,4 +53,47 @@ public class CountOutput extends AbstractActionOutput
{
this.count = count;
}
+
+
+
+ /*******************************************************************************
+ ** Getter for distinctCount
+ *******************************************************************************/
+ public Integer getDistinctCount()
+ {
+ return (this.distinctCount);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for distinctCount
+ *******************************************************************************/
+ public void setDistinctCount(Integer distinctCount)
+ {
+ this.distinctCount = distinctCount;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for distinctCount
+ *******************************************************************************/
+ public CountOutput withDistinctCount(Integer distinctCount)
+ {
+ this.distinctCount = distinctCount;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for count
+ *******************************************************************************/
+ public CountOutput withCount(Integer count)
+ {
+ this.count = count;
+ return (this);
+ }
+
}
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 5a66e1e3..2a52e634 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
@@ -32,12 +32,16 @@ import java.util.Objects;
import java.util.Set;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
+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.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
+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.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@@ -46,6 +50,8 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
*******************************************************************************/
public class JoinsContext
{
+ private static final QLogger LOG = QLogger.getLogger(JoinsContext.class);
+
private final QInstance instance;
private final String mainTableName;
private final List queryJoins;
@@ -65,7 +71,7 @@ public class JoinsContext
{
this.instance = instance;
this.mainTableName = tableName;
- this.queryJoins = CollectionUtils.nonNullList(queryJoins);
+ this.queryJoins = new MutableList<>(queryJoins);
for(QueryJoin queryJoin : this.queryJoins)
{
@@ -123,6 +129,8 @@ public class JoinsContext
ensureFilterIsRepresented(filter);
+ addJoinsFromExposedJoinPaths();
+
/* todo!!
for(QueryJoin queryJoin : queryJoins)
{
@@ -132,7 +140,147 @@ public class JoinsContext
// addCriteriaForRecordSecurityLock(instance, session, joinTable, securityCriteria, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias());
}
}
- */
+ */
+ }
+
+
+
+ /*******************************************************************************
+ ** 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.
+ *******************************************************************************/
+ private void addJoinsFromExposedJoinPaths() throws QException
+ {
+ ////////////////////////////////////////////////////////////////////////////////
+ // 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;
+ do
+ {
+ addedJoin = false;
+ for(QueryJoin queryJoin : queryJoins)
+ {
+ /////////////////////////////////////////////////////////////////////
+ // if the join has joinMetaData, then we don't need to process it. //
+ /////////////////////////////////////////////////////////////////////
+ if(queryJoin.getJoinMetaData() == null)
+ {
+ //////////////////////////////////////////////////////////////////////
+ // 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. //
+ //////////////////////////////////////////////////////////////////////
+ String baseTableName = Objects.requireNonNullElse(resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), mainTableName);
+ QJoinMetaData found = findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
+ if(found != null)
+ {
+ queryJoin.setJoinMetaData(found);
+ }
+ else
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // else, the join must be indirect - so look for an exposedJoin that will have a joinPath that will connect us //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ LOG.debug("Looking for an exposed join...", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()));
+
+ QTableMetaData mainTable = instance.getTable(mainTableName);
+ boolean addedAnyQueryJoins = false;
+ for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(mainTable.getExposedJoins()))
+ {
+ if(queryJoin.getJoinTable().equals(exposedJoin.getJoinTable()))
+ {
+ LOG.debug("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) //
+ // adding joins to the table (if they aren't already in the query) //
+ /////////////////////////////////////////////////////////////////////////////////////
+ String tmpTable = queryJoin.getJoinTable();
+ for(int i = exposedJoin.getJoinPath().size() - 1; i >= 0; i--)
+ {
+ String joinName = exposedJoin.getJoinPath().get(i);
+ QJoinMetaData joinToAdd = instance.getJoin(joinName);
+
+ /////////////////////////////////////////////////////////////////////////////
+ // get the name from the opposite side of the join (flipping it if needed) //
+ /////////////////////////////////////////////////////////////////////////////
+ String nextTable;
+ if(joinToAdd.getRightTable().equals(tmpTable))
+ {
+ nextTable = joinToAdd.getLeftTable();
+ }
+ else
+ {
+ nextTable = joinToAdd.getRightTable();
+ joinToAdd = joinToAdd.flip();
+ }
+
+ if(doesJoinNeedAddedToQuery(joinName))
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if this is the last element in the joinPath, then we want to set this joinMetaData on the outer queryJoin //
+ // - else, we need to add a new queryJoin to this context //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(i == exposedJoin.getJoinPath().size() - 1)
+ {
+ if(queryJoin.getBaseTableOrAlias() == null)
+ {
+ queryJoin.setBaseTableOrAlias(nextTable);
+ }
+ queryJoin.setJoinMetaData(joinToAdd);
+ }
+ else
+ {
+ QueryJoin queryJoinToAdd = makeQueryJoinFromJoinAndTableNames(nextTable, tmpTable, joinToAdd);
+ queryJoinToAdd.setType(queryJoin.getType());
+ addedAnyQueryJoins = true;
+ this.queryJoins.add(queryJoinToAdd); // todo something else with aliases? probably.
+ processQueryJoin(queryJoinToAdd);
+ }
+ }
+
+ tmpTable = nextTable;
+ }
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ // break the inner loop (it would fail due to a concurrent modification), but continue the outer //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ if(addedAnyQueryJoins)
+ {
+ addedJoin = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ while(addedJoin);
+
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ 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... //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ for(QueryJoin queryJoin : queryJoins)
+ {
+ if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName))
+ {
+ return (false);
+ }
+ }
+
+ return (true);
}
@@ -256,34 +404,54 @@ public class JoinsContext
{
if(!aliasToTableNameMap.containsKey(filterTable) && !Objects.equals(mainTableName, filterTable))
{
+ boolean found = false;
for(QJoinMetaData join : CollectionUtils.nonNullMap(QContext.getQInstance().getJoins()).values())
{
- QueryJoin queryJoin = null;
- if(join.getLeftTable().equals(mainTableName) && join.getRightTable().equals(filterTable))
- {
- queryJoin = new QueryJoin().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
- }
- else
- {
- join = join.flip();
- if(join.getLeftTable().equals(mainTableName) && join.getRightTable().equals(filterTable))
- {
- queryJoin = new QueryJoin().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
- }
- }
-
+ QueryJoin queryJoin = makeQueryJoinFromJoinAndTableNames(mainTableName, filterTable, join);
if(queryJoin != null)
{
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
processQueryJoin(queryJoin);
+ found = true;
+ break;
}
}
+
+ if(!found)
+ {
+ QueryJoin queryJoin = new QueryJoin().withJoinTable(filterTable).withType(QueryJoin.Type.INNER);
+ this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
+ processQueryJoin(queryJoin);
+ }
}
}
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QueryJoin makeQueryJoinFromJoinAndTableNames(String tableA, String tableB, QJoinMetaData join)
+ {
+ QueryJoin queryJoin = null;
+ if(join.getLeftTable().equals(tableA) && join.getRightTable().equals(tableB))
+ {
+ queryJoin = new QueryJoin().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
+ }
+ else
+ {
+ join = join.flip();
+ if(join.getLeftTable().equals(tableA) && join.getRightTable().equals(tableB))
+ {
+ queryJoin = new QueryJoin().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
+ }
+ }
+ return queryJoin;
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
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 f89d23e4..6ce122bb 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
@@ -47,6 +47,13 @@ public class QQueryFilter implements Serializable, Cloneable
private BooleanOperator booleanOperator = BooleanOperator.AND;
private List subFilters = new ArrayList<>();
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // skip & limit are meant to only apply to QueryAction (at least at the initial time they are added here) //
+ // e.g., they are ignored in CountAction, AggregateAction, etc, where their meanings may be less obvious //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ private Integer skip;
+ private Integer limit;
+
/*******************************************************************************
@@ -398,4 +405,66 @@ public class QQueryFilter implements Serializable, Cloneable
}
}
+
+
+ /*******************************************************************************
+ ** Getter for skip
+ *******************************************************************************/
+ public Integer getSkip()
+ {
+ return (this.skip);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for skip
+ *******************************************************************************/
+ public void setSkip(Integer skip)
+ {
+ this.skip = skip;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for skip
+ *******************************************************************************/
+ public QQueryFilter withSkip(Integer skip)
+ {
+ this.skip = skip;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for limit
+ *******************************************************************************/
+ public Integer getLimit()
+ {
+ return (this.limit);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for limit
+ *******************************************************************************/
+ public void setLimit(Integer limit)
+ {
+ this.limit = limit;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for limit
+ *******************************************************************************/
+ public QQueryFilter withLimit(Integer limit)
+ {
+ this.limit = limit;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java
index 76c4101f..6cb2c556 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java
@@ -39,8 +39,6 @@ public class QueryInput extends AbstractTableActionInput
{
private QBackendTransaction transaction;
private QQueryFilter filter;
- private Integer skip;
- private Integer limit;
private RecordPipe recordPipe;
@@ -57,7 +55,8 @@ public class QueryInput extends AbstractTableActionInput
/////////////////////////////////////////////////////////////////////////////////////////
private Set fieldsToTranslatePossibleValues;
- private List queryJoins = null;
+ private List queryJoins = null;
+ private boolean selectDistinct = false;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. //
@@ -100,50 +99,6 @@ public class QueryInput extends AbstractTableActionInput
- /*******************************************************************************
- ** Getter for skip
- **
- *******************************************************************************/
- public Integer getSkip()
- {
- return skip;
- }
-
-
-
- /*******************************************************************************
- ** Setter for skip
- **
- *******************************************************************************/
- public void setSkip(Integer skip)
- {
- this.skip = skip;
- }
-
-
-
- /*******************************************************************************
- ** Getter for limit
- **
- *******************************************************************************/
- public Integer getLimit()
- {
- return limit;
- }
-
-
-
- /*******************************************************************************
- ** Setter for limit
- **
- *******************************************************************************/
- public void setLimit(Integer limit)
- {
- this.limit = limit;
- }
-
-
-
/*******************************************************************************
** Getter for recordPipe
**
@@ -361,28 +316,6 @@ public class QueryInput extends AbstractTableActionInput
- /*******************************************************************************
- ** Fluent setter for skip
- *******************************************************************************/
- public QueryInput withSkip(Integer skip)
- {
- this.skip = skip;
- return (this);
- }
-
-
-
- /*******************************************************************************
- ** Fluent setter for limit
- *******************************************************************************/
- public QueryInput withLimit(Integer limit)
- {
- this.limit = limit;
- return (this);
- }
-
-
-
/*******************************************************************************
** Fluent setter for recordPipe
*******************************************************************************/
@@ -561,4 +494,35 @@ public class QueryInput extends AbstractTableActionInput
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for selectDistinct
+ *******************************************************************************/
+ public boolean getSelectDistinct()
+ {
+ return (this.selectDistinct);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for selectDistinct
+ *******************************************************************************/
+ public void setSelectDistinct(boolean selectDistinct)
+ {
+ this.selectDistinct = selectDistinct;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for selectDistinct
+ *******************************************************************************/
+ public QueryInput withSelectDistinct(boolean selectDistinct)
+ {
+ this.selectDistinct = selectDistinct;
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
index be3b7074..0b9f45ee 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
@@ -30,6 +30,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey;
@@ -106,6 +107,8 @@ public class QInstance
private Map memoizedTablePaths = new HashMap<>();
private Map memoizedProcessPaths = new HashMap<>();
+ private JoinGraph joinGraph;
+
/*******************************************************************************
@@ -1136,4 +1139,30 @@ public class QInstance
this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData);
return (this);
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public JoinGraph getJoinGraph()
+ {
+ return (this.joinGraph);
+ }
+
+
+
+ /*******************************************************************************
+ ** Only the validation (and enrichment) code should set the instance's joinGraph
+ ** so, we take a package-only-constructable validation key as a param along with
+ ** the joinGraph - and we throw IllegalArgumentException if a non-null key is given.
+ *******************************************************************************/
+ public void setJoinGraph(QInstanceValidationKey key, JoinGraph joinGraph) throws IllegalArgumentException
+ {
+ if(key == null)
+ {
+ throw (new IllegalArgumentException("A ValidationKey must be provided"));
+ }
+ this.joinGraph = joinGraph;
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetQueryField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetQueryField.java
index f5997b6d..197ce195 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetQueryField.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/nocode/WidgetQueryField.java
@@ -62,8 +62,9 @@ public class WidgetQueryField extends AbstractWidgetValueSourceWithFilter
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
- queryInput.setFilter(getEffectiveFilter(input));
- queryInput.setLimit(1);
+ QQueryFilter effectiveFilter = getEffectiveFilter(input);
+ queryInput.setFilter(effectiveFilter);
+ effectiveFilter.setLimit(1);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendExposedJoin.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendExposedJoin.java
new file mode 100644
index 00000000..f89816c8
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendExposedJoin.java
@@ -0,0 +1,178 @@
+/*
+ * 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.model.metadata.frontend;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
+
+
+/*******************************************************************************
+ ** Version of an ExposedJoin for a frontend to see
+ *******************************************************************************/
+public class QFrontendExposedJoin
+{
+ private String label;
+ private Boolean isMany;
+ private QFrontendTableMetaData joinTable;
+ private List joinPath;
+
+
+
+ /*******************************************************************************
+ ** Getter for label
+ *******************************************************************************/
+ public String getLabel()
+ {
+ return (this.label);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for label
+ *******************************************************************************/
+ public void setLabel(String label)
+ {
+ this.label = label;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for label
+ *******************************************************************************/
+ public QFrontendExposedJoin withLabel(String label)
+ {
+ this.label = label;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for joinTable
+ *******************************************************************************/
+ public QFrontendTableMetaData getJoinTable()
+ {
+ return (this.joinTable);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for joinTable
+ *******************************************************************************/
+ public void setJoinTable(QFrontendTableMetaData joinTable)
+ {
+ this.joinTable = joinTable;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for joinTable
+ *******************************************************************************/
+ public QFrontendExposedJoin withJoinTable(QFrontendTableMetaData joinTable)
+ {
+ this.joinTable = joinTable;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for joinPath
+ *******************************************************************************/
+ public List getJoinPath()
+ {
+ return (this.joinPath);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for joinPath
+ *******************************************************************************/
+ public void setJoinPath(List joinPath)
+ {
+ this.joinPath = joinPath;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for joinPath
+ *******************************************************************************/
+ public QFrontendExposedJoin withJoinPath(List joinPath)
+ {
+ this.joinPath = joinPath;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Add one join to the join path in here
+ *******************************************************************************/
+ public void addJoin(QJoinMetaData join)
+ {
+ if(this.joinPath == null)
+ {
+ this.joinPath = new ArrayList<>();
+ }
+ this.joinPath.add(join);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for isMany
+ *******************************************************************************/
+ public Boolean getIsMany()
+ {
+ return (this.isMany);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for isMany
+ *******************************************************************************/
+ public void setIsMany(Boolean isMany)
+ {
+ this.isMany = isMany;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for isMany
+ *******************************************************************************/
+ public QFrontendExposedJoin withIsMany(Boolean isMany)
+ {
+ this.isMany = isMany;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java
index c53cc933..199d42a6 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.frontend;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -32,12 +33,16 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
+import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
+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.tables.Capability;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
@@ -58,6 +63,8 @@ public class QFrontendTableMetaData
private Map fields;
private List sections;
+ private List exposedJoins;
+
private Set capabilities;
private boolean readPermission;
@@ -74,7 +81,7 @@ public class QFrontendTableMetaData
/*******************************************************************************
**
*******************************************************************************/
- public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields)
+ public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields, boolean includeJoins)
{
this.name = tableMetaData.getName();
this.label = tableMetaData.getLabel();
@@ -92,6 +99,27 @@ public class QFrontendTableMetaData
this.sections = tableMetaData.getSections();
}
+ if(includeJoins)
+ {
+ QInstance qInstance = QContext.getQInstance();
+
+ this.exposedJoins = new ArrayList<>();
+ for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(tableMetaData.getExposedJoins()))
+ {
+ QFrontendExposedJoin frontendExposedJoin = new QFrontendExposedJoin();
+ this.exposedJoins.add(frontendExposedJoin);
+
+ QTableMetaData joinTable = qInstance.getTable(exposedJoin.getJoinTable());
+ frontendExposedJoin.setLabel(exposedJoin.getLabel());
+ frontendExposedJoin.setIsMany(exposedJoin.getIsMany());
+ frontendExposedJoin.setJoinTable(new QFrontendTableMetaData(actionInput, backendForTable, joinTable, includeFields, false));
+ for(String joinName : exposedJoin.getJoinPath())
+ {
+ frontendExposedJoin.addJoin(qInstance.getJoin(joinName));
+ }
+ }
+ }
+
if(tableMetaData.getIcon() != null)
{
this.iconName = tableMetaData.getIcon().getName();
@@ -259,4 +287,15 @@ public class QFrontendTableMetaData
{
return deletePermission;
}
+
+
+
+ /*******************************************************************************
+ ** Getter for exposedJoins
+ **
+ *******************************************************************************/
+ public List getExposedJoins()
+ {
+ return exposedJoins;
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/ExposedJoin.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/ExposedJoin.java
new file mode 100644
index 00000000..f66b1b23
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/ExposedJoin.java
@@ -0,0 +1,208 @@
+/*
+ * 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.model.metadata.tables;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.context.QContext;
+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.joins.JoinType;
+import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ExposedJoin
+{
+ private static final QLogger LOG = QLogger.getLogger(ExposedJoin.class);
+
+ private String label;
+ private String joinTable;
+ private List joinPath;
+
+ //////////////////////////////////////////////////////////////////
+ // no setter for this - derive it the first time it's requested //
+ //////////////////////////////////////////////////////////////////
+ private Boolean isMany = null;
+
+
+
+ /*******************************************************************************
+ ** Getter for label
+ *******************************************************************************/
+ public String getLabel()
+ {
+ return (this.label);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Boolean getIsMany()
+ {
+ if(isMany == null)
+ {
+ if(CollectionUtils.nullSafeHasContents(joinPath))
+ {
+ try
+ {
+ QInstance qInstance = QContext.getQInstance();
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // loop backward through the joinPath, starting at the join table (since we don't know the table that this exposedJoin is attached to!) //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ String currentTable = joinTable;
+ for(int i = joinPath.size() - 1; i >= 0; i--)
+ {
+ String joinName = joinPath.get(i);
+ QJoinMetaData join = qInstance.getJoin(joinName);
+ if(join.getRightTable().equals(currentTable))
+ {
+ currentTable = join.getLeftTable();
+ if(join.getType().equals(JoinType.ONE_TO_MANY) || join.getType().equals(JoinType.MANY_TO_MANY))
+ {
+ isMany = true;
+ break;
+ }
+ }
+ else if(join.getLeftTable().equals(currentTable))
+ {
+ currentTable = join.getRightTable();
+ if(join.getType().equals(JoinType.MANY_TO_ONE) || join.getType().equals(JoinType.MANY_TO_MANY))
+ {
+ isMany = true;
+ break;
+ }
+ }
+ else
+ {
+ throw (new IllegalStateException("Current join table [" + currentTable + "] in path traversal was not found at element [" + joinName + "]"));
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if we successfully got through the loop, and never found a reason to mark this join as "many", then it must not be, so set isMany to false //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(isMany == null)
+ {
+ isMany = false;
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error deriving if ExposedJoin through [" + joinPath + "] to [" + joinTable + "] isMany", e);
+ }
+ }
+ }
+
+ return (isMany);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for label
+ *******************************************************************************/
+ public void setLabel(String label)
+ {
+ this.label = label;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for label
+ *******************************************************************************/
+ public ExposedJoin withLabel(String label)
+ {
+ this.label = label;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for joinTable
+ *******************************************************************************/
+ public String getJoinTable()
+ {
+ return (this.joinTable);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for joinTable
+ *******************************************************************************/
+ public void setJoinTable(String joinTable)
+ {
+ this.joinTable = joinTable;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for joinTable
+ *******************************************************************************/
+ public ExposedJoin withJoinTable(String joinTable)
+ {
+ this.joinTable = joinTable;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for joinPath
+ *******************************************************************************/
+ public List getJoinPath()
+ {
+ return (this.joinPath);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for joinPath
+ *******************************************************************************/
+ public void setJoinPath(List joinPath)
+ {
+ this.joinPath = joinPath;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for joinPath
+ *******************************************************************************/
+ public ExposedJoin withJoinPath(List joinPath)
+ {
+ this.joinPath = joinPath;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
index 77249eef..2fa71424 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java
@@ -101,6 +101,8 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
private Map middlewareMetaData;
+ private List exposedJoins;
+
/*******************************************************************************
@@ -1296,4 +1298,51 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
{
qInstance.addTable(this);
}
+
+
+
+ /*******************************************************************************
+ ** Getter for exposedJoins
+ *******************************************************************************/
+ public List getExposedJoins()
+ {
+ return (this.exposedJoins);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for exposedJoins
+ *******************************************************************************/
+ public void setExposedJoins(List exposedJoins)
+ {
+ this.exposedJoins = exposedJoins;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for exposedJoins
+ *******************************************************************************/
+ public QTableMetaData withExposedJoins(List exposedJoins)
+ {
+ this.exposedJoins = exposedJoins;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for exposedJoins
+ *******************************************************************************/
+ public QTableMetaData withExposedJoin(ExposedJoin exposedJoin)
+ {
+ if(this.exposedJoins == null)
+ {
+ this.exposedJoins = new ArrayList<>();
+ }
+ this.exposedJoins.add(exposedJoin);
+ return (this);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryAction.java
index 039e0123..c04d1dd1 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationQueryAction.java
@@ -69,7 +69,7 @@ public class EnumerationQueryAction implements QueryInterface
}
BackendQueryFilterUtils.sortRecordList(queryInput.getFilter(), recordList);
- recordList = BackendQueryFilterUtils.applySkipAndLimit(queryInput, recordList);
+ recordList = BackendQueryFilterUtils.applySkipAndLimit(queryInput.getFilter(), recordList);
QueryOutput queryOutput = new QueryOutput(queryInput);
queryOutput.addRecords(recordList);
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 6cc00b8f..b25cac78 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
@@ -38,13 +38,16 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
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.update.UpdateInput;
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.fields.QFieldMetaData;
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.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@@ -181,7 +184,7 @@ public class MemoryRecordStore
}
BackendQueryFilterUtils.sortRecordList(input.getFilter(), records);
- records = BackendQueryFilterUtils.applySkipAndLimit(input, records);
+ records = BackendQueryFilterUtils.applySkipAndLimit(input.getFilter(), records);
return (records);
}
@@ -191,8 +194,11 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
- private Collection buildJoinCrossProduct(QueryInput input)
+ private Collection buildJoinCrossProduct(QueryInput input) throws QException
{
+ QInstance qInstance = QContext.getQInstance();
+ JoinsContext joinsContext = new JoinsContext(qInstance, input.getTableName(), input.getQueryJoins(), input.getFilter());
+
List crossProduct = new ArrayList<>();
QTableMetaData leftTable = input.getTable();
for(QRecord record : getTableData(leftTable).values())
@@ -204,16 +210,26 @@ public class MemoryRecordStore
for(QueryJoin queryJoin : input.getQueryJoins())
{
- QTableMetaData nextTable = QContext.getQInstance().getTable(queryJoin.getJoinTable());
+ 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);
+ });
+
List nextLevelProduct = new ArrayList<>();
for(QRecord productRecord : crossProduct)
{
boolean matchFound = false;
for(QRecord nextTableRecord : nextTableRecords)
{
- if(joinMatches(productRecord, nextTableRecord, queryJoin))
+ if(joinMatches(productRecord, nextTableRecord, queryJoin, joinMetaData))
{
QRecord joinRecord = new QRecord(productRecord);
addRecordToProduct(joinRecord, nextTableRecord, queryJoin.getJoinTableOrItsAlias());
@@ -239,9 +255,9 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
- private boolean joinMatches(QRecord productRecord, QRecord nextTableRecord, QueryJoin queryJoin)
+ private boolean joinMatches(QRecord productRecord, QRecord nextTableRecord, QueryJoin queryJoin, QJoinMetaData joinMetaData)
{
- for(JoinOn joinOn : queryJoin.getJoinMetaData().getJoinOns())
+ for(JoinOn joinOn : joinMetaData.getJoinOns())
{
Serializable leftValue = productRecord.getValues().containsKey(queryJoin.getBaseTableOrAlias() + "." + joinOn.getLeftField())
? productRecord.getValue(queryJoin.getBaseTableOrAlias() + "." + joinOn.getLeftField())
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java
index f99b6efb..9fc87831 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java
@@ -27,7 +27,6 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;
-import java.util.Objects;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@@ -36,6 +35,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
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.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
/*******************************************************************************
@@ -59,7 +59,8 @@ public class MockQueryAction implements QueryInterface
QueryOutput queryOutput = new QueryOutput(queryInput);
- int rows = Objects.requireNonNullElse(queryInput.getLimit(), 1);
+ @SuppressWarnings("UnnecessaryUnboxing") // force an un-boxing, to force an NPE if it's null, to get to the "else 1"
+ int rows = ObjectUtils.tryElse(() -> queryInput.getFilter().getLimit().intValue(), 1);
for(int i = 0; i < rows; i++)
{
QRecord record = new QRecord();
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java
index abf691d8..e05d0bbd 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java
@@ -33,7 +33,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
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.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@@ -129,42 +128,42 @@ public class BackendQueryFilterUtils
public static boolean doesCriteriaMatch(QFilterCriteria criterion, String fieldName, Serializable value)
{
boolean criterionMatches = switch(criterion.getOperator())
+ {
+ case EQUALS -> testEquals(criterion, value);
+ case NOT_EQUALS -> !testEquals(criterion, value);
+ case IN -> testIn(criterion, value);
+ case NOT_IN -> !testIn(criterion, value);
+ case IS_BLANK -> testBlank(criterion, value);
+ case IS_NOT_BLANK -> !testBlank(criterion, value);
+ case CONTAINS -> testContains(criterion, fieldName, value);
+ case NOT_CONTAINS -> !testContains(criterion, fieldName, value);
+ case IS_NULL_OR_IN -> testBlank(criterion, value) || testIn(criterion, value);
+ case LIKE -> testLike(criterion, fieldName, value);
+ case NOT_LIKE -> !testLike(criterion, fieldName, value);
+ case STARTS_WITH -> testStartsWith(criterion, fieldName, value);
+ case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value);
+ case ENDS_WITH -> testEndsWith(criterion, fieldName, value);
+ case NOT_ENDS_WITH -> !testEndsWith(criterion, fieldName, value);
+ case GREATER_THAN -> testGreaterThan(criterion, value);
+ case GREATER_THAN_OR_EQUALS -> testGreaterThan(criterion, value) || testEquals(criterion, value);
+ case LESS_THAN -> !testGreaterThan(criterion, value) && !testEquals(criterion, value);
+ case LESS_THAN_OR_EQUALS -> !testGreaterThan(criterion, value);
+ case BETWEEN ->
{
- case EQUALS -> testEquals(criterion, value);
- case NOT_EQUALS -> !testEquals(criterion, value);
- case IN -> testIn(criterion, value);
- case NOT_IN -> !testIn(criterion, value);
- case IS_BLANK -> testBlank(criterion, value);
- case IS_NOT_BLANK -> !testBlank(criterion, value);
- case CONTAINS -> testContains(criterion, fieldName, value);
- case NOT_CONTAINS -> !testContains(criterion, fieldName, value);
- case IS_NULL_OR_IN -> testBlank(criterion, value) || testIn(criterion, value);
- case LIKE -> testLike(criterion, fieldName, value);
- case NOT_LIKE -> !testLike(criterion, fieldName, value);
- case STARTS_WITH -> testStartsWith(criterion, fieldName, value);
- case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value);
- case ENDS_WITH -> testEndsWith(criterion, fieldName, value);
- case NOT_ENDS_WITH -> !testEndsWith(criterion, fieldName, value);
- case GREATER_THAN -> testGreaterThan(criterion, value);
- case GREATER_THAN_OR_EQUALS -> testGreaterThan(criterion, value) || testEquals(criterion, value);
- case LESS_THAN -> !testGreaterThan(criterion, value) && !testEquals(criterion, value);
- case LESS_THAN_OR_EQUALS -> !testGreaterThan(criterion, value);
- case BETWEEN ->
- {
- QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
- QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
- criteria1.getValues().remove(0);
- yield (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
- }
- case NOT_BETWEEN ->
- {
- QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
- QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
- criteria1.getValues().remove(0);
- boolean between = (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
- yield !between;
- }
- };
+ QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
+ QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
+ criteria1.getValues().remove(0);
+ yield (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
+ }
+ case NOT_BETWEEN ->
+ {
+ QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
+ QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
+ criteria1.getValues().remove(0);
+ boolean between = (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
+ yield !between;
+ }
+ };
return criterionMatches;
}
@@ -524,9 +523,14 @@ public class BackendQueryFilterUtils
/*******************************************************************************
** Apply skip & limit attributes from queryInput to a list of records.
*******************************************************************************/
- public static List applySkipAndLimit(QueryInput queryInput, List recordList)
+ public static List applySkipAndLimit(QQueryFilter queryFilter, List recordList)
{
- Integer skip = queryInput.getSkip();
+ if(queryFilter == null)
+ {
+ return (recordList);
+ }
+
+ Integer skip = queryFilter.getSkip();
if(skip != null && skip > 0)
{
if(skip < recordList.size())
@@ -539,7 +543,7 @@ public class BackendQueryFilterUtils
}
}
- Integer limit = queryInput.getLimit();
+ Integer limit = queryFilter.getLimit();
if(limit != null && limit >= 0 && limit < recordList.size())
{
recordList = recordList.subList(0, limit);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTransformStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTransformStep.java
index f1f9c873..b3dddc99 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTransformStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditTransformStep.java
@@ -211,14 +211,11 @@ public class BulkEditTransformStep extends AbstractTransformStep
@Override
public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
{
- if(isForResultScreen)
- {
- okSummary.setMessage(tableLabel + " records were edited.");
- }
- else
- {
- okSummary.setMessage(tableLabel + " records will be edited.");
- }
+ okSummary.setSingularFutureMessage(tableLabel + " record will be edited.");
+ okSummary.setPluralFutureMessage(tableLabel + " records will be edited.");
+ okSummary.setSingularPastMessage(tableLabel + " record was edited.");
+ okSummary.setPluralPastMessage(tableLabel + " records were edited.");
+ okSummary.pickMessage(isForResultScreen);
ArrayList rs = new ArrayList<>();
rs.add(okSummary);
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 ee23b94f..8f38b8fd 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
@@ -29,6 +29,7 @@ import java.util.List;
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;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
@@ -52,6 +53,8 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
*******************************************************************************/
public class ExtractViaQueryStep extends AbstractExtractStep
{
+ private static final QLogger LOG = QLogger.getLogger(ExtractViaQueryStep.class);
+
public static final String FIELD_SOURCE_TABLE = "sourceTable";
private QQueryFilter queryFilter;
@@ -77,11 +80,33 @@ public class ExtractViaQueryStep extends AbstractExtractStep
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
+ //////////////////////////////////////////////////////////////////
+ // clone the filter, since we're going to edit it (set a limit) //
+ //////////////////////////////////////////////////////////////////
+ QQueryFilter filterClone = queryFilter.clone();
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // if there's a limit in the extract step (e.g., the 20-record limit on the preview screen) //
+ // then set that limit in the filter - UNLESS - there's already a limit in the filter for //
+ // a smaller number of records. //
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ if(getLimit() != null)
+ {
+ if(filterClone.getLimit() != null && filterClone.getLimit() < getLimit())
+ {
+ LOG.trace("Using filter's limit [" + filterClone.getLimit() + "] rather than step's limit [" + getLimit() + "]");
+ }
+ else
+ {
+ filterClone.setLimit(getLimit());
+ }
+ }
+
QueryInput queryInput = new QueryInput();
queryInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE));
- queryInput.setFilter(queryFilter);
+ queryInput.setFilter(filterClone);
+ queryInput.setSelectDistinct(true);
queryInput.setRecordPipe(getRecordPipe());
- queryInput.setLimit(getLimit());
queryInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback());
customizeInputPreQuery(queryInput);
@@ -114,8 +139,20 @@ public class ExtractViaQueryStep extends AbstractExtractStep
CountInput countInput = new CountInput();
countInput.setTableName(runBackendStepInput.getValueString(FIELD_SOURCE_TABLE));
countInput.setFilter(queryFilter);
+ countInput.setIncludeDistinctCount(true);
CountOutput countOutput = new CountAction().execute(countInput);
- return (countOutput.getCount());
+ Integer count = countOutput.getDistinctCount();
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // in case the filter we're running has a limit, but the count found more than that limit, //
+ // well then, just return that limit - as the process won't run on more rows than that. //
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ if(count != null & queryFilter.getLimit() != null && count > queryFilter.getLimit())
+ {
+ count = queryFilter.getLimit();
+ }
+
+ return count;
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java
index db523e40..2efb278e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java
@@ -85,8 +85,7 @@ public class StoreScriptRevisionProcessStep implements BackendStep
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
- );
- queryInput.setLimit(1);
+ .withLimit(1));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(!queryOutput.getRecords().isEmpty())
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java
index 4cde210b..98a41a4f 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/utils/GeneralProcessUtils.java
@@ -209,8 +209,7 @@ public class GeneralProcessUtils
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
- queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, fieldValue)));
- queryInput.setLimit(1);
+ queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, fieldValue)).withLimit(1));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
return (queryOutput.getRecords().stream().findFirst());
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableList.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableList.java
index b43fd241..78084bbc 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableList.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableList.java
@@ -27,6 +27,7 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
+import java.util.Objects;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.utils.lambdas.VoidVoidMethod;
@@ -38,8 +39,8 @@ import com.kingsrook.qqq.backend.core.utils.lambdas.VoidVoidMethod;
*******************************************************************************/
public class MutableList implements List
{
- private List sourceList;
- private Class extends List> mutableTypeIfNeeded;
+ private List sourceList;
+ private Supplier> supplierIfNeeded;
@@ -49,7 +50,7 @@ public class MutableList implements List
*******************************************************************************/
public MutableList(List sourceList)
{
- this(sourceList, (Class) ArrayList.class);
+ this(sourceList, ArrayList::new);
}
@@ -58,10 +59,20 @@ public class MutableList implements List
** Constructor
**
*******************************************************************************/
- public MutableList(List sourceList, Class extends List> mutableTypeIfNeeded)
+ public MutableList(List sourceList, Supplier> supplierIfNeeded)
{
- this.sourceList = sourceList;
- this.mutableTypeIfNeeded = mutableTypeIfNeeded;
+ this.sourceList = Objects.requireNonNullElseGet(sourceList, supplierIfNeeded);
+ this.supplierIfNeeded = supplierIfNeeded;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ List getUnderlyingList()
+ {
+ return (sourceList);
}
@@ -73,13 +84,13 @@ public class MutableList implements List
{
try
{
- List replacementList = mutableTypeIfNeeded.getConstructor().newInstance();
+ List replacementList = supplierIfNeeded.get();
replacementList.addAll(sourceList);
sourceList = replacementList;
}
catch(Exception e)
{
- throw (new IllegalStateException("The mutable type provided for this MutableList [" + mutableTypeIfNeeded.getName() + "] could not be instantiated."));
+ throw (new IllegalStateException("Error getting from the supplier provided for this MutableList.", e));
}
}
@@ -88,7 +99,7 @@ public class MutableList implements List
/*******************************************************************************
**
*******************************************************************************/
- private T doMutableOperationForValue(Supplier supplier)
+ private V doMutableOperationForValue(Supplier supplier)
{
try
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableMap.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableMap.java
index 79102a07..87789756 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableMap.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableMap.java
@@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.utils.collections;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.utils.lambdas.VoidVoidMethod;
@@ -37,8 +38,8 @@ import com.kingsrook.qqq.backend.core.utils.lambdas.VoidVoidMethod;
*******************************************************************************/
public class MutableMap implements Map
{
- private Map sourceMap;
- private Class extends Map> mutableTypeIfNeeded;
+ private Map sourceMap;
+ private Supplier