Merge pull request #155 from Kingsrook/feature/join-record-enhancements

Feature/join record enhancements
This commit is contained in:
2025-01-31 10:54:38 -06:00
committed by GitHub
15 changed files with 708 additions and 62 deletions

View File

@ -71,6 +71,7 @@ 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.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAndJoinTable;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
@ -567,7 +568,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
// all pivotFields that are possible value sources are implicitly translated // // all pivotFields that are possible value sources are implicitly translated //
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
QTableMetaData mainTable = QContext.getQInstance().getTable(dataSource.getSourceTable()); QTableMetaData mainTable = QContext.getQInstance().getTable(dataSource.getSourceTable());
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(mainTable, summaryFieldName); FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(mainTable, summaryFieldName);
if(fieldAndJoinTable.field().getPossibleValueSourceName() != null) if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
{ {
fieldsToTranslatePossibleValues.add(summaryFieldName); fieldsToTranslatePossibleValues.add(summaryFieldName);
@ -580,32 +581,6 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
public static FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException
{
if(fieldName.indexOf('.') > -1)
{
String joinTableName = fieldName.replaceAll("\\..*", "");
String joinFieldName = fieldName.replaceAll(".*\\.", "");
QTableMetaData joinTable = QContext.getQInstance().getTable(joinTableName);
if(joinTable == null)
{
throw (new QException("Unrecognized join table name: " + joinTableName));
}
return new FieldAndJoinTable(joinTable.getField(joinFieldName), joinTable);
}
else
{
return new FieldAndJoinTable(mainTable.getField(fieldName), mainTable);
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -756,7 +731,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
SummaryKey key = new SummaryKey(); SummaryKey key = new SummaryKey();
for(String summaryFieldName : view.getSummaryFields()) for(String summaryFieldName : view.getSummaryFields())
{ {
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName); FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, summaryFieldName);
Serializable summaryValue = record.getValue(summaryFieldName); Serializable summaryValue = record.getValue(summaryFieldName);
if(fieldAndJoinTable.field().getPossibleValueSourceName() != null) if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
{ {
@ -811,7 +786,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
// todo - memoize this, if we ever need to optimize // // todo - memoize this, if we ever need to optimize //
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, fieldName); FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, fieldName);
field = fieldAndJoinTable.field(); field = fieldAndJoinTable.field();
} }
catch(Exception e) catch(Exception e)
@ -956,7 +931,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
List<QFieldMetaData> fields = new ArrayList<>(); List<QFieldMetaData> fields = new ArrayList<>();
for(String summaryFieldName : view.getSummaryFields()) for(String summaryFieldName : view.getSummaryFields())
{ {
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName); FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, summaryFieldName);
fields.add(new QFieldMetaData(summaryFieldName, fieldAndJoinTable.field().getType()).withLabel(fieldAndJoinTable.field().getLabel())); // todo do we need the type? if so need table as input here fields.add(new QFieldMetaData(summaryFieldName, fieldAndJoinTable.field().getType()).withLabel(fieldAndJoinTable.field().getLabel())); // todo do we need the type? if so need table as input here
} }
for(QReportField column : view.getColumns()) for(QReportField column : view.getColumns())
@ -1208,27 +1183,4 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
{ {
} }
/*******************************************************************************
**
*******************************************************************************/
public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable)
{
/*******************************************************************************
**
*******************************************************************************/
public String getLabel(QTableMetaData mainTable)
{
if(mainTable.getName().equals(joinTable.getName()))
{
return (field.getLabel());
}
else
{
return (joinTable.getLabel() + ": " + field.getLabel());
}
}
}
} }

View File

@ -533,10 +533,16 @@ public class QueryStatManager
//////////////////////// ////////////////////////
if(getOutput.getRecord() == null) if(getOutput.getRecord() == null)
{ {
QTableMetaData tableMetaData = getInstance().qInstance.getTable(tableName);
if(tableMetaData == null)
{
LOG.info("No such table", logPair("tableName", tableName));
return (null);
}
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
// insert the record (into the table, not the cache) // // insert the record (into the table, not the cache) //
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
QTableMetaData tableMetaData = getInstance().qInstance.getTable(tableName);
InsertInput insertInput = new InsertInput(); InsertInput insertInput = new InsertInput();
insertInput.setTableName(QQQTable.TABLE_NAME); insertInput.setTableName(QQQTable.TABLE_NAME);
insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel()))); insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel())));

