Better testing on join reports, possible value translations; renamed left & right in QueryJoin (now joinTable, baseTable)

This commit is contained in:
2022-12-22 13:39:37 -06:00
parent 799b695e14
commit 428f48602b
19 changed files with 851 additions and 120 deletions

View File

@ -27,9 +27,11 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -119,6 +121,10 @@ public class GenerateReportAction
{
report = reportInput.getInstance().getReport(reportInput.getReportName());
reportFormat = reportInput.getReportFormat();
if(reportFormat == null)
{
throw new QException("Report format was not specified.");
}
reportStreamer = reportFormat.newReportStreamer();
////////////////////////////////////////////////////////////////////////////////////////////////
@ -300,7 +306,9 @@ public class GenerateReportAction
queryInput.setTableName(dataSource.getSourceTable());
queryInput.setFilter(queryFilter);
queryInput.setQueryJoins(dataSource.getQueryJoins());
queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this?
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource, new JoinsContext(reportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins())));
if(dataSource.getQueryInputCustomizer() != null)
{
@ -355,6 +363,45 @@ public class GenerateReportAction
/*******************************************************************************
**
*******************************************************************************/
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext)
{
Set<String> fieldsToTranslatePossibleValues = new HashSet<>();
for(QReportView view : report.getViews())
{
for(QReportField column : CollectionUtils.nonNullList(view.getColumns()))
{
////////////////////////////////////////////////////////////////////////////////////////
// if this is a column marked as ShowPossibleValueLabel, then we need to translate it //
////////////////////////////////////////////////////////////////////////////////////////
if(column.getShowPossibleValueLabel())
{
String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName());
fieldsToTranslatePossibleValues.add(effectiveFieldName);
}
}
for(String summaryField : CollectionUtils.nonNullList(view.getPivotFields()))
{
///////////////////////////////////////////////////////////////////////////////
// all pivotFields that are possible value sources are implicitly translated //
///////////////////////////////////////////////////////////////////////////////
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
if(table.getField(summaryField).getPossibleValueSourceName() != null)
{
fieldsToTranslatePossibleValues.add(summaryField);
}
}
}
return (fieldsToTranslatePossibleValues);
}
/*******************************************************************************
**
*******************************************************************************/
@ -375,8 +422,7 @@ public class GenerateReportAction
for(Serializable value : criterion.getValues())
{
String valueAsString = ValueUtils.getValueAsString(value);
// Serializable interpretedValue = variableInterpreter.interpret(valueAsString);
String valueAsString = ValueUtils.getValueAsString(value);
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
newValues.add(interpretedValue);
}
@ -476,6 +522,9 @@ public class GenerateReportAction
Serializable summaryValue = record.getValue(summaryField);
if(table.getField(summaryField).getPossibleValueSourceName() != null)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// so, this is kinda a thing - where we implicitly use possible-value labels (e.g., display values) for pivot fields... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
summaryValue = record.getDisplayValue(summaryField);
}
key.add(summaryField, summaryValue);

View File

@ -110,7 +110,7 @@ public class QueryAction
{
qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession());
}
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records, queryInput.getQueryJoins());
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records, queryInput.getQueryJoins(), queryInput.getFieldsToTranslatePossibleValues());
}
if(queryInput.getShouldGenerateDisplayValues())

View File

