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