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