@ -96,7 +96,7 @@ public class QPossibleValueTranslator
*******************************************************************************/
public void translatePossibleValuesInRecords(QTableMetaData table, List<QRecord> records)
{
translatePossibleValuesInRecords(table, records, Collections.emptyList());
translatePossibleValuesInRecords(table, records, Collections.emptyList(), null);
}
@ -104,15 +104,21 @@ public class QPossibleValueTranslator
/*******************************************************************************
** For a list of records, translate their possible values (populating their display values)
*******************************************************************************/
public void translatePossibleValuesInRecords(QTableMetaData table, List<QRecord> records, List<QueryJoin> queryJoins)
public void translatePossibleValuesInRecords(QTableMetaData table, List<QRecord> records, List<QueryJoin> queryJoins, Set<String> limitedToFieldNames)
{
if(records == null || table == null)
{
return;
}
if(limitedToFieldNames != null && limitedToFieldNames.isEmpty())
{
LOG.debug("We were asked to translate possible values, but then given an empty set of fields to translate, so noop.");
return;
}
LOG.debug("Translating possible values in [" + records.size() + "] records from the [" + table.getName() + "] table.");
primePvsCache(table, records, queryJoins);
primePvsCache(table, records, queryJoins, limitedToFieldNames);
for(QRecord record : records)
{
@ -120,7 +126,10 @@ public class QPossibleValueTranslator
{
if(field.getPossibleValueSourceName() != null)
{
record.setDisplayValue(field.getName(), translatePossibleValue(field, record.getValue(field.getName())));
if(limitedToFieldNames == null || limitedToFieldNames.contains(field.getName()))
{
record.setDisplayValue(field.getName(), translatePossibleValue(field, record.getValue(field.getName())));
}
}
}
@ -130,25 +139,25 @@ public class QPossibleValueTranslator
{
try
{
////////////////////////////////////////////
// todo - aliases aren't be handled right //
////////////////////////////////////////////
QTableMetaData joinTable = qInstance.getTable(queryJoin.getRightTable());
QTableMetaData joinTable = qInstance.getTable(queryJoin.getJoinTable());
for(QFieldMetaData field : joinTable.getFields().values())
{
String joinFieldName = Objects.requireNonNullElse(queryJoin.getAlias(), joinTable.getName()) + "." + field.getName();
if(field.getPossibleValueSourceName() != null)
{
///////////////////////////////////////////////
// avoid circling-back upon the source table //
///////////////////////////////////////////////
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType()) && table.getName().equals(possibleValueSource.getTableName()))
if(limitedToFieldNames == null || limitedToFieldNames.contains(joinFieldName))
{
continue;
}
///////////////////////////////////////////////
// avoid circling-back upon the source table //
///////////////////////////////////////////////
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType()) && table.getName().equals(possibleValueSource.getTableName()))
{
continue;
}
String joinFieldName = joinTable.getName() + "." + field.getName();
record.setDisplayValue(joinFieldName, translatePossibleValue(field, record.getValue(joinFieldName)));
record.setDisplayValue(joinFieldName, translatePossibleValue(field, record.getValue(joinFieldName)));
}
}
}
}
@ -379,11 +388,13 @@ public class QPossibleValueTranslator
/*******************************************************************************
** prime the cache (e.g., by doing bulk-queries) for table-based PVS's
** @param table the table that the records are from
*
* @param table the table that the records are from
** @param records the records that have the possible value id's (e.g., foreign keys)
* @param queryJoins
* @param queryJoins joins that were used as part of the query that led to the records.
* @param limitedToFieldNames set of names that are the only fields that get translated (null means all fields).
*******************************************************************************/
void primePvsCache(QTableMetaData table, List<QRecord> records, List<QueryJoin> queryJoins)
void primePvsCache(QTableMetaData table, List<QRecord> records, List<QueryJoin> queryJoins, Set<String> limitedToFieldNames)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is a map of String(tableName - the PVS table) to Pair(String (either "" for main table in a query, or join-table + "."), field (from the table being selected from)) //
@ -395,13 +406,13 @@ public class QPossibleValueTranslator
///////////////////////////////////////////////////////////////////////////////////////
ListingHash<String, QPossibleValueSource> pvsesByTable = new ListingHash<>();
primePvsCacheTableListingHashLoader(table, fieldsByPvsTable, pvsesByTable, "");
primePvsCacheTableListingHashLoader(table, fieldsByPvsTable, pvsesByTable, "", table.getName(), limitedToFieldNames);
for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins))
{
if(queryJoin.getSelect())
{
// todo - aliases probably not handled right
primePvsCacheTableListingHashLoader(qInstance.getTable(queryJoin.getRightTable()), fieldsByPvsTable, pvsesByTable, queryJoin.getRightTable() + ".");
String aliasOrTableName = Objects.requireNonNullElse(queryJoin.getAlias(), queryJoin.getJoinTable());
primePvsCacheTableListingHashLoader(qInstance.getTable(queryJoin.getJoinTable()), fieldsByPvsTable, pvsesByTable, aliasOrTableName + ".", queryJoin.getJoinTable(), limitedToFieldNames);
}
}
@ -426,8 +437,8 @@ public class QPossibleValueTranslator
//////////////////////////////////////
// check if value is already cached //
//////////////////////////////////////
QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0);
Map<Serializable, String> cacheForPvs = possibleValueCache.computeIfAbsent(possibleValueSource.getName(), x -> new HashMap<>());
QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0);
Map<Serializable, String> cacheForPvs = possibleValueCache.computeIfAbsent(possibleValueSource.getName(), x -> new HashMap<>());
if(!cacheForPvs.containsKey(fieldValue))
{
@ -448,13 +459,19 @@ public class QPossibleValueTranslator
/*******************************************************************************
** Helper for the primePvsCache method
*******************************************************************************/
private void primePvsCacheTableListingHashLoader(QTableMetaData table, ListingHash<String, Pair<String, QFieldMetaData>> fieldsByPvsTable, ListingHash<String, QPossibleValueSource> pvsesByTable, String fieldNamePrefix)
private void primePvsCacheTableListingHashLoader(QTableMetaData table, ListingHash<String, Pair<String, QFieldMetaData>> fieldsByPvsTable, ListingHash<String, QPossibleValueSource> pvsesByTable, String fieldNamePrefix, String tableName, Set<String> limitedToFieldNames)
{
for(QFieldMetaData field : table.getFields().values())
{
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
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.");
continue;
}
fieldsByPvsTable.add(possibleValueSource.getTableName(), Pair.of(fieldNamePrefix, field));
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -491,14 +508,38 @@ public class QPossibleValueTranslator
queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page)));
/////////////////////////////////////////////////////////////////////////////////////////
// this is needed to get record labels, which are what we use here... unclear if best! //
/////////////////////////////////////////////////////////////////////////////////////////
if(notTooDeep())
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// when querying for possible values, we do want to generate their display values, which makes record labels, which are usually used as PVS labels //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
queryInput.setShouldGenerateDisplayValues(true);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// also, if this table uses any possible value fields as part of its own record label, then THOSE possible values need translated. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Set<String> possibleValueFieldsToTranslate = new HashSet<>();
for(QPossibleValueSource possibleValueSource : possibleValueSources)
{
// todo not commit...
// queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true);
if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
for(String recordLabelField : CollectionUtils.nonNullList(table.getRecordLabelFields()))
{
QFieldMetaData field = table.getField(recordLabelField);
if(field.getPossibleValueSourceName() != null)
{
possibleValueFieldsToTranslate.add(field.getName());
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// an earlier version of this code got into stack overflows, so do a "cheap" check for recursion depth too... //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(!possibleValueFieldsToTranslate.isEmpty() && notTooDeep())
{
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setFieldsToTranslatePossibleValues(possibleValueFieldsToTranslate);
}
LOG.debug("Priming PVS cache for [" + page.size() + "] ids from [" + tableName + "] table.");

View File

@ -1107,7 +1107,7 @@ public class QInstanceValidator
String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1);
for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins))
{
QTableMetaData joinTable = qInstance.getTable(queryJoin.getRightTable());
QTableMetaData joinTable = qInstance.getTable(queryJoin.getJoinTable());
if(joinTable != null)
{
try

View File

@ -38,9 +38,13 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
*******************************************************************************/
public class JoinsContext
{
private final QInstance instance;
private final String mainTableName;
private final List<QueryJoin> queryJoins;
private final QInstance instance;
private final String mainTableName;
private final List<QueryJoin> queryJoins;
////////////////////////////////////////////////////////////////
// note - will have entries for all tables, not just aliases. //
////////////////////////////////////////////////////////////////
private final Map<String, String> aliasToTableNameMap = new HashMap<>();
@ -57,8 +61,8 @@ public class JoinsContext
for(QueryJoin queryJoin : this.queryJoins)
{
QTableMetaData joinTable = instance.getTable(queryJoin.getRightTable());
String tableNameOrAlias = queryJoin.getAliasOrRightTable();
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
String tableNameOrAlias = queryJoin.getJoinTableOrItsAlias();
if(aliasToTableNameMap.containsKey(tableNameOrAlias))
{
throw (new QException("Duplicate table name or alias: " + tableNameOrAlias));
@ -81,7 +85,8 @@ public class JoinsContext
/*******************************************************************************
**
** For a given name (whether that's a table name or an alias in the query),
** get the actual table name (e.g., that could be passed to qInstance.getTable())
*******************************************************************************/
public String resolveTableNameOrAliasToTableName(String nameOrAlias)
{
@ -95,7 +100,8 @@ public class JoinsContext
/*******************************************************************************
**
** For a given fieldName, which we expect may start with a tableNameOrAlias + '.',
** find the QFieldMetaData and the tableNameOrAlias that it corresponds to.
*******************************************************************************/
public FieldAndTableNameOrAlias getFieldAndTableNameOrAlias(String fieldName)
{
@ -124,6 +130,40 @@ public class JoinsContext
/*******************************************************************************
** Check if the given table name exists in the query - but that name may NOT
** be an alias - it must be an actual table name.
**
** e.g., Given:
** FROM `order` INNER JOIN line_item li
** hasAliasOrTable("order") => true
** hasAliasOrTable("li") => false
** hasAliasOrTable("line_item") => true
*******************************************************************************/
public boolean hasTable(String table)
{
return (mainTableName.equals(table) || aliasToTableNameMap.containsValue(table));
}
/*******************************************************************************
** Check if the given tableOrAlias exists in the query - but note, if a table
** is in the query, but with an alias, then it would not be found by this method.
**
** e.g., Given:
** FROM `order` INNER JOIN line_item li
** hasAliasOrTable("order") => false
** hasAliasOrTable("li") => true
** hasAliasOrTable("line_item") => false
*******************************************************************************/
public boolean hasAliasOrTable(String tableOrAlias)
{
return (mainTableName.equals(tableOrAlias) || aliasToTableNameMap.containsKey(tableOrAlias));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
@ -47,6 +48,13 @@ public class QueryInput extends AbstractTableActionInput
private boolean shouldTranslatePossibleValues = false;
private boolean shouldGenerateDisplayValues = false;
/////////////////////////////////////////////////////////////////////////////////////////
// this field - only applies if shouldTranslatePossibleValues is true. //
// if this field is null, then ALL possible value fields get translated. //
// if this field is non-null, then ONLY the fieldNames in this set will be translated. //
/////////////////////////////////////////////////////////////////////////////////////////
private Set<String> fieldsToTranslatePossibleValues;
private List<QueryJoin> queryJoins = null;
@ -295,4 +303,37 @@ public class QueryInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for fieldsToTranslatePossibleValues
**
*******************************************************************************/
public Set<String> getFieldsToTranslatePossibleValues()
{
return fieldsToTranslatePossibleValues;
}
/*******************************************************************************
** Setter for fieldsToTranslatePossibleValues
**
*******************************************************************************/
public void setFieldsToTranslatePossibleValues(Set<String> fieldsToTranslatePossibleValues)
{
this.fieldsToTranslatePossibleValues = fieldsToTranslatePossibleValues;
}
/*******************************************************************************
** Fluent setter for fieldsToTranslatePossibleValues
**
*******************************************************************************/
public QueryInput withFieldsToTranslatePossibleValues(Set<String> fieldsToTranslatePossibleValues)
{
this.fieldsToTranslatePossibleValues = fieldsToTranslatePossibleValues;
return (this);
}
}

View File

@ -22,17 +22,38 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Part of query (or count, aggregate) input, to do a Join as part of a query.
**
** Conceptually, when you're adding a QueryJoin to a query, you're adding a new
** table to the query - this is named the `joinTable` in this class. This table
** can be given an alias, which can be referenced in the rest of the query.
**
** Every joinTable needs to have a `baseTable` that it is "joined" with - e.g.,
** the table that the joinOn clauses link up with.
**
** However - the caller doesn't necessarily need to specify the `baseTable` -
** as the framework will look for Joins defined in the qInstance, and if an
** unambiguous one is found (between the joinTable and other tables in the
** query), then it'll use the "other" table in that Join as the baseTable.
**
** For use-cases where a baseTable has been included in a query multiple times,
** with aliases, then the baseTableOrAlias field must be set to the appropriate alias.
**
** If there are multiple Joins defined between the base & join tables, then the
** specific joinMetaData to use must be set. The joinMetaData field can also be
** used instead of specify joinTable and baseTableOrAlias, but only for cases
** where the baseTable is not an alias.
*******************************************************************************/
public class QueryJoin
{
private String leftTableOrAlias;
private String rightTable;
private String baseTableOrAlias;
private String joinTable;
private QJoinMetaData joinMetaData;
private String alias;
private boolean select = false;
@ -59,19 +80,42 @@ public class QueryJoin
/*******************************************************************************
** Constructor
** Constructor that only takes a joinTable. Unless you also set the baseTableOrAlias,
** the framework will attempt to ascertain the baseTableOrAlias, based on Joins
** defined in the instance and other tables in the query.
**
*******************************************************************************/
public QueryJoin(String leftTableOrAlias, String rightTable)
public QueryJoin(String joinTable)
{
this.leftTableOrAlias = leftTableOrAlias;
this.rightTable = rightTable;
this.joinTable = joinTable;
}
/*******************************************************************************
** Constructor
** Constructor that takes baseTableOrAlias and joinTable. Useful if it's not
** explicitly clear what the base table should be just from the joinTable. e.g.,
** if the baseTable has an alias, or if there's more than 1 join in the instance
** that matches the joinTable and the other tables in the query.
**
*******************************************************************************/
public QueryJoin(String baseTableOrAlias, String joinTable)
{
this.baseTableOrAlias = baseTableOrAlias;
this.joinTable = joinTable;
}
/*******************************************************************************
** Constructor that takes a joinMetaData - the rightTable in the joinMetaData will
** be used as the joinTable. The leftTable in the joinMetaData will be used as
** the baseTable.
**
** This is probably (only?) what you want to use if you have a table that joins
** more than once to another table (e.g., order.shipToCustomerId and order.billToCustomerId).
**
** Alternatively, you could just do new QueryJoin("customer").withJoinMetaData("orderJoinShipToCustomer").
**
*******************************************************************************/
public QueryJoin(QJoinMetaData joinMetaData)
@ -82,68 +126,68 @@ public class QueryJoin
/*******************************************************************************
** Getter for leftTableOrAlias
** Getter for baseTableOrAlias
**
*******************************************************************************/
public String getLeftTableOrAlias()
public String getBaseTableOrAlias()
{
return leftTableOrAlias;
return baseTableOrAlias;
}
/*******************************************************************************
** Setter for leftTableOrAlias
** Setter for baseTableOrAlias
**
*******************************************************************************/
public void setLeftTableOrAlias(String leftTableOrAlias)
public void setBaseTableOrAlias(String baseTableOrAlias)
{
this.leftTableOrAlias = leftTableOrAlias;
this.baseTableOrAlias = baseTableOrAlias;
}
/*******************************************************************************
** Fluent setter for leftTableOrAlias
** Fluent setter for baseTableOrAlias
**
*******************************************************************************/
public QueryJoin withLeftTableOrAlias(String leftTableOrAlias)
public QueryJoin withBaseTableOrAlias(String baseTableOrAlias)
{
this.leftTableOrAlias = leftTableOrAlias;
this.baseTableOrAlias = baseTableOrAlias;
return (this);
}
/*******************************************************************************
** Getter for rightTable
** Getter for joinTable
**
*******************************************************************************/
public String getRightTable()
public String getJoinTable()
{
return rightTable;
return joinTable;
}
/*******************************************************************************
** Setter for rightTable
** Setter for joinTable
**
*******************************************************************************/
public void setRightTable(String rightTable)
public void setJoinTable(String joinTable)
{
this.rightTable = rightTable;
this.joinTable = joinTable;
}
/*******************************************************************************
** Fluent setter for rightTable
** Fluent setter for joinTable
**
*******************************************************************************/
public QueryJoin withRightTable(String rightTable)
public QueryJoin withJoinTable(String joinTable)
{
this.rightTable = rightTable;
this.joinTable = joinTable;
return (this);
}
@ -220,13 +264,13 @@ public class QueryJoin
/*******************************************************************************
**
*******************************************************************************/
public String getAliasOrRightTable()
public String getJoinTableOrItsAlias()
{
if(StringUtils.hasContent(alias))
{
return (alias);
}
return (rightTable);
return (joinTable);
}
@ -282,12 +326,13 @@ public class QueryJoin
*******************************************************************************/
public void setJoinMetaData(QJoinMetaData joinMetaData)
{
Objects.requireNonNull(joinMetaData, "JoinMetaData was null.");
this.joinMetaData = joinMetaData;
if(!StringUtils.hasContent(this.leftTableOrAlias) && !StringUtils.hasContent(this.rightTable))
if(!StringUtils.hasContent(this.baseTableOrAlias) && !StringUtils.hasContent(this.joinTable))
{
setLeftTableOrAlias(joinMetaData.getLeftTable());
setRightTable(joinMetaData.getRightTable());
setBaseTableOrAlias(joinMetaData.getLeftTable());
setJoinTable(joinMetaData.getRightTable());
}
}

View File

@ -304,6 +304,6 @@ public class QJoinMetaData
{
throw (new IllegalStateException("Missing either a left or right table name when trying to set inferred name for join"));
}
return (withName(getLeftTable() + "Join" + getRightTable()));
return (withName(getLeftTable() + "Join" + StringUtils.ucFirst(getRightTable())));
}
}

View File

@ -466,6 +466,22 @@ public class QReportView implements Cloneable
/*******************************************************************************
** Fluent setter to add a single column
**
*******************************************************************************/
public QReportView withColumn(QReportField column)
{
if(this.columns == null)
{
this.columns = new ArrayList<>();
}
this.columns.add(column);
return (this);
}
/*******************************************************************************
** Getter for orderByFields
**

View File

@ -28,6 +28,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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;
@ -39,6 +40,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
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;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
/*******************************************************************************
@ -53,9 +55,11 @@ public class MemoryRecordStore
private static boolean collectStatistics = false;
public static final String STAT_QUERIES_RAN = "queriesRan";
private static final Map<String, Integer> statistics = Collections.synchronizedMap(new HashMap<>());
public static final String STAT_QUERIES_RAN = "queriesRan";
public static final ListingHash<Class<? extends AbstractActionInput>, AbstractActionInput> actionInputs = new ListingHash<>();
@ -114,7 +118,7 @@ public class MemoryRecordStore
*******************************************************************************/
public List<QRecord> query(QueryInput input)
{
incrementStatistic(STAT_QUERIES_RAN);
incrementStatistic(input);
Map<Serializable, QRecord> tableData = getTableData(input.getTable());
List<QRecord> records = new ArrayList<>();
@ -295,6 +299,24 @@ public class MemoryRecordStore
/*******************************************************************************
** Increment a statistic
**
*******************************************************************************/
public static void incrementStatistic(AbstractActionInput input)
{
if(collectStatistics)
{
actionInputs.add(input.getClass(), input);
if(input instanceof QueryInput)
{
incrementStatistic(STAT_QUERIES_RAN);
}
}
}
/*******************************************************************************
** Increment a statistic
**
@ -317,6 +339,7 @@ public class MemoryRecordStore
public static void resetStatistics()
{
statistics.clear();
actionInputs.clear();
}
@ -330,4 +353,15 @@ public class MemoryRecordStore
return statistics;
}
/*******************************************************************************
** Getter for the actionInputs that were recorded - only while collectStatistics
** was true.
*******************************************************************************/
public static ListingHash<Class<? extends AbstractActionInput>, AbstractActionInput> getActionInputs()
{
return (actionInputs);
}
}

View File

@ -46,7 +46,7 @@ public class SleepUtils
try
{
long millisToSleep = end - System.currentTimeMillis();
Thread.sleep(millisToSleep);
Thread.sleep(Math.max(0, millisToSleep)); // avoid negative sleep, which fails.
}
catch(InterruptedException e)
{

View File

@ -554,4 +554,33 @@ public class GenerateReportActionTest
assertThat(row).containsOnlyKeys("Birth Date");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testReportWithPossibleValueColumns() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
insertPersonRecords(qInstance);
ReportInput reportInput = new ReportInput(qInstance);
reportInput.setSession(new QSession());
reportInput.setReportName(TestUtils.REPORT_NAME_PERSON_SIMPLE);
reportInput.setReportFormat(ReportFormat.LIST_OF_MAPS);
reportInput.setReportOutputStream(new ByteArrayOutputStream());
new GenerateReportAction().execute(reportInput);
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("Simple Report");
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
assertThat(row).containsKeys("Id", "First Name", "Last Name", "Home State Id", "Home State Name");
row = iterator.next();
assertThat(row.get("Home State Id")).isEqualTo("1");
assertThat(row.get("Home State Name")).isEqualTo("IL");
}
}

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -188,7 +189,7 @@ public class QPossibleValueTranslatorTest
);
QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON);
MemoryRecordStore.resetStatistics();
possibleValueTranslator.primePvsCache(personTable, personRecords, null); // todo - test non-null queryJoins
possibleValueTranslator.primePvsCache(personTable, personRecords, null, null); // todo - test non-null queryJoins
assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query");
possibleValueTranslator.translatePossibleValue(shapeField, 1);
possibleValueTranslator.translatePossibleValue(shapeField, 2);
@ -360,4 +361,101 @@ public class QPossibleValueTranslatorTest
assertEquals("MO", records.get(1).getDisplayValue("homeStateId"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPossibleValueWithSecondaryPossibleValueLabel() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
qInstance.addTable(new QTableMetaData()
.withName("city")
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("name", QFieldType.STRING))
.withField(new QFieldMetaData("regionId", QFieldType.INTEGER).withPossibleValueSourceName("region")));
qInstance.addTable(new QTableMetaData()
.withName("region")
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withRecordLabelFormat("%s of %s")
.withRecordLabelFields("name", "countryId")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("name", QFieldType.STRING))
.withField(new QFieldMetaData("countryId", QFieldType.INTEGER).withPossibleValueSourceName("country")));
qInstance.addTable(new QTableMetaData()
.withName("country")
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withRecordLabelFormat("%s")
.withRecordLabelFields("name")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("name", QFieldType.STRING)));
qInstance.addPossibleValueSource(new QPossibleValueSource()
.withName("region")
.withType(QPossibleValueSourceType.TABLE)
.withTableName("region")
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY));
qInstance.addPossibleValueSource(new QPossibleValueSource()
.withName("country")
.withType(QPossibleValueSourceType.TABLE)
.withTableName("country")
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY));
List<QRecord> regions = List.of(new QRecord().withValue("id", 11).withValue("name", "Missouri").withValue("countryId", 111));
List<QRecord> countries = List.of(new QRecord().withValue("id", 111).withValue("name", "U.S.A"));
TestUtils.insertRecords(qInstance, qInstance.getTable("region"), regions);
TestUtils.insertRecords(qInstance, qInstance.getTable("country"), countries);
MemoryRecordStore.resetStatistics();
MemoryRecordStore.setCollectStatistics(true);
QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(qInstance, new QSession());
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// verify that if we run w/ an empty set for the param limitedToFieldNames, that we do NOT translate the regionId //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
List<QRecord> cities = List.of(new QRecord().withValue("id", 1).withValue("name", "St. Louis").withValue("regionId", 11));
possibleValueTranslator.translatePossibleValuesInRecords(qInstance.getTable("city"), cities, null, Set.of());
assertNull(cities.get(0).getDisplayValue("regionId"));
}
////////////////////////////////////////////////////////////////////////
// ditto a set that contains something, but not the field in question //
////////////////////////////////////////////////////////////////////////
{
List<QRecord> cities = List.of(new QRecord().withValue("id", 1).withValue("name", "St. Louis").withValue("regionId", 11));
possibleValueTranslator.translatePossibleValuesInRecords(qInstance.getTable("city"), cities, null, Set.of("foobar"));
assertNull(cities.get(0).getDisplayValue("regionId"));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// now re-run, w/ regionId - and we should see it get translated - and - the possible-value that it uses (countryId) as part of its label also gets translated. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
List<QRecord> cities = List.of(new QRecord().withValue("id", 1).withValue("name", "St. Louis").withValue("regionId", 11));
possibleValueTranslator.translatePossibleValuesInRecords(qInstance.getTable("city"), cities, null, Set.of("regionId"));
assertEquals("Missouri of U.S.A", cities.get(0).getDisplayValue("regionId"));
}
/////////////////////////////////////////////////////////////////////////////////
// finally, verify that a null limitedToFieldNames means to translate them all //
/////////////////////////////////////////////////////////////////////////////////
{
List<QRecord> cities = List.of(new QRecord().withValue("id", 1).withValue("name", "St. Louis").withValue("regionId", 11));
possibleValueTranslator.translatePossibleValuesInRecords(qInstance.getTable("city"), cities, null, null);
assertEquals("Missouri of U.S.A", cities.get(0).getDisplayValue("regionId"));
}
}
}

