diff --git a/.gitignore b/.gitignore index a1c2a238..ae1ac990 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +target/ +*.iml + +############################################# +## Original contents from github template: ## +############################################# # Compiled class file *.class diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 00000000..fadf2cda --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..5c735af1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,139 @@ + + + 4.0.0 + + com.kingsrook.qqq + qqq-backend-module-rdbms + 1.0-SNAPSHOT + + + + + + + UTF-8 + UTF-8 + 17 + 17 + true + true + + + + + + com.kingsrook.qqq + qqq-backend-core + 0.0-SNAPSHOT + + + + + mysql + mysql-connector-java + 8.0.26 + + + com.h2database + h2 + 1.4.197 + test + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.2 + + + org.apache.logging.log4j + log4j-api + 2.14.1 + + + org.apache.logging.log4j + log4j-core + 2.14.1 + + + org.junit.jupiter + junit-jupiter-engine + 5.8.1 + test + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + -Xlint:unchecked + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.2 + + + com.puppycrawl.tools + checkstyle + 9.0 + + + + + validate + validate + + checkstyle.xml + + UTF-8 + true + false + true + warning + **/target/generated-sources/*.* + + + + check + + + + + + + + + + github + GitHub kingsrook Maven Packages + https://maven.pkg.github.com/Kingsrook/qqq-maven-registry + + + + + + github + GitHub kingsrook Maven Packages + https://maven.pkg.github.com/Kingsrook/qqq-maven-registry + + + + diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBSMBackendMetaData.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBSMBackendMetaData.java new file mode 100644 index 00000000..7d624cc0 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBSMBackendMetaData.java @@ -0,0 +1,83 @@ +package com.kingsrook.qqq.backend.module.rdbms; + + +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RDBSMBackendMetaData extends QBackendMetaData +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public RDBSMBackendMetaData(QBackendMetaData source) + { + super(); + setName(source.getName()); + setValues(source.getValues()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getVendor() + { + return getValue("vendor"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getHostName() + { + return getValue("hostName"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getPort() + { + return getValue("port"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getDatabaseName() + { + return getValue("databaseName"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getUsername() + { + return getValue("username"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getPassword() + { + return getValue("password"); + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBSMModule.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBSMModule.java new file mode 100644 index 00000000..564ba151 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/RDBSMModule.java @@ -0,0 +1,35 @@ +package com.kingsrook.qqq.backend.module.rdbms; + + +import com.kingsrook.qqq.backend.core.modules.interfaces.InsertInterface; +import com.kingsrook.qqq.backend.core.modules.interfaces.QModuleInterface; +import com.kingsrook.qqq.backend.core.modules.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSInsertAction; +import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSQueryAction; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RDBSMModule implements QModuleInterface +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QueryInterface getQueryInterface() + { + return new RDBMSQueryAction(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public InsertInterface getInsertInterface() + { + return (new RDBMSInsertAction()); + } +} 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 new file mode 100644 index 00000000..d66034a9 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -0,0 +1,25 @@ +package com.kingsrook.qqq.backend.module.rdbms.actions; + + +import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public abstract class AbstractRDBMSAction +{ + + /******************************************************************************* + ** + *******************************************************************************/ + protected String getColumnName(QFieldMetaData field) + { + if(field.getBackendName() != null) + { + return (field.getBackendName()); + } + return (field.getName()); + } + +} 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 new file mode 100644 index 00000000..63f6727d --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java @@ -0,0 +1,168 @@ +package com.kingsrook.qqq.backend.module.rdbms.actions; + + +import java.io.Serializable; +import java.sql.Connection; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.InsertRequest; +import com.kingsrook.qqq.backend.core.model.actions.InsertResult; +import com.kingsrook.qqq.backend.core.model.actions.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.QFilterOrderBy; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.data.QRecordWithStatus; +import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; +import com.kingsrook.qqq.backend.core.modules.interfaces.InsertInterface; +import com.kingsrook.qqq.backend.module.rdbms.RDBSMBackendMetaData; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public InsertResult execute(InsertRequest insertRequest) throws QException + { + try + { + InsertResult rs = new InsertResult(); + QTableMetaData table = insertRequest.getTable(); + + List insertableFields = table.getFields().stream() + .filter(field -> !field.getName().equals("id")) // todo - intent here is to avoid non-insertable fields. + .toList(); + + String columns = insertableFields.stream() + .map(this::getColumnName) + .collect(Collectors.joining(", ")); + String questionMarks = insertableFields.stream() + .map(x -> "?") + .collect(Collectors.joining(", ")); + + String tableName = table.getName(); + StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append("(").append(columns).append(") VALUES"); + List params = new ArrayList<>(); + + int recordIndex = 0; + for(QRecord record : insertRequest.getRecords()) + { + if(recordIndex++ > 0) + { + sql.append(","); + } + sql.append("(").append(questionMarks).append(")"); + for(QFieldMetaData field : insertableFields) + { + params.add(record.getValue(field.getName())); + } + } + + // todo sql customization - can edit sql and/or param list + + ConnectionManager connectionManager = new ConnectionManager(); + Connection connection = connectionManager.getConnection(new RDBSMBackendMetaData(insertRequest.getBackend())); + + // QueryResult rs = new QueryResult(); + // List records = new ArrayList<>(); + // rs.setRecords(records); + + // todo - non-serial-id style tables + // todo - other generated values, e.g., createDate... maybe need to re-select? + List idList = QueryManager.executeInsertForGeneratedIds(connection, sql.toString(), params); + List recordsWithStatus = new ArrayList<>(); + rs.setRecords(recordsWithStatus); + int index = 0; + for(QRecord record : insertRequest.getRecords()) + { + Integer id = idList.get(index); + QRecordWithStatus recordWithStatus = new QRecordWithStatus(record); + recordWithStatus.setPrimaryKey(id); + recordWithStatus.setValue(table.getPrimaryKeyField(), id); + recordsWithStatus.add(recordWithStatus); + } + + return rs; + } + catch(Exception e) + { + throw new QException("Error executing insert: " + e.getMessage(), e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + 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()); + 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 (" + criterion.getValues().stream().map(x -> "?").collect(Collectors.joining(",")) + ") "; + break; + } + default: + { + throw new IllegalArgumentException("Unexpected operator: " + criterion.getOperator()); + } + } + clauses.add(clause); + if(expectedNoOfParams != null && criterion.getValues().size() != expectedNoOfParams) + { + throw new IllegalArgumentException("Incorrect number of values given for criteria [" + field.getName() + "]"); + } + params.addAll(criterion.getValues()); + } + + return (String.join(" AND ", clauses)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String makeOrderByClause(QTableMetaData table, List orderBys) + { + List clauses = new ArrayList<>(); + + for(QFilterOrderBy orderBy : orderBys) + { + QFieldMetaData field = table.getField(orderBy.getFieldName()); + String column = getColumnName(field); + clauses.add(column + " " + (orderBy.getIsAscending() ? "ASC" : "DESC")); + } + return (String.join(", ", clauses)); + } + +} diff --git a/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 new file mode 100644 index 00000000..5bd14abd --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -0,0 +1,184 @@ +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.LinkedHashMap; +import java.util.List; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.QFilterOrderBy; +import com.kingsrook.qqq.backend.core.model.actions.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.QueryRequest; +import com.kingsrook.qqq.backend.core.model.actions.QueryResult; +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.QTableMetaData; +import com.kingsrook.qqq.backend.core.modules.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.module.rdbms.RDBSMBackendMetaData; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public QueryResult execute(QueryRequest queryRequest) throws QException + { + try + { + QTableMetaData table = queryRequest.getTable(); + String tableName = table.getName(); + + String columns = table.getFields().stream() + .map(this::getColumnName) + .collect(Collectors.joining(", ")); + + String sql = "SELECT " + columns + " FROM " + tableName; + + QQueryFilter filter = queryRequest.getFilter(); + List params = new ArrayList<>(); + if(filter != null && CollectionUtils.nullSafeHasContents(filter.getCriteria())) + { + sql += " WHERE " + makeWhereClause(table, filter.getCriteria(), params); + } + + if(filter != null && CollectionUtils.nullSafeHasContents(filter.getOrderBys())) + { + sql += " ORDER BY " + makeOrderByClause(table, filter.getOrderBys()); + } + + if(queryRequest.getLimit() != null) + { + sql += " LIMIT " + queryRequest.getLimit(); + + if(queryRequest.getSkip() != null) + { + // todo - other sql grammars? + sql += " OFFSET " + queryRequest.getSkip(); + } + } + + // todo sql customization - can edit sql and/or param list + + ConnectionManager connectionManager = new ConnectionManager(); + Connection connection = connectionManager.getConnection(new RDBSMBackendMetaData(queryRequest.getBackend())); + + QueryResult rs = new QueryResult(); + List records = new ArrayList<>(); + rs.setRecords(records); + + QueryManager.executeStatement(connection, sql, ((ResultSet resultSet) -> + { + ResultSetMetaData metaData = resultSet.getMetaData(); + while(resultSet.next()) + { + // todo - should refactor this for view etc to use too. + QRecord record = new QRecord(); + records.add(record); + record.setTableName(table.getName()); + LinkedHashMap values = new LinkedHashMap<>(); + record.setValues(values); + + for(int i = 1; i <= metaData.getColumnCount(); i++) + { + QFieldMetaData qFieldMetaData = table.getFields().get(i - 1); + String value = QueryManager.getString(resultSet, i); // todo - types! + values.put(qFieldMetaData.getName(), value); + if(qFieldMetaData.getName().equals(table.getPrimaryKeyField())) + { + record.setPrimaryKey(value); + } + } + } + + }), params); + + return rs; + } + catch(Exception e) + { + e.printStackTrace(); + throw new QException("Error executing query", e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + 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()); + 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 (" + criterion.getValues().stream().map(x -> "?").collect(Collectors.joining(",")) + ") "; + break; + } + default: + { + throw new IllegalArgumentException("Unexpected operator: " + criterion.getOperator()); + } + } + clauses.add(clause); + if(expectedNoOfParams != null && criterion.getValues().size() != expectedNoOfParams) + { + throw new IllegalArgumentException("Incorrect number of values given for criteria [" + field.getName() + "]"); + } + params.addAll(criterion.getValues()); + } + + return (String.join(" AND ", clauses)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String makeOrderByClause(QTableMetaData table, List orderBys) + { + List clauses = new ArrayList<>(); + + for(QFilterOrderBy orderBy : orderBys) + { + QFieldMetaData field = table.getField(orderBy.getFieldName()); + String column = getColumnName(field); + clauses.add(column + " " + (orderBy.getIsAscending() ? "ASC" : "DESC")); + } + return (String.join(", ", clauses)); + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java new file mode 100644 index 00000000..fc66e943 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java @@ -0,0 +1,44 @@ +package com.kingsrook.qqq.backend.module.rdbms.jdbc; + + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import com.kingsrook.qqq.backend.module.rdbms.RDBSMBackendMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ConnectionManager +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public Connection getConnection(RDBSMBackendMetaData backend) throws SQLException + { + String jdbcURL; + + switch (backend.getVendor()) + { + case "mysql": + { + jdbcURL = "jdbc:mysql://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=convertToNull"; + break; + } + case "h2": + { + jdbcURL = "jdbc:h2:" + backend.getHostName() + ":" + backend.getDatabaseName() + ";MODE=MySQL"; + break; + } + default: + { + throw new IllegalArgumentException("Unsupported rdbms backend vendor: " + backend.getVendor()); + } + } + + return DriverManager.getConnection(jdbcURL, backend.getUsername(), backend.getPassword()); + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java new file mode 100644 index 00000000..f6fa4be1 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java @@ -0,0 +1,1410 @@ +package com.kingsrook.qqq.backend.module.rdbms.jdbc; + + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QueryManager +{ + private static final int PAGE_SIZE = 2000; + private static final int MS_PER_SEC = 1000; + private static final int NINETEEN_HUNDRED = 1900; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @FunctionalInterface + public interface ResultSetProcessor + { + /******************************************************************************* + ** + *******************************************************************************/ + void processResultSet(ResultSet rs) throws SQLException; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void executeStatement(Connection connection, String sql, ResultSetProcessor procesor, Object... params) throws SQLException + { + PreparedStatement statement = null; + ResultSet resultSet = null; + + try + { + statement = prepareStatementAndBindParams(connection, sql, params); + statement.execute(); + resultSet = statement.getResultSet(); + + procesor.processResultSet(resultSet); + } + finally + { + if(statement != null) + { + statement.close(); + } + + if(resultSet != null) + { + resultSet.close(); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void executeStatementForeachResult(Connection connection, String sql, ResultSetProcessor processor, Object... params) throws SQLException + { + PreparedStatement statement = null; + ResultSet resultSet = null; + + try + { + if(params.length == 1 && params[0] instanceof Collection) + { + params = ((Collection) params[0]).toArray(); + } + + statement = prepareStatementAndBindParams(connection, sql, params); + statement.execute(); + resultSet = statement.getResultSet(); + + while(resultSet.next()) + { + processor.processResultSet(resultSet); + } + } + finally + { + if(statement != null) + { + statement.close(); + } + + if(resultSet != null) + { + resultSet.close(); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public static T executeStatementForSingleValue(Connection connection, Class returnClass, String sql, Object... params) throws SQLException + { + PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); + statement.execute(); + ResultSet resultSet = statement.getResultSet(); + if(resultSet.next()) + { + Object object = resultSet.getObject(1); + if(resultSet.wasNull() || object == null) + { + return (null); + } + + if(object instanceof Long && returnClass.equals(Integer.class)) + { + return (T) Integer.valueOf(((Long) object).intValue()); + } + else if(object instanceof BigInteger && returnClass.equals(Integer.class)) + { + return (T) Integer.valueOf(((BigInteger) object).intValue()); + } + else if(object instanceof BigDecimal && returnClass.equals(Integer.class)) + { + return (T) Integer.valueOf(((BigDecimal) object).intValue()); + } + else if(object instanceof Integer && returnClass.equals(Long.class)) + { + return (T) Long.valueOf(((Integer) object)); + } + else if(object instanceof Timestamp && returnClass.equals(LocalDateTime.class)) + { + Timestamp timestamp = (Timestamp) object; + return ((T) LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp.getTime()), ZoneId.systemDefault())); + } + else + { + if(returnClass.equals(String.class)) + { + return (T) String.valueOf(object); + } + else + { + return (returnClass.cast(object)); + } + } + } + else + { + return (null); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Map executeStatementForSingleRow(Connection connection, String sql, Object... params) throws SQLException + { + PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); + statement.execute(); + ResultSet resultSet = statement.getResultSet(); + if(resultSet.next()) + { + Map rs = new LinkedHashMap<>(); + + ResultSetMetaData metaData = resultSet.getMetaData(); + for(int i = 1; i <= metaData.getColumnCount(); i++) + { + rs.put(metaData.getColumnName(i), getObject(resultSet, i)); + } + + return (rs); + } + else + { + return (null); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static SimpleEntity executeStatementForSimpleEntity(Connection connection, String sql, Object... params) throws SQLException + { + PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); + statement.execute(); + ResultSet resultSet = statement.getResultSet(); + if(resultSet.next()) + { + return (buildSimpleEntity(resultSet)); + } + else + { + return (null); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List> executeStatementForRows(Connection connection, String sql, Object... params) throws SQLException + { + List> rs = new ArrayList<>(); + + PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); + statement.execute(); + ResultSet resultSet = statement.getResultSet(); + while(resultSet.next()) + { + Map row = new HashMap<>(); + rs.add(row); + + ResultSetMetaData metaData = resultSet.getMetaData(); + for(int i = 1; i <= metaData.getColumnCount(); i++) + { + row.put(metaData.getColumnName(i), getObject(resultSet, i)); + } + } + + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List executeStatementForSimpleEntityList(Connection connection, String sql, Object... params) throws SQLException + { + List rs = new ArrayList<>(); + + PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); + statement.execute(); + ResultSet resultSet = statement.getResultSet(); + while(resultSet.next()) + { + SimpleEntity row = buildSimpleEntity(resultSet); + + rs.add(row); + } + + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static SimpleEntity buildSimpleEntity(ResultSet resultSet) throws SQLException + { + SimpleEntity row = new SimpleEntity(); + + ResultSetMetaData metaData = resultSet.getMetaData(); + for(int i = 1; i <= metaData.getColumnCount(); i++) + { + row.put(metaData.getColumnName(i), getObject(resultSet, i)); + } + return row; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static PreparedStatement executeUpdate(Connection connection, String sql, Object... params) throws SQLException + { + PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); + statement.executeUpdate(); + return (statement); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static PreparedStatement executeUpdate(Connection connection, String sql, List params) throws SQLException + { + PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); + statement.executeUpdate(); + return (statement); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void executeUpdateVoid(Connection connection, String sql, Object... params) throws SQLException + { + try(PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params)) + { + statement.executeUpdate(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void executeUpdateVoid(Connection connection, String sql, List params) throws SQLException + { + try(PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params)) + { + statement.executeUpdate(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Integer executeUpdateForRowCount(Connection connection, String sql, Object... params) throws SQLException + { + try(PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params)) + { + statement.executeUpdate(); + return (statement.getUpdateCount()); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Integer executeUpdateForRowCount(Connection connection, String sql, List params) throws SQLException + { + try(PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params)) + { + statement.executeUpdate(); + return (statement.getUpdateCount()); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Integer executeInsertForGeneratedId(Connection connection, String sql, Object... params) throws SQLException + { + try(PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) + { + bindParams(params, statement); + statement.executeUpdate(); + ResultSet generatedKeys = statement.getGeneratedKeys(); + if(generatedKeys.next()) + { + return (getInteger(generatedKeys, 1)); + } + else + { + return (null); + } + } + } + + + + /******************************************************************************* + ** todo - needs unit test + *******************************************************************************/ + public static List executeInsertForGeneratedIds(Connection connection, String sql, List params) throws SQLException + { + List rs = new ArrayList<>(); + try(PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) + { + bindParams(params.toArray(), statement); + statement.executeUpdate(); + ResultSet generatedKeys = statement.getGeneratedKeys(); + while(generatedKeys.next()) + { + rs.add(getInteger(generatedKeys, 1)); + } + } + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void executeInsertForList(Connection connection, List entityList) throws SQLException + { + List> pages = CollectionUtils.getPages(entityList, PAGE_SIZE); + for(List page : pages) + { + ArrayList columns = new ArrayList<>(page.get(0).keySet()); + String sql = "INSERT INTO " + page.get(0).getTableName() + "(" + StringUtils.join(",", columns) + ") VALUES (" + columns.stream().map(s -> "?").collect(Collectors.joining(",")) + ")"; + + PreparedStatement insertPS = connection.prepareStatement(sql); + for(SimpleEntity entity : page) + { + Object[] params = new Object[columns.size()]; + for(int i = 0; i < columns.size(); i++) + { + params[i] = entity.get(columns.get(i)); + } + + bindParams(insertPS, params); + insertPS.addBatch(); + } + insertPS.executeBatch(); + } + + for(List page : pages) + { + page.clear(); + } + pages.clear(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Integer executeInsert(Connection connection, SimpleEntity entity) throws SQLException + { + ArrayList columns = new ArrayList<>(entity.keySet()); + String sql = "INSERT INTO " + entity.getTableName() + "(" + StringUtils.join(",", columns) + ") VALUES (" + columns.stream().map(s -> "?").collect(Collectors.joining(",")) + ")"; + + Object[] params = new Object[columns.size()]; + for(int i = 0; i < columns.size(); i++) + { + params[i] = entity.get(columns.get(i)); + } + + return (executeInsertForGeneratedId(connection, sql, params)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static PreparedStatement prepareStatementAndBindParams(Connection connection, String sql, Object[] params) throws SQLException + { + PreparedStatement statement = connection.prepareStatement(sql); + bindParams(params, statement); + return statement; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static PreparedStatement prepareStatementAndBindParams(Connection connection, String sql, List params) throws SQLException + { + PreparedStatement statement = connection.prepareStatement(sql); + + if(params != null) + { + for(int i = 0; i < params.size(); i++) + { + bindParamObject(statement, (i + 1), params.get(i)); + } + } + return statement; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void bindParams(Object[] params, PreparedStatement statement) throws SQLException + { + int paramIndex = 0; + if(params != null) + { + for(int i = 0; i < params.length; i++) + { + int paramsBound = bindParamObject(statement, (paramIndex + 1), params[i]); + paramIndex += paramsBound; + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void bindParams(PreparedStatement statement, Object... params) throws SQLException + { + int paramIndex = 0; + if(params != null) + { + for(int i = 0; i < params.length; i++) + { + int paramsBound = bindParamObject(statement, (paramIndex + 1), params[i]); + paramIndex += paramsBound; + } + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + @SuppressWarnings("unchecked") + public static int bindParamObject(PreparedStatement statement, int index, Object value) throws SQLException + { + if(value instanceof TypeValuePair) + { + bindParamTypeValuePair(statement, index, (TypeValuePair) value); + return (1); + } + else if(value instanceof Integer) + { + bindParam(statement, index, (Integer) value); + return (1); + } + else if(value instanceof Short) + { + bindParam(statement, index, ((Short) value).intValue()); + return (1); + } + else if(value instanceof Long) + { + bindParam(statement, index, ((Long) value).intValue()); + return (1); + } + else if(value instanceof String) + { + bindParam(statement, index, (String) value); + return (1); + } + else if(value instanceof Boolean) + { + bindParam(statement, index, (Boolean) value); + return (1); + } + else if(value instanceof Timestamp) + { + bindParam(statement, index, (Timestamp) value); + return (1); + } + else if(value instanceof Date) + { + bindParam(statement, index, (Date) value); + return (1); + } + else if(value instanceof Calendar) + { + bindParam(statement, index, (Calendar) value); + return (1); + } + else if(value instanceof BigDecimal) + { + bindParam(statement, index, (BigDecimal) value); + return (1); + } + else if(value == null) + { + statement.setNull(index, Types.CHAR); + return (1); + } + else if(value instanceof Collection) + { + Collection collection = (Collection) value; + int paramsBound = 0; + for(Object o : collection) + { + paramsBound += bindParamObject(statement, (index + paramsBound), o); + } + return (paramsBound); + } + else if(value instanceof byte[]) + { + statement.setBytes(index, (byte[]) value); + return (1); + } + else if(value instanceof Instant) + { + Timestamp timestamp = new Timestamp(((Instant) value).toEpochMilli()); + statement.setTimestamp(index, timestamp); + return (1); + } + else if(value instanceof LocalDate) + { + Timestamp timestamp = new Timestamp(((LocalDate) value).atTime(0, 0).toEpochSecond(ZoneOffset.UTC) * MS_PER_SEC); + statement.setTimestamp(index, timestamp); + return (1); + } + else if(value instanceof LocalDateTime) + { + Timestamp timestamp = new Timestamp(((LocalDateTime) value).toEpochSecond(ZoneOffset.UTC) * MS_PER_SEC); + statement.setTimestamp(index, timestamp); + return (1); + } + else + { + throw (new SQLException("Unexpected value type [" + value.getClass().getSimpleName() + "] in bindParamObject.")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static TypeValuePair param(Class c, T v) + { + return (new TypeValuePair<>(c, v)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void bindParamTypeValuePair(PreparedStatement statement, int index, TypeValuePair value) throws SQLException + { + Object v = value.getValue(); + Class t = value.getType(); + + if(t.equals(Integer.class)) + { + bindParam(statement, index, (Integer) v); + } + else if(t.equals(String.class)) + { + bindParam(statement, index, (String) v); + } + else if(t.equals(Boolean.class)) + { + bindParam(statement, index, (Boolean) v); + } + else if(t.equals(Timestamp.class)) + { + bindParam(statement, index, (Timestamp) v); + } + else if(t.equals(Date.class)) + { + bindParam(statement, index, (Date) v); + } + else if(t.equals(Calendar.class)) + { + bindParam(statement, index, (Calendar) v); + } + else if(t.equals(LocalDate.class)) + { + bindParam(statement, index, (LocalDate) v); + } + else if(t.equals(LocalDateTime.class)) + { + bindParam(statement, index, (LocalDateTime) v); + } + else if(t.equals(BigDecimal.class)) + { + bindParam(statement, index, (BigDecimal) v); + } + else + { + throw (new SQLException("Unexpected value type [" + t.getSimpleName() + "] in bindParamTypeValuePair.")); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + public static void bindParam(PreparedStatement statement, int index, Integer value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.INTEGER); + } + else + { + statement.setInt(index, value); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + public static void bindParam(PreparedStatement statement, int index, String value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.CHAR); + } + else + { + statement.setString(index, value); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + public static void bindParam(PreparedStatement statement, int index, Boolean value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.BOOLEAN); + } + else + { + statement.setBoolean(index, value); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + public static void bindParam(PreparedStatement statement, int index, Date value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.DATE); + } + else + { + statement.setDate(index, new java.sql.Date(value.getTime())); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + public static void bindParam(PreparedStatement statement, int index, Timestamp value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.TIMESTAMP); + } + else + { + statement.setTimestamp(index, value); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + public static void bindParam(PreparedStatement statement, int index, Calendar value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.DATE); + } + else + { + statement.setTimestamp(index, new Timestamp(value.getTimeInMillis())); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + public static void bindParam(PreparedStatement statement, int index, LocalDate value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.DATE); + } + else + { + LocalDateTime localDateTime = value.atTime(0, 0); + Timestamp timestamp = new Timestamp(localDateTime.atZone(ZoneId.systemDefault()).toEpochSecond() * MS_PER_SEC); // TimeStamp expects millis, not seconds, after epoch + statement.setTimestamp(index, timestamp); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + public static void bindParam(PreparedStatement statement, int index, LocalDateTime value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.TIMESTAMP); + } + else + { + Timestamp timestamp = new Timestamp(value.atZone(ZoneId.systemDefault()).toEpochSecond() * MS_PER_SEC); // TimeStamp expects millis, not seconds, after epoch + statement.setTimestamp(index, timestamp); + } + } + + + + /******************************************************************************* + ** + ** + *******************************************************************************/ + public static void bindParam(PreparedStatement statement, int index, BigDecimal value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.DECIMAL); + } + else + { + statement.setBigDecimal(index, value); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void bindParam(PreparedStatement statement, int index, byte[] value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.ARRAY); + } + else + { + statement.setBytes(index, value); + } + } + + + + /******************************************************************************* + ** + ** + *******************************************************************************/ + public static void bindParamNull(PreparedStatement statement, int index) throws SQLException + { + statement.setNull(index, Types.NULL); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Integer getInteger(ResultSet resultSet, String column) throws SQLException + { + int value = resultSet.getInt(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Integer getInteger(ResultSet resultSet, int column) throws SQLException + { + int value = resultSet.getInt(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static byte[] getByteArray(ResultSet resultSet, String column) throws SQLException + { + byte[] value = resultSet.getBytes(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static byte[] getByteArray(ResultSet resultSet, int column) throws SQLException + { + byte[] value = resultSet.getBytes(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Object getObject(ResultSet resultSet, String column) throws SQLException + { + Object value = resultSet.getObject(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Object getObject(ResultSet resultSet, int column) throws SQLException + { + Object value = resultSet.getObject(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getString(ResultSet resultSet, String column) throws SQLException + { + String value = resultSet.getString(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getString(ResultSet resultSet, int column) throws SQLException + { + String value = resultSet.getString(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static BigDecimal getBigDecimal(ResultSet resultSet, String column) throws SQLException + { + BigDecimal value = resultSet.getBigDecimal(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static BigDecimal getBigDecimal(ResultSet resultSet, int column) throws SQLException + { + BigDecimal value = resultSet.getBigDecimal(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Date getDate(ResultSet resultSet, String column) throws SQLException + { + Date value = resultSet.getDate(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Date getDate(ResultSet resultSet, int column) throws SQLException + { + Date value = resultSet.getDate(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Calendar getCalendar(ResultSet resultSet, String column) throws SQLException + { + Timestamp value = resultSet.getTimestamp(column); + if(resultSet.wasNull()) + { + return (null); + } + Calendar rs = Calendar.getInstance(); + rs.setTimeInMillis(value.getTime()); + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Calendar getCalendar(ResultSet resultSet, int column) throws SQLException + { + Timestamp value = resultSet.getTimestamp(column); + if(resultSet.wasNull()) + { + return (null); + } + Calendar rs = Calendar.getInstance(); + rs.setTimeInMillis(value.getTime()); + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("deprecation") + public static LocalDate getLocalDate(ResultSet resultSet, String column) throws SQLException + { + Timestamp value = resultSet.getTimestamp(column); + if(resultSet.wasNull()) + { + return (null); + } + + LocalDate date = LocalDate.of(value.getYear() + NINETEEN_HUNDRED, value.getMonth() + 1, value.getDate()); + return (date); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("deprecation") + public static LocalDateTime getLocalDateTime(ResultSet resultSet, String column) throws SQLException + { + Timestamp value = resultSet.getTimestamp(column); + if(resultSet.wasNull()) + { + return (null); + } + + LocalDateTime dateTime = LocalDateTime.of(value.getYear() + NINETEEN_HUNDRED, value.getMonth() + 1, value.getDate(), value.getHours(), value.getMinutes(), value.getSeconds(), 0); + return (dateTime); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("deprecation") + public static OffsetDateTime getOffsetDateTime(ResultSet resultSet, String column) throws SQLException + { + Timestamp value = resultSet.getTimestamp(column); + if(resultSet.wasNull()) + { + return (null); + } + + OffsetDateTime dateTime = OffsetDateTime.of(value.getYear() + NINETEEN_HUNDRED, value.getMonth() + 1, value.getDate(), value.getHours(), value.getMinutes(), value.getSeconds(), 0, OffsetDateTime.now().getOffset()); + return (dateTime); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Boolean getBoolean(ResultSet resultSet, String column) throws SQLException + { + Boolean value = resultSet.getBoolean(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Boolean getBoolean(ResultSet resultSet, int column) throws SQLException + { + Boolean value = resultSet.getBoolean(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Long getLong(ResultSet resultSet, int column) throws SQLException + { + long value = resultSet.getLong(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Long getLong(ResultSet resultSet, String column) throws SQLException + { + long value = resultSet.getLong(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Timestamp getTimestamp(ResultSet resultSet, int column) throws SQLException + { + Timestamp value = resultSet.getTimestamp(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Timestamp getTimestamp(ResultSet resultSet, String column) throws SQLException + { + Timestamp value = resultSet.getTimestamp(column); + if(resultSet.wasNull()) + { + return (null); + } + return (value); + } + + + + /******************************************************************************* + ** Find an id from a "large" table that was created X days ago (assumes the date + ** field in the table isn't indexed, but id is - so do a binary search on id, + ** selecting the date of the min & max & mid id, then sub-dividing until the goal + ** days-ago is found). + ** + *******************************************************************************/ + public static Integer findIdForDaysAgo(Connection connection, String tableName, String dateFieldName, int goalDaysAgo) throws SQLException + { + return (findIdForTimeUnitAgo(connection, tableName, dateFieldName, goalDaysAgo, ChronoUnit.DAYS)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Integer findIdForTimestamp(Connection connection, String tableName, String dateFieldName, LocalDateTime timestamp) throws SQLException + { + long between = ChronoUnit.SECONDS.between(timestamp, LocalDateTime.now()); + return (findIdForTimeUnitAgo(connection, tableName, dateFieldName, (int) between, ChronoUnit.SECONDS)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Integer findIdForTimeUnitAgo(Connection connection, String tableName, String dateFieldName, int goalUnitsAgo, ChronoUnit unit) throws SQLException + { + Integer maxId = executeStatementForSingleValue(connection, Integer.class, "SELECT MAX(id) FROM " + tableName); + Integer minId = executeStatementForSingleValue(connection, Integer.class, "SELECT MIN(id) FROM " + tableName); + + if(maxId == null || minId == null) + { + // Logger.logDebug("For [" + tableName + "], returning null id for X time-units ago, because either a min or max wasn't found."); + return (null); + } + + Integer idForGoal = findIdForTimeUnitAgo(connection, tableName, dateFieldName, goalUnitsAgo, minId, maxId, unit); + long foundUnitsAgo = getTimeUnitAgo(connection, tableName, dateFieldName, idForGoal, unit); + + // Logger.logDebug("For [" + tableName + "], using min id [" + idForGoal + "], which is from [" + foundUnitsAgo + "] Units[" + unit + "] ago."); + + return (idForGoal); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Integer findIdForTimeUnitAgo(Connection connection, String tableName, String dateFieldName, int goalUnitsAgo, Integer minId, Integer maxId, ChronoUnit unit) throws SQLException + { + Integer midId = minId + ((maxId - minId) / 2); + if(midId.equals(minId) || midId.equals(maxId)) + { + return (midId); + } + + long foundUnitsAgo = getTimeUnitAgo(connection, tableName, dateFieldName, midId, unit); + if(foundUnitsAgo == goalUnitsAgo) + { + return (midId); + } + else if(foundUnitsAgo > goalUnitsAgo) + { + return (findIdForTimeUnitAgo(connection, tableName, dateFieldName, goalUnitsAgo, midId, maxId, unit)); + } + else + { + return (findIdForTimeUnitAgo(connection, tableName, dateFieldName, goalUnitsAgo, minId, midId, unit)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static long getTimeUnitAgo(Connection connection, String tableName, String dateFieldName, Integer id, ChronoUnit unit) throws SQLException + { + LocalDateTime now = LocalDateTime.now(); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // note, we used to just do where id=? here - but if that row is ever missing, we have a bad time - so - do id >= ? order by id, and just the first row. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + LocalDateTime date = executeStatementForSingleValue(connection, LocalDateTime.class, "SELECT " + dateFieldName + " FROM " + tableName + " WHERE id >= ? ORDER BY id LIMIT 1", id); + // System.out.println(date); + + // if(date == null) + { + // return now. + } + // else + { + long diff = unit.between(date, now); + // System.out.println("Unit[" + unit + "]'s ago: " + diff); + return diff; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class TypeValuePair + { + private Class type; + private T value; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public TypeValuePair(T value) + { + this.value = value; + this.type = (Class) value.getClass(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public TypeValuePair(Class type, T value) + { + this.type = type; + this.value = value; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public T getValue() + { + return (value); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Class getType() + { + return (type); + } + + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/SimpleEntity.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/SimpleEntity.java new file mode 100755 index 00000000..e72e89a0 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/SimpleEntity.java @@ -0,0 +1,208 @@ +package com.kingsrook.qqq.backend.module.rdbms.jdbc; + + +import java.math.BigDecimal; +import java.util.HashMap; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class SimpleEntity extends HashMap +{ + private String tableName; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public SimpleEntity() + { + super(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public SimpleEntity with(String key, Object value) + { + put(key, value); + return (this); + } + + + + /******************************************************************************* + ** Return the current value of tableName + ** + ** @return tableName + *******************************************************************************/ + public String getTableName() + { + return (tableName); + } + + + + /******************************************************************************* + ** Set the current value of tableName + ** + ** @param tableName + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public SimpleEntity withTableName(String tableName) + { + setTableName(tableName); + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Boolean getBoolean(String columnName) + { + Object o = get(columnName); + if(o == null) + { + return (null); + } + + if(o instanceof Boolean) + { + return ((Boolean) o); + } + else if(o instanceof Number) + { + int i = ((Number) o).intValue(); + return (i != 0); + } + else if(o instanceof String) + { + String s = (String) o; + return (s.equalsIgnoreCase("1") || s.equalsIgnoreCase("true") || s.equalsIgnoreCase("t")); + } + else + { + throw new IllegalArgumentException("Could not get value of object of type [" + o.getClass() + "] as Boolean."); + } + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getString(String columnName) + { + Object o = get(columnName); + if(o == null) + { + return (null); + } + if(o instanceof String) + { + return ((String) o); + } + else if(o instanceof byte[]) + { + return (new String((byte[]) o)); + } + + return String.valueOf(o); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Integer getInteger(String columnName) + { + Object o = get(columnName); + if(o instanceof Long) + { + return ((Long) o).intValue(); + } + else if(o instanceof Short) + { + return ((Short) o).intValue(); + } + else if(o instanceof String) + { + return (Integer.parseInt((String) o)); + } + + return ((Integer) o); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public BigDecimal getBigDecimal(String columnName) + { + Object o = get(columnName); + if(o == null) + { + return (null); + } + + if(o instanceof BigDecimal) + { + return ((BigDecimal) o); + } + else + { + return new BigDecimal(String.valueOf(o)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Long getLong(String columnName) + { + Object o = get(columnName); + if(o instanceof Integer) + { + return ((Integer) o).longValue(); + } + + return ((Long) o); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void trimStrings() + { + for(String key : keySet()) + { + Object value = get(key); + if(value != null && value instanceof String) + { + put(key, ((String) value).trim()); + } + } + } +} diff --git a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSActionTest.java b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSActionTest.java new file mode 100644 index 00000000..90d64730 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSActionTest.java @@ -0,0 +1,92 @@ +package com.kingsrook.qqq.backend.module.rdbms.actions; + + +import java.io.InputStream; +import java.sql.Connection; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +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; +import com.kingsrook.qqq.backend.module.rdbms.RDBSMBackendMetaData; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; +import org.apache.commons.io.IOUtils; +import static junit.framework.Assert.assertNotNull; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RDBMSActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + protected QInstance defineInstance() + { + QInstance qInstance = new QInstance(); + qInstance.addBackend(defineBackend()); + qInstance.addTable(defineTablePerson()); + return (qInstance); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected QBackendMetaData defineBackend() + { + return new QBackendMetaData() + .withName("default") + .withType("rdbms") + .withValue("vendor", "h2") + .withValue("hostName", "mem") + .withValue("databaseName", "test_database") + .withValue("username", "sa") + .withValue("password", ""); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData defineTablePerson() + { + return new QTableMetaData() + .withName("person") + .withBackendName(defineBackend().getName()) + .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("firstName", QFieldType.STRING).withBackendName("first_name")) + .withField(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name")) + .withField(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date")) + .withField(new QFieldMetaData("email", QFieldType.STRING)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + protected void primeTestDatabase() throws Exception + { + ConnectionManager connectionManager = new ConnectionManager(); + Connection connection = connectionManager.getConnection(new RDBSMBackendMetaData(defineBackend())); + InputStream primeTestDatabaseSqlStream = RDBMSActionTest.class.getResourceAsStream("/prime-test-database.sql"); + assertNotNull(primeTestDatabaseSqlStream); + List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); + String joinedSQL = String.join("\n", lines); + for(String sql : joinedSQL.split(";")) + { + QueryManager.executeUpdate(connection, sql); + } + } +} diff --git a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java new file mode 100644 index 00000000..fc555482 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -0,0 +1,78 @@ +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.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.QueryRequest; +import com.kingsrook.qqq.backend.core.model.actions.QueryResult; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RDBMSQueryActionTest extends RDBMSActionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + public void beforeEach() throws Exception + { + super.primeTestDatabase(); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testUnfilteredQuery() throws QException + { + QueryRequest queryRequest = initQueryRequest(); + QueryResult queryResult = new RDBMSQueryAction().execute(queryRequest); + Assertions.assertEquals(5, queryResult.getRecords().size(), "Unfiltered query should find all rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testEqualsQuery() throws QException + { + String email = "darin.kelkhoff@gmail.com"; + + QueryRequest queryRequest = initQueryRequest(); + queryRequest.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.EQUALS) + .withValues(List.of(email))) + ); + QueryResult queryResult = new RDBMSQueryAction().execute(queryRequest); + Assertions.assertEquals(1, queryResult.getRecords().size(), "Equals query should find 1 row"); + Assertions.assertEquals(email, queryResult.getRecords().get(0).getValueString("email"), "Should find expected email address"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QueryRequest initQueryRequest() + { + QueryRequest queryRequest = new QueryRequest(); + queryRequest.setInstance(defineInstance()); + queryRequest.setTableName(defineTablePerson().getName()); + return queryRequest; + } + +} \ No newline at end of file diff --git a/src/test/resources/prime-test-database.sql b/src/test/resources/prime-test-database.sql new file mode 100644 index 00000000..e5adab34 --- /dev/null +++ b/src/test/resources/prime-test-database.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS person; +CREATE TABLE person +( + id SERIAL, + create_date TIMESTAMP DEFAULT now(), + modify_date TIMESTAMP DEFAULT now(), + + first_name VARCHAR(80) NOT NULL, + last_name VARCHAR(80) NOT NULL, + birth_date DATE, + email VARCHAR(250) NOT NULL +); + +INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com'); +INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com'); +INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com'); +INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (4, 'Tyler', 'Samples', '1990-01-01', 'tsamples@mmltholdings.com'); +INSERT INTO person (id, first_name, last_name, birth_date, email) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com');