View File

@ -0,0 +1,31 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
/*******************************************************************************
**
*******************************************************************************/
public enum CriteriaOption implements CriteriaOptionInterface
{
CASE_INSENSITIVE;
}

View File

@ -0,0 +1,30 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
/*******************************************************************************
**
*******************************************************************************/
public interface CriteriaOptionInterface
{
}

View File

@ -26,8 +26,10 @@ import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer; import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer;
@ -53,6 +55,8 @@ public class QFilterCriteria implements Serializable, Cloneable
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private String otherFieldName; private String otherFieldName;
private Set<CriteriaOptionInterface> options = null;
/******************************************************************************* /*******************************************************************************
@ -69,6 +73,13 @@ public class QFilterCriteria implements Serializable, Cloneable
clone.values = new ArrayList<>(); clone.values = new ArrayList<>();
clone.values.addAll(values); clone.values.addAll(values);
} }
if(options != null)
{
clone.options = new HashSet<>();
clone.options.addAll(options);
}
return clone; return clone;
} }
catch(CloneNotSupportedException e) catch(CloneNotSupportedException e)
@ -385,4 +396,78 @@ public class QFilterCriteria implements Serializable, Cloneable
return Objects.hash(fieldName, operator, values, otherFieldName); return Objects.hash(fieldName, operator, values, otherFieldName);
} }
/*******************************************************************************
** Getter for options
*******************************************************************************/
public Set<CriteriaOptionInterface> getOptions()
{
return (this.options);
}
/*******************************************************************************
** Setter for options
*******************************************************************************/
public void setOptions(Set<CriteriaOptionInterface> options)
{
this.options = options;
}
/*******************************************************************************
** Fluent setter for options
*******************************************************************************/
public QFilterCriteria withOptions(Set<CriteriaOptionInterface> options)
{
this.options = options;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public QFilterCriteria withOption(CriteriaOptionInterface option)
{
if(options == null)
{
options = new HashSet<>();
}
options.add(option);
return (this);
}
/***************************************************************************
**
***************************************************************************/
public QFilterCriteria withoutOption(CriteriaOptionInterface option)
{
if(options != null)
{
options.remove(option);
}
return (this);
}
/***************************************************************************
**
***************************************************************************/
public boolean hasOption(CriteriaOptionInterface option)
{
if(options == null)
{
return (false);
}
return (options.contains(option));
}
} }

View File

@ -853,4 +853,20 @@ public class QQueryFilter implements Serializable, Cloneable
} }
/***************************************************************************
**
***************************************************************************/
public void applyCriteriaOptionToAllCriteria(CriteriaOptionInterface criteriaOption)
{
for(QFilterCriteria criteria : CollectionUtils.nonNullList(this.criteria))
{
criteria.withOption(criteriaOption);
}
for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
{
subFilter.applyCriteriaOptionToAllCriteria(criteriaOption);
}
}
} }

View File