View File

@ -47,7 +47,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
@ -141,7 +140,8 @@ public class TestUtils
public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly";
public static final String TABLE_NAME_BASEPULL = "basepullTest";
public static final String REPORT_NAME_SHAPES_PERSON = "shapesPersonReport";
public static final String REPORT_NAME_PERSON_JOIN_SHAPE = "simplePersonReport";
public static final String REPORT_NAME_PERSON_SIMPLE = "simplePersonReport";
public static final String REPORT_NAME_PERSON_JOIN_SHAPE = "personJoinShapeReport";
public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type
public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type
@ -195,6 +195,7 @@ public class TestUtils
qInstance.addReport(defineShapesPersonsReport());
qInstance.addProcess(defineShapesPersonReportProcess());
qInstance.addReport(definePersonJoinShapeReport());
qInstance.addReport(definePersonSimpleReport());
qInstance.addAutomationProvider(definePollingAutomationProvider());
@ -1113,6 +1114,32 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
private static QReportMetaData definePersonSimpleReport()
{
return new QReportMetaData()
.withName(REPORT_NAME_PERSON_SIMPLE)
.withDataSource(
new QReportDataSource()
.withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
)
.withView(new QReportView()
.withType(ReportType.TABLE)
.withLabel("Simple Report")
.withColumns(List.of(
new QReportField("id"),
new QReportField("firstName"),
new QReportField("lastName"),
new QReportField("homeStateId").withLabel("Home State Id"),
new QReportField("homeStateName").withSourceFieldName("homeStateId").withShowPossibleValueLabel(true).withLabel("Home State Name")
))
);
}
/*******************************************************************************
**
*******************************************************************************/