From b2d76e820688f01c5183dce32c5b13f900bbb54f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 23 Nov 2022 16:37:54 -0600 Subject: [PATCH] Much implementation of joins for RDBMS --- .../reporting/GenerateReportAction.java | 15 +- .../core/instances/QInstanceEnricher.java | 46 +++ .../tables/aggregate/AggregateInput.java | 53 +++ .../actions/tables/count/CountInput.java | 53 +++ .../actions/tables/query/JoinsContext.java | 134 ++++++ .../actions/tables/query/QFilterCriteria.java | 60 ++- .../actions/tables/query/QueryInput.java | 54 +++ .../model/actions/tables/query/QueryJoin.java | 306 ++++++++++++++ .../core/model/metadata/joins/JoinOn.java | 13 +- .../core/model/metadata/joins/JoinType.java | 20 +- .../model/metadata/joins/QJoinMetaData.java | 18 +- .../metadata/reporting/QReportDataSource.java | 62 ++- .../metadata/reporting/QReportField.java | 21 + .../metadata/reporting/QReportMetaData.java | 48 +++ .../reports/RunReportForRecordProcess.java | 16 + .../instances/QInstanceValidatorTest.java | 29 +- .../rdbms/actions/AbstractRDBMSAction.java | 128 +++++- .../rdbms/actions/RDBMSAggregateAction.java | 42 +- .../rdbms/actions/RDBMSCountAction.java | 11 +- .../rdbms/actions/RDBMSDeleteAction.java | 7 +- .../rdbms/actions/RDBMSQueryAction.java | 83 +++- .../qqq/backend/module/rdbms/TestUtils.java | 150 ++++++- .../actions/RDBMSAggregateActionTest.java | 68 ++++ .../rdbms/actions/RDBMSCountActionTest.java | 69 +++- .../rdbms/actions/RDBMSQueryActionTest.java | 380 ++++++++++++++++-- .../test/resources/prime-test-database.sql | 91 +++++ 26 files changed, 1873 insertions(+), 104 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index 76c0eb0e..668217ce 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -46,6 +46,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; 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; @@ -185,7 +186,7 @@ public class GenerateReportAction /******************************************************************************* ** *******************************************************************************/ - private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView) throws QReportingException + private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView) throws QException { QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable()); @@ -200,6 +201,8 @@ public class GenerateReportAction exportInput.setIncludeHeaderRow(reportView.getIncludeHeaderRow()); exportInput.setReportOutputStream(reportInput.getReportOutputStream()); + JoinsContext joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins()); + List fields; if(CollectionUtils.nullSafeHasContents(reportView.getColumns())) { @@ -212,7 +215,14 @@ public class GenerateReportAction } else { - QFieldMetaData field = table.getField(column.getName()).clone(); + JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(column.getName()); + if(fieldAndTableNameOrAlias.field() == null) + { + throw new QReportingException("Could not find field named [" + column.getName() + "] on table [" + table.getName() + "]"); + } + + QFieldMetaData field = fieldAndTableNameOrAlias.field().clone(); + field.setName(column.getName()); if(StringUtils.hasContent(column.getLabel())) { field.setLabel(column.getLabel()); @@ -278,6 +288,7 @@ public class GenerateReportAction queryInput.setRecordPipe(recordPipe); queryInput.setTableName(dataSource.getSourceTable()); queryInput.setFilter(queryFilter); + queryInput.setQueryJoins(dataSource.getQueryJoins()); queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this? return (new QueryAction().execute(queryInput)); } 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 ec28ecae..4f85eee3 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 @@ -49,7 +49,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponen import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; @@ -365,6 +367,50 @@ public class QInstanceEnricher { report.getInputFields().forEach(this::enrichField); } + + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // if there's only 1 data source in the report, and it doesn't have a name, give it a default name // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + String singleDataSourceName = null; + if(report.getDataSources() != null) + { + if(report.getDataSources().size() == 1) + { + QReportDataSource dataSource = report.getDataSources().get(0); + if(!StringUtils.hasContent(dataSource.getName())) + { + dataSource.setName("DEFAULT"); + } + singleDataSourceName = dataSource.getName(); + } + } + + if(report.getViews() != null) + { + ////////////////////////////////////////////////////////////////////////////////////////////// + // if there's only 1 view in the report, and it doesn't have a name, give it a default name // + ////////////////////////////////////////////////////////////////////////////////////////////// + if(report.getViews().size() == 1) + { + QReportView view = report.getViews().get(0); + if(!StringUtils.hasContent(view.getName())) + { + view.setName("DEFAULT"); + } + } + + ///////////////////////////////////////////////////////////////////////////// + // for any views in the report, if they don't specify a data source name, // + // but there's only 1 data source, then use that single data source's name // + ///////////////////////////////////////////////////////////////////////////// + for(QReportView view : report.getViews()) + { + if(!StringUtils.hasContent(view.getDataSourceName()) && singleDataSourceName != null) + { + view.setDataSourceName(singleDataSourceName); + } + } + } } 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 index 1a055940..1ab93c43 100644 --- 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 @@ -26,6 +26,7 @@ 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.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -39,6 +40,8 @@ public class AggregateInput extends AbstractTableActionInput private List aggregates; private List groupByFieldNames; + private List queryJoins = null; + /******************************************************************************* @@ -192,4 +195,54 @@ public class AggregateInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for queryJoins + ** + *******************************************************************************/ + public List getQueryJoins() + { + return queryJoins; + } + + + + /******************************************************************************* + ** Setter for queryJoins + ** + *******************************************************************************/ + public void setQueryJoins(List queryJoins) + { + this.queryJoins = queryJoins; + } + + + + /******************************************************************************* + ** Fluent setter for queryJoins + ** + *******************************************************************************/ + public AggregateInput withQueryJoins(List queryJoins) + { + this.queryJoins = queryJoins; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryJoins + ** + *******************************************************************************/ + public AggregateInput withQueryJoin(QueryJoin queryJoin) + { + if(this.queryJoins == null) + { + this.queryJoins = new ArrayList<>(); + } + this.queryJoins.add(queryJoin); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java index ae2d62e8..622e0001 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java @@ -22,8 +22,11 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.count; +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.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -35,6 +38,8 @@ public class CountInput extends AbstractTableActionInput { private QQueryFilter filter; + private List queryJoins = null; + /******************************************************************************* @@ -78,4 +83,52 @@ public class CountInput extends AbstractTableActionInput + /******************************************************************************* + ** Getter for queryJoins + ** + *******************************************************************************/ + public List getQueryJoins() + { + return queryJoins; + } + + + + /******************************************************************************* + ** Setter for queryJoins + ** + *******************************************************************************/ + public void setQueryJoins(List queryJoins) + { + this.queryJoins = queryJoins; + } + + + + /******************************************************************************* + ** Fluent setter for queryJoins + ** + *******************************************************************************/ + public CountInput withQueryJoins(List queryJoins) + { + this.queryJoins = queryJoins; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryJoins + ** + *******************************************************************************/ + public CountInput withQueryJoin(QueryJoin queryJoin) + { + if(this.queryJoins == null) + { + this.queryJoins = new ArrayList<>(); + } + this.queryJoins.add(queryJoin); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java new file mode 100644 index 00000000..1d7fc793 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java @@ -0,0 +1,134 @@ +/* + * 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.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** Helper object used throughout query (and related (count, aggregate, reporting)) + ** actions that need to track joins and aliases. + *******************************************************************************/ +public class JoinsContext +{ + private final QInstance instance; + private final String mainTableName; + private final List queryJoins; + private final Map aliasToTableNameMap = new HashMap<>(); + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public JoinsContext(QInstance instance, String tableName, List queryJoins) throws QException + { + this.instance = instance; + this.mainTableName = tableName; + this.queryJoins = CollectionUtils.nonNullList(queryJoins); + + for(QueryJoin queryJoin : this.queryJoins) + { + QTableMetaData joinTable = instance.getTable(queryJoin.getRightTable()); + String tableNameOrAlias = queryJoin.getAliasOrRightTable(); + if(aliasToTableNameMap.containsKey(tableNameOrAlias)) + { + throw (new QException("Duplicate table name or alias: " + tableNameOrAlias)); + } + aliasToTableNameMap.put(tableNameOrAlias, joinTable.getName()); + } + } + + + + /******************************************************************************* + ** Getter for queryJoins + ** + *******************************************************************************/ + public List getQueryJoins() + { + return queryJoins; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String resolveTableNameOrAliasToTableName(String nameOrAlias) + { + if(aliasToTableNameMap.containsKey(nameOrAlias)) + { + return (aliasToTableNameMap.get(nameOrAlias)); + } + return (nameOrAlias); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public FieldAndTableNameOrAlias getFieldAndTableNameOrAlias(String fieldName) + { + if(fieldName.contains(".")) + { + String[] parts = fieldName.split("\\."); + if(parts.length != 2) + { + throw new IllegalArgumentException("Mal-formatted field name in query: " + fieldName); + } + + String tableOrAlias = parts[0]; + String baseFieldName = parts[1]; + String tableName = resolveTableNameOrAliasToTableName(tableOrAlias); + + QTableMetaData table = instance.getTable(tableName); + if(table == null) + { + throw new IllegalArgumentException("Could not find table [" + tableName + "] in instance for query"); + } + return new FieldAndTableNameOrAlias(table.getField(baseFieldName), tableOrAlias); + } + + return new FieldAndTableNameOrAlias(instance.getTable(mainTableName).getField(fieldName), mainTableName); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public record FieldAndTableNameOrAlias(QFieldMetaData field, String tableNameOrAlias) + { + } + +} 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 6c87d1a0..47b3d45d 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 @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -43,6 +44,8 @@ public class QFilterCriteria implements Serializable, Cloneable private QCriteriaOperator operator; private List values; + private String otherFieldName; + /******************************************************************************* @@ -220,6 +223,40 @@ public class QFilterCriteria implements Serializable, Cloneable + /******************************************************************************* + ** Getter for otherFieldName + ** + *******************************************************************************/ + public String getOtherFieldName() + { + return otherFieldName; + } + + + + /******************************************************************************* + ** Setter for otherFieldName + ** + *******************************************************************************/ + public void setOtherFieldName(String otherFieldName) + { + this.otherFieldName = otherFieldName; + } + + + + /******************************************************************************* + ** Fluent setter for otherFieldName + ** + *******************************************************************************/ + public QFilterCriteria withOtherFieldName(String otherFieldName) + { + this.otherFieldName = otherFieldName; + return (this); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -232,21 +269,28 @@ public class QFilterCriteria implements Serializable, Cloneable rs.append(" ").append(operator).append(" "); if(CollectionUtils.nullSafeHasContents(values)) { - if(values.size() == 1) + if(StringUtils.hasContent(otherFieldName)) { - rs.append(values.get(0)); + rs.append(otherFieldName); } else { - int index = 0; - for(Serializable value : values) + if(values.size() == 1) { - if(index++ > 9) + rs.append(values.get(0)); + } + else + { + int index = 0; + for(Serializable value : values) { - rs.append("and ").append(values.size() - index).append(" more"); - break; + if(index++ > 9) + { + rs.append("and ").append(values.size() - index).append(" more"); + break; + } + rs.append(value).append(","); } - rs.append(value).append(","); } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java index 6ca98286..12d9790d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java @@ -22,6 +22,8 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; +import java.util.ArrayList; +import java.util.List; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; @@ -45,6 +47,8 @@ public class QueryInput extends AbstractTableActionInput private boolean shouldTranslatePossibleValues = false; private boolean shouldGenerateDisplayValues = false; + private List queryJoins = null; + /******************************************************************************* @@ -241,4 +245,54 @@ public class QueryInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for queryJoins + ** + *******************************************************************************/ + public List getQueryJoins() + { + return queryJoins; + } + + + + /******************************************************************************* + ** Setter for queryJoins + ** + *******************************************************************************/ + public void setQueryJoins(List queryJoins) + { + this.queryJoins = queryJoins; + } + + + + /******************************************************************************* + ** Fluent setter for queryJoins + ** + *******************************************************************************/ + public QueryInput withQueryJoins(List queryJoins) + { + this.queryJoins = queryJoins; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryJoins + ** + *******************************************************************************/ + public QueryInput withQueryJoin(QueryJoin queryJoin) + { + if(this.queryJoins == null) + { + this.queryJoins = new ArrayList<>(); + } + this.queryJoins.add(queryJoin); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java new file mode 100644 index 00000000..def81c53 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryJoin.java @@ -0,0 +1,306 @@ +/* + * 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 com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + ** Part of query (or count, aggregate) input, to do a Join as part of a query. + *******************************************************************************/ +public class QueryJoin +{ + private String leftTableOrAlias; + private String rightTable; + private QJoinMetaData joinMetaData; + private String alias; + private boolean select = false; + private Type type = Type.INNER; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public enum Type + {INNER, LEFT, RIGHT, FULL} + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QueryJoin() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QueryJoin(String leftTableOrAlias, String rightTable) + { + this.leftTableOrAlias = leftTableOrAlias; + this.rightTable = rightTable; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QueryJoin(QJoinMetaData joinMetaData) + { + setJoinMetaData(joinMetaData); + } + + + + /******************************************************************************* + ** Getter for leftTableOrAlias + ** + *******************************************************************************/ + public String getLeftTableOrAlias() + { + return leftTableOrAlias; + } + + + + /******************************************************************************* + ** Setter for leftTableOrAlias + ** + *******************************************************************************/ + public void setLeftTableOrAlias(String leftTableOrAlias) + { + this.leftTableOrAlias = leftTableOrAlias; + } + + + + /******************************************************************************* + ** Fluent setter for leftTableOrAlias + ** + *******************************************************************************/ + public QueryJoin withLeftTableOrAlias(String leftTableOrAlias) + { + this.leftTableOrAlias = leftTableOrAlias; + return (this); + } + + + + /******************************************************************************* + ** Getter for rightTable + ** + *******************************************************************************/ + public String getRightTable() + { + return rightTable; + } + + + + /******************************************************************************* + ** Setter for rightTable + ** + *******************************************************************************/ + public void setRightTable(String rightTable) + { + this.rightTable = rightTable; + } + + + + /******************************************************************************* + ** Fluent setter for rightTable + ** + *******************************************************************************/ + public QueryJoin withRightTable(String rightTable) + { + this.rightTable = rightTable; + return (this); + } + + + + /******************************************************************************* + ** Getter for alias + ** + *******************************************************************************/ + public String getAlias() + { + return alias; + } + + + + /******************************************************************************* + ** Setter for alias + ** + *******************************************************************************/ + public void setAlias(String alias) + { + this.alias = alias; + } + + + + /******************************************************************************* + ** Fluent setter for alias + ** + *******************************************************************************/ + public QueryJoin withAlias(String alias) + { + this.alias = alias; + return (this); + } + + + + /******************************************************************************* + ** Getter for select + ** + *******************************************************************************/ + public boolean getSelect() + { + return select; + } + + + + /******************************************************************************* + ** Setter for select + ** + *******************************************************************************/ + public void setSelect(boolean select) + { + this.select = select; + } + + + + /******************************************************************************* + ** Fluent setter for select + ** + *******************************************************************************/ + public QueryJoin withSelect(boolean select) + { + this.select = select; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getAliasOrRightTable() + { + if(StringUtils.hasContent(alias)) + { + return (alias); + } + return (rightTable); + } + + + + /******************************************************************************* + ** Getter for type + ** + *******************************************************************************/ + public Type getType() + { + return type; + } + + + + /******************************************************************************* + ** Setter for type + ** + *******************************************************************************/ + public void setType(Type type) + { + this.type = type; + } + + + + /******************************************************************************* + ** Fluent setter for type + ** + *******************************************************************************/ + public QueryJoin withType(Type type) + { + this.type = type; + return (this); + } + + + + /******************************************************************************* + ** Getter for joinMetaData + ** + *******************************************************************************/ + public QJoinMetaData getJoinMetaData() + { + return joinMetaData; + } + + + + /******************************************************************************* + ** Setter for joinMetaData + ** + *******************************************************************************/ + public void setJoinMetaData(QJoinMetaData joinMetaData) + { + this.joinMetaData = joinMetaData; + + if(!StringUtils.hasContent(this.leftTableOrAlias) && !StringUtils.hasContent(this.rightTable)) + { + setLeftTableOrAlias(joinMetaData.getLeftTable()); + setRightTable(joinMetaData.getRightTable()); + } + } + + + + /******************************************************************************* + ** Fluent setter for joinMetaData + ** + *******************************************************************************/ + public QueryJoin withJoinMetaData(QJoinMetaData joinMetaData) + { + setJoinMetaData(joinMetaData); + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/JoinOn.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/JoinOn.java index 13f5ee76..0dc8ed98 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/JoinOn.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/JoinOn.java @@ -23,7 +23,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.joins; /******************************************************************************* - ** + ** Specification for (at least part of) how two tables join together - e.g., + ** leftField = rightField. Used as part of a list in a QJoinMetaData. *******************************************************************************/ public class JoinOn { @@ -54,6 +55,16 @@ public class JoinOn + /******************************************************************************* + ** Return a new JoinOn, with the fields of this one, but flipped (right ←→ left) + *******************************************************************************/ + public JoinOn flip() + { + return new JoinOn(rightField, leftField); + } + + + /******************************************************************************* ** Getter for leftField ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/JoinType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/JoinType.java index 1e575b08..dfbf812b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/JoinType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/JoinType.java @@ -25,7 +25,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.joins; /******************************************************************************* ** Type for a QJoin. ** - ** - One to One - what about zero?? + ** - One to One - or zero, i guess... ** - One to Many - e.g., where the parent record really "owns" all of the child ** records. Like Order -> OrderLine. ** - Many to One - e.g., where a child references a parent, but we'd never really @@ -37,5 +37,21 @@ public enum JoinType ONE_TO_ONE, ONE_TO_MANY, MANY_TO_ONE, - MANY_TO_MANY + MANY_TO_MANY; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:indentation") + public JoinType flip() + { + return switch(this) + { + case ONE_TO_MANY -> MANY_TO_ONE; + case MANY_TO_ONE -> ONE_TO_MANY; + case MANY_TO_MANY, ONE_TO_ONE -> this; + }; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/QJoinMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/QJoinMetaData.java index 68b0b1ee..d81f2375 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/QJoinMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/joins/QJoinMetaData.java @@ -29,7 +29,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* - ** + ** Definition of how 2 tables join together within a QQQ Instance. *******************************************************************************/ public class QJoinMetaData { @@ -43,6 +43,22 @@ public class QJoinMetaData + /******************************************************************************* + ** Return a new QJoinMetaData, with all the same data as this one, but flipped + ** right ←→ left, for the tables, the type, and the joinOns. + *******************************************************************************/ + public QJoinMetaData flip() + { + return (new QJoinMetaData() + .withLeftTable(rightTable) + .withRightTable(leftTable) + .withType(type.flip()) + .withJoinOns(joinOns.stream().map(JoinOn::flip).toList())); + // todo - what about order bys?? + } + + + /******************************************************************************* ** Getter for name ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java index f0c96b5e..260e51bb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java @@ -22,7 +22,10 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting; +import java.util.ArrayList; +import java.util.List; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; @@ -32,9 +35,12 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; *******************************************************************************/ public class QReportDataSource { - private String name; - private String sourceTable; - private QQueryFilter queryFilter; + private String name; + + private String sourceTable; + private QQueryFilter queryFilter; + private List queryJoins = null; + private QCodeReference staticDataSupplier; @@ -174,4 +180,54 @@ public class QReportDataSource return (this); } + + + /******************************************************************************* + ** Getter for queryJoins + ** + *******************************************************************************/ + public List getQueryJoins() + { + return queryJoins; + } + + + + /******************************************************************************* + ** Setter for queryJoins + ** + *******************************************************************************/ + public void setQueryJoins(List queryJoins) + { + this.queryJoins = queryJoins; + } + + + + /******************************************************************************* + ** Fluent setter for queryJoins + ** + *******************************************************************************/ + public QReportDataSource withQueryJoins(List queryJoins) + { + this.queryJoins = queryJoins; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryJoins + ** + *******************************************************************************/ + public QReportDataSource withQueryJoin(QueryJoin queryJoin) + { + if(this.queryJoins == null) + { + this.queryJoins = new ArrayList<>(); + } + this.queryJoins.add(queryJoin); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java index 835e08a8..ab8885c7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java @@ -45,6 +45,27 @@ public class QReportField + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QReportField() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QReportField(String name) + { + this.name = name; + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java index 67ef09b1..772a0690 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting; +import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; @@ -150,6 +151,21 @@ public class QReportMetaData implements QAppChildMetaData + /******************************************************************************* + ** + *******************************************************************************/ + public QReportMetaData withInputField(QFieldMetaData inputField) + { + if(this.inputFields == null) + { + this.inputFields = new ArrayList<>(); + } + this.inputFields.add(inputField); + return (this); + } + + + /******************************************************************************* ** Getter for processName ** @@ -218,6 +234,22 @@ public class QReportMetaData implements QAppChildMetaData + /******************************************************************************* + ** Fluent setter for dataSources + ** + *******************************************************************************/ + public QReportMetaData withDataSource(QReportDataSource dataSource) + { + if(this.dataSources == null) + { + this.dataSources = new ArrayList<>(); + } + this.dataSources.add(dataSource); + return (this); + } + + + /******************************************************************************* ** Getter for views ** @@ -252,6 +284,22 @@ public class QReportMetaData implements QAppChildMetaData + /******************************************************************************* + ** Fluent setter for views + ** + *******************************************************************************/ + public QReportMetaData withView(QReportView view) + { + if(this.views == null) + { + this.views = new ArrayList<>(); + } + this.views.add(view); + return (this); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java index cd4c5aca..09ad980c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/RunReportForRecordProcess.java @@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMeta import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; /******************************************************************************* @@ -63,6 +64,21 @@ public class RunReportForRecordProcess + /******************************************************************************* + ** Create a process meta data builder for this type of process, pre-populated + ** with attributes based on a given report. + *******************************************************************************/ + public static Builder processMetaDataBuilder(QReportMetaData reportMetaData) + { + return (new Builder(defineProcessMetaData()) + .withProcessName(reportMetaData.getProcessName()) + .withReportName(reportMetaData.getName()) + .withTableName(reportMetaData.getDataSources().get(0).getSourceTable()) + .withIcon(reportMetaData.getIcon())); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index 8cbfc987..84033870 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -55,6 +55,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; @@ -1344,11 +1345,35 @@ class QInstanceValidatorTest @Test void testReportDataSourceNames() { - assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setName(null), + assertValidationFailureReasons((qInstance) -> + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // enricher will give us a default name if only 1 data source, so, set 1st one to null name, then add a second // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + QReportMetaData report = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON); + report.setDataSources(new ArrayList<>(report.getDataSources())); + report.getDataSources().get(0).setName(null); + report.getDataSources().add(new QReportDataSource() + .withName("2nd") + .withSourceTable(TestUtils.TABLE_NAME_PERSON) + ); + }, "Missing name for a dataSource", "unrecognized dataSourceName"); - assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setName(""), + assertValidationFailureReasons((qInstance) -> + { + /////////////////////////////////// + // same as above, but "" vs null // + /////////////////////////////////// + QReportMetaData report = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON); + report.setDataSources(new ArrayList<>(report.getDataSources())); + report.getDataSources().get(0).setName(""); + report.getDataSources().add(new QReportDataSource() + .withName("2nd") + .withSourceTable(TestUtils.TABLE_NAME_PERSON) + ); + }, "Missing name for a dataSource", "unrecognized dataSourceName"); 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 aed6c691..b0585069 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 @@ -27,7 +27,9 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; @@ -36,12 +38,17 @@ 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.JoinsContext; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; +import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; 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; @@ -168,9 +175,88 @@ public abstract class AbstractRDBMSAction implements QActionInterface /******************************************************************************* ** *******************************************************************************/ - protected String makeWhereClause(QTableMetaData table, QQueryFilter filter, List params) throws IllegalArgumentException + protected String makeFromClause(QInstance instance, String tableName, JoinsContext joinsContext) throws QException { - String clause = makeWhereClause(table, filter.getCriteria(), filter.getBooleanOperator(), params); + StringBuilder rs = new StringBuilder(escapeIdentifier(getTableName(instance.getTable(tableName))) + " AS " + escapeIdentifier(tableName)); + + for(QueryJoin queryJoin : joinsContext.getQueryJoins()) + { + QTableMetaData joinTable = instance.getTable(queryJoin.getRightTable()); + String tableNameOrAlias = queryJoin.getAliasOrRightTable(); + + rs.append(" ").append(queryJoin.getType()).append(" JOIN ") + .append(escapeIdentifier(getTableName(joinTable))) + .append(" AS ").append(escapeIdentifier(tableNameOrAlias)); + + //////////////////////////////////////////////////////////// + // find the join in the instance, to see the 'on' clause // + //////////////////////////////////////////////////////////// + List joinClauseList = new ArrayList<>(); + String leftTableName = joinsContext.resolveTableNameOrAliasToTableName(queryJoin.getLeftTableOrAlias()); + QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () -> findJoinMetaData(instance, leftTableName, queryJoin.getRightTable())); + for(JoinOn joinOn : joinMetaData.getJoinOns()) + { + QTableMetaData leftTable = instance.getTable(joinMetaData.getLeftTable()); + QTableMetaData rightTable = instance.getTable(joinMetaData.getRightTable()); + + String leftTableOrAlias = queryJoin.getLeftTableOrAlias(); + String aliasOrRightTable = queryJoin.getAliasOrRightTable(); + + joinClauseList.add(escapeIdentifier(leftTableOrAlias) + + "." + escapeIdentifier(getColumnName(leftTable.getField(joinOn.getLeftField()))) + + " = " + escapeIdentifier(aliasOrRightTable) + + "." + escapeIdentifier(getColumnName((rightTable.getField(joinOn.getRightField()))))); + } + rs.append(" ON ").append(StringUtils.join(" AND ", joinClauseList)); + } + + return (rs.toString()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QJoinMetaData findJoinMetaData(QInstance instance, String leftTable, String rightTable) + { + List matches = new ArrayList<>(); + for(QJoinMetaData join : instance.getJoins().values()) + { + if(join.getLeftTable().equals(leftTable) && join.getRightTable().equals(rightTable)) + { + matches.add(join); + } + + ////////////////////////////// + // look in both directions! // + ////////////////////////////// + if(join.getRightTable().equals(leftTable) && join.getLeftTable().equals(rightTable)) + { + matches.add(join.flip()); + } + } + + if(matches.size() == 1) + { + return (matches.get(0)); + } + else if(matches.size() > 1) + { + throw (new RuntimeException("More than 1 join was found between [" + leftTable + "] and [" + rightTable + "]. Specify which one in your QueryJoin.")); + } + + return (null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected String makeWhereClause(QInstance instance, QTableMetaData table, JoinsContext joinsContext, QQueryFilter filter, List params) throws IllegalArgumentException, QException + { + String clause = makeSimpleWhereClause(instance, table, joinsContext, filter.getCriteria(), filter.getBooleanOperator(), params); if(!CollectionUtils.nullSafeHasContents(filter.getSubFilters())) { /////////////////////////////////////////////////////////////// @@ -189,7 +275,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface } for(QQueryFilter subFilter : filter.getSubFilters()) { - String subClause = makeWhereClause(table, subFilter, params); + String subClause = makeWhereClause(instance, table, joinsContext, subFilter, params); if(StringUtils.hasContent(subClause)) { clauses.add("(" + subClause + ")"); @@ -203,14 +289,16 @@ public abstract class AbstractRDBMSAction implements QActionInterface /******************************************************************************* ** *******************************************************************************/ - private String makeWhereClause(QTableMetaData table, List criteria, QQueryFilter.BooleanOperator booleanOperator, List params) throws IllegalArgumentException + private String makeSimpleWhereClause(QInstance instance, QTableMetaData table, JoinsContext joinsContext, List criteria, QQueryFilter.BooleanOperator booleanOperator, List params) throws IllegalArgumentException { List clauses = new ArrayList<>(); for(QFilterCriteria criterion : criteria) { - QFieldMetaData field = table.getField(criterion.getFieldName()); + JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(criterion.getFieldName()); + List values = criterion.getValues() == null ? new ArrayList<>() : new ArrayList<>(criterion.getValues()); - String column = getColumnName(field); + QFieldMetaData field = fieldAndTableNameOrAlias.field(); + String column = escapeIdentifier(fieldAndTableNameOrAlias.tableNameOrAlias()) + "." + escapeIdentifier(getColumnName(field)); String clause = column; Integer expectedNoOfParams = null; switch(criterion.getOperator()) @@ -360,15 +448,29 @@ public abstract class AbstractRDBMSAction implements QActionInterface throw new IllegalArgumentException("Unexpected operator: " + criterion.getOperator()); } } - clauses.add("(" + clause + ")"); + if(expectedNoOfParams != null) { - if(!expectedNoOfParams.equals(values.size())) + if(expectedNoOfParams.equals(1) && StringUtils.hasContent(criterion.getOtherFieldName())) + { + JoinsContext.FieldAndTableNameOrAlias otherFieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(criterion.getOtherFieldName()); + + String otherColumn = escapeIdentifier(otherFieldAndTableNameOrAlias.tableNameOrAlias()) + "." + escapeIdentifier(getColumnName(otherFieldAndTableNameOrAlias.field())); + clause = clause.replace("?", otherColumn); + + ///////////////////////////////////////////////////////////////////// + // make sure we don't add any values in this case, just in case... // + ///////////////////////////////////////////////////////////////////// + values = Collections.emptyList(); + } + else if(!expectedNoOfParams.equals(values.size())) { throw new IllegalArgumentException("Incorrect number of values given for criteria [" + field.getName() + "]"); } } + clauses.add("(" + clause + ")"); + params.addAll(values); } @@ -470,7 +572,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface /******************************************************************************* ** *******************************************************************************/ - protected String makeOrderByClause(QTableMetaData table, List orderBys) + protected String makeOrderByClause(QTableMetaData table, List orderBys, JoinsContext joinsContext) { List clauses = new ArrayList<>(); @@ -485,9 +587,11 @@ public abstract class AbstractRDBMSAction implements QActionInterface } else { - QFieldMetaData field = table.getField(orderBy.getFieldName()); - String column = escapeIdentifier(getColumnName(field)); - clauses.add(column + " " + ascOrDesc); + JoinsContext.FieldAndTableNameOrAlias otherFieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(orderBy.getFieldName()); + + QFieldMetaData field = otherFieldAndTableNameOrAlias.field(); + String column = getColumnName(field); + clauses.add(escapeIdentifier(otherFieldAndTableNameOrAlias.tableNameOrAlias()) + "." + escapeIdentifier(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 index 99c422b0..912653e3 100644 --- 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 @@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateIn 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.JoinsContext; 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; @@ -61,29 +62,30 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega { try { - QTableMetaData table = aggregateInput.getTable(); - String tableName = getTableName(table); + QTableMetaData table = aggregateInput.getTable(); - List selectClauses = buildSelectClauses(aggregateInput); + JoinsContext joinsContext = new JoinsContext(aggregateInput.getInstance(), table.getName(), aggregateInput.getQueryJoins()); + String fromClause = makeFromClause(aggregateInput.getInstance(), table.getName(), joinsContext); + List selectClauses = buildSelectClauses(aggregateInput, joinsContext); String sql = "SELECT " + StringUtils.join(", ", selectClauses) - + " FROM " + escapeIdentifier(tableName); + + " FROM " + fromClause; QQueryFilter filter = aggregateInput.getFilter(); List params = new ArrayList<>(); if(filter != null && filter.hasAnyCriteria()) { - sql += " WHERE " + makeWhereClause(table, filter, params); + sql += " WHERE " + makeWhereClause(aggregateInput.getInstance(), table, joinsContext, filter, params); } if(CollectionUtils.nullSafeHasContents(aggregateInput.getGroupByFieldNames())) { - sql += " GROUP BY " + makeGroupByClause(aggregateInput); + sql += " GROUP BY " + makeGroupByClause(aggregateInput, joinsContext); } if(filter != null && CollectionUtils.nullSafeHasContents(filter.getOrderBys())) { - sql += " ORDER BY " + makeOrderByClause(table, filter.getOrderBys()); + sql += " ORDER BY " + makeOrderByClause(table, filter.getOrderBys(), joinsContext); } // todo sql customization - can edit sql and/or param list @@ -105,13 +107,16 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega int selectionIndex = 1; for(String groupByFieldName : CollectionUtils.nonNullList(aggregateInput.getGroupByFieldNames())) { - Serializable value = getFieldValueFromResultSet(table.getField(groupByFieldName), resultSet, selectionIndex++); + JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(groupByFieldName); + Serializable value = getFieldValueFromResultSet(fieldAndTableNameOrAlias.field(), resultSet, selectionIndex++); result.withGroupByValue(groupByFieldName, value); } for(Aggregate aggregate : aggregateInput.getAggregates()) { - QFieldMetaData field = table.getField(aggregate.getFieldName()); + JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(aggregate.getFieldName()); + QFieldMetaData field = fieldAndTableNameOrAlias.field(); + if(field.getType().equals(QFieldType.INTEGER) && aggregate.getOperator().equals(AggregateOperator.AVG)) { field = new QFieldMetaData().withType(QFieldType.DECIMAL); @@ -139,19 +144,20 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega /******************************************************************************* ** *******************************************************************************/ - private List buildSelectClauses(AggregateInput aggregateInput) + private List buildSelectClauses(AggregateInput aggregateInput, JoinsContext joinsContext) { - QTableMetaData table = aggregateInput.getTable(); - List rs = new ArrayList<>(); + List rs = new ArrayList<>(); for(String groupByFieldName : CollectionUtils.nonNullList(aggregateInput.getGroupByFieldNames())) { - rs.add(escapeIdentifier(getColumnName(table.getField(groupByFieldName)))); + JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(groupByFieldName); + rs.add(escapeIdentifier(fieldAndTableNameOrAlias.tableNameOrAlias()) + "." + escapeIdentifier(getColumnName(fieldAndTableNameOrAlias.field()))); } for(Aggregate aggregate : aggregateInput.getAggregates()) { - rs.add(aggregate.getOperator() + "(" + escapeIdentifier(getColumnName(table.getField(aggregate.getFieldName()))) + ")"); + JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(aggregate.getFieldName()); + rs.add(aggregate.getOperator() + "(" + escapeIdentifier(fieldAndTableNameOrAlias.tableNameOrAlias()) + "." + escapeIdentifier(getColumnName(fieldAndTableNameOrAlias.field())) + ")"); } return (rs); } @@ -161,13 +167,13 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega /******************************************************************************* ** *******************************************************************************/ - private String makeGroupByClause(AggregateInput aggregateInput) + private String makeGroupByClause(AggregateInput aggregateInput, JoinsContext joinsContext) { - QTableMetaData table = aggregateInput.getTable(); - List columns = new ArrayList<>(); + List columns = new ArrayList<>(); for(String groupByFieldName : aggregateInput.getGroupByFieldNames()) { - columns.add(escapeIdentifier(getColumnName(table.getField(groupByFieldName)))); + JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(groupByFieldName); + columns.add(escapeIdentifier(fieldAndTableNameOrAlias.tableNameOrAlias()) + "." + escapeIdentifier(getColumnName(fieldAndTableNameOrAlias.field()))); } return (StringUtils.join(",", columns)); diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java index 92b4d752..c3ae3dea 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java @@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; @@ -54,16 +55,18 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf { try { - QTableMetaData table = countInput.getTable(); - String tableName = getTableName(table); + QTableMetaData table = countInput.getTable(); - String sql = "SELECT count(*) as record_count FROM " + escapeIdentifier(tableName); + JoinsContext joinsContext = new JoinsContext(countInput.getInstance(), countInput.getTableName(), countInput.getQueryJoins()); + + String sql = "SELECT count(*) as record_count FROM " + + makeFromClause(countInput.getInstance(), table.getName(), joinsContext); QQueryFilter filter = countInput.getFilter(); List params = new ArrayList<>(); if(filter != null && filter.hasAnyCriteria()) { - sql += " WHERE " + makeWhereClause(table, filter, params); + sql += " WHERE " + makeWhereClause(countInput.getInstance(), table, joinsContext, filter, params); } // todo sql customization - can edit sql and/or param list diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java index 36efffa4..a891b01f 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.io.Serializable; import java.sql.Connection; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; @@ -32,6 +33,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; 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.tables.QTableMetaData; @@ -258,8 +260,9 @@ public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInte List params = new ArrayList<>(); QTableMetaData table = deleteInput.getTable(); - String tableName = getTableName(table); - String whereClause = makeWhereClause(table, filter, params); + String tableName = getTableName(table); + JoinsContext joinsContext = new JoinsContext(deleteInput.getInstance(), table.getName(), Collections.emptyList()); + String whereClause = makeWhereClause(deleteInput.getInstance(), table, joinsContext, filter, params); // todo sql customization - can edit sql and/or param list? String sql = "DELETE FROM " 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 c99271da..2d1916d2 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,10 +34,13 @@ 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.JoinsContext; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +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.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -64,35 +67,34 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf try { QTableMetaData table = queryInput.getTable(); - String tableName = getTableName(table); + String tableName = queryInput.getTableName(); - List fieldList = new ArrayList<>(table.getFields().values()); - String columns = fieldList.stream() - .map(this::getColumnName) - .collect(Collectors.joining(", ")); + StringBuilder sql = new StringBuilder("SELECT ").append(makeSelectClause(queryInput.getInstance(), tableName, queryInput.getQueryJoins())); - String sql = "SELECT " + columns + " FROM " + escapeIdentifier(tableName); + JoinsContext joinsContext = new JoinsContext(queryInput.getInstance(), tableName, queryInput.getQueryJoins()); + sql.append(" FROM ").append(makeFromClause(queryInput.getInstance(), tableName, joinsContext)); QQueryFilter filter = queryInput.getFilter(); List params = new ArrayList<>(); + if(filter != null && filter.hasAnyCriteria()) { - sql += " WHERE " + makeWhereClause(table, filter, params); + sql.append(" WHERE ").append(makeWhereClause(queryInput.getInstance(), table, joinsContext, filter, params)); } if(filter != null && CollectionUtils.nullSafeHasContents(filter.getOrderBys())) { - sql += " ORDER BY " + makeOrderByClause(table, filter.getOrderBys()); + sql.append(" ORDER BY ").append(makeOrderByClause(table, filter.getOrderBys(), joinsContext)); } if(queryInput.getLimit() != null) { - sql += " LIMIT " + queryInput.getLimit(); + sql.append(" LIMIT ").append(queryInput.getLimit()); if(queryInput.getSkip() != null) { // todo - other sql grammars? - sql += " OFFSET " + queryInput.getSkip(); + sql.append(" OFFSET ").append(queryInput.getSkip()); } } @@ -111,10 +113,31 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf needToCloseConnection = true; } + //////////////////////////////////////////////////////////////////////////// + // build the list of fields that will be processed in the result-set loop // + //////////////////////////////////////////////////////////////////////////// + List fieldList = new ArrayList<>(table.getFields().values()); + for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryInput.getQueryJoins())) + { + if(queryJoin.getSelect()) + { + QTableMetaData joinTable = queryInput.getInstance().getTable(queryJoin.getRightTable()); + String tableNameOrAlias = queryJoin.getAliasOrRightTable(); + for(QFieldMetaData joinField : joinTable.getFields().values()) + { + fieldList.add(joinField.clone().withName(tableNameOrAlias + "." + joinField.getName())); + } + } + } + try { - QueryOutput queryOutput = new QueryOutput(queryInput); - PreparedStatement statement = createStatement(connection, sql, queryInput); + ////////////////////////////////////////////// + // execute the query - iterate over results // + ////////////////////////////////////////////// + QueryOutput queryOutput = new QueryOutput(queryInput); + System.out.println(sql); + PreparedStatement statement = createStatement(connection, sql.toString(), queryInput); QueryManager.executeStatement(statement, ((ResultSet resultSet) -> { ResultSetMetaData metaData = resultSet.getMetaData(); @@ -162,6 +185,42 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf + /******************************************************************************* + ** + *******************************************************************************/ + private String makeSelectClause(QInstance instance, String tableName, List queryJoins) throws QException + { + QTableMetaData table = instance.getTable(tableName); + List fieldList = new ArrayList<>(table.getFields().values()); + String columns = fieldList.stream() + .map(field -> escapeIdentifier(tableName) + "." + escapeIdentifier(getColumnName(field))) + .collect(Collectors.joining(", ")); + StringBuilder rs = new StringBuilder(columns); + + for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins)) + { + if(queryJoin.getSelect()) + { + QTableMetaData joinTable = instance.getTable(queryJoin.getRightTable()); + String tableNameOrAlias = queryJoin.getAliasOrRightTable(); + if(joinTable == null) + { + throw new QException("Requested join table [" + queryJoin.getRightTable() + "] is not a defined table."); + } + + List joinFieldList = new ArrayList<>(joinTable.getFields().values()); + String joinColumns = joinFieldList.stream() + .map(field -> escapeIdentifier(tableNameOrAlias) + "." + escapeIdentifier(getColumnName(field))) + .collect(Collectors.joining(", ")); + rs.append(", ").append(joinColumns); + } + } + + return (rs.toString()); + } + + + /******************************************************************************* ** *******************************************************************************/ 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 87694d3b..5b6c1727 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 @@ -29,6 +29,9 @@ 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.joins.JoinOn; +import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType; +import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest; @@ -45,9 +48,15 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; *******************************************************************************/ public class TestUtils { - public static final String DEFAULT_BACKEND_NAME = "default"; + public static final String TABLE_NAME_PERSON = "personTable"; + public static final String TABLE_NAME_PERSONAL_ID_CARD = "personalIdCard"; + public static final String TABLE_NAME_STORE = "store"; + public static final String TABLE_NAME_ORDER = "order"; + public static final String TABLE_NAME_ITEM = "item"; + public static final String TABLE_NAME_ORDER_LINE = "orderLine"; + /******************************************************************************* @@ -81,6 +90,9 @@ public class TestUtils QInstance qInstance = new QInstance(); qInstance.addBackend(defineBackend()); qInstance.addTable(defineTablePerson()); + qInstance.addTable(defineTablePersonalIdCard()); + qInstance.addJoin(defineJoinPersonAndPersonalIdCard()); + addOmsTablesAndJoins(qInstance); qInstance.setAuthentication(defineAuthentication()); return (qInstance); } @@ -121,9 +133,9 @@ public class TestUtils public static QTableMetaData defineTablePerson() { return new QTableMetaData() - .withName("a-person") // use this name, so it isn't the same as the actual database-table name (which must come from the backend details) + .withName(TABLE_NAME_PERSON) .withLabel("Person") - .withBackendName(defineBackend().getName()) + .withBackendName(DEFAULT_BACKEND_NAME) .withPrimaryKeyField("id") .withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date")) @@ -139,4 +151,136 @@ public class TestUtils .withTableName("person")); } + + + /******************************************************************************* + ** Define a 1:1 table with Person. + ** + *******************************************************************************/ + private static QTableMetaData defineTablePersonalIdCard() + { + return new QTableMetaData() + .withName(TABLE_NAME_PERSONAL_ID_CARD) + .withLabel("Personal Id Card") + .withBackendName(DEFAULT_BACKEND_NAME) + .withBackendDetails(new RDBMSTableBackendDetails() + .withTableName("personal_id_card")) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date")) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date")) + .withField(new QFieldMetaData("personId", QFieldType.INTEGER).withBackendName("person_id")) + .withField(new QFieldMetaData("idNumber", QFieldType.STRING).withBackendName("id_number")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QJoinMetaData defineJoinPersonAndPersonalIdCard() + { + return new QJoinMetaData() + .withLeftTable(TABLE_NAME_PERSON) + .withRightTable(TABLE_NAME_PERSONAL_ID_CARD) + .withInferredName() + .withType(JoinType.ONE_TO_ONE) + .withJoinOn(new JoinOn("id", "personId")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void addOmsTablesAndJoins(QInstance qInstance) + { + qInstance.addTable(defineBaseTable(TABLE_NAME_STORE, "store") + .withField(new QFieldMetaData("name", QFieldType.STRING)) + ); + + qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER, "order") + .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id")) + .withField(new QFieldMetaData("billToPersonId", QFieldType.INTEGER).withBackendName("bill_to_person_id")) + .withField(new QFieldMetaData("shipToPersonId", QFieldType.INTEGER).withBackendName("ship_to_person_id")) + ); + + qInstance.addTable(defineBaseTable(TABLE_NAME_ITEM, "item") + .withField(new QFieldMetaData("sku", QFieldType.STRING)) + .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id")) + ); + + qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER_LINE, "order_line") + .withField(new QFieldMetaData("orderId", QFieldType.INTEGER).withBackendName("order_id")) + .withField(new QFieldMetaData("sku", QFieldType.STRING)) + .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id")) + .withField(new QFieldMetaData("quantity", QFieldType.INTEGER)) + ); + + qInstance.addJoin(new QJoinMetaData() + .withName("orderJoinStore") + .withLeftTable(TABLE_NAME_ORDER) + .withRightTable(TABLE_NAME_STORE) + .withType(JoinType.MANY_TO_ONE) + .withJoinOn(new JoinOn("storeId", "id")) + ); + + qInstance.addJoin(new QJoinMetaData() + .withName("orderJoinBillToPerson") + .withLeftTable(TABLE_NAME_ORDER) + .withRightTable(TABLE_NAME_PERSON) + .withType(JoinType.MANY_TO_ONE) + .withJoinOn(new JoinOn("billToPersonId", "id")) + ); + + qInstance.addJoin(new QJoinMetaData() + .withName("orderJoinShipToPerson") + .withLeftTable(TABLE_NAME_ORDER) + .withRightTable(TABLE_NAME_PERSON) + .withType(JoinType.MANY_TO_ONE) + .withJoinOn(new JoinOn("shipToPersonId", "id")) + ); + + qInstance.addJoin(new QJoinMetaData() + .withName("itemJoinStore") + .withLeftTable(TABLE_NAME_ITEM) + .withRightTable(TABLE_NAME_STORE) + .withType(JoinType.MANY_TO_ONE) + .withJoinOn(new JoinOn("storeId", "id")) + ); + + qInstance.addJoin(new QJoinMetaData() + .withName("orderJoinOrderLine") + .withLeftTable(TABLE_NAME_ORDER) + .withRightTable(TABLE_NAME_ORDER_LINE) + .withType(JoinType.ONE_TO_MANY) + .withJoinOn(new JoinOn("id", "orderId")) + ); + + qInstance.addJoin(new QJoinMetaData() + .withName("orderLineJoinItem") + .withLeftTable(TABLE_NAME_ORDER_LINE) + .withRightTable(TABLE_NAME_ITEM) + .withType(JoinType.MANY_TO_ONE) + .withJoinOn(new JoinOn("sku", "sku")) + .withJoinOn(new JoinOn("storeId", "storeId")) + ); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QTableMetaData defineBaseTable(String tableName, String backendTableName) + { + return new QTableMetaData() + .withName(tableName) + .withBackendName(DEFAULT_BACKEND_NAME) + .withBackendDetails(new RDBMSTableBackendDetails().withTableName(backendTableName)) + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER)); + } + } 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 index 4a323143..6af0fb55 100644 --- 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 @@ -38,13 +38,16 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperat import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; 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.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /******************************************************************************* @@ -294,6 +297,71 @@ public class RDBMSAggregateActionTest extends RDBMSActionTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOmsJoinAggregate() throws Exception + { + AggregateInput aggregateInput = new AggregateInput(TestUtils.defineInstance()); + Aggregate sumOfQuantity = new Aggregate(TestUtils.TABLE_NAME_ORDER_LINE + ".quantity", AggregateOperator.SUM); + aggregateInput.setSession(new QSession()); + aggregateInput.setTableName(TestUtils.TABLE_NAME_ORDER); + aggregateInput.withAggregate(sumOfQuantity); + aggregateInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_ORDER_LINE)); + + AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput); + AggregateResult aggregateResult = aggregateOutput.getResults().get(0); + Assertions.assertEquals(43, aggregateResult.getAggregateValue(sumOfQuantity)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOmsJoinGroupBy() throws Exception + { + AggregateInput aggregateInput = new AggregateInput(TestUtils.defineInstance()); + Aggregate sumOfQuantity = new Aggregate(TestUtils.TABLE_NAME_ORDER_LINE + ".quantity", AggregateOperator.SUM); + aggregateInput.setSession(new QSession()); + aggregateInput.setTableName(TestUtils.TABLE_NAME_ORDER); + aggregateInput.withAggregate(sumOfQuantity); + aggregateInput.withGroupByFieldName(TestUtils.TABLE_NAME_ORDER_LINE + ".sku"); + aggregateInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_ORDER_LINE)); + + AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput); + assertEquals(6, aggregateOutput.getResults().size()); + assertSkuQuantity("QM-1", 30, aggregateOutput.getResults()); + assertSkuQuantity("QM-2", 1, aggregateOutput.getResults()); + assertSkuQuantity("QM-3", 1, aggregateOutput.getResults()); + assertSkuQuantity("QRU-1", 3, aggregateOutput.getResults()); + assertSkuQuantity("QRU-2", 2, aggregateOutput.getResults()); + assertSkuQuantity("QD-1", 6, aggregateOutput.getResults()); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void assertSkuQuantity(String sku, int quantity, List results) + { + for(AggregateResult result : results) + { + if(result.getGroupByValue("orderLine.sku").equals(sku)) + { + assertEquals(quantity, result.getAggregateValues().values().iterator().next()); + return; + } + } + fail("Didn't find SKU " + sku + " in aggregate results"); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountActionTest.java index 02de11c1..ef49e90c 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountActionTest.java @@ -23,16 +23,19 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.util.List; +import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; +import com.kingsrook.qqq.backend.core.model.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.assertEquals; /******************************************************************************* @@ -60,7 +63,7 @@ public class RDBMSCountActionTest extends RDBMSActionTest { CountInput countInput = initCountRequest(); CountOutput countOutput = new RDBMSCountAction().execute(countInput); - Assertions.assertEquals(5, countOutput.getCount(), "Unfiltered query should find all rows"); + assertEquals(5, countOutput.getCount(), "Unfiltered query should find all rows"); } @@ -81,7 +84,7 @@ public class RDBMSCountActionTest extends RDBMSActionTest .withValues(List.of(email))) ); CountOutput countOutput = new RDBMSCountAction().execute(countInput); - Assertions.assertEquals(1, countOutput.getCount(), "Expected # of rows"); + assertEquals(1, countOutput.getCount(), "Expected # of rows"); } @@ -102,7 +105,7 @@ public class RDBMSCountActionTest extends RDBMSActionTest .withValues(List.of(email))) ); CountOutput countOutput = new RDBMSCountAction().execute(countInput); - Assertions.assertEquals(4, countOutput.getCount(), "Expected # of rows"); + assertEquals(4, countOutput.getCount(), "Expected # of rows"); } @@ -114,8 +117,66 @@ public class RDBMSCountActionTest extends RDBMSActionTest { CountInput countInput = new CountInput(); countInput.setInstance(TestUtils.defineInstance()); + countInput.setSession(new QSession()); countInput.setTableName(TestUtils.defineTablePerson().getName()); return countInput; } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOneToOneInnerJoinWithoutWhere() throws QException + { + CountInput countInput = initCountRequest(); + countInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD)); + CountOutput countOutput = new CountAction().execute(countInput); + assertEquals(3, countOutput.getCount(), "Join count should find 3 rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOneToOneLeftJoinWithoutWhere() throws QException + { + CountInput countInput = initCountRequest(); + countInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.LEFT)); + CountOutput countOutput = new CountAction().execute(countInput); + assertEquals(5, countOutput.getCount(), "Left Join count should find 5 rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOneToOneRightJoinWithoutWhere() throws QException + { + CountInput countInput = initCountRequest(); + countInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.RIGHT)); + CountOutput countOutput = new CountAction().execute(countInput); + assertEquals(6, countOutput.getCount(), "Right Join count should find 6 rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOneToOneInnerJoinWithWhere() throws QException + { + CountInput countInput = initCountRequest(); + countInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true)); + countInput.setFilter(new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber", QCriteriaOperator.STARTS_WITH, "1980"))); + CountOutput countOutput = new CountAction().execute(countInput); + assertEquals(2, countOutput.getCount(), "Right Join count should find 2 rows"); + } + } \ No newline at end of file diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index a221bcc5..8253cd70 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; @@ -30,16 +31,21 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; 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.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; /******************************************************************************* @@ -67,7 +73,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest { QueryInput queryInput = initQueryRequest(); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(5, queryOutput.getRecords().size(), "Unfiltered query should find all rows"); + assertEquals(5, queryOutput.getRecords().size(), "Unfiltered query should find all rows"); } @@ -88,8 +94,8 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of(email))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertEquals(email, queryOutput.getRecords().get(0).getValueString("email"), "Should find expected email address"); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(email, queryOutput.getRecords().get(0).getValueString("email"), "Should find expected email address"); } @@ -110,7 +116,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of(email))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").equals(email)), "Should NOT find expected email address"); } @@ -130,7 +136,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of(2, 4))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(2) || r.getValueInteger("id").equals(4)), "Should find expected ids"); } @@ -150,7 +156,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of(2, 3, 4))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(5)), "Should find expected ids"); } @@ -170,7 +176,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of("darin"))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches("darin.*")), "Should find matching email address"); } @@ -190,7 +196,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of("kelkhoff"))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); } @@ -210,7 +216,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of("gmail.com"))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*gmail.com")), "Should find matching email address"); } @@ -230,7 +236,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of("darin"))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches("darin.*")), "Should find matching email address"); } @@ -250,7 +256,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of("kelkhoff"))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); } @@ -270,7 +276,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of("gmail.com"))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*gmail.com")), "Should find matching email address"); } @@ -290,7 +296,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of(3))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(2)), "Should find expected ids"); } @@ -310,7 +316,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of(2))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(2)), "Should find expected ids"); } @@ -330,7 +336,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of(3))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(4) || r.getValueInteger("id").equals(5)), "Should find expected ids"); } @@ -350,7 +356,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of(4))) ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(4) || r.getValueInteger("id").equals(5)), "Should find expected ids"); } @@ -369,7 +375,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withOperator(QCriteriaOperator.IS_BLANK) )); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("birthDate") == null), "Should find expected row"); } @@ -388,7 +394,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withOperator(QCriteriaOperator.IS_NOT_BLANK) )); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(5, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(5, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("firstName") != null), "Should find expected rows"); } @@ -408,7 +414,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of(2, 4)) )); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(3, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(3, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(2) || r.getValueInteger("id").equals(3) || r.getValueInteger("id").equals(4)), "Should find expected ids"); } @@ -428,7 +434,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of(2, 4)) )); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); - Assertions.assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(5)), "Should find expected ids"); } @@ -441,7 +447,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest { QueryInput queryInput = new QueryInput(); queryInput.setInstance(TestUtils.defineInstance()); - queryInput.setTableName(TestUtils.defineTablePerson().getName()); + queryInput.setTableName(TestUtils.TABLE_NAME_PERSON); queryInput.setSession(new QSession()); return queryInput; } @@ -459,7 +465,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest QueryInput queryInput = initQueryRequest(); queryInput.setShouldGenerateDisplayValues(true); QueryOutput queryOutput = new QueryAction().execute(queryInput); - Assertions.assertEquals(5, queryOutput.getRecords().size(), "Unfiltered query should find all rows"); + assertEquals(5, queryOutput.getRecords().size(), "Unfiltered query should find all rows"); for(QRecord record : queryOutput.getRecords()) { @@ -479,7 +485,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest { InsertInput insertInput = new InsertInput(TestUtils.defineInstance()); insertInput.setSession(new QSession()); - insertInput.setTableName(TestUtils.defineTablePerson().getName()); + insertInput.setTableName(TestUtils.TABLE_NAME_PERSON); InsertAction insertAction = new InsertAction(); QBackendTransaction transaction = insertAction.openTransaction(insertInput); @@ -493,12 +499,12 @@ public class RDBMSQueryActionTest extends RDBMSActionTest QueryInput queryInput = initQueryRequest(); QueryOutput queryOutput = new QueryAction().execute(queryInput); - Assertions.assertEquals(5, queryOutput.getRecords().size(), "Query without the transaction should not see the new row."); + assertEquals(5, queryOutput.getRecords().size(), "Query without the transaction should not see the new row."); queryInput = initQueryRequest(); queryInput.setTransaction(transaction); queryOutput = new QueryAction().execute(queryInput); - Assertions.assertEquals(6, queryOutput.getRecords().size(), "Query with the transaction should see the new row."); + assertEquals(6, queryOutput.getRecords().size(), "Query with the transaction should see the new row."); transaction.rollback(); } @@ -514,11 +520,11 @@ public class RDBMSQueryActionTest extends RDBMSActionTest QueryInput queryInput = initQueryRequest(); queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.IN, List.of()))); QueryOutput queryOutput = new QueryAction().execute(queryInput); - Assertions.assertEquals(0, queryOutput.getRecords().size(), "IN empty list should find nothing."); + assertEquals(0, queryOutput.getRecords().size(), "IN empty list should find nothing."); queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.NOT_IN, List.of()))); queryOutput = new QueryAction().execute(queryInput); - Assertions.assertEquals(5, queryOutput.getRecords().size(), "NOT_IN empty list should find everything."); + assertEquals(5, queryOutput.getRecords().size(), "NOT_IN empty list should find everything."); } @@ -536,7 +542,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Tim"))) ); QueryOutput queryOutput = new QueryAction().execute(queryInput); - Assertions.assertEquals(2, queryOutput.getRecords().size(), "OR should find 2 rows"); + assertEquals(2, queryOutput.getRecords().size(), "OR should find 2 rows"); assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin")); assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim")); } @@ -564,7 +570,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest )) ); QueryOutput queryOutput = new QueryAction().execute(queryInput); - Assertions.assertEquals(2, queryOutput.getRecords().size(), "Complex query should find 2 rows"); + assertEquals(2, queryOutput.getRecords().size(), "Complex query should find 2 rows"); assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("lastName").equals("Maes")); assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("lastName").equals("Kelkhoff")); } @@ -592,8 +598,322 @@ public class RDBMSQueryActionTest extends RDBMSActionTest )) ); QueryOutput queryOutput = new QueryAction().execute(queryInput); - Assertions.assertEquals(1, queryOutput.getRecords().size(), "Complex query should find 1 row"); + assertEquals(1, queryOutput.getRecords().size(), "Complex query should find 1 row"); assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("lastName").equals("Chamberlain")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOneToOneInnerJoinWithoutWhere() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true)); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(3, queryOutput.getRecords().size(), "Join query should find 3 rows"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("personalIdCard.idNumber").equals("19760528")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOneToOneLeftJoinWithoutWhere() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.LEFT).withSelect(true)); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size(), "Left Join query should find 5 rows"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("personalIdCard.idNumber").equals("19760528")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Garret") && r.getValue("personalIdCard.idNumber") == null); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tyler") && r.getValue("personalIdCard.idNumber") == null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOneToOneRightJoinWithoutWhere() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.RIGHT).withSelect(true)); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(6, queryOutput.getRecords().size(), "Right Join query should find 6 rows"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("personalIdCard.idNumber").equals("19760528")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValue("firstName") == null && r.getValueString("personalIdCard.idNumber").equals("123123123")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValue("firstName") == null && r.getValueString("personalIdCard.idNumber").equals("987987987")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValue("firstName") == null && r.getValueString("personalIdCard.idNumber").equals("456456456")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOneToOneInnerJoinWithWhere() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true)); + queryInput.setFilter(new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber", QCriteriaOperator.STARTS_WITH, "1980"))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size(), "Join query should find 2 rows"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("personalIdCard.idNumber").equals("19800531")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("personalIdCard.idNumber").equals("19800515")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOneToOneInnerJoinWithOrderBy() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QueryInput queryInput = initQueryRequest(); + queryInput.withQueryJoin(new QueryJoin(qInstance.getJoin(TestUtils.TABLE_NAME_PERSON + "Join" + TestUtils.TABLE_NAME_PERSONAL_ID_CARD)).withSelect(true)); + queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber"))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(3, queryOutput.getRecords().size(), "Join query should find 3 rows"); + List idNumberListFromQuery = queryOutput.getRecords().stream().map(r -> r.getValueString(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")).toList(); + assertEquals(List.of("19760528", "19800515", "19800531"), idNumberListFromQuery); + + ///////////////////////// + // repeat, sorted desc // + ///////////////////////// + queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber", false))); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(3, queryOutput.getRecords().size(), "Join query should find 3 rows"); + idNumberListFromQuery = queryOutput.getRecords().stream().map(r -> r.getValueString(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")).toList(); + assertEquals(List.of("19800531", "19800515", "19760528"), idNumberListFromQuery); + } + + + + /******************************************************************************* + ** In the prime data, we've got 1 order line set up with an item from a different + ** store than its order. Write a query to find such a case. + *******************************************************************************/ + @Test + void testFiveTableOmsJoinFindMismatchedStoreId() throws Exception + { + QueryInput queryInput = new QueryInput(TestUtils.defineInstance(), new QSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_STORE).withAlias("orderStore").withSelect(true)); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_ORDER_LINE).withSelect(true)); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_LINE, TestUtils.TABLE_NAME_ITEM).withSelect(true)); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ITEM, TestUtils.TABLE_NAME_STORE).withAlias("itemStore").withSelect(true)); + + queryInput.setFilter(new QQueryFilter(new QFilterCriteria().withFieldName("orderStore.id").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("item.storeId"))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query"); + QRecord qRecord = queryOutput.getRecords().get(0); + assertEquals(2, qRecord.getValueInteger("id")); + assertEquals(1, qRecord.getValueInteger("orderStore.id")); + assertEquals(2, qRecord.getValueInteger("itemStore.id")); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // run the same setup, but this time, use the other-field-name as itemStore.id, instead of item.storeId // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + queryInput.setFilter(new QQueryFilter(new QFilterCriteria().withFieldName("orderStore.id").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("itemStore.id"))); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query"); + qRecord = queryOutput.getRecords().get(0); + assertEquals(2, qRecord.getValueInteger("id")); + assertEquals(1, qRecord.getValueInteger("orderStore.id")); + assertEquals(2, qRecord.getValueInteger("itemStore.id")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOmsQueryByOrderLines() throws Exception + { + AtomicInteger orderLineCount = new AtomicInteger(); + runTestSql("SELECT COUNT(*) from order_line", (rs) -> + { + rs.next(); + orderLineCount.set(rs.getInt(1)); + }); + + QueryInput queryInput = new QueryInput(TestUtils.defineInstance(), new QSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER_LINE); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER_LINE, TestUtils.TABLE_NAME_ORDER).withSelect(true)); + + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(orderLineCount.get(), queryOutput.getRecords().size(), "# of rows found by query"); + assertEquals(3, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("order.id").equals(1)).count()); + assertEquals(1, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("order.id").equals(2)).count()); + assertEquals(1, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("orderId").equals(3)).count()); + assertEquals(2, queryOutput.getRecords().stream().filter(r -> r.getValueInteger("orderId").equals(4)).count()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOmsQueryByPersons() throws Exception + { + QInstance instance = TestUtils.defineInstance(); + QueryInput queryInput = new QueryInput(instance, new QSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + ///////////////////////////////////////////////////// + // inner join on bill-to person should find 6 rows // + ///////////////////////////////////////////////////// + queryInput.withQueryJoins(List.of(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_PERSON).withJoinMetaData(instance.getJoin("orderJoinBillToPerson")).withSelect(true))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(6, queryOutput.getRecords().size(), "# of rows found by query"); + + ///////////////////////////////////////////////////// + // inner join on ship-to person should find 7 rows // + ///////////////////////////////////////////////////// + queryInput.withQueryJoins(List.of(new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withSelect(true))); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(7, queryOutput.getRecords().size(), "# of rows found by query"); + + ///////////////////////////////////////////////////////////////////////////// + // inner join on both bill-to person and ship-to person should find 5 rows // + ///////////////////////////////////////////////////////////////////////////// + queryInput.withQueryJoins(List.of( + new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true), + new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true) + )); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size(), "# of rows found by query"); + + ///////////////////////////////////////////////////////////////////////////// + // left join on both bill-to person and ship-to person should find 8 rows // + ///////////////////////////////////////////////////////////////////////////// + queryInput.withQueryJoins(List.of( + new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withType(QueryJoin.Type.LEFT).withAlias("shipToPerson").withSelect(true), + new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withType(QueryJoin.Type.LEFT).withAlias("billToPerson").withSelect(true) + )); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(8, queryOutput.getRecords().size(), "# of rows found by query"); + + ////////////////////////////////////////////////// + // now join through to personalIdCard table too // + ////////////////////////////////////////////////// + queryInput.withQueryJoins(List.of( + new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson").withSelect(true), + new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson").withSelect(true), + new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToIdCard").withSelect(true), + new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToIdCard").withSelect(true) + )); + queryInput.setFilter(new QQueryFilter() + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // look for billToPersons w/ idNumber starting with 1980 - should only be James and Darin (assert on that below). // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + .withCriteria(new QFilterCriteria("billToIdCard.idNumber", QCriteriaOperator.STARTS_WITH, "1980")) + ); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(3, queryOutput.getRecords().size(), "# of rows found by query"); + assertThat(queryOutput.getRecords().stream().map(r -> r.getValueString("billToPerson.firstName")).toList()).allMatch(p -> p.equals("Darin") || p.equals("James")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOmsQueryByPersonsExtraKelkhoffOrder() throws Exception + { + QInstance instance = TestUtils.defineInstance(); + QueryInput queryInput = new QueryInput(instance, new QSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // insert a second person w/ last name Kelkhoff, then an order for Darin Kelkhoff and this new Kelkhoff - // + // then query for orders w/ bill to person & ship to person both lastname = Kelkhoff, but different ids. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + Integer specialOrderId = 1701; + runTestSql("INSERT INTO person (id, first_name, last_name, email) VALUES (6, 'Jimmy', 'Kelkhoff', 'dk@gmail.com')", null); + runTestSql("INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (" + specialOrderId + ", 1, 1, 6)", null); + queryInput.withQueryJoins(List.of( + new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withType(QueryJoin.Type.LEFT).withAlias("shipToPerson").withSelect(true), + new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withType(QueryJoin.Type.LEFT).withAlias("billToPerson").withSelect(true) + )); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.lastName").withOperator(QCriteriaOperator.EQUALS).withOtherFieldName("billToPerson.lastName")) + .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.id").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("billToPerson.id")) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query"); + assertEquals(specialOrderId, queryOutput.getRecords().get(0).getValueInteger("id")); + + //////////////////////////////////////////////////////////// + // re-run that query using personIds from the order table // + //////////////////////////////////////////////////////////// + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.lastName").withOperator(QCriteriaOperator.EQUALS).withOtherFieldName("billToPerson.lastName")) + .withCriteria(new QFilterCriteria().withFieldName("order.shipToPersonId").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("order.billToPersonId")) + ); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query"); + assertEquals(specialOrderId, queryOutput.getRecords().get(0).getValueInteger("id")); + + /////////////////////////////////////////////////////////////////////////////////////////////// + // re-run that query using personIds from the order table, but not specifying the table name // + /////////////////////////////////////////////////////////////////////////////////////////////// + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria().withFieldName("shipToPerson.lastName").withOperator(QCriteriaOperator.EQUALS).withOtherFieldName("billToPerson.lastName")) + .withCriteria(new QFilterCriteria().withFieldName("shipToPersonId").withOperator(QCriteriaOperator.NOT_EQUALS).withOtherFieldName("billToPersonId")) + ); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "# of rows found by query"); + assertEquals(specialOrderId, queryOutput.getRecords().get(0).getValueInteger("id")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDuplicateAliases() + { + QInstance instance = TestUtils.defineInstance(); + QueryInput queryInput = new QueryInput(instance, new QSession()); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + queryInput.withQueryJoins(List.of( + new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson"), + new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson"), + new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true), + new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true) // w/o alias, should get exception here - dupe table. + )); + assertThatThrownBy(() -> new QueryAction().execute(queryInput)) + .hasRootCauseMessage("Duplicate table name or alias: personalIdCard"); + + queryInput.withQueryJoins(List.of( + new QueryJoin(instance.getJoin("orderJoinShipToPerson")).withAlias("shipToPerson"), + new QueryJoin(instance.getJoin("orderJoinBillToPerson")).withAlias("billToPerson"), + new QueryJoin("shipToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("shipToPerson").withSelect(true), // dupe alias, should get exception here + new QueryJoin("billToPerson", TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withAlias("billToPerson").withSelect(true) + )); + assertThatThrownBy(() -> new QueryAction().execute(queryInput)) + .hasRootCauseMessage("Duplicate table name or alias: shipToPerson"); + } + } \ 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 69b421d6..0e0f2f8b 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 @@ -41,6 +41,23 @@ INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, a 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 personal_id_card; +CREATE TABLE personal_id_card +( + id INT AUTO_INCREMENT primary key , + create_date TIMESTAMP DEFAULT now(), + modify_date TIMESTAMP DEFAULT now(), + person_id INTEGER, + id_number VARCHAR(250) +); + +INSERT INTO personal_id_card (person_id, id_number) VALUES (1, '19800531'); +INSERT INTO personal_id_card (person_id, id_number) VALUES (2, '19800515'); +INSERT INTO personal_id_card (person_id, id_number) VALUES (3, '19760528'); +INSERT INTO personal_id_card (person_id, id_number) VALUES (6, '123123123'); +INSERT INTO personal_id_card (person_id, id_number) VALUES (null, '987987987'); +INSERT INTO personal_id_card (person_id, id_number) VALUES (null, '456456456'); + DROP TABLE IF EXISTS carrier; CREATE TABLE carrier ( @@ -61,3 +78,77 @@ INSERT INTO carrier (id, name, company_code, service_level) VALUES (8, 'USPS Sup INSERT INTO carrier (id, name, company_code, service_level) VALUES (9, 'USPS Super Fast', 'USPS', '0'); INSERT INTO carrier (id, name, company_code, service_level) VALUES (10, 'DHL International', 'DHL', 'I'); INSERT INTO carrier (id, name, company_code, service_level) VALUES (11, 'GSO', 'GSO', 'G'); + +DROP TABLE IF EXISTS order_line; +DROP TABLE IF EXISTS item; +DROP TABLE IF EXISTS `order`; +DROP TABLE IF EXISTS store; + +CREATE TABLE store +( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80) NOT NULL +); + +-- define 3 stores +INSERT INTO store (id, name) VALUES (1, 'Q-Mart'); +INSERT INTO store (id, name) VALUES (2, 'QQQ ''R'' Us'); +INSERT INTO store (id, name) VALUES (3, 'QDepot'); + +CREATE TABLE item +( + id INT AUTO_INCREMENT PRIMARY KEY, + sku VARCHAR(80) NOT NULL, + store_id INT NOT NULL REFERENCES store +); + +-- three items for each store +INSERT INTO item (id, sku, store_id) VALUES (1, 'QM-1', 1); +INSERT INTO item (id, sku, store_id) VALUES (2, 'QM-2', 1); +INSERT INTO item (id, sku, store_id) VALUES (3, 'QM-3', 1); +INSERT INTO item (id, sku, store_id) VALUES (4, 'QRU-1', 2); +INSERT INTO item (id, sku, store_id) VALUES (5, 'QRU-2', 2); +INSERT INTO item (id, sku, store_id) VALUES (6, 'QRU-3', 2); +INSERT INTO item (id, sku, store_id) VALUES (7, 'QD-1', 3); +INSERT INTO item (id, sku, store_id) VALUES (8, 'QD-2', 3); +INSERT INTO item (id, sku, store_id) VALUES (9, 'QD-3', 3); + +CREATE TABLE `order` +( + id INT AUTO_INCREMENT PRIMARY KEY, + store_id INT REFERENCES store, + bill_to_person_id INT, + ship_to_person_id INT +); + +-- variable orders +INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (1, 1, 1, 1); +INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (2, 1, 1, 2); +INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (3, 1, 2, 3); +INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (4, 2, 4, 5); +INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (5, 2, 5, 4); +INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (6, 3, 5, null); +INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (7, 3, null, 5); +INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (8, 3, null, 5); + +CREATE TABLE order_line +( + id INT AUTO_INCREMENT PRIMARY KEY, + order_id INT REFERENCES `order`, + sku VARCHAR(80), + store_id INT REFERENCES store, -- todo - as a challenge, if this field wasn't here, so we had to join through order... + quantity INT +); + +-- various lines +INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (1, 'QM-1', 1, 10); +INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (1, 'QM-2', 1, 1); +INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (1, 'QM-3', 1, 1); +INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (2, 'QRU-1', 2, 1); -- this line has an item from a different store than its order. +INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (3, 'QM-1', 1, 20); +INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (4, 'QRU-1', 2, 1); +INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (4, 'QRU-2', 2, 2); +INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (5, 'QRU-1', 2, 1); +INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (6, 'QD-1', 3, 1); +INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (7, 'QD-1', 3, 2); +INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (8, 'QD-1', 3, 3);