@ -154,7 +154,7 @@ public class QRecord implements Serializable
return (null); return (null);
} }
Map<String, V> clone = new LinkedHashMap<>(); Map<String, V> clone = new LinkedHashMap<>(map.size());
for(Map.Entry<String, V> entry : map.entrySet()) for(Map.Entry<String, V> entry : map.entrySet())
{ {
Serializable value = entry.getValue(); Serializable value = entry.getValue();
@ -246,6 +246,24 @@ public class QRecord implements Serializable
} }
/***************************************************************************
** copy all values from 'joinedRecord' into this record's values map,
** prefixing field names with joinTableNam + "."
***************************************************************************/
public void addJoinedRecordValues(String joinTableName, QRecord joinedRecord)
{
if(joinedRecord == null)
{
return;
}
for(Map.Entry<String, Serializable> entry : joinedRecord.getValues().entrySet())
{
setValue(joinTableName + "." + entry.getKey(), entry.getValue());
}
}
/******************************************************************************* /*******************************************************************************
** **

View File

@ -0,0 +1,201 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiFunction;
/*******************************************************************************
** Extension on QRecord, intended to be used where you've got records from
** multiple tables, and you want to combine them into a single "wide" joined
** record - but to do so without copying or modifying any of the individual
** records.
**
** e.g., given:
** - Order (id, orderNo, orderDate) (main table)
** - LineItem (id, sku, quantity)
** - Extrinsic (id, key, value)
**
** If set up in here as:
** - new QRecordWithJoinedRecords(order)
** .withJoinedRecordValues(lineItem)
** .withJoinedRecordValues(extrinsic)
**
** Then we'd have the appearance of values in the object like:
** - id, orderNo, orderDate, lineItem.id, lineItem.sku, lineItem.quantity, extrinsic.id, extrinsic.key, extrinsic.value
**
** Which, by the by, is how a query that returns joined records looks, and, is
** what BackendQueryFilterUtils can use to do filter.
**
** This is done without copying or mutating any of the records (which, if you just use
** QRecord.withJoinedRecordValues, then those values are copied into the main record)
** - because this object is just storing references to the input records.
**
** Note that this implies that, values changed in this record (e.g, calls to setValue)
** WILL impact the underlying records!
*******************************************************************************/
public class QRecordWithJoinedRecords extends QRecord
{
private QRecord mainRecord;
private Map<String, QRecord> components = new LinkedHashMap<>();
/***************************************************************************
**
***************************************************************************/
public QRecordWithJoinedRecords(QRecord mainRecord)
{
this.mainRecord = mainRecord;
}
/*************************************************************************
**
***************************************************************************/
@Override
public void addJoinedRecordValues(String joinTableName, QRecord joinedRecord)
{
components.put(joinTableName, joinedRecord);
}
/*************************************************************************
**
***************************************************************************/
public QRecordWithJoinedRecords withJoinedRecordValues(QRecord record, String joinTableName)
{
addJoinedRecordValues(joinTableName, record);
return (this);
}
/***************************************************************************
**
***************************************************************************/
@Override
public Serializable getValue(String fieldName)
{
return performFunctionOnRecordBasedOnFieldName(fieldName, ((record, f) -> record.getValue(f)));
}
/***************************************************************************
*
***************************************************************************/
@Override
public void setValue(String fieldName, Object value)
{
performFunctionOnRecordBasedOnFieldName(fieldName, ((record, f) ->
{
record.setValue(f, value);
return (null);
}));
}
/***************************************************************************
*
***************************************************************************/
@Override
public void setValue(String fieldName, Serializable value)
{
performFunctionOnRecordBasedOnFieldName(fieldName, ((record, f) ->
{
record.setValue(f, value);
return (null);
}));
}
/***************************************************************************
**
***************************************************************************/
@Override
public void removeValue(String fieldName)
{
performFunctionOnRecordBasedOnFieldName(fieldName, ((record, f) ->
{
record.removeValue(f);
return (null);
}));
}
/***************************************************************************
** avoid having this same block in all the functions that call it...
** given a fieldName, which may be a joinTable.fieldName, apply the function
** to the right entity.
***************************************************************************/
private Serializable performFunctionOnRecordBasedOnFieldName(String fieldName, BiFunction<QRecord, String, Serializable> functionToPerform)
{
if(fieldName.contains("."))
{
String[] parts = fieldName.split("\\.");
QRecord component = components.get(parts[0]);
if(component != null)
{
return functionToPerform.apply(component, parts[1]);
}
else
{
return null;
}
}
else
{
return functionToPerform.apply(mainRecord, fieldName);
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public Map<String, Serializable> getValues()
{
Map<String, Serializable> rs = new LinkedHashMap<>(mainRecord.getValues());
for(Map.Entry<String, QRecord> componentEntry : components.entrySet())
{
String joinTableName = componentEntry.getKey();
QRecord componentRecord = componentEntry.getValue();
for(Map.Entry<String, Serializable> entry : componentRecord.getValues().entrySet())
{
rs.put(joinTableName + "." + entry.getKey(), entry.getValue());
}
}
return (rs);
}
}

View File

@ -0,0 +1,86 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.fields;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Wrapper (record) that holds a QFieldMetaData and a QTableMetaData -
**
** With a factory method (`get()`) to go from the use-case of, a String that's
** "joinTable.fieldName" or "fieldName" to the pair.
**
** Note that the "joinTable" member here - could be the "mainTable" passed in
** to that `get()` method.
**
*******************************************************************************/
public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable)
{
/***************************************************************************
** given a table, and a field-name string (which should either be the name
** of a field on that table, or another tableName + "." + fieldName (from
** that table) - get back the pair of table & field metaData that the
** input string is talking about.
***************************************************************************/
public static FieldAndJoinTable get(QTableMetaData mainTable, String fieldName) throws QException
{
if(fieldName.indexOf('.') > -1)
{
String joinTableName = fieldName.replaceAll("\\..*", "");
String joinFieldName = fieldName.replaceAll(".*\\.", "");
QTableMetaData joinTable = QContext.getQInstance().getTable(joinTableName);
if(joinTable == null)
{
throw (new QException("Unrecognized join table name: " + joinTableName));
}
return new FieldAndJoinTable(joinTable.getField(joinFieldName), joinTable);
}
else
{
return new FieldAndJoinTable(mainTable.getField(fieldName), mainTable);
}
}
/*******************************************************************************
**
*******************************************************************************/
public String getLabel(QTableMetaData mainTable)
{
if(mainTable.getName().equals(joinTable.getName()))
{
return (field.getLabel());
}
else
{
return (joinTable.getLabel() + ": " + field.getLabel());
}
}
}

View File

@ -28,7 +28,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
@ -43,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput; import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.DynamicFormWidgetData; import com.kingsrook.qqq.backend.core.model.dashboard.widgets.DynamicFormWidgetData;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAndJoinTable;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter; import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter;
@ -127,7 +127,7 @@ public class ReportValuesDynamicFormWidgetRenderer extends AbstractWidgetRendere
{ {
if(criteriaValue instanceof FilterVariableExpression filterVariableExpression) if(criteriaValue instanceof FilterVariableExpression filterVariableExpression)
{ {
GenerateReportAction.FieldAndJoinTable fieldAndJoinTable = GenerateReportAction.getFieldAndJoinTable(table, criteria.getFieldName()); FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, criteria.getFieldName());
QFieldMetaData fieldMetaData = fieldAndJoinTable.field().clone(); QFieldMetaData fieldMetaData = fieldAndJoinTable.field().clone();
///////////////////////////////// /////////////////////////////////

View File

@ -28,7 +28,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition; import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
@ -39,6 +38,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAndJoinTable;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
@ -311,7 +311,7 @@ public class SavedReportTableCustomizer implements TableCustomizerInterface
{ {
try try
{ {
GenerateReportAction.FieldAndJoinTable fieldAndJoinTable = GenerateReportAction.getFieldAndJoinTable(table, fieldName); FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, fieldName);
return (fieldAndJoinTable.getLabel(table)); return (fieldAndJoinTable.getLabel(table));
} }
catch(Exception e) catch(Exception e)

View File

@ -34,6 +34,7 @@ import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.CriteriaOption;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; 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.QFilterOrderBy;
@ -268,6 +269,11 @@ public class BackendQueryFilterUtils
String regex = sqlLikeToRegex(criterionValue); String regex = sqlLikeToRegex(criterionValue);
if(criterion.hasOption(CriteriaOption.CASE_INSENSITIVE))
{
return (stringValue.toLowerCase().matches(regex.toLowerCase()));
}
return (stringValue.matches(regex)); return (stringValue.matches(regex));
} }
@ -427,6 +433,23 @@ public class BackendQueryFilterUtils
} }
} }
if(criterion.hasOption(CriteriaOption.CASE_INSENSITIVE))
{
if(CollectionUtils.nullSafeHasContents(criterion.getValues()))
{
if(criterion.getValues().get(0) instanceof String)
{
for(Serializable criterionValue : criterion.getValues())
{
if(criterionValue instanceof String criterionValueString && value instanceof String valueString && criterionValueString.equalsIgnoreCase(valueString))
{
return (true);
}
}
}
}
}
if(value == null || !criterion.getValues().contains(value)) if(value == null || !criterion.getValues().contains(value))
{ {
return (false); return (false);
@ -456,6 +479,14 @@ public class BackendQueryFilterUtils
value = String.valueOf(value); value = String.valueOf(value);
} }
if(criterion.hasOption(CriteriaOption.CASE_INSENSITIVE))
{
if(value instanceof String valueString && criteriaValue instanceof String criteriaValueString && valueString.equalsIgnoreCase(criteriaValueString))
{
return (true);
}
}
if(!value.equals(criteriaValue)) if(!value.equals(criteriaValue))
{ {
return (false); return (false);
@ -473,6 +504,14 @@ public class BackendQueryFilterUtils
String stringValue = getStringFieldValue(value, fieldName, criterion); String stringValue = getStringFieldValue(value, fieldName, criterion);
String criterionValue = getFirstStringCriterionValue(criterion); String criterionValue = getFirstStringCriterionValue(criterion);
if(criterion.hasOption(CriteriaOption.CASE_INSENSITIVE))
{
if(stringValue.toLowerCase().contains(criterionValue.toLowerCase()))
{
return (true);
}
}
if(!stringValue.contains(criterionValue)) if(!stringValue.contains(criterionValue))
{ {
return (false); return (false);
@ -491,6 +530,14 @@ public class BackendQueryFilterUtils
String stringValue = getStringFieldValue(value, fieldName, criterion); String stringValue = getStringFieldValue(value, fieldName, criterion);
String criterionValue = getFirstStringCriterionValue(criterion); String criterionValue = getFirstStringCriterionValue(criterion);
if(criterion.hasOption(CriteriaOption.CASE_INSENSITIVE))
{
if(stringValue.toLowerCase().startsWith(criterionValue.toLowerCase()))
{
return (true);
}
}
if(!stringValue.startsWith(criterionValue)) if(!stringValue.startsWith(criterionValue))
{ {
return (false); return (false);
@ -509,6 +556,14 @@ public class BackendQueryFilterUtils
String stringValue = getStringFieldValue(value, fieldName, criterion); String stringValue = getStringFieldValue(value, fieldName, criterion);
String criterionValue = getFirstStringCriterionValue(criterion); String criterionValue = getFirstStringCriterionValue(criterion);
if(criterion.hasOption(CriteriaOption.CASE_INSENSITIVE))
{
if(stringValue.toLowerCase().endsWith(criterionValue.toLowerCase()))
{
return (true);
}
}
if(!stringValue.endsWith(criterionValue)) if(!stringValue.endsWith(criterionValue))
{ {
return (false); return (false);
@ -665,4 +720,5 @@ public class BackendQueryFilterUtils
regex.append("$"); regex.append("$");
return regex.toString(); return regex.toString();
} }
} }

View File

@ -291,4 +291,24 @@ class QRecordTest extends BaseTest
assertFalse(jsonObject.has("errorsAsString")); assertFalse(jsonObject.has("errorsAsString"));
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAddJoinedRecordValues()
{
QRecord order = new QRecord().withValue("id", 1).withValue("shipTo", "St. Louis");
order.addJoinedRecordValues("orderInstructions", null);
assertEquals(2, order.getValues().size());
QRecord orderInstructions = new QRecord().withValue("id", 100).withValue("instructions", "Be Careful");
order.addJoinedRecordValues("orderInstructions", orderInstructions);
assertEquals(4, order.getValues().size());
assertEquals(100, order.getValue("orderInstructions.id"));
assertEquals("Be Careful", order.getValue("orderInstructions.instructions"));
}
} }

View File

@ -0,0 +1,76 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data;
import java.time.LocalDate;
import java.time.Month;
import com.kingsrook.qqq.backend.core.BaseTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for QRecordWithJoinedRecords
*******************************************************************************/
class QRecordWithJoinedRecordsTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
QRecord order = new QRecord().withValue("id", 1).withValue("orderNo", "101").withValue("orderDate", LocalDate.of(2025, Month.JANUARY, 1));
QRecord lineItem = new QRecord().withValue("id", 2).withValue("sku", "ABC").withValue("quantity", 47);
QRecord extrinsic = new QRecord().withValue("id", 3).withValue("key", "MyKey").withValue("value", "MyValue");
QRecordWithJoinedRecords joinedRecords = new QRecordWithJoinedRecords(order);
joinedRecords.addJoinedRecordValues("lineItem", lineItem);
joinedRecords.addJoinedRecordValues("extrinsic", extrinsic);
assertEquals(1, joinedRecords.getValue("id"));
assertEquals("101", joinedRecords.getValue("orderNo"));
assertEquals(LocalDate.of(2025, Month.JANUARY, 1), joinedRecords.getValue("orderDate"));
assertEquals(2, joinedRecords.getValue("lineItem.id"));
assertEquals("ABC", joinedRecords.getValue("lineItem.sku"));
assertEquals(47, joinedRecords.getValue("lineItem.quantity"));
assertEquals(3, joinedRecords.getValue("extrinsic.id"));
assertEquals("MyKey", joinedRecords.getValue("extrinsic.key"));
assertEquals("MyValue", joinedRecords.getValue("extrinsic.value"));
assertEquals(9, joinedRecords.getValues().size());
assertEquals(1, joinedRecords.getValues().get("id"));
assertEquals(2, joinedRecords.getValues().get("lineItem.id"));
assertEquals(3, joinedRecords.getValues().get("extrinsic.id"));
joinedRecords.setValue("lineItem.color", "RED");
assertEquals("RED", joinedRecords.getValue("lineItem.color"));
assertEquals("RED", lineItem.getValue("color"));
joinedRecords.setValue("shipToCity", "St. Louis");
assertEquals("St. Louis", joinedRecords.getValue("shipToCity"));
assertEquals("St. Louis", order.getValue("shipToCity"));
}
}

