diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/AggregateInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/AggregateInterface.java
new file mode 100644
index 00000000..2ce856b7
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/AggregateInterface.java
@@ -0,0 +1,40 @@
+/*
+ * 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.core.actions.interfaces;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
+
+
+/*******************************************************************************
+ ** Interface for the Aggregate action.
+ **
+ *******************************************************************************/
+public interface AggregateInterface
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ AggregateOutput execute(AggregateInput aggregateInput) throws QException;
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateAction.java
new file mode 100644
index 00000000..de4bdb93
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateAction.java
@@ -0,0 +1,53 @@
+/*
+ * 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.core.actions.tables;
+
+
+import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
+
+
+/*******************************************************************************
+ ** Action to run an aggregate against a table.
+ **
+ *******************************************************************************/
+public class AggregateAction
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public AggregateOutput execute(AggregateInput aggregateInput) throws QException
+ {
+ ActionHelper.validateSession(aggregateInput);
+
+ QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
+ QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(aggregateInput.getBackend());
+ // todo pre-customization - just get to modify the request?
+ AggregateOutput aggregateOutput = qModule.getAggregateInterface().execute(aggregateInput);
+ // todo post-customization - can do whatever w/ the result if you want
+ return aggregateOutput;
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/RenderTemplateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/RenderTemplateAction.java
new file mode 100644
index 00000000..423fe26a
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/templates/RenderTemplateAction.java
@@ -0,0 +1,94 @@
+/*
+ * 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.core.actions.templates;
+
+
+import java.io.StringWriter;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
+import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateInput;
+import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateOutput;
+import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.Velocity;
+import org.apache.velocity.context.Context;
+
+
+/*******************************************************************************
+ ** Basic action to render a template!
+ *******************************************************************************/
+public class RenderTemplateAction extends AbstractQActionFunction
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public RenderTemplateOutput execute(RenderTemplateInput input) throws QException
+ {
+ RenderTemplateOutput output = new RenderTemplateOutput();
+
+ if(TemplateType.VELOCITY.equals(input.getTemplateType()))
+ {
+ Velocity.init();
+ Context context = new VelocityContext(input.getContext());
+ StringWriter stringWriter = new StringWriter();
+ Velocity.evaluate(context, stringWriter, "logTag", input.getCode());
+ output.setResult(stringWriter.getBuffer().toString());
+ }
+ else
+ {
+ throw (new QException("Unsupported Template Type: " + input.getTemplateType()));
+ }
+
+ return (output);
+ }
+
+
+
+ /*******************************************************************************
+ ** Most convenient static wrapper to render a Velocity template.
+ *******************************************************************************/
+ public static String renderVelocity(AbstractActionInput parentActionInput, Map context, String code) throws QException
+ {
+ return (render(parentActionInput, TemplateType.VELOCITY, context, code));
+ }
+
+
+
+ /*******************************************************************************
+ ** Convenient static wrapper to render a template of an arbitrary type (language).
+ *******************************************************************************/
+ public static String render(AbstractActionInput parentActionInput, TemplateType templateType, Map context, String code) throws QException
+ {
+ RenderTemplateInput renderTemplateInput = new RenderTemplateInput(parentActionInput.getInstance());
+ renderTemplateInput.setSession(parentActionInput.getSession());
+ renderTemplateInput.setCode(code);
+ renderTemplateInput.setContext(context);
+ renderTemplateInput.setTemplateType(templateType);
+ RenderTemplateOutput output = new RenderTemplateAction().execute(renderTemplateInput);
+ return (output.getResult());
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
index 98631e8b..ec28ecae 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
@@ -462,9 +462,25 @@ public class QInstanceEnricher
.withTableName(table.getName())
.withIsHidden(true);
- List editableFields = table.getFields().values().stream()
- .filter(QFieldMetaData::getIsEditable)
- .toList();
+ List editableFields = new ArrayList<>();
+ for(QFieldSection section : CollectionUtils.nonNullList(table.getSections()))
+ {
+ for(String fieldName : CollectionUtils.nonNullList(section.getFieldNames()))
+ {
+ try
+ {
+ QFieldMetaData field = table.getField(fieldName);
+ if(field.getIsEditable())
+ {
+ editableFields.add(field);
+ }
+ }
+ catch(Exception e)
+ {
+ // shrug?
+ }
+ }
+ }
String fieldsForHelpText = editableFields.stream()
.map(QFieldMetaData::getLabel)
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 f6bab631..ee58535c 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
@@ -114,6 +114,7 @@ public class QInstanceValidator
}
catch(Exception e)
{
+ LOG.error("Error enriching instance prior to validation", e);
throw (new QInstanceValidationException("Error enriching qInstance prior to validation.", e));
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/Aggregate.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/Aggregate.java
new file mode 100644
index 00000000..5598f154
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/Aggregate.java
@@ -0,0 +1,156 @@
+/*
+ * 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.core.model.actions.tables.aggregate;
+
+
+import java.io.Serializable;
+import java.util.Objects;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class Aggregate implements Serializable
+{
+ private String fieldName;
+ private AggregateOperator operator;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Aggregate()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public boolean equals(Object o)
+ {
+ if(this == o)
+ {
+ return true;
+ }
+ if(o == null || getClass() != o.getClass())
+ {
+ return false;
+ }
+ Aggregate aggregate = (Aggregate) o;
+ return Objects.equals(fieldName, aggregate.fieldName) && operator == aggregate.operator;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(fieldName, operator);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Aggregate(String fieldName, AggregateOperator operator)
+ {
+ this.fieldName = fieldName;
+ this.operator = operator;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for fieldName
+ **
+ *******************************************************************************/
+ public String getFieldName()
+ {
+ return fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldName
+ **
+ *******************************************************************************/
+ public void setFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldName
+ **
+ *******************************************************************************/
+ public Aggregate withFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for operator
+ **
+ *******************************************************************************/
+ public AggregateOperator getOperator()
+ {
+ return operator;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for operator
+ **
+ *******************************************************************************/
+ public void setOperator(AggregateOperator operator)
+ {
+ this.operator = operator;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for operator
+ **
+ *******************************************************************************/
+ public Aggregate withOperator(AggregateOperator operator)
+ {
+ this.operator = operator;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateInput.java
new file mode 100644
index 00000000..1a055940
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateInput.java
@@ -0,0 +1,195 @@
+/*
+ * 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.core.model.actions.tables.aggregate;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+
+
+/*******************************************************************************
+ ** Input data for the Count action
+ **
+ *******************************************************************************/
+public class AggregateInput extends AbstractTableActionInput
+{
+ private QQueryFilter filter;
+ private List aggregates;
+ private List groupByFieldNames;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public AggregateInput()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public AggregateInput(QInstance instance)
+ {
+ super(instance);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for filter
+ **
+ *******************************************************************************/
+ public QQueryFilter getFilter()
+ {
+ return filter;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for filter
+ **
+ *******************************************************************************/
+ public void setFilter(QQueryFilter filter)
+ {
+ this.filter = filter;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for filter
+ **
+ *******************************************************************************/
+ public AggregateInput withFilter(QQueryFilter filter)
+ {
+ setFilter(filter);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for aggregates
+ **
+ *******************************************************************************/
+ public List getAggregates()
+ {
+ return aggregates;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for aggregates
+ **
+ *******************************************************************************/
+ public void setAggregates(List aggregates)
+ {
+ this.aggregates = aggregates;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for aggregates
+ **
+ *******************************************************************************/
+ public AggregateInput withAggregates(List aggregates)
+ {
+ this.aggregates = aggregates;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for aggregates
+ **
+ *******************************************************************************/
+ public AggregateInput withAggregate(Aggregate aggregate)
+ {
+ if(this.aggregates == null)
+ {
+ this.aggregates = new ArrayList<>();
+ }
+ this.aggregates.add(aggregate);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for groupByFieldNames
+ **
+ *******************************************************************************/
+ public List getGroupByFieldNames()
+ {
+ return groupByFieldNames;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for groupByFieldNames
+ **
+ *******************************************************************************/
+ public void setGroupByFieldNames(List groupByFieldNames)
+ {
+ this.groupByFieldNames = groupByFieldNames;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for groupByFieldNames
+ **
+ *******************************************************************************/
+ public AggregateInput withGroupByFieldNames(List groupByFieldNames)
+ {
+ this.groupByFieldNames = groupByFieldNames;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for groupByFieldNames
+ **
+ *******************************************************************************/
+ public AggregateInput withGroupByFieldName(String groupByFieldName)
+ {
+ if(this.groupByFieldNames == null)
+ {
+ this.groupByFieldNames = new ArrayList<>();
+ }
+ this.groupByFieldNames.add(groupByFieldName);
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOperator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOperator.java
new file mode 100644
index 00000000..a83724ac
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOperator.java
@@ -0,0 +1,35 @@
+/*
+ * 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.core.model.actions.tables.aggregate;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public enum AggregateOperator
+{
+ COUNT,
+ SUM,
+ MIN,
+ MAX,
+ AVG
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOutput.java
new file mode 100644
index 00000000..31821d13
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateOutput.java
@@ -0,0 +1,59 @@
+/*
+ * 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.core.model.actions.tables.aggregate;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
+
+
+/*******************************************************************************
+ ** Output for an aggregate action
+ **
+ *******************************************************************************/
+public class AggregateOutput extends AbstractActionOutput
+{
+ private List results;
+
+
+
+ /*******************************************************************************
+ ** Getter for results
+ **
+ *******************************************************************************/
+ public List getResults()
+ {
+ return results;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for results
+ **
+ *******************************************************************************/
+ public void setResults(List results)
+ {
+ this.results = results;
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateResult.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateResult.java
new file mode 100644
index 00000000..b7c3686c
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateResult.java
@@ -0,0 +1,158 @@
+/*
+ * 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.core.model.actions.tables.aggregate;
+
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class AggregateResult
+{
+ private Map aggregateValues = new LinkedHashMap<>();
+ private Map groupByValues = new LinkedHashMap<>();
+
+
+
+ /*******************************************************************************
+ ** Getter for aggregateValues
+ **
+ *******************************************************************************/
+ public Map getAggregateValues()
+ {
+ return aggregateValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for aggregateValues
+ **
+ *******************************************************************************/
+ public void setAggregateValues(Map aggregateValues)
+ {
+ this.aggregateValues = aggregateValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for aggregateValues
+ **
+ *******************************************************************************/
+ public AggregateResult withAggregateValues(Map aggregateValues)
+ {
+ this.aggregateValues = aggregateValues;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for groupByValues
+ **
+ *******************************************************************************/
+ public AggregateResult withAggregateValue(Aggregate aggregate, Serializable value)
+ {
+ if(this.aggregateValues == null)
+ {
+ this.aggregateValues = new LinkedHashMap<>();
+ }
+ this.aggregateValues.put(aggregate, value);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Serializable getAggregateValue(Aggregate aggregate)
+ {
+ return (this.aggregateValues.get(aggregate));
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for groupByValues
+ **
+ *******************************************************************************/
+ public Map getGroupByValues()
+ {
+ return groupByValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for groupByValues
+ **
+ *******************************************************************************/
+ public void setGroupByValues(Map groupByValues)
+ {
+ this.groupByValues = groupByValues;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for groupByValues
+ **
+ *******************************************************************************/
+ public AggregateResult withGroupByValues(Map groupByValues)
+ {
+ this.groupByValues = groupByValues;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for groupByValues
+ **
+ *******************************************************************************/
+ public AggregateResult withGroupByValue(String fieldName, Serializable value)
+ {
+ if(this.groupByValues == null)
+ {
+ this.groupByValues = new LinkedHashMap<>();
+ }
+ this.groupByValues.put(fieldName, value);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Serializable getGroupByValue(String fieldName)
+ {
+ return (this.groupByValues.get(fieldName));
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/QFilterOrderByAggregate.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/QFilterOrderByAggregate.java
new file mode 100644
index 00000000..bd8ed9d7
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/QFilterOrderByAggregate.java
@@ -0,0 +1,123 @@
+/*
+ * 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.core.model.actions.tables.aggregate;
+
+
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
+
+
+/*******************************************************************************
+ ** Bean representing an element of a query order-by clause - ordering by an
+ ** aggregate field.
+ **
+ *******************************************************************************/
+public class QFilterOrderByAggregate extends QFilterOrderBy implements Cloneable
+{
+ private Aggregate aggregate;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QFilterOrderByAggregate clone()
+ {
+ return (QFilterOrderByAggregate) super.clone();
+ }
+
+
+
+ /*******************************************************************************
+ ** Default no-arg constructor
+ *******************************************************************************/
+ public QFilterOrderByAggregate()
+ {
+
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor that sets field name, but leaves default for isAscending (true)
+ *******************************************************************************/
+ public QFilterOrderByAggregate(Aggregate aggregate)
+ {
+ this.aggregate = aggregate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor that takes field name and isAscending.
+ *******************************************************************************/
+ public QFilterOrderByAggregate(Aggregate aggregate, boolean isAscending)
+ {
+ this.aggregate = aggregate;
+ setIsAscending(isAscending);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for aggregate
+ **
+ *******************************************************************************/
+ public Aggregate getAggregate()
+ {
+ return aggregate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for aggregate
+ **
+ *******************************************************************************/
+ public void setAggregate(Aggregate aggregate)
+ {
+ this.aggregate = aggregate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for aggregate
+ **
+ *******************************************************************************/
+ public QFilterOrderByAggregate withAggregate(Aggregate aggregate)
+ {
+ this.aggregate = aggregate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ return (aggregate + " " + (getIsAscending() ? "ASC" : "DESC"));
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
index f83e959d..6c87d1a0 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.logging.log4j.LogManager;
@@ -73,6 +74,10 @@ public class QFilterCriteria implements Serializable, Cloneable
*******************************************************************************/
public QFilterCriteria()
{
+ ///////////////////////////////
+ // don't let values be null. //
+ ///////////////////////////////
+ values = new ArrayList<>();
}
@@ -84,7 +89,31 @@ public class QFilterCriteria implements Serializable, Cloneable
{
this.fieldName = fieldName;
this.operator = operator;
- this.values = values;
+ this.values = values == null ? new ArrayList<>() : values;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QFilterCriteria(String fieldName, QCriteriaOperator operator, Serializable... values)
+ {
+ this.fieldName = fieldName;
+ this.operator = operator;
+
+ if(values == null || (values.length == 1 && values[0] == null))
+ {
+ ////////////////////////////////////////////////////////////////////
+ // this ... could be a sign of an issue... debug juuuust in case? //
+ ////////////////////////////////////////////////////////////////////
+ LOG.debug("null passed as singleton varargs array will be ignored");
+ this.values = new ArrayList<>();
+ }
+ else
+ {
+ this.values = Arrays.stream(values).toList();
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
index 6d1fc376..9b02e1f8 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.logging.log4j.LogManager;
@@ -57,6 +58,27 @@ public class QQueryFilter implements Serializable, Cloneable
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QQueryFilter()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QQueryFilter(QFilterCriteria... criteria)
+ {
+ this.criteria = new ArrayList<>(Arrays.stream(criteria).toList());
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/RenderTemplateInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/RenderTemplateInput.java
new file mode 100644
index 00000000..2b3b0dd7
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/RenderTemplateInput.java
@@ -0,0 +1,152 @@
+/*
+ * 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.core.model.templates;
+
+
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RenderTemplateInput extends AbstractActionInput
+{
+ private String code; // todo - TemplateReference, like CodeReference??
+ private TemplateType templateType;
+
+ private Map context;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public RenderTemplateInput(QInstance instance)
+ {
+ super(instance);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for code
+ **
+ *******************************************************************************/
+ public String getCode()
+ {
+ return code;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for code
+ **
+ *******************************************************************************/
+ public void setCode(String code)
+ {
+ this.code = code;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for code
+ **
+ *******************************************************************************/
+ public RenderTemplateInput withCode(String code)
+ {
+ this.code = code;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for templateType
+ **
+ *******************************************************************************/
+ public TemplateType getTemplateType()
+ {
+ return templateType;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for templateType
+ **
+ *******************************************************************************/
+ public void setTemplateType(TemplateType templateType)
+ {
+ this.templateType = templateType;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for templateType
+ **
+ *******************************************************************************/
+ public RenderTemplateInput withTemplateType(TemplateType templateType)
+ {
+ this.templateType = templateType;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for context
+ **
+ *******************************************************************************/
+ public Map getContext()
+ {
+ return context;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for context
+ **
+ *******************************************************************************/
+ public void setContext(Map context)
+ {
+ this.context = context;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for context
+ **
+ *******************************************************************************/
+ public RenderTemplateInput withContext(Map context)
+ {
+ this.context = context;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/RenderTemplateOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/RenderTemplateOutput.java
new file mode 100644
index 00000000..40d0e0f1
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/RenderTemplateOutput.java
@@ -0,0 +1,69 @@
+/*
+ * 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.core.model.templates;
+
+
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RenderTemplateOutput extends AbstractActionOutput
+{
+ private String result;
+
+
+
+ /*******************************************************************************
+ ** Getter for result
+ **
+ *******************************************************************************/
+ public String getResult()
+ {
+ return result;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for result
+ **
+ *******************************************************************************/
+ public void setResult(String result)
+ {
+ this.result = result;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for result
+ **
+ *******************************************************************************/
+ public RenderTemplateOutput withResult(String result)
+ {
+ this.result = result;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/TemplateType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/TemplateType.java
new file mode 100644
index 00000000..823608ed
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/templates/TemplateType.java
@@ -0,0 +1,31 @@
+/*
+ * 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.core.model.templates;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public enum TemplateType
+{
+ VELOCITY
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
index 70d51742..6e3f5811 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.modules.backend;
+import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
@@ -116,6 +117,15 @@ public interface QBackendModuleInterface
return null;
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default AggregateInterface getAggregateInterface()
+ {
+ throwNotImplemented("Aggregate");
+ return null;
+ }
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/RenderTemplateActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/RenderTemplateActionTest.java
new file mode 100644
index 00000000..ad5a96bd
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/templates/RenderTemplateActionTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.core.actions.templates;
+
+
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateInput;
+import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateOutput;
+import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for RenderTemplateAction
+ *******************************************************************************/
+class RenderTemplateActionTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ RenderTemplateInput renderTemplateInput = new RenderTemplateInput(TestUtils.defineInstance());
+ renderTemplateInput.setSession(new QSession());
+ renderTemplateInput.setCode("""
+ Hello, $name""");
+ renderTemplateInput.setContext(Map.of("name", "Darin"));
+ renderTemplateInput.setTemplateType(TemplateType.VELOCITY);
+ RenderTemplateOutput output = new RenderTemplateAction().execute(renderTemplateInput);
+ assertEquals("Hello, Darin", output.getResult());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testConvenientWrapper() throws QException
+ {
+ RenderTemplateInput parentActionInput = new RenderTemplateInput(TestUtils.defineInstance());
+ parentActionInput.setSession(new QSession());
+
+ String template = "Hello, $name";
+ assertEquals("Hello, Darin", RenderTemplateAction.renderVelocity(parentActionInput, Map.of("name", "Darin"), template));
+ assertEquals("Hello, Tim", RenderTemplateAction.renderVelocity(parentActionInput, Map.of("name", "Tim"), template));
+ assertEquals("Hello, $name", RenderTemplateAction.renderVelocity(parentActionInput, Map.of(), template));
+
+ template = "Hello, $!name";
+ assertEquals("Hello, ", RenderTemplateAction.renderVelocity(parentActionInput, Map.of(), template));
+ assertEquals("Hello, ", RenderTemplateAction.renderVelocity(parentActionInput, null, template));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testMissingType()
+ {
+ RenderTemplateInput parentActionInput = new RenderTemplateInput(TestUtils.defineInstance());
+ parentActionInput.setSession(new QSession());
+
+ assertThatThrownBy(() -> RenderTemplateAction.render(parentActionInput, null, Map.of("name", "Darin"), "Hello, $name"))
+ .isInstanceOf(QException.class)
+ .hasMessageContaining("Unsupported Template Type");
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteriaTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteriaTest.java
new file mode 100644
index 00000000..bb94dd5c
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteriaTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.core.model.actions.tables.query;
+
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IS_BLANK;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for QFilterCriteria
+ *******************************************************************************/
+class QFilterCriteriaTest
+{
+
+ /*******************************************************************************
+ ** Make sure that the constructors that takes a List or Serializable... and does
+ ** the right thing - e.g., never making a List-of-List, or List of array, and
+ ** that we never have null values - always an empty list as the degenerate case.
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ assertEquals(1, new QFilterCriteria("foo", EQUALS, "A").getValues().size());
+ assertEquals(1, new QFilterCriteria("foo", EQUALS, List.of("A")).getValues().size());
+ assertEquals(2, new QFilterCriteria("foo", EQUALS, List.of("A", "B")).getValues().size());
+ assertEquals(2, new QFilterCriteria("foo", EQUALS, "A", "B").getValues().size());
+
+ List list = List.of("A", "B", "C");
+ assertEquals(3, new QFilterCriteria("foo", EQUALS, list).getValues().size());
+ assertEquals(List.of("A", "B", "C"), new QFilterCriteria("foo", EQUALS, list).getValues());
+
+ Serializable[] array = new Serializable[] { "A", "B", "C", "D" };
+ assertEquals(4, new QFilterCriteria("foo", EQUALS, array).getValues().size());
+ assertEquals(List.of("A", "B", "C", "D"), new QFilterCriteria("foo", EQUALS, array).getValues());
+
+ assertEquals(3, new QFilterCriteria("foo", EQUALS, new ArrayList<>(list)).getValues().size());
+ assertEquals(List.of("A", "B", "C"), new QFilterCriteria("foo", EQUALS, new ArrayList<>(list)).getValues());
+
+ assertEquals(0, new QFilterCriteria("foo", IS_BLANK).getValues().size());
+
+ Serializable maybeNull = null;
+ assertEquals(0, new QFilterCriteria("foo", EQUALS, maybeNull).getValues().size());
+
+ List nullList = null;
+ assertEquals(0, new QFilterCriteria("foo", EQUALS, nullList).getValues().size());
+
+ assertEquals(0, new QFilterCriteria("foo", EQUALS, (List) null).getValues().size());
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java
index 194b81e0..5a95bea0 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.module.rdbms;
+import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
@@ -30,6 +31,7 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
+import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSAggregateAction;
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSCountAction;
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSDeleteAction;
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSInsertAction;
@@ -87,7 +89,6 @@ public class RDBMSBackendModule implements QBackendModuleInterface
-
/*******************************************************************************
**
*******************************************************************************/
@@ -130,4 +131,15 @@ public class RDBMSBackendModule implements QBackendModuleInterface
return (new RDBMSDeleteAction());
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public AggregateInterface getAggregateInterface()
+ {
+ return (new RDBMSAggregateAction());
+ }
+
}
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 a4f91cf2..aed6c691 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
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.module.rdbms.actions;
import java.io.Serializable;
import java.sql.Connection;
+import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@@ -33,7 +34,10 @@ import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.interfaces.QActionInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate;
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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@@ -43,6 +47,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
+import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
import org.apache.logging.log4j.LogManager;
@@ -411,4 +416,80 @@ public abstract class AbstractRDBMSAction implements QActionInterface
return ("`" + id + "`");
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected Serializable getFieldValueFromResultSet(QFieldMetaData qFieldMetaData, ResultSet resultSet, int i) throws SQLException
+ {
+ switch(qFieldMetaData.getType())
+ {
+ case STRING:
+ case TEXT:
+ case HTML:
+ case PASSWORD:
+ {
+ return (QueryManager.getString(resultSet, i));
+ }
+ case INTEGER:
+ {
+ return (QueryManager.getInteger(resultSet, i));
+ }
+ case DECIMAL:
+ {
+ return (QueryManager.getBigDecimal(resultSet, i));
+ }
+ case DATE:
+ {
+ // todo - queryManager.getLocalDate?
+ return (QueryManager.getDate(resultSet, i));
+ }
+ case TIME:
+ {
+ return (QueryManager.getLocalTime(resultSet, i));
+ }
+ case DATE_TIME:
+ {
+ return (QueryManager.getInstant(resultSet, i));
+ }
+ case BOOLEAN:
+ {
+ return (QueryManager.getBoolean(resultSet, i));
+ }
+ default:
+ {
+ throw new IllegalStateException("Unexpected field type: " + qFieldMetaData.getType());
+ }
+ }
+
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected String makeOrderByClause(QTableMetaData table, List orderBys)
+ {
+ List clauses = new ArrayList<>();
+
+ for(QFilterOrderBy orderBy : orderBys)
+ {
+ String ascOrDesc = orderBy.getIsAscending() ? "ASC" : "DESC";
+ if(orderBy instanceof QFilterOrderByAggregate orderByAggregate)
+ {
+ Aggregate aggregate = orderByAggregate.getAggregate();
+ String clause = (aggregate.getOperator() + "(" + escapeIdentifier(getColumnName(table.getField(aggregate.getFieldName()))) + ")");
+ clauses.add(clause + " " + ascOrDesc);
+ }
+ else
+ {
+ QFieldMetaData field = table.getField(orderBy.getFieldName());
+ String column = escapeIdentifier(getColumnName(field));
+ clauses.add(column + " " + ascOrDesc);
+ }
+ }
+ return (String.join(", ", clauses));
+ }
}
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java
new file mode 100644
index 00000000..99c422b0
--- /dev/null
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java
@@ -0,0 +1,176 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.module.rdbms.actions;
+
+
+import java.io.Serializable;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateResult;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RDBMSAggregateAction extends AbstractRDBMSAction implements AggregateInterface
+{
+ private static final Logger LOG = LogManager.getLogger(RDBMSAggregateAction.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public AggregateOutput execute(AggregateInput aggregateInput) throws QException
+ {
+ try
+ {
+ QTableMetaData table = aggregateInput.getTable();
+ String tableName = getTableName(table);
+
+ List selectClauses = buildSelectClauses(aggregateInput);
+
+ String sql = "SELECT " + StringUtils.join(", ", selectClauses)
+ + " FROM " + escapeIdentifier(tableName);
+
+ QQueryFilter filter = aggregateInput.getFilter();
+ List params = new ArrayList<>();
+ if(filter != null && filter.hasAnyCriteria())
+ {
+ sql += " WHERE " + makeWhereClause(table, filter, params);
+ }
+
+ if(CollectionUtils.nullSafeHasContents(aggregateInput.getGroupByFieldNames()))
+ {
+ sql += " GROUP BY " + makeGroupByClause(aggregateInput);
+ }
+
+ if(filter != null && CollectionUtils.nullSafeHasContents(filter.getOrderBys()))
+ {
+ sql += " ORDER BY " + makeOrderByClause(table, filter.getOrderBys());
+ }
+
+ // todo sql customization - can edit sql and/or param list
+ LOG.debug(sql); // todo not commit - downgrade to trace
+
+ AggregateOutput rs = new AggregateOutput();
+ List results = new ArrayList<>();
+ rs.setResults(results);
+
+ try(Connection connection = getConnection(aggregateInput))
+ {
+ QueryManager.executeStatement(connection, sql, ((ResultSet resultSet) ->
+ {
+ while(resultSet.next())
+ {
+ AggregateResult result = new AggregateResult();
+ results.add(result);
+
+ int selectionIndex = 1;
+ for(String groupByFieldName : CollectionUtils.nonNullList(aggregateInput.getGroupByFieldNames()))
+ {
+ Serializable value = getFieldValueFromResultSet(table.getField(groupByFieldName), resultSet, selectionIndex++);
+ result.withGroupByValue(groupByFieldName, value);
+ }
+
+ for(Aggregate aggregate : aggregateInput.getAggregates())
+ {
+ QFieldMetaData field = table.getField(aggregate.getFieldName());
+ if(field.getType().equals(QFieldType.INTEGER) && aggregate.getOperator().equals(AggregateOperator.AVG))
+ {
+ field = new QFieldMetaData().withType(QFieldType.DECIMAL);
+ }
+
+ Serializable value = getFieldValueFromResultSet(field, resultSet, selectionIndex++);
+ result.withAggregateValue(aggregate, value);
+ }
+ }
+
+ }), params);
+ }
+
+ return rs;
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error executing aggregate", e);
+ throw new QException("Error executing aggregate", e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private List buildSelectClauses(AggregateInput aggregateInput)
+ {
+ QTableMetaData table = aggregateInput.getTable();
+ List rs = new ArrayList<>();
+
+ for(String groupByFieldName : CollectionUtils.nonNullList(aggregateInput.getGroupByFieldNames()))
+ {
+ rs.add(escapeIdentifier(getColumnName(table.getField(groupByFieldName))));
+ }
+
+ for(Aggregate aggregate : aggregateInput.getAggregates())
+ {
+ rs.add(aggregate.getOperator() + "(" + escapeIdentifier(getColumnName(table.getField(aggregate.getFieldName()))) + ")");
+ }
+ return (rs);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String makeGroupByClause(AggregateInput aggregateInput)
+ {
+ QTableMetaData table = aggregateInput.getTable();
+ List columns = new ArrayList<>();
+ for(String groupByFieldName : aggregateInput.getGroupByFieldNames())
+ {
+ columns.add(escapeIdentifier(getColumnName(table.getField(groupByFieldName))));
+ }
+
+ return (StringUtils.join(",", columns));
+ }
+
+}
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 c326f865..c99271da 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
@@ -34,7 +34,6 @@ import java.util.List;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
-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.QueryOutput;
@@ -129,7 +128,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
for(int i = 1; i <= metaData.getColumnCount(); i++)
{
QFieldMetaData qFieldMetaData = fieldList.get(i - 1);
- Serializable value = getValue(qFieldMetaData, resultSet, i);
+ Serializable value = getFieldValueFromResultSet(qFieldMetaData, resultSet, i);
values.put(qFieldMetaData.getName(), value);
}
@@ -187,71 +186,4 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
return (statement);
}
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private Serializable getValue(QFieldMetaData qFieldMetaData, ResultSet resultSet, int i) throws SQLException
- {
- switch(qFieldMetaData.getType())
- {
- case STRING:
- case TEXT:
- case HTML:
- case PASSWORD:
- {
- return (QueryManager.getString(resultSet, i));
- }
- case INTEGER:
- {
- return (QueryManager.getInteger(resultSet, i));
- }
- case DECIMAL:
- {
- return (QueryManager.getBigDecimal(resultSet, i));
- }
- case DATE:
- {
- // todo - queryManager.getLocalDate?
- return (QueryManager.getDate(resultSet, i));
- }
- case TIME:
- {
- return (QueryManager.getLocalTime(resultSet, i));
- }
- case DATE_TIME:
- {
- return (QueryManager.getInstant(resultSet, i));
- }
- case BOOLEAN:
- {
- return (QueryManager.getBoolean(resultSet, i));
- }
- default:
- {
- throw new IllegalStateException("Unexpected field type: " + qFieldMetaData.getType());
- }
- }
-
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private String makeOrderByClause(QTableMetaData table, List orderBys)
- {
- List clauses = new ArrayList<>();
-
- for(QFilterOrderBy orderBy : orderBys)
- {
- QFieldMetaData field = table.getField(orderBy.getFieldName());
- String column = getColumnName(field);
- clauses.add(column + " " + (orderBy.getIsAscending() ? "ASC" : "DESC"));
- }
- return (String.join(", ", clauses));
- }
-
}
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 ce9b3ae5..87694d3b 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
@@ -26,9 +26,9 @@ import java.io.InputStream;
import java.sql.Connection;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
-import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
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;
@@ -131,7 +131,10 @@ public class TestUtils
.withField(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name"))
.withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name"))
.withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date"))
- .withField(new QFieldMetaData("email", QFieldType.STRING))
+ .withField(new QFieldMetaData("email", QFieldType.STRING).withBackendName("email"))
+ .withField(new QFieldMetaData("isEmployed", QFieldType.BOOLEAN).withBackendName("is_employed"))
+ .withField(new QFieldMetaData("annualSalary", QFieldType.DECIMAL).withBackendName("annual_salary"))
+ .withField(new QFieldMetaData("daysWorked", QFieldType.INTEGER).withBackendName("days_worked"))
.withBackendDetails(new RDBMSTableBackendDetails()
.withTableName("person"));
}
diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateActionTest.java
new file mode 100644
index 00000000..4a323143
--- /dev/null
+++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateActionTest.java
@@ -0,0 +1,327 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.module.rdbms.actions;
+
+
+import java.math.BigDecimal;
+import java.util.Iterator;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateResult;
+import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class RDBMSAggregateActionTest extends RDBMSActionTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ public void beforeEach() throws Exception
+ {
+ super.primeTestDatabase();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void testUnfilteredNoGroupBy() throws QException
+ {
+ AggregateInput aggregateInput = initAggregateRequest();
+ Aggregate countOfId = new Aggregate("id", AggregateOperator.COUNT);
+ Aggregate sumOfId = new Aggregate("id", AggregateOperator.SUM);
+ Aggregate averageOfDaysWorked = new Aggregate("daysWorked", AggregateOperator.AVG);
+ Aggregate maxAnnualSalary = new Aggregate("annualSalary", AggregateOperator.MAX);
+ Aggregate minFirstName = new Aggregate("firstName", AggregateOperator.MIN);
+ aggregateInput.withAggregate(countOfId);
+ aggregateInput.withAggregate(sumOfId);
+ aggregateInput.withAggregate(averageOfDaysWorked);
+ aggregateInput.withAggregate(maxAnnualSalary);
+ aggregateInput.withAggregate(minFirstName);
+
+ AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
+
+ AggregateResult aggregateResult = aggregateOutput.getResults().get(0);
+ Assertions.assertEquals(5, aggregateResult.getAggregateValue(countOfId));
+ Assertions.assertEquals(15, aggregateResult.getAggregateValue(sumOfId));
+ Assertions.assertEquals(new BigDecimal("96.4"), aggregateResult.getAggregateValue(averageOfDaysWorked));
+ Assertions.assertEquals(new BigDecimal("1000000.00"), aggregateResult.getAggregateValue(maxAnnualSalary));
+ Assertions.assertEquals("Darin", aggregateResult.getAggregateValue(minFirstName));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void testFilteredNoGroupBy() throws QException
+ {
+ AggregateInput aggregateInput = initAggregateRequest();
+ Aggregate countOfId = new Aggregate("id", AggregateOperator.COUNT);
+ Aggregate sumOfId = new Aggregate("id", AggregateOperator.SUM);
+ Aggregate averageOfDaysWorked = new Aggregate("daysWorked", AggregateOperator.AVG);
+ Aggregate maxAnnualSalary = new Aggregate("annualSalary", AggregateOperator.MAX);
+ Aggregate minFirstName = new Aggregate("firstName", AggregateOperator.MIN);
+ aggregateInput.withAggregate(countOfId);
+ aggregateInput.withAggregate(sumOfId);
+ aggregateInput.withAggregate(averageOfDaysWorked);
+ aggregateInput.withAggregate(maxAnnualSalary);
+ aggregateInput.withAggregate(minFirstName);
+
+ aggregateInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.IN, List.of("Tim", "James"))));
+ AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
+
+ AggregateResult aggregateResult = aggregateOutput.getResults().get(0);
+ Assertions.assertEquals(2, aggregateResult.getAggregateValue(countOfId));
+ Assertions.assertEquals(5, aggregateResult.getAggregateValue(sumOfId));
+ Assertions.assertEquals(new BigDecimal("62.0"), aggregateResult.getAggregateValue(averageOfDaysWorked));
+ Assertions.assertEquals(new BigDecimal("26000.00"), aggregateResult.getAggregateValue(maxAnnualSalary));
+ Assertions.assertEquals("James", aggregateResult.getAggregateValue(minFirstName));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void testUnfilteredWithGroupBy() throws QException
+ {
+ ////////////////////////////////////////////////////
+ // insert a few extra rows from the core data set //
+ ////////////////////////////////////////////////////
+ insertExtraPersonRecords();
+
+ AggregateInput aggregateInput = initAggregateRequest();
+ Aggregate countOfId = new Aggregate("id", AggregateOperator.COUNT);
+ Aggregate sumOfDaysWorked = new Aggregate("daysWorked", AggregateOperator.SUM);
+ aggregateInput.withAggregate(countOfId);
+ aggregateInput.withAggregate(sumOfDaysWorked);
+
+ aggregateInput.withGroupByFieldName("lastName");
+ aggregateInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("lastName")));
+
+ AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
+ {
+ AggregateResult aggregateResult = aggregateOutput.getResults().get(0);
+ Assertions.assertEquals("Chamberlain", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals(2, aggregateResult.getAggregateValue(countOfId));
+ Assertions.assertEquals(17, aggregateResult.getAggregateValue(sumOfDaysWorked));
+ }
+ {
+ AggregateResult aggregateResult = aggregateOutput.getResults().get(1);
+ Assertions.assertEquals("Kelkhoff", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals(4, aggregateResult.getAggregateValue(countOfId));
+ Assertions.assertEquals(11364, aggregateResult.getAggregateValue(sumOfDaysWorked));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void testUnfilteredWithMultiGroupBy() throws QException
+ {
+ ////////////////////////////////////////////////////
+ // insert a few extra rows from the core data set //
+ ////////////////////////////////////////////////////
+ insertExtraPersonRecords();
+
+ AggregateInput aggregateInput = initAggregateRequest();
+ Aggregate countOfId = new Aggregate("id", AggregateOperator.COUNT);
+ Aggregate sumOfDaysWorked = new Aggregate("daysWorked", AggregateOperator.SUM);
+ aggregateInput.withAggregate(countOfId);
+ aggregateInput.withAggregate(sumOfDaysWorked);
+
+ aggregateInput.withGroupByFieldName("lastName");
+ aggregateInput.withGroupByFieldName("firstName");
+
+ aggregateInput.setFilter(new QQueryFilter()
+ .withOrderBy(new QFilterOrderBy("lastName"))
+ .withOrderBy(new QFilterOrderBy("firstName")));
+
+ AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
+ Iterator iterator = aggregateOutput.getResults().iterator();
+ AggregateResult aggregateResult;
+
+ aggregateResult = iterator.next();
+ Assertions.assertEquals("Chamberlain", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals("Donny", aggregateResult.getGroupByValue("firstName"));
+ Assertions.assertEquals(1, aggregateResult.getAggregateValue(countOfId));
+
+ aggregateResult = iterator.next();
+ Assertions.assertEquals("Chamberlain", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals("Tim", aggregateResult.getGroupByValue("firstName"));
+ Assertions.assertEquals(1, aggregateResult.getAggregateValue(countOfId));
+
+ aggregateResult = iterator.next();
+ Assertions.assertEquals("Kelkhoff", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals("Aaron", aggregateResult.getGroupByValue("firstName"));
+ Assertions.assertEquals(1, aggregateResult.getAggregateValue(countOfId));
+
+ aggregateResult = iterator.next();
+ Assertions.assertEquals("Kelkhoff", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals("Darin", aggregateResult.getGroupByValue("firstName"));
+ Assertions.assertEquals(2, aggregateResult.getAggregateValue(countOfId));
+
+ aggregateResult = iterator.next();
+ Assertions.assertEquals("Kelkhoff", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals("Trevor", aggregateResult.getGroupByValue("firstName"));
+ Assertions.assertEquals(1, aggregateResult.getAggregateValue(countOfId));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void testOrderByAggregate() throws QException
+ {
+ ////////////////////////////////////////////////////
+ // insert a few extra rows from the core data set //
+ ////////////////////////////////////////////////////
+ insertExtraPersonRecords();
+
+ AggregateInput aggregateInput = initAggregateRequest();
+ Aggregate countOfId = new Aggregate("id", AggregateOperator.COUNT);
+ Aggregate sumOfDaysWorked = new Aggregate("daysWorked", AggregateOperator.SUM);
+ aggregateInput.withAggregate(countOfId);
+ // note - don't query this value - just order by it!! aggregateInput.withAggregate(sumOfDaysWorked);
+
+ aggregateInput.withGroupByFieldName("lastName");
+
+ aggregateInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderByAggregate(sumOfDaysWorked, false)));
+
+ AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
+ Iterator iterator = aggregateOutput.getResults().iterator();
+ AggregateResult aggregateResult;
+
+ aggregateResult = iterator.next();
+ Assertions.assertEquals("Kelkhoff", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals(4, aggregateResult.getAggregateValue(countOfId));
+
+ aggregateResult = iterator.next();
+ Assertions.assertEquals("Richardson", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals(1, aggregateResult.getAggregateValue(countOfId));
+
+ aggregateResult = iterator.next();
+ Assertions.assertEquals("Maes", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals(1, aggregateResult.getAggregateValue(countOfId));
+
+ aggregateResult = iterator.next();
+ Assertions.assertEquals("Samples", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals(1, aggregateResult.getAggregateValue(countOfId));
+
+ aggregateResult = iterator.next();
+ Assertions.assertEquals("Chamberlain", aggregateResult.getGroupByValue("lastName"));
+ Assertions.assertEquals(2, aggregateResult.getAggregateValue(countOfId));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ public void testNoRowsFound() throws QException
+ {
+ AggregateInput aggregateInput = initAggregateRequest();
+ Aggregate countOfId = new Aggregate("id", AggregateOperator.COUNT);
+ aggregateInput.withAggregate(countOfId);
+ aggregateInput.withFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, -9)));
+
+ ////////////////////////////////////////////////////////////
+ // when there's no group-by, we get a row, but w/ 0 count //
+ ////////////////////////////////////////////////////////////
+ AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
+ AggregateResult aggregateResult = aggregateOutput.getResults().get(0);
+ Assertions.assertEquals(0, aggregateResult.getAggregateValue(countOfId));
+
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // but re-run w/ a group-by -- then, if no rows are found, there are 0 result objects. //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ aggregateInput.withGroupByFieldName("lastName");
+ aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
+ assertTrue(aggregateOutput.getResults().isEmpty());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void insertExtraPersonRecords() throws QException
+ {
+ InsertInput insertInput = new InsertInput(TestUtils.defineInstance());
+ insertInput.setSession(new QSession());
+ insertInput.setTableName(TestUtils.defineTablePerson().getName());
+ insertInput.setRecords(List.of(
+ new QRecord().withValue("lastName", "Kelkhoff").withValue("firstName", "Trevor").withValue("email", "tk@kr.com").withValue("daysWorked", 1024),
+ new QRecord().withValue("lastName", "Kelkhoff").withValue("firstName", "Darin").withValue("email", "dk2@kr.com").withValue("daysWorked", 314),
+ new QRecord().withValue("lastName", "Kelkhoff").withValue("firstName", "Aaron").withValue("email", "ak@kr.com").withValue("daysWorked", 9999),
+ new QRecord().withValue("lastName", "Chamberlain").withValue("firstName", "Donny").withValue("email", "dc@kr.com").withValue("daysWorked", 17)
+ ));
+ new InsertAction().execute(insertInput);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private AggregateInput initAggregateRequest()
+ {
+ AggregateInput aggregateInput = new AggregateInput();
+ aggregateInput.setInstance(TestUtils.defineInstance());
+ aggregateInput.setTableName(TestUtils.defineTablePerson().getName());
+ return aggregateInput;
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-module-rdbms/src/test/resources/prime-test-database.sql b/qqq-backend-module-rdbms/src/test/resources/prime-test-database.sql
index 07ab6ac6..69b421d6 100644
--- a/qqq-backend-module-rdbms/src/test/resources/prime-test-database.sql
+++ b/qqq-backend-module-rdbms/src/test/resources/prime-test-database.sql
@@ -29,14 +29,17 @@ CREATE TABLE person
first_name VARCHAR(80) NOT NULL,
last_name VARCHAR(80) NOT NULL,
birth_date DATE,
- email VARCHAR(250) NOT NULL
+ email VARCHAR(250) NOT NULL,
+ is_employed BOOLEAN,
+ annual_salary DECIMAL(12,2),
+ days_worked INTEGER
);
-INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com');
-INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com');
-INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com');
-INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com');
-INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com');
+INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 1, 25000, 27);
+INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 1, 26000, 124);
+INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 0, null, 0);
+INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 1, 30000, 99);
+INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 1, 1000000, 232);
DROP TABLE IF EXISTS carrier;
CREATE TABLE carrier