Initial checkin

This commit is contained in:
Darin Kelkhoff
2021-10-27 21:12:01 -05:00
parent f2a9a73e54
commit 468c18795c
14 changed files with 2732 additions and 0 deletions

View File

@ -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");
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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<QFieldMetaData> 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<Object> 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<QRecord> 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<Integer> idList = QueryManager.executeInsertForGeneratedIds(connection, sql.toString(), params);
List<QRecordWithStatus> 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<QFilterCriteria> criteria, List<Serializable> params) throws IllegalArgumentException
{
List<String> 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<QFilterOrderBy> orderBys)
{
List<String> 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));
}
}

View File

@ -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<Serializable> 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<QRecord> 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<String, Serializable> 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<QFilterCriteria> criteria, List<Serializable> params) throws IllegalArgumentException
{
List<String> 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<QFilterOrderBy> orderBys)
{
List<String> 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));
}
}

View File

@ -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());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<String, Object>
{
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());
}
}
}
}

View File

@ -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<String> lines = (List<String>) IOUtils.readLines(primeTestDatabaseSqlStream);
String joinedSQL = String.join("\n", lines);
for(String sql : joinedSQL.split(";"))
{
QueryManager.executeUpdate(connection, sql);
}
}
}

View File

@ -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;
}
}

View File

@ -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');