View File

@ -22,8 +22,10 @@
package com.kingsrook.qqq.backend.core.modules.backend.implementations.utils; package com.kingsrook.qqq.backend.core.modules.backend.implementations.utils;
import java.io.Serializable;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.CriteriaOption;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; 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.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -305,6 +307,73 @@ class BackendQueryFilterUtilsTest
/***************************************************************************
**
***************************************************************************/
private QFilterCriteria newCaseInsensitiveCriteria(String fieldName, QCriteriaOperator operator, Serializable... values)
{
return new QFilterCriteria(fieldName, operator, values).withOption(CriteriaOption.CASE_INSENSITIVE);
}
/***************************************************************************
**
***************************************************************************/
private QFilterCriteria newCaseInsensitiveCriteria(String fieldName, QCriteriaOperator operator, List<Serializable> values)
{
return new QFilterCriteria(fieldName, operator, values).withOption(CriteriaOption.CASE_INSENSITIVE);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDoesCriterionMatchCaseInsensitive()
{
////////////////
// like & not //
////////////////
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.LIKE, "Test"), "f", "test"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.LIKE, "test"), "f", "Test"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.LIKE, "T%"), "f", "test"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.LIKE, "t%"), "f", "Test"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.LIKE, "T_st"), "f", "test"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.LIKE, "t_st"), "f", "Test"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_LIKE, "Test"), "f", "Tst"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_LIKE, "Test"), "f", "tst"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_LIKE, "T%"), "f", "Rest"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_LIKE, "T_st"), "f", "Toast"));
//////////////
// IN & NOT //
//////////////
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.IN, "A"), "f", "a"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.IN, "a"), "f", "A"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.IN, "A", "B"), "f", "a"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.IN, "A", "b"), "f", "B"));
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.IN, List.of()), "f", "A"));
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.IN, ListBuilder.of(null)), "f", "A"));
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_IN, "A"), "f", "A"));
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_IN, "A", "B"), "f", "a"));
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_IN, "A", "b"), "f", "B"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_IN, List.of()), "f", "A"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_IN, ListBuilder.of(null)), "f", "A"));
///////////////////////////
// NOT_EQUALS_OR_IS_NULL //
///////////////////////////
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, "A"), "f", "A"));
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, "A"), "f", "a"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, "A"), "f", "B"));
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(newCaseInsensitiveCriteria("f", QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, "A"), "f", null));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/