diff --git a/pom.xml b/pom.xml index be2edde4..43b16a88 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ com.kingsrook.qqq qqq-backend-core - 0.0.0 + 0.1.0-20220708.195335-5 diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java index 752b53c9..0820c159 100644 --- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBMSBackendModule.java @@ -24,11 +24,13 @@ package com.kingsrook.qqq.backend.module.rdbms; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QTableBackendDetails; +import com.kingsrook.qqq.backend.core.modules.interfaces.CountInterface; import com.kingsrook.qqq.backend.core.modules.interfaces.DeleteInterface; import com.kingsrook.qqq.backend.core.modules.interfaces.InsertInterface; import com.kingsrook.qqq.backend.core.modules.interfaces.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.modules.interfaces.QueryInterface; import com.kingsrook.qqq.backend.core.modules.interfaces.UpdateInterface; +import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSCountAction; import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSDeleteAction; import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSInsertAction; import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSQueryAction; @@ -74,6 +76,18 @@ public class RDBMSBackendModule implements QBackendModuleInterface + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public CountInterface getCountInterface() + { + return (new RDBMSCountAction()); + } + + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index 0f1c7110..e8bde0f3 100644 --- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -26,7 +26,12 @@ import java.io.Serializable; import java.sql.Connection; import java.sql.SQLException; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.model.actions.AbstractQTableRequest; +import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; @@ -113,4 +118,182 @@ public abstract class AbstractRDBMSAction return (value); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected String makeWhereClause(QTableMetaData table, List criteria, List params) throws IllegalArgumentException + { + List clauses = new ArrayList<>(); + for(QFilterCriteria criterion : criteria) + { + QFieldMetaData field = table.getField(criterion.getFieldName()); + List values = criterion.getValues() == null ? new ArrayList<>() : new ArrayList<>(criterion.getValues()); + String column = getColumnName(field); + String clause = column; + Integer expectedNoOfParams = null; + switch(criterion.getOperator()) + { + case EQUALS: + { + clause += " = ? "; + expectedNoOfParams = 1; + break; + } + case NOT_EQUALS: + { + clause += " != ? "; + expectedNoOfParams = 1; + break; + } + case IN: + { + clause += " IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ") "; + break; + } + case NOT_IN: + { + clause += " NOT IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ") "; + break; + } + case STARTS_WITH: + { + clause += " LIKE ? "; + editFirstValue(values, (s -> s + "%")); + expectedNoOfParams = 1; + break; + } + case ENDS_WITH: + { + clause += " LIKE ? "; + editFirstValue(values, (s -> "%" + s)); + expectedNoOfParams = 1; + break; + } + case CONTAINS: + { + clause += " LIKE ? "; + editFirstValue(values, (s -> "%" + s + "%")); + expectedNoOfParams = 1; + break; + } + case NOT_STARTS_WITH: + { + clause += " NOT LIKE ? "; + editFirstValue(values, (s -> s + "%")); + expectedNoOfParams = 1; + break; + } + case NOT_ENDS_WITH: + { + clause += " NOT LIKE ? "; + editFirstValue(values, (s -> "%" + s)); + expectedNoOfParams = 1; + break; + } + case NOT_CONTAINS: + { + clause += " NOT LIKE ? "; + editFirstValue(values, (s -> "%" + s + "%")); + expectedNoOfParams = 1; + break; + } + case LESS_THAN: + { + clause += " < ? "; + expectedNoOfParams = 1; + break; + } + case LESS_THAN_OR_EQUALS: + { + clause += " <= ? "; + expectedNoOfParams = 1; + break; + } + case GREATER_THAN: + { + clause += " > ? "; + expectedNoOfParams = 1; + break; + } + case GREATER_THAN_OR_EQUALS: + { + clause += " >= ? "; + expectedNoOfParams = 1; + break; + } + case IS_BLANK: + { + clause += " IS NULL "; + if(isString(field.getType())) + { + clause += " OR " + column + " = '' "; + } + expectedNoOfParams = 0; + break; + } + case IS_NOT_BLANK: + { + clause += " IS NOT NULL "; + if(isString(field.getType())) + { + clause += " AND " + column + " !+ '' "; + } + expectedNoOfParams = 0; + break; + } + case BETWEEN: + { + clause += " BETWEEN ? AND ? "; + expectedNoOfParams = 2; + break; + } + case NOT_BETWEEN: + { + clause += " NOT BETWEEN ? AND ? "; + expectedNoOfParams = 2; + break; + } + default: + { + throw new IllegalArgumentException("Unexpected operator: " + criterion.getOperator()); + } + } + clauses.add("(" + clause + ")"); + if(expectedNoOfParams != null) + { + if(!expectedNoOfParams.equals(values.size())) + { + throw new IllegalArgumentException("Incorrect number of values given for criteria [" + field.getName() + "]"); + } + } + + params.addAll(values); + } + + return (String.join(" AND ", clauses)); + } + + /******************************************************************************* + ** + *******************************************************************************/ + private static void editFirstValue(List values, Function editFunction) + { + if(values.size() > 0) + { + values.set(0, editFunction.apply(String.valueOf(values.get(0)))); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean isString(QFieldType fieldType) + { + return fieldType == QFieldType.STRING || fieldType == QFieldType.TEXT || fieldType == QFieldType.HTML || fieldType == QFieldType.PASSWORD; + } } diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java new file mode 100644 index 00000000..1976b273 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java @@ -0,0 +1,95 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.rdbms.actions; + + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.count.CountRequest; +import com.kingsrook.qqq.backend.core.model.actions.count.CountResult; +import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; +import com.kingsrook.qqq.backend.core.modules.interfaces.CountInterface; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterface +{ + private static final Logger LOG = LogManager.getLogger(RDBMSCountAction.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public CountResult execute(CountRequest countRequest) throws QException + { + try + { + QTableMetaData table = countRequest.getTable(); + String tableName = getTableName(table); + + String sql = "SELECT count(*) as record_count FROM " + tableName; + + QQueryFilter filter = countRequest.getFilter(); + List params = new ArrayList<>(); + if(filter != null && CollectionUtils.nullSafeHasContents(filter.getCriteria())) + { + sql += " WHERE " + makeWhereClause(table, filter.getCriteria(), params); + } + + // todo sql customization - can edit sql and/or param list + + CountResult rs = new CountResult(); + + Connection connection = getConnection(countRequest); + QueryManager.executeStatement(connection, sql, ((ResultSet resultSet) -> + { + ResultSetMetaData metaData = resultSet.getMetaData(); + if(resultSet.next()) + { + rs.setCount(resultSet.getInt("record_count")); + } + + }), params); + + return rs; + } + catch(Exception e) + { + LOG.warn("Error executing count", e); + throw new QException("Error executing count", e); + } + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index 6c892ee8..65f1eba9 100644 --- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -56,6 +56,8 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf { private static final Logger LOG = LogManager.getLogger(RDBMSQueryAction.class); + + /******************************************************************************* ** *******************************************************************************/ @@ -177,186 +179,6 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf - /******************************************************************************* - ** - *******************************************************************************/ - private String makeWhereClause(QTableMetaData table, List criteria, List params) throws IllegalArgumentException - { - List clauses = new ArrayList<>(); - for(QFilterCriteria criterion : criteria) - { - QFieldMetaData field = table.getField(criterion.getFieldName()); - List values = criterion.getValues() == null ? new ArrayList<>() : new ArrayList<>(criterion.getValues()); - String column = getColumnName(field); - String clause = column; - Integer expectedNoOfParams = null; - switch(criterion.getOperator()) - { - case EQUALS: - { - clause += " = ? "; - expectedNoOfParams = 1; - break; - } - case NOT_EQUALS: - { - clause += " != ? "; - expectedNoOfParams = 1; - break; - } - case IN: - { - clause += " IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ") "; - break; - } - case NOT_IN: - { - clause += " NOT IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ") "; - break; - } - case STARTS_WITH: - { - clause += " LIKE ? "; - editFirstValue(values, (s -> s + "%")); - expectedNoOfParams = 1; - break; - } - case ENDS_WITH: - { - clause += " LIKE ? "; - editFirstValue(values, (s -> "%" + s)); - expectedNoOfParams = 1; - break; - } - case CONTAINS: - { - clause += " LIKE ? "; - editFirstValue(values, (s -> "%" + s + "%")); - expectedNoOfParams = 1; - break; - } - case NOT_STARTS_WITH: - { - clause += " NOT LIKE ? "; - editFirstValue(values, (s -> s + "%")); - expectedNoOfParams = 1; - break; - } - case NOT_ENDS_WITH: - { - clause += " NOT LIKE ? "; - editFirstValue(values, (s -> "%" + s)); - expectedNoOfParams = 1; - break; - } - case NOT_CONTAINS: - { - clause += " NOT LIKE ? "; - editFirstValue(values, (s -> "%" + s + "%")); - expectedNoOfParams = 1; - break; - } - case LESS_THAN: - { - clause += " < ? "; - expectedNoOfParams = 1; - break; - } - case LESS_THAN_OR_EQUALS: - { - clause += " <= ? "; - expectedNoOfParams = 1; - break; - } - case GREATER_THAN: - { - clause += " > ? "; - expectedNoOfParams = 1; - break; - } - case GREATER_THAN_OR_EQUALS: - { - clause += " >= ? "; - expectedNoOfParams = 1; - break; - } - case IS_BLANK: - { - clause += " IS NULL "; - if(isString(field.getType())) - { - clause += " OR " + column + " = '' "; - } - expectedNoOfParams = 0; - break; - } - case IS_NOT_BLANK: - { - clause += " IS NOT NULL "; - if(isString(field.getType())) - { - clause += " AND " + column + " !+ '' "; - } - expectedNoOfParams = 0; - break; - } - case BETWEEN: - { - clause += " BETWEEN ? AND ? "; - expectedNoOfParams = 2; - break; - } - case NOT_BETWEEN: - { - clause += " NOT BETWEEN ? AND ? "; - expectedNoOfParams = 2; - break; - } - default: - { - throw new IllegalArgumentException("Unexpected operator: " + criterion.getOperator()); - } - } - clauses.add("(" + clause + ")"); - if(expectedNoOfParams != null) - { - if(!expectedNoOfParams.equals(values.size())) - { - throw new IllegalArgumentException("Incorrect number of values given for criteria [" + field.getName() + "]"); - } - } - - params.addAll(values); - } - - return (String.join(" AND ", clauses)); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private void editFirstValue(List values, Function editFunction) - { - if(values.size() > 0) - { - values.set(0, editFunction.apply(String.valueOf(values.get(0)))); - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private boolean isString(QFieldType fieldType) - { - return fieldType == QFieldType.STRING || fieldType == QFieldType.TEXT || fieldType == QFieldType.HTML || fieldType == QFieldType.PASSWORD; - } - - - /******************************************************************************* ** *******************************************************************************/ diff --git a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountActionTest.java b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountActionTest.java new file mode 100644 index 00000000..62869e7e --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountActionTest.java @@ -0,0 +1,121 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.rdbms.actions; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.count.CountRequest; +import com.kingsrook.qqq.backend.core.model.actions.count.CountResult; +import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; +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; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RDBMSCountActionTest extends RDBMSActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + public void beforeEach() throws Exception + { + super.primeTestDatabase(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testUnfilteredCount() throws QException + { + CountRequest countRequest = initCountRequest(); + CountResult countResult = new RDBMSCountAction().execute(countRequest); + Assertions.assertEquals(5, countResult.getCount(), "Unfiltered query should find all rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testEqualsQueryCount() throws QException + { + String email = "darin.kelkhoff@gmail.com"; + + CountRequest countRequest = initCountRequest(); + countRequest.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.EQUALS) + .withValues(List.of(email))) + ); + CountResult countResult = new RDBMSCountAction().execute(countRequest); + Assertions.assertEquals(1, countResult.getCount(), "Expected # of rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testNotEqualsQuery() throws QException + { + String email = "darin.kelkhoff@gmail.com"; + + CountRequest countRequest = initCountRequest(); + countRequest.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.NOT_EQUALS) + .withValues(List.of(email))) + ); + CountResult countResult = new RDBMSCountAction().execute(countRequest); + Assertions.assertEquals(4, countResult.getCount(), "Expected # of rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private CountRequest initCountRequest() + { + CountRequest countRequest = new CountRequest(); + countRequest.setInstance(TestUtils.defineInstance()); + countRequest.setTableName(TestUtils.defineTablePerson().getName()); + return countRequest; + } + +} \ No newline at end of file