diff --git a/.circleci/config.yml b/.circleci/config.yml
index 0ef02745..fe4c371c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -42,7 +42,7 @@ jobs:
executor: java17
steps:
- run_maven:
- maven_subcommand: test
+ maven_subcommand: verify
- slack/notify:
event: fail
diff --git a/.gitignore b/.gitignore
index ae1ac990..edbffa9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,4 @@ target/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
+.DS_Store
diff --git a/checkstyle.xml b/checkstyle.xml
index 76f872ed..f5e7412d 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -181,8 +181,8 @@
-->
-
@@ -67,6 +67,7 @@
test
+
org.apache.maven.plugins
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..85166328 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
@@ -25,8 +25,13 @@ package com.kingsrook.qqq.backend.module.rdbms.actions;
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.data.QRecord;
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;
@@ -92,7 +97,7 @@ public abstract class AbstractRDBMSAction
** Handle obvious problems with values - like empty string for integer should be null.
**
*******************************************************************************/
- protected Serializable scrubValue(QFieldMetaData field, Serializable value)
+ protected Serializable scrubValue(QFieldMetaData field, Serializable value, boolean isInsert)
{
if("".equals(value))
{
@@ -103,14 +108,201 @@ public abstract class AbstractRDBMSAction
}
}
- //////////////////////////////////////////////////////
- // todo - let this come from something in the field //
- //////////////////////////////////////////////////////
- if(value == null && (field.getName().equals("createDate") || field.getName().equals("modifyDate")))
- {
- value = OffsetDateTime.now();
- }
-
return (value);
}
+
+
+
+ /*******************************************************************************
+ ** If the table has a field with the given name, then set the given value in the
+ ** given record.
+ *******************************************************************************/
+ protected void setValueIfTableHasField(QRecord record, QTableMetaData table, String fieldName, Serializable value)
+ {
+ QFieldMetaData field = table.getField(fieldName);
+ if(field != null)
+ {
+ record.setValue(fieldName, 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..d0970a73
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java
@@ -0,0 +1,97 @@
+/*
+ * 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();
+
+ try(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/RDBMSDeleteAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
index 3495a621..ee75105d 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java
@@ -65,16 +65,18 @@ public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInte
// todo sql customization - can edit sql and/or param list
- Connection connection = getConnection(deleteRequest);
- QueryManager.executeUpdateForRowCount(connection, sql, params);
- List outputRecords = new ArrayList<>();
- rs.setRecords(outputRecords);
- for(Serializable primaryKey : deleteRequest.getPrimaryKeys())
+ try(Connection connection = getConnection(deleteRequest))
{
- QRecord qRecord = new QRecord().withTableName(deleteRequest.getTableName()).withValue("id", primaryKey);
- // todo uh, identify any errors?
- QRecord outputRecord = new QRecord(qRecord);
- outputRecords.add(outputRecord);
+ QueryManager.executeUpdateForRowCount(connection, sql, params);
+ List outputRecords = new ArrayList<>();
+ rs.setRecords(outputRecords);
+ for(Serializable primaryKey : deleteRequest.getPrimaryKeys())
+ {
+ QRecord qRecord = new QRecord().withTableName(deleteRequest.getTableName()).withValue("id", primaryKey);
+ // todo uh, identify any errors?
+ QRecord outputRecord = new QRecord(qRecord);
+ outputRecords.add(outputRecord);
+ }
}
return rs;
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java
index c55fff9f..0176f96c 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.module.rdbms.actions;
import java.io.Serializable;
import java.sql.Connection;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@@ -36,6 +37,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.interfaces.InsertInterface;
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;
/*******************************************************************************
@@ -43,22 +46,38 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
*******************************************************************************/
public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInterface
{
+ private static final Logger LOG = LogManager.getLogger(RDBMSInsertAction.class);
+
+
/*******************************************************************************
**
*******************************************************************************/
public InsertResult execute(InsertRequest insertRequest) throws QException
{
+ InsertResult rs = new InsertResult();
+
if(CollectionUtils.nullSafeIsEmpty(insertRequest.getRecords()))
{
- throw (new QException("Request to insert 0 records."));
+ LOG.info("Insert request called with 0 records. Returning with no-op");
+ rs.setRecords(new ArrayList<>());
+ return (rs);
+ }
+
+ QTableMetaData table = insertRequest.getTable();
+ Instant now = Instant.now();
+
+ for(QRecord record : insertRequest.getRecords())
+ {
+ ///////////////////////////////////////////
+ // todo .. better (not hard-coded names) //
+ ///////////////////////////////////////////
+ setValueIfTableHasField(record, table, "createDate", now);
+ setValueIfTableHasField(record, table, "modifyDate", now);
}
try
{
- InsertResult rs = new InsertResult();
- QTableMetaData table = insertRequest.getTable();
-
List insertableFields = table.getFields().values().stream()
.filter(field -> !field.getName().equals("id")) // todo - intent here is to avoid non-insertable fields.
.toList();
@@ -74,42 +93,41 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte
StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append("(").append(columns).append(") VALUES");
List