diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index e0cd8d21..e05b18c6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -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 setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext) + { + Set 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); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index a63de9b9..561acf80 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -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()) 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 b2728052..f1272be1 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 @@ -96,7 +96,7 @@ public class QPossibleValueTranslator *******************************************************************************/ public void translatePossibleValuesInRecords(QTableMetaData table, List 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 records, List queryJoins) + public void translatePossibleValuesInRecords(QTableMetaData table, List records, List queryJoins, Set 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 records, List queryJoins) + void primePvsCache(QTableMetaData table, List records, List queryJoins, Set 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 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 cacheForPvs = possibleValueCache.computeIfAbsent(possibleValueSource.getName(), x -> new HashMap<>()); + QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0); + Map 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> fieldsByPvsTable, ListingHash pvsesByTable, String fieldNamePrefix) + private void primePvsCacheTableListingHashLoader(QTableMetaData table, ListingHash> fieldsByPvsTable, ListingHash pvsesByTable, String fieldNamePrefix, String tableName, Set 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 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."); 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 b77c2519..213f37a1 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 @@ -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 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 1d7fc793..8110ba82 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 @@ -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 queryJoins; + private final QInstance instance; + private final String mainTableName; + private final List queryJoins; + + //////////////////////////////////////////////////////////////// + // note - will have entries for all tables, not just aliases. // + //////////////////////////////////////////////////////////////// private final Map 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)); + } + + + /******************************************************************************* ** *******************************************************************************/ 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 12d9790d..396b773a 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 @@ -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 fieldsToTranslatePossibleValues; + private List queryJoins = null; @@ -295,4 +303,37 @@ public class QueryInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for fieldsToTranslatePossibleValues + ** + *******************************************************************************/ + public Set getFieldsToTranslatePossibleValues() + { + return fieldsToTranslatePossibleValues; + } + + + + /******************************************************************************* + ** Setter for fieldsToTranslatePossibleValues + ** + *******************************************************************************/ + public void setFieldsToTranslatePossibleValues(Set fieldsToTranslatePossibleValues) + { + this.fieldsToTranslatePossibleValues = fieldsToTranslatePossibleValues; + } + + + /******************************************************************************* + ** Fluent setter for fieldsToTranslatePossibleValues + ** + *******************************************************************************/ + public QueryInput withFieldsToTranslatePossibleValues(Set fieldsToTranslatePossibleValues) + { + this.fieldsToTranslatePossibleValues = fieldsToTranslatePossibleValues; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java index def81c53..c1e103e3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java @@ -22,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()); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/QJoinMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/QJoinMetaData.java index d81f2375..a63ceddb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/QJoinMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/QJoinMetaData.java @@ -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()))); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java index a486f0df..1247ddfe 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java @@ -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 ** 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 255b481b..11940619 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 @@ -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 statistics = Collections.synchronizedMap(new HashMap<>()); - public static final String STAT_QUERIES_RAN = "queriesRan"; + public static final ListingHash, AbstractActionInput> actionInputs = new ListingHash<>(); @@ -114,7 +118,7 @@ public class MemoryRecordStore *******************************************************************************/ public List query(QueryInput input) { - incrementStatistic(STAT_QUERIES_RAN); + incrementStatistic(input); Map tableData = getTableData(input.getTable()); List 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, AbstractActionInput> getActionInputs() + { + return (actionInputs); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/SleepUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/SleepUtils.java index fc89d1da..4487821a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/SleepUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/SleepUtils.java @@ -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) { diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java index c8e2ca79..989e1a17 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java @@ -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> list = ListOfMapsExportStreamer.getList("Simple Report"); + Iterator> iterator = list.iterator(); + Map 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"); + } + } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java index d1f98e40..2b4839f3 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java @@ -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 regions = List.of(new QRecord().withValue("id", 11).withValue("name", "Missouri").withValue("countryId", 111)); + List 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 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 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 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 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")); + } + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 347d1417..68d8cbe8 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -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") + )) + ); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index 7de6d174..211e09a4 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -183,38 +183,48 @@ public abstract class AbstractRDBMSAction implements QActionInterface for(QueryJoin queryJoin : joinsContext.getQueryJoins()) { - QTableMetaData joinTable = instance.getTable(queryJoin.getRightTable()); - String tableNameOrAlias = queryJoin.getAliasOrRightTable(); + QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable()); + String tableNameOrAlias = queryJoin.getJoinTableOrItsAlias(); rs.append(" ").append(queryJoin.getType()).append(" JOIN ") .append(escapeIdentifier(getTableName(joinTable))) .append(" AS ").append(escapeIdentifier(tableNameOrAlias)); //////////////////////////////////////////////////////////// - // find the join in the instance, to see the 'on' clause // + // find the join in the instance, to set the 'on' clause // //////////////////////////////////////////////////////////// List joinClauseList = new ArrayList<>(); - String leftTableName = joinsContext.resolveTableNameOrAliasToTableName(queryJoin.getLeftTableOrAlias()); + String baseTableName = joinsContext.resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()); QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () -> { - QJoinMetaData found = findJoinMetaData(instance, leftTableName, queryJoin.getRightTable()); + QJoinMetaData found = findJoinMetaData(instance, joinsContext, baseTableName, queryJoin.getJoinTable()); if(found == null) { - throw (new RuntimeException("Could not find a join between tables [" + leftTableName + "][" + queryJoin.getRightTable() + "]")); + throw (new RuntimeException("Could not find a join between tables [" + baseTableName + "][" + queryJoin.getJoinTable() + "]")); } return (found); }); + for(JoinOn joinOn : joinMetaData.getJoinOns()) { QTableMetaData leftTable = instance.getTable(joinMetaData.getLeftTable()); QTableMetaData rightTable = instance.getTable(joinMetaData.getRightTable()); - String leftTableOrAlias = queryJoin.getLeftTableOrAlias(); - String aliasOrRightTable = queryJoin.getAliasOrRightTable(); + String baseTableOrAlias = queryJoin.getBaseTableOrAlias(); + if(baseTableOrAlias == null) + { + baseTableOrAlias = leftTable.getName(); + if(!joinsContext.hasAliasOrTable(baseTableOrAlias)) + { + throw (new RuntimeException("Could not find a table or alias [" + baseTableOrAlias + "] in query. May need to be more specific setting up QueryJoins.")); + } + } - joinClauseList.add(escapeIdentifier(leftTableOrAlias) + String joinTableOrAlias = queryJoin.getJoinTableOrItsAlias(); + + joinClauseList.add(escapeIdentifier(baseTableOrAlias) + "." + escapeIdentifier(getColumnName(leftTable.getField(joinOn.getLeftField()))) - + " = " + escapeIdentifier(aliasOrRightTable) + + " = " + escapeIdentifier(joinTableOrAlias) + "." + escapeIdentifier(getColumnName((rightTable.getField(joinOn.getRightField()))))); } rs.append(" ON ").append(StringUtils.join(" AND ", joinClauseList)); @@ -228,22 +238,49 @@ public abstract class AbstractRDBMSAction implements QActionInterface /******************************************************************************* ** *******************************************************************************/ - private QJoinMetaData findJoinMetaData(QInstance instance, String leftTable, String rightTable) + private QJoinMetaData findJoinMetaData(QInstance instance, JoinsContext joinsContext, String baseTableName, String joinTableName) { List matches = new ArrayList<>(); - for(QJoinMetaData join : instance.getJoins().values()) + if(baseTableName != null) { - if(join.getLeftTable().equals(leftTable) && join.getRightTable().equals(rightTable)) + /////////////////////////////////////////////////////////////////////////// + // if query specified a left-table, look for a join between left & right // + /////////////////////////////////////////////////////////////////////////// + for(QJoinMetaData join : instance.getJoins().values()) { - matches.add(join); - } + if(join.getLeftTable().equals(baseTableName) && join.getRightTable().equals(joinTableName)) + { + matches.add(join); + } - ////////////////////////////// - // look in both directions! // - ////////////////////////////// - if(join.getRightTable().equals(leftTable) && join.getLeftTable().equals(rightTable)) + ////////////////////////////// + // look in both directions! // + ////////////////////////////// + if(join.getRightTable().equals(baseTableName) && join.getLeftTable().equals(joinTableName)) + { + matches.add(join.flip()); + } + } + } + else + { + ///////////////////////////////////////////////////////////////////////////////////// + // if query didn't specify a left-table, then look for any join to the right table // + ///////////////////////////////////////////////////////////////////////////////////// + for(QJoinMetaData join : instance.getJoins().values()) { - matches.add(join.flip()); + if(join.getRightTable().equals(joinTableName) && joinsContext.hasTable(join.getLeftTable())) + { + matches.add(join); + } + + ////////////////////////////// + // look in both directions! // + ////////////////////////////// + if(join.getLeftTable().equals(joinTableName) && joinsContext.hasTable(join.getRightTable())) + { + matches.add(join.flip()); + } } } @@ -253,7 +290,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface } else if(matches.size() > 1) { - throw (new RuntimeException("More than 1 join was found between [" + leftTable + "] and [" + rightTable + "]. Specify which one in your QueryJoin.")); + throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "]. Specify which one in your QueryJoin.")); } return (null); diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index 409b8774..29342ade 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -121,8 +121,8 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf { if(queryJoin.getSelect()) { - QTableMetaData joinTable = queryInput.getInstance().getTable(queryJoin.getRightTable()); - String tableNameOrAlias = queryJoin.getAliasOrRightTable(); + QTableMetaData joinTable = queryInput.getInstance().getTable(queryJoin.getJoinTable()); + String tableNameOrAlias = queryJoin.getJoinTableOrItsAlias(); for(QFieldMetaData joinField : joinTable.getFields().values()) { fieldList.add(joinField.clone().withName(tableNameOrAlias + "." + joinField.getName())); @@ -201,11 +201,11 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf { if(queryJoin.getSelect()) { - QTableMetaData joinTable = instance.getTable(queryJoin.getRightTable()); - String tableNameOrAlias = queryJoin.getAliasOrRightTable(); + QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable()); + String tableNameOrAlias = queryJoin.getJoinTableOrItsAlias(); if(joinTable == null) { - throw new QException("Requested join table [" + queryJoin.getRightTable() + "] is not a defined table."); + throw new QException("Requested join table [" + queryJoin.getJoinTable() + "] is not a defined table."); } List joinFieldList = new ArrayList<>(joinTable.getFields().values()); diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java index 5b6c1727..a5470939 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java @@ -32,6 +32,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest; @@ -90,6 +93,7 @@ public class TestUtils QInstance qInstance = new QInstance(); qInstance.addBackend(defineBackend()); qInstance.addTable(defineTablePerson()); + qInstance.addPossibleValueSource(definePvsPerson()); qInstance.addTable(defineTablePersonalIdCard()); qInstance.addJoin(defineJoinPersonAndPersonalIdCard()); addOmsTablesAndJoins(qInstance); @@ -135,6 +139,8 @@ public class TestUtils return new QTableMetaData() .withName(TABLE_NAME_PERSON) .withLabel("Person") + .withRecordLabelFormat("%s %s") + .withRecordLabelFields("firstName", "lastName") .withBackendName(DEFAULT_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) @@ -153,6 +159,21 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + private static QPossibleValueSource definePvsPerson() + { + return (new QPossibleValueSource() + .withName(TABLE_NAME_PERSON) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(TABLE_NAME_PERSON) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY) + ); + } + + + /******************************************************************************* ** Define a 1:1 table with Person. ** @@ -196,24 +217,26 @@ public class TestUtils private static void addOmsTablesAndJoins(QInstance qInstance) { qInstance.addTable(defineBaseTable(TABLE_NAME_STORE, "store") + .withRecordLabelFormat("%s") + .withRecordLabelFields("name") .withField(new QFieldMetaData("name", QFieldType.STRING)) ); qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER, "order") - .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id")) - .withField(new QFieldMetaData("billToPersonId", QFieldType.INTEGER).withBackendName("bill_to_person_id")) - .withField(new QFieldMetaData("shipToPersonId", QFieldType.INTEGER).withBackendName("ship_to_person_id")) + .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE)) + .withField(new QFieldMetaData("billToPersonId", QFieldType.INTEGER).withBackendName("bill_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON)) + .withField(new QFieldMetaData("shipToPersonId", QFieldType.INTEGER).withBackendName("ship_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON)) ); qInstance.addTable(defineBaseTable(TABLE_NAME_ITEM, "item") .withField(new QFieldMetaData("sku", QFieldType.STRING)) - .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id")) + .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE)) ); qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER_LINE, "order_line") .withField(new QFieldMetaData("orderId", QFieldType.INTEGER).withBackendName("order_id")) .withField(new QFieldMetaData("sku", QFieldType.STRING)) - .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id")) + .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE)) .withField(new QFieldMetaData("quantity", QFieldType.INTEGER)) ); @@ -266,6 +289,12 @@ public class TestUtils .withJoinOn(new JoinOn("storeId", "storeId")) ); + qInstance.addPossibleValueSource(new QPossibleValueSource() + .withName("store") + .withType(QPossibleValueSourceType.TABLE) + .withTableName(TABLE_NAME_STORE) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY) + ); } diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index 8253cd70..4a42ee15 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -611,7 +612,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest void testOneToOneInnerJoinWithoutWhere() throws QException { QueryInput queryInput = initQueryRequest(); - queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true)); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true)); QueryOutput queryOutput = new QueryAction().execute(queryInput); assertEquals(3, queryOutput.getRecords().size(), "Join query should find 3 rows"); assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531")); @@ -628,7 +629,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest void testOneToOneLeftJoinWithoutWhere() throws QException { QueryInput queryInput = initQueryRequest(); - queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.LEFT).withSelect(true)); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.LEFT).withSelect(true)); QueryOutput queryOutput = new QueryAction().execute(queryInput); assertEquals(5, queryOutput.getRecords().size(), "Left Join query should find 5 rows"); assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531")); @@ -647,7 +648,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest void testOneToOneRightJoinWithoutWhere() throws QException { QueryInput queryInput = initQueryRequest(); - queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.RIGHT).withSelect(true)); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.RIGHT).withSelect(true)); QueryOutput queryOutput = new QueryAction().execute(queryInput); assertEquals(6, queryOutput.getRecords().size(), "Right Join query should find 6 rows"); assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531")); @@ -667,7 +668,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest void testOneToOneInnerJoinWithWhere() throws QException { QueryInput queryInput = initQueryRequest(); - queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true)); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true)); queryInput.setFilter(new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber", QCriteriaOperator.STARTS_WITH, "1980"))); QueryOutput queryOutput = new QueryAction().execute(queryInput); assertEquals(2, queryOutput.getRecords().size(), "Join query should find 2 rows"); @@ -685,7 +686,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest { QInstance qInstance = TestUtils.defineInstance(); QueryInput queryInput = initQueryRequest(); - queryInput.withQueryJoin(new QueryJoin(qInstance.getJoin(TestUtils.TABLE_NAME_PERSON + "Join" + TestUtils.TABLE_NAME_PERSONAL_ID_CARD)).withSelect(true)); + queryInput.withQueryJoin(new QueryJoin(qInstance.getJoin(TestUtils.TABLE_NAME_PERSON + "Join" + StringUtils.ucFirst(TestUtils.TABLE_NAME_PERSONAL_ID_CARD))).withSelect(true)); queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber"))); QueryOutput queryOutput = new QueryAction().execute(queryInput); assertEquals(3, queryOutput.getRecords().size(), "Join query should find 3 rows"); @@ -755,7 +756,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest QueryInput queryInput = new QueryInput(TestUtils.defineInstance(), new QSession()); queryInput.setTableName(TestUtils.TABLE_NAME_ORDER_LINE); - queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_LINE, TestUtils.TABLE_NAME_ORDER).withSelect(true)); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER).withSelect(true)); QueryOutput queryOutput = new QueryAction().execute(queryInput); assertEquals(orderLineCount.get(), queryOutput.getRecords().size(), "# of rows found by query"); @@ -780,7 +781,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ///////////////////////////////////////////////////// // inner join on bill-to person should find 6 rows // ///////////////////////////////////////////////////// - queryInput.withQueryJoins(List.of(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_PERSON).withJoinMetaData(instance.getJoin("orderJoinBillToPerson")).withSelect(true))); + queryInput.withQueryJoins(List.of(new QueryJoin(TestUtils.TABLE_NAME_PERSON).withJoinMetaData(instance.getJoin("orderJoinBillToPerson")).withSelect(true))); QueryOutput queryOutput = new QueryAction().execute(queryInput); assertEquals(6, queryOutput.getRecords().size(), "# of rows found by query"); @@ -829,6 +830,42 @@ public class RDBMSQueryActionTest extends RDBMSActionTest queryOutput = new QueryAction().execute(queryInput); assertEquals(3, queryOutput.getRecords().size(), "# of rows found by query"); assertThat(queryOutput.getRecords().stream().map(r -> r.getValueString("billToPerson.firstName")).toList()).allMatch(p -> p.equals("Darin") || p.equals("James")); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ensure we throw if either of the ambiguous joins from person to id-card doesn't specify its left-table // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + queryInput.withQueryJoins(List.of( + new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true), + new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true), + new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true), + new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true) + )); + assertThatThrownBy(() -> new QueryAction().execute(queryInput)) + .hasRootCauseMessage("Could not find a table or alias [personTable] in query. May need to be more specific setting up QueryJoins."); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ensure we throw if either of the ambiguous joins from person to id-card doesn't specify its left-table // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + queryInput.withQueryJoins(List.of( + new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true), + new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true), + new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true), + new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true) + )); + assertThatThrownBy(() -> new QueryAction().execute(queryInput)) + .hasRootCauseMessage("Could not find a table or alias [personTable] in query. May need to be more specific setting up QueryJoins."); + + //////////////////////////////////////////////////////////////////////// + // ensure we throw if we have a bogus alias name given as a left-side // + //////////////////////////////////////////////////////////////////////// + queryInput.withQueryJoins(List.of( + new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true), + new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true), + new QueryJoin("notATable", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true), + new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true) + )); + assertThatThrownBy(() -> new QueryAction().execute(queryInput)) + .hasRootCauseMessage("Could not find a join between tables [notATable][personalIdCard]"); } diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/reporting/GenerateReportActionRDBMSTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/reporting/GenerateReportActionRDBMSTest.java new file mode 100644 index 00000000..3386d340 --- /dev/null +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/reporting/GenerateReportActionRDBMSTest.java @@ -0,0 +1,208 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.rdbms.reporting; + + +import java.io.ByteArrayOutputStream; +import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; +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.reporting.ReportType; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.module.rdbms.TestUtils; +import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Do some tests on the qqq-backend-core GenerateReportAction, that are kinda + ** hard to do in a backend that doesn't support joins, but that we can do in + ** RDBMS. + *******************************************************************************/ +public class GenerateReportActionRDBMSTest extends RDBMSActionTest +{ + private static final String TEST_REPORT = "testReport"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + public void beforeEach() throws Exception + { + super.primeTestDatabase(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTwoJoinsToSameTable() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QReportMetaData report = new QReportMetaData() + .withName(TEST_REPORT) + .withDataSource(new QReportDataSource() + .withSourceTable(TestUtils.TABLE_NAME_ORDER) + .withQueryJoin(new QueryJoin(qInstance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withType(QueryJoin.Type.LEFT).withSelect(true)) + .withQueryJoin(new QueryJoin(qInstance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withType(QueryJoin.Type.LEFT).withSelect(true)) + ) + .withView(new QReportView() + .withType(ReportType.TABLE) + .withColumn(new QReportField("id")) + .withColumn(new QReportField("storeId").withLabel("Store Id")) + .withColumn(new QReportField("storeName").withShowPossibleValueLabel(true).withSourceFieldName("storeId").withLabel("Store Name")) + .withColumn(new QReportField("billToPerson.id")) + .withColumn(new QReportField("billToPerson.firstName").withLabel("Bill To First Name")) + .withColumn(new QReportField("billToPersonName").withShowPossibleValueLabel(true).withSourceFieldName("billToPersonId")) + .withColumn(new QReportField("shipToPerson.id")) + .withColumn(new QReportField("shipToPerson.firstName").withLabel("Ship To First Name")) + .withColumn(new QReportField("shipToPersonName").withShowPossibleValueLabel(true).withSourceFieldName("billToPersonId")) + ); + qInstance.addReport(report); + + String csv = runReport(qInstance); + // System.out.println(csv); + + assertEquals(""" + "Id","Store Id","Store Name","Id","Bill To First Name","Bill To Person","Id","Ship To First Name","Bill To Person" + "1","1","Q-Mart","1","Darin","Darin Kelkhoff","1","Darin","Darin Kelkhoff" + "2","1","Q-Mart","1","Darin","Darin Kelkhoff","2","James","Darin Kelkhoff" + "3","1","Q-Mart","2","James","James Maes","3","Tim","James Maes" + "4","2","QQQ 'R' Us","4","Tyler","Tyler Samples","5","Garret","Tyler Samples" + "5","2","QQQ 'R' Us","5","Garret","Garret Richardson","4","Tyler","Garret Richardson" + "6","3","QDepot","5","Garret","Garret Richardson","","","Garret Richardson" + "7","3","QDepot","","","","5","Garret","" + "8","3","QDepot","","","","5","Garret","" + """, csv); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTwoTablesWithSamePossibleValue() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QReportMetaData report = new QReportMetaData() + .withName(TEST_REPORT) + .withDataSource(new QReportDataSource() + .withSourceTable(TestUtils.TABLE_NAME_ORDER_LINE) + .withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER).withSelect(true)) + .withQueryFilter(new QQueryFilter(new QFilterCriteria("storeId", QCriteriaOperator.NOT_EQUALS).withOtherFieldName("order.storeId"))) + ) + .withView(new QReportView() + .withType(ReportType.TABLE) + .withColumn(new QReportField("storeId").withLabel("Line Item Store Id")) + .withColumn(new QReportField("storeName").withShowPossibleValueLabel(true).withSourceFieldName("storeId").withLabel("Line Item Store Name")) + .withColumn(new QReportField("order.storeId").withLabel("Order Store Id")) + .withColumn(new QReportField("order.storeName").withShowPossibleValueLabel(true).withSourceFieldName("order.storeId").withLabel("Order Store Name")) + ); + qInstance.addReport(report); + + String csv = runReport(qInstance); + // System.out.println(csv); + + assertEquals(""" + "Line Item Store Id","Line Item Store Name","Order Store Id","Order Store Name" + "2","QQQ 'R' Us","1","Q-Mart" + """, csv); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPossibleValueThroughAlias() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QReportMetaData report = new QReportMetaData() + .withName(TEST_REPORT) + .withDataSource(new QReportDataSource() + .withSourceTable(TestUtils.TABLE_NAME_ORDER_LINE) + .withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ITEM).withAlias("i").withSelect(true)) + ) + .withView(new QReportView() + .withType(ReportType.TABLE) + .withColumn(new QReportField("id").withLabel("Line Item Id")) + .withColumn(new QReportField("sku").withLabel("Item SKU")) + .withColumn(new QReportField("i.storeId").withLabel("Item Store Id")) + .withColumn(new QReportField("i.storeName").withShowPossibleValueLabel(true).withSourceFieldName("i.storeId").withLabel("Item Store Name")) + ); + qInstance.addReport(report); + + String csv = runReport(qInstance); + System.out.println(csv); + + assertEquals(""" + "Line Item Id","Item SKU","Item Store Id","Item Store Name" + "1","QM-1","1","Q-Mart" + "5","QM-1","1","Q-Mart" + "2","QM-2","1","Q-Mart" + "3","QM-3","1","Q-Mart" + "4","QRU-1","2","QQQ 'R' Us" + "6","QRU-1","2","QQQ 'R' Us" + "8","QRU-1","2","QQQ 'R' Us" + "7","QRU-2","2","QQQ 'R' Us" + "9","QD-1","3","QDepot" + "10","QD-1","3","QDepot" + "11","QD-1","3","QDepot" + """, csv); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String runReport(QInstance qInstance) throws QException + { + ReportInput reportInput = new ReportInput(qInstance); + reportInput.setSession(new QSession()); + reportInput.setReportName(TEST_REPORT); + reportInput.setReportFormat(ReportFormat.CSV); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + reportInput.setReportOutputStream(outputStream); + new GenerateReportAction().execute(reportInput); + return (outputStream.toString()); + } + +}