Initial checkin of sqlite module

This commit is contained in:
2025-01-03 19:36:11 -06:00
parent db1269824c
commit 2260fbde84
15 changed files with 3654 additions and 0 deletions

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ QQQ - Low-code Application Framework for Engineers.
~ Copyright (C) 2021-2022. Kingsrook, LLC
~ 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
~ contact@kingsrook.com
~ https://github.com/Kingsrook/
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as
~ published by the Free Software Foundation, either version 3 of the
~ License, or (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>qqq-backend-module-sqlite</artifactId>
<parent>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-parent-project</artifactId>
<version>${revision}</version>
</parent>
<properties>
<!-- props specifically to this module -->
<!-- none at this time -->
</properties>
<dependencies>
<!-- other qqq modules deps -->
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-module-rdbms</artifactId>
<version>${revision}</version>
</dependency>
<!-- 3rd party deps specifically for this module -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.47.1.0</version>
</dependency>
<!-- Common deps for all qqq modules -->
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>${plugin.shade.phase}</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,57 @@
package com.kingsrook.qqq.backend.module.sqlite;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule;
import com.kingsrook.qqq.backend.module.sqlite.model.metadata.SQLiteBackendMetaData;
import com.kingsrook.qqq.backend.module.sqlite.model.metadata.SQLiteTableBackendDetails;
/*******************************************************************************
**
*******************************************************************************/
public class SQLiteBackendModule extends RDBMSBackendModule
{
private static final QLogger LOG = QLogger.getLogger(SQLiteBackendModule.class);
private static final String NAME = "sqlite";
static
{
QBackendModuleDispatcher.registerBackendModule(new SQLiteBackendModule());
}
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).
*******************************************************************************/
public String getBackendType()
{
return NAME;
}
/*******************************************************************************
** Method to identify the class used for backend meta data for this module.
*******************************************************************************/
@Override
public Class<? extends QBackendMetaData> getBackendMetaDataClass()
{
return (SQLiteBackendMetaData.class);
}
/*******************************************************************************
** Method to identify the class used for table-backend details for this module.
*******************************************************************************/
@Override
public Class<? extends QTableBackendDetails> getTableBackendDetailsClass()
{
return (SQLiteTableBackendDetails.class);
}
}

View File

@ -0,0 +1,119 @@
package com.kingsrook.qqq.backend.module.sqlite.model.metadata;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import com.kingsrook.qqq.backend.module.rdbms.strategy.RDBMSActionStrategyInterface;
import com.kingsrook.qqq.backend.module.sqlite.SQLiteBackendModule;
import com.kingsrook.qqq.backend.module.sqlite.strategy.SQLiteRDBMSActionStrategy;
import org.sqlite.JDBC;
/*******************************************************************************
** Meta-data to provide details of an SQLite backend (e.g., path to the database file)
*******************************************************************************/
public class SQLiteBackendMetaData extends RDBMSBackendMetaData
{
private String path;
// todo - overrides to setters for unsupported fields?
// todo - or - change rdbms connection manager to not require an RDBMSBackendMetaData?
/*******************************************************************************
** Default Constructor.
*******************************************************************************/
public SQLiteBackendMetaData()
{
super();
setVendor("sqlite");
setBackendType(SQLiteBackendModule.class);
}
/***************************************************************************
**
***************************************************************************/
@Override
public String buildConnectionString()
{
return "jdbc:sqlite:" + this.path;
}
/***************************************************************************
**
***************************************************************************/
@Override
public String getJdbcDriverClassName()
{
return (JDBC.class.getName());
}
/***************************************************************************
**
***************************************************************************/
public SQLiteBackendMetaData withName(String name)
{
setName(name);
return (this);
}
/*******************************************************************************
** Getter for path
*******************************************************************************/
public String getPath()
{
return (this.path);
}
/*******************************************************************************
** Setter for path
*******************************************************************************/
public void setPath(String path)
{
this.path = path;
}
/*******************************************************************************
** Fluent setter for path
*******************************************************************************/
public SQLiteBackendMetaData withPath(String path)
{
this.path = path;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public RDBMSActionStrategyInterface getActionStrategy()
{
if(getActionStrategyField() == null)
{
if(getActionStrategyCodeReference() != null)
{
setActionStrategyField(QCodeLoader.getAdHoc(RDBMSActionStrategyInterface.class, getActionStrategyCodeReference()));
}
else
{
setActionStrategyField(new SQLiteRDBMSActionStrategy());
}
}
return (getActionStrategyField());
}
}

View File

@ -0,0 +1,45 @@
package com.kingsrook.qqq.backend.module.sqlite.model.metadata;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
/*******************************************************************************
**
*******************************************************************************/
public class SQLiteTableBackendDetails extends QTableBackendDetails
{
private String tableName;
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
public SQLiteTableBackendDetails withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
}

View File

@ -0,0 +1,133 @@
package com.kingsrook.qqq.backend.module.sqlite.strategy;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.strategy.BaseRDBMSActionStrategy;
/*******************************************************************************
** SQLite specialization of the default RDBMS/JDBC action strategy
*******************************************************************************/
public class SQLiteRDBMSActionStrategy extends BaseRDBMSActionStrategy
{
/***************************************************************************
** deal with sqlite not having temporal types... so temporal values
** i guess are stored as strings, as that's how they come back to us - so
** the JDBC methods fail trying to getDate or whatever from them - but
** getting the values as strings, they parse nicely, so do that.
***************************************************************************/
@Override
public Serializable getFieldValueFromResultSet(QFieldType type, ResultSet resultSet, int i) throws SQLException
{
return switch(type)
{
case DATE ->
{
try
{
yield parseString(s -> LocalDate.parse(s), resultSet, i);
}
catch(Exception e)
{
/////////////////////////////////////////////////////////////////////////////////
// handle the case of, the value we got back is actually a date-time -- so -- //
// let's parse it as such, and then map into a LocalDate in the session zoneId //
/////////////////////////////////////////////////////////////////////////////////
Instant instant = (Instant) parseString(s -> Instant.parse(s), resultSet, i);
if(instant == null)
{
yield null;
}
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
yield instant.atZone(zoneId).toLocalDate();
}
}
case TIME -> parseString(s -> LocalTime.parse(s), resultSet, i);
case DATE_TIME -> parseString(s -> Instant.parse(s), resultSet, i);
default -> super.getFieldValueFromResultSet(type, resultSet, i);
};
}
/***************************************************************************
** helper method for getFieldValueFromResultSet
***************************************************************************/
private Serializable parseString(Function<String, Serializable> parser, ResultSet resultSet, int i) throws SQLException
{
String valueString = QueryManager.getString(resultSet, i);
if(valueString == null)
{
return (null);
}
else
{
return parser.apply(valueString);
}
}
/***************************************************************************
* bind temporal types as strings (see above comment re: sqlite temporal types)
***************************************************************************/
@Override
protected int bindParamObject(PreparedStatement statement, int index, Object value) throws SQLException
{
if(value instanceof Instant || value instanceof LocalTime || value instanceof LocalDate)
{
bindParam(statement, index, value.toString());
return 1;
}
else
{
return super.bindParamObject(statement, index, value);
}
}
/***************************************************************************
** per discussion (and rejected PR mentioned) on https://github.com/prrvchr/sqlite-jdbc
** sqlite jdbc by default will only return the latest generated serial. but we can get
** them all by appending this "RETURNING id" to the query, and then calling execute()
** (instead of executeUpdate()) and getResultSet (instead of getGeneratedKeys())
***************************************************************************/
@Override
public List<Serializable> executeInsertForGeneratedIds(Connection connection, String sql, List<Object> params, QFieldMetaData primaryKeyField) throws SQLException
{
sql = sql + " RETURNING " + getColumnName(primaryKeyField);
try(PreparedStatement statement = connection.prepareStatement(sql))
{
bindParams(params.toArray(), statement);
incrementStatistic(STAT_QUERIES_RAN);
statement.execute();
ResultSet generatedKeys = statement.getResultSet();
List<Serializable> rs = new ArrayList<>();
while(generatedKeys.next())
{
rs.add(getFieldValueFromResultSet(primaryKeyField.getType(), generatedKeys, 1));
}
return (rs);
}
}
}

View File

@ -0,0 +1,124 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.sqlite;
import java.sql.Connection;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import com.kingsrook.qqq.backend.module.rdbms.strategy.BaseRDBMSActionStrategy;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
/*******************************************************************************
**
*******************************************************************************/
public class BaseTest
{
private static final QLogger LOG = QLogger.getLogger(BaseTest.class);
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void baseBeforeEach() throws Exception
{
QContext.init(TestUtils.defineInstance(), new QSession());
TestUtils.primeTestDatabase("prime-test-database.sql");
}
/*******************************************************************************
**
*******************************************************************************/
@AfterEach
void baseAfterEach()
{
BaseRDBMSActionStrategy actionStrategy = getBaseRDBMSActionStrategy();
actionStrategy.setPageSize(BaseRDBMSActionStrategy.DEFAULT_PAGE_SIZE);
actionStrategy.resetStatistics();
actionStrategy.setCollectStatistics(false);
QContext.clear();
}
/***************************************************************************
*
***************************************************************************/
protected static BaseRDBMSActionStrategy getBaseRDBMSActionStrategy()
{
RDBMSBackendMetaData backend = (RDBMSBackendMetaData) QContext.getQInstance().getBackend(TestUtils.DEFAULT_BACKEND_NAME);
BaseRDBMSActionStrategy actionStrategy = (BaseRDBMSActionStrategy) backend.getActionStrategy();
return actionStrategy;
}
/***************************************************************************
*
***************************************************************************/
protected static BaseRDBMSActionStrategy getBaseRDBMSActionStrategyAndActivateCollectingStatistics()
{
BaseRDBMSActionStrategy actionStrategy = getBaseRDBMSActionStrategy();
actionStrategy.setCollectStatistics(true);
actionStrategy.resetStatistics();
return actionStrategy;
}
/*******************************************************************************
**
*******************************************************************************/
protected static void reInitInstanceInContext(QInstance qInstance)
{
if(qInstance.equals(QContext.getQInstance()))
{
LOG.warn("Unexpected condition - the same qInstance that is already in the QContext was passed into reInit. You probably want a new QInstance object instance.");
}
QContext.init(qInstance, new QSession());
}
/*******************************************************************************
**
*******************************************************************************/
protected void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception
{
ConnectionManager connectionManager = new ConnectionManager();
Connection connection = connectionManager.getConnection(TestUtils.defineBackend());
QueryManager.executeStatement(connection, sql, resultSetProcessor);
connection.close();
}
}

View File

@ -0,0 +1,470 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.sqlite;
import java.io.File;
import java.io.InputStream;
import java.sql.Connection;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.C3P0PooledConnectionProvider;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
import com.kingsrook.qqq.backend.module.sqlite.model.metadata.SQLiteBackendMetaData;
import org.apache.commons.io.IOUtils;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
**
*******************************************************************************/
public class TestUtils
{
public static final String DEFAULT_BACKEND_NAME = "default";
public static final String MEMORY_BACKEND_NAME = "memory";
public static final String TABLE_NAME_PERSON = "personTable";
public static final String TABLE_NAME_PERSONAL_ID_CARD = "personalIdCard";
public static final String TABLE_NAME_STORE = "store";
public static final String TABLE_NAME_ORDER = "order";
public static final String TABLE_NAME_ORDER_INSTRUCTIONS = "orderInstructions";
public static final String TABLE_NAME_ITEM = "item";
public static final String TABLE_NAME_ORDER_LINE = "orderLine";
public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic";
public static final String TABLE_NAME_WAREHOUSE = "warehouse";
public static final String TABLE_NAME_WAREHOUSE_STORE_INT = "warehouseStoreInt";
public static final String SECURITY_KEY_STORE_ALL_ACCESS = "storeAllAccess";
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public static void primeTestDatabase(String sqlFileName) throws Exception
{
SQLiteBackendMetaData backend = TestUtils.defineBackend();
File file = new File(backend.getPath());
/*
if(file.exists())
{
if(!file.delete())
{
throw (new Exception("SQLite database at [" + file.getAbsolutePath() + "] exists, and could not be deleted before (re)priming the database."));
}
}
*/
file.getParentFile().mkdirs();
try(Connection connection = ConnectionManager.getConnection(backend))
{
InputStream primeTestDatabaseSqlStream = SQLiteBackendModule.class.getResourceAsStream("/" + sqlFileName);
assertNotNull(primeTestDatabaseSqlStream);
List<String> lines = (List<String>) IOUtils.readLines(primeTestDatabaseSqlStream);
lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList();
String joinedSQL = String.join("\n", lines);
for(String sql : joinedSQL.split(";"))
{
if(sql.matches("(?s).*[a-zA-Z0-9_].*"))
{
QueryManager.executeUpdate(connection, sql);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
public static QInstance defineInstance()
{
QInstance qInstance = new QInstance();
qInstance.addBackend(defineBackend());
qInstance.addBackend(defineMemoryBackend());
qInstance.addTable(defineTablePerson());
qInstance.addPossibleValueSource(definePvsPerson());
qInstance.addTable(defineTablePersonalIdCard());
qInstance.addJoin(defineJoinPersonAndPersonalIdCard());
addOmsTablesAndJoins(qInstance);
qInstance.setAuthentication(defineAuthentication());
return (qInstance);
}
/*******************************************************************************
** Define the in-memory backend used in standard tests
*******************************************************************************/
public static QBackendMetaData defineMemoryBackend()
{
return new QBackendMetaData()
.withName(MEMORY_BACKEND_NAME)
.withBackendType(MemoryBackendModule.class);
}
/*******************************************************************************
** Define the authentication used in standard tests - using 'mock' type.
**
*******************************************************************************/
public static QAuthenticationMetaData defineAuthentication()
{
return new QAuthenticationMetaData()
.withName("mock")
.withType(QAuthenticationType.MOCK);
}
/*******************************************************************************
**
*******************************************************************************/
public static SQLiteBackendMetaData defineBackend()
{
SQLiteBackendMetaData sqLiteBackendMetaData = new SQLiteBackendMetaData()
.withName(DEFAULT_BACKEND_NAME)
.withPath("/tmp/sqlite/test.db");
sqLiteBackendMetaData.setQueriesForNewConnections(List.of(
"PRAGMA foreign_keys = ON"
));
sqLiteBackendMetaData.setConnectionProvider(new QCodeReference(C3P0PooledConnectionProvider.class));
return sqLiteBackendMetaData;
}
/*******************************************************************************
**
*******************************************************************************/
public static QTableMetaData defineTablePerson()
{
return new QTableMetaData()
.withName(TABLE_NAME_PERSON)
.withLabel("Person")
.withRecordLabelFormat("%s %s")
.withRecordLabelFields("firstName", "lastName")
.withBackendName(DEFAULT_BACKEND_NAME)
.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).withBackendName("email"))
.withField(new QFieldMetaData("isEmployed", QFieldType.BOOLEAN).withBackendName("is_employed"))
.withField(new QFieldMetaData("annualSalary", QFieldType.DECIMAL).withBackendName("annual_salary"))
.withField(new QFieldMetaData("daysWorked", QFieldType.INTEGER).withBackendName("days_worked"))
.withField(new QFieldMetaData("homeTown", QFieldType.STRING).withBackendName("home_town"))
.withField(new QFieldMetaData("startTime", QFieldType.TIME).withBackendName("start_time"))
.withBackendDetails(new RDBMSTableBackendDetails()
.withTableName("person"));
}
/*******************************************************************************
**
*******************************************************************************/
private static QPossibleValueSource definePvsPerson()
{
return (new QPossibleValueSource()
.withName(TABLE_NAME_PERSON)
.withType(QPossibleValueSourceType.TABLE)
.withTableName(TABLE_NAME_PERSON)
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY));
}
/*******************************************************************************
** Define a 1:1 table with Person.
**
*******************************************************************************/
private static QTableMetaData defineTablePersonalIdCard()
{
return new QTableMetaData()
.withName(TABLE_NAME_PERSONAL_ID_CARD)
.withLabel("Personal Id Card")
.withBackendName(DEFAULT_BACKEND_NAME)
.withBackendDetails(new RDBMSTableBackendDetails()
.withTableName("personal_id_card"))
.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("personId", QFieldType.INTEGER).withBackendName("person_id"))
.withField(new QFieldMetaData("idNumber", QFieldType.STRING).withBackendName("id_number"));
}
/*******************************************************************************
**
*******************************************************************************/
private static QJoinMetaData defineJoinPersonAndPersonalIdCard()
{
return new QJoinMetaData()
.withLeftTable(TABLE_NAME_PERSON)
.withRightTable(TABLE_NAME_PERSONAL_ID_CARD)
.withInferredName()
.withType(JoinType.ONE_TO_ONE)
.withJoinOn(new JoinOn("id", "personId"));
}
/*******************************************************************************
**
*******************************************************************************/
private static void addOmsTablesAndJoins(QInstance qInstance)
{
qInstance.addTable(defineBaseTable(TABLE_NAME_STORE, "store")
.withRecordLabelFormat("%s")
.withRecordLabelFields("name")
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("id"))
.withField(new QFieldMetaData("name", QFieldType.STRING))
);
qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER, "order")
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId"))
.withAssociation(new Association().withName("orderLine").withAssociatedTableName(TABLE_NAME_ORDER_LINE).withJoinName("orderJoinOrderLine"))
.withExposedJoin(new ExposedJoin().withJoinTable(TABLE_NAME_ITEM).withJoinPath(List.of("orderJoinOrderLine", "orderLineJoinItem")))
.withExposedJoin(new ExposedJoin().withJoinTable(TABLE_NAME_ORDER_INSTRUCTIONS).withJoinPath(List.of("orderJoinCurrentOrderInstructions")).withLabel("Current Order Instructions"))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
.withField(new QFieldMetaData("billToPersonId", QFieldType.INTEGER).withBackendName("bill_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
.withField(new QFieldMetaData("shipToPersonId", QFieldType.INTEGER).withBackendName("ship_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
.withField(new QFieldMetaData("currentOrderInstructionsId", QFieldType.INTEGER).withBackendName("current_order_instructions_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
);
qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER_INSTRUCTIONS, "order_instructions")
.withRecordSecurityLock(new RecordSecurityLock()
.withSecurityKeyType(TABLE_NAME_STORE)
.withFieldName("order.storeId")
.withJoinNameChain(List.of("orderInstructionsJoinOrder")))
.withField(new QFieldMetaData("orderId", QFieldType.INTEGER).withBackendName("order_id"))
.withField(new QFieldMetaData("instructions", QFieldType.STRING))
.withExposedJoin(new ExposedJoin().withJoinTable(TABLE_NAME_ORDER).withJoinPath(List.of("orderInstructionsJoinOrder")))
);
qInstance.addTable(defineBaseTable(TABLE_NAME_ITEM, "item")
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId"))
.withExposedJoin(new ExposedJoin().withJoinTable(TABLE_NAME_ORDER).withJoinPath(List.of("orderLineJoinItem", "orderJoinOrderLine")))
.withField(new QFieldMetaData("sku", QFieldType.STRING))
.withField(new QFieldMetaData("description", QFieldType.STRING))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
);
qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER_LINE, "order_line")
.withRecordSecurityLock(new RecordSecurityLock()
.withSecurityKeyType(TABLE_NAME_STORE)
.withFieldName("order.storeId")
.withJoinNameChain(List.of("orderJoinOrderLine")))
.withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_LINE_ITEM_EXTRINSIC).withJoinName("orderLineJoinLineItemExtrinsic"))
.withField(new QFieldMetaData("orderId", QFieldType.INTEGER).withBackendName("order_id"))
.withField(new QFieldMetaData("sku", QFieldType.STRING))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
.withField(new QFieldMetaData("quantity", QFieldType.INTEGER))
);
qInstance.addTable(defineBaseTable(TABLE_NAME_LINE_ITEM_EXTRINSIC, "line_item_extrinsic")
.withRecordSecurityLock(new RecordSecurityLock()
.withSecurityKeyType(TABLE_NAME_STORE)
.withFieldName("order.storeId")
.withJoinNameChain(List.of("orderJoinOrderLine", "orderLineJoinLineItemExtrinsic")))
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("orderLineId", QFieldType.INTEGER).withBackendName("order_line_id"))
.withField(new QFieldMetaData("key", QFieldType.STRING))
.withField(new QFieldMetaData("value", QFieldType.STRING))
);
qInstance.addTable(defineBaseTable(TABLE_NAME_WAREHOUSE_STORE_INT, "warehouse_store_int")
.withField(new QFieldMetaData("warehouseId", QFieldType.INTEGER).withBackendName("warehouse_id"))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id"))
);
qInstance.addTable(defineBaseTable(TABLE_NAME_WAREHOUSE, "warehouse")
.withRecordSecurityLock(new RecordSecurityLock()
.withSecurityKeyType(TABLE_NAME_STORE)
.withFieldName(TABLE_NAME_WAREHOUSE_STORE_INT + ".storeId")
.withJoinNameChain(List.of(QJoinMetaData.makeInferredJoinName(TestUtils.TABLE_NAME_WAREHOUSE, TestUtils.TABLE_NAME_WAREHOUSE_STORE_INT)))
)
.withField(new QFieldMetaData("name", QFieldType.STRING).withBackendName("name"))
);
qInstance.addJoin(new QJoinMetaData()
.withType(JoinType.ONE_TO_MANY)
.withLeftTable(TestUtils.TABLE_NAME_WAREHOUSE)
.withRightTable(TestUtils.TABLE_NAME_WAREHOUSE_STORE_INT)
.withInferredName()
.withJoinOn(new JoinOn("id", "warehouseId"))
);
qInstance.addJoin(new QJoinMetaData()
.withName("orderJoinStore")
.withLeftTable(TABLE_NAME_ORDER)
.withRightTable(TABLE_NAME_STORE)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("storeId", "id"))
);
qInstance.addJoin(new QJoinMetaData()
.withName("orderJoinBillToPerson")
.withLeftTable(TABLE_NAME_ORDER)
.withRightTable(TABLE_NAME_PERSON)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("billToPersonId", "id"))
);
qInstance.addJoin(new QJoinMetaData()
.withName("orderJoinShipToPerson")
.withLeftTable(TABLE_NAME_ORDER)
.withRightTable(TABLE_NAME_PERSON)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("shipToPersonId", "id"))
);
qInstance.addJoin(new QJoinMetaData()
.withName("itemJoinStore")
.withLeftTable(TABLE_NAME_ITEM)
.withRightTable(TABLE_NAME_STORE)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("storeId", "id"))
);
qInstance.addJoin(new QJoinMetaData()
.withName("orderJoinOrderLine")
.withLeftTable(TABLE_NAME_ORDER)
.withRightTable(TABLE_NAME_ORDER_LINE)
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn("id", "orderId"))
);
qInstance.addJoin(new QJoinMetaData()
.withName("orderLineJoinItem")
.withLeftTable(TABLE_NAME_ORDER_LINE)
.withRightTable(TABLE_NAME_ITEM)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("sku", "sku"))
.withJoinOn(new JoinOn("storeId", "storeId"))
);
qInstance.addJoin(new QJoinMetaData()
.withName("orderLineJoinLineItemExtrinsic")
.withLeftTable(TABLE_NAME_ORDER_LINE)
.withRightTable(TABLE_NAME_LINE_ITEM_EXTRINSIC)
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn("id", "orderLineId"))
);
qInstance.addJoin(new QJoinMetaData()
.withName("orderJoinCurrentOrderInstructions")
.withLeftTable(TABLE_NAME_ORDER)
.withRightTable(TABLE_NAME_ORDER_INSTRUCTIONS)
.withType(JoinType.ONE_TO_ONE)
.withJoinOn(new JoinOn("currentOrderInstructionsId", "id"))
);
qInstance.addJoin(new QJoinMetaData()
.withName("orderInstructionsJoinOrder")
.withRightTable(TABLE_NAME_ORDER_INSTRUCTIONS)
.withLeftTable(TABLE_NAME_ORDER)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("id", "orderId"))
);
qInstance.addPossibleValueSource(new QPossibleValueSource()
.withName("store")
.withType(QPossibleValueSourceType.TABLE)
.withTableName(TABLE_NAME_STORE)
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY)
);
qInstance.addSecurityKeyType(new QSecurityKeyType()
.withName(TABLE_NAME_STORE)
.withAllAccessKeyName(SECURITY_KEY_STORE_ALL_ACCESS)
.withPossibleValueSourceName(TABLE_NAME_STORE));
}
/*******************************************************************************
**
*******************************************************************************/
private static QTableMetaData defineBaseTable(String tableName, String backendTableName)
{
return new QTableMetaData()
.withName(tableName)
.withBackendName(DEFAULT_BACKEND_NAME)
.withBackendDetails(new RDBMSTableBackendDetails().withTableName(backendTableName))
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER));
}
/*******************************************************************************
**
*******************************************************************************/
public static List<QRecord> queryTable(String tableName) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
return (queryOutput.getRecords());
}
}

View File

@ -0,0 +1,208 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.sqlite.actions;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.module.sqlite.BaseTest;
import com.kingsrook.qqq.backend.module.sqlite.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
**
*******************************************************************************/
public class SQLiteCountActionTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testUnfilteredCount() throws QException
{
CountInput countInput = initCountRequest();
CountOutput countOutput = new CountAction().execute(countInput);
assertEquals(5, countOutput.getCount(), "Unfiltered query should find all rows");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testEqualsQueryCount() throws QException
{
String email = "darin.kelkhoff@gmail.com";
CountInput countInput = initCountRequest();
countInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria()
.withFieldName("email")
.withOperator(QCriteriaOperator.EQUALS)
.withValues(List.of(email)))
);
CountOutput countOutput = new CountAction().execute(countInput);
assertEquals(1, countOutput.getCount(), "Expected # of rows");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testNotEqualsQuery() throws QException
{
String email = "darin.kelkhoff@gmail.com";
CountInput countInput = initCountRequest();
countInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria()
.withFieldName("email")
.withOperator(QCriteriaOperator.NOT_EQUALS)
.withValues(List.of(email)))
);
CountOutput countOutput = new CountAction().execute(countInput);
assertEquals(4, countOutput.getCount(), "Expected # of rows");
}
/*******************************************************************************
**
*******************************************************************************/
private CountInput initCountRequest()
{
CountInput countInput = new CountInput();
countInput.setTableName(TestUtils.defineTablePerson().getName());
return countInput;
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOneToOneInnerJoinWithoutWhere() throws QException
{
CountInput countInput = initCountRequest();
countInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD));
CountOutput countOutput = new CountAction().execute(countInput);
assertEquals(3, countOutput.getCount(), "Join count should find 3 rows");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOneToOneLeftJoinWithoutWhere() throws QException
{
CountInput countInput = initCountRequest();
countInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.LEFT));
CountOutput countOutput = new CountAction().execute(countInput);
assertEquals(5, countOutput.getCount(), "Left Join count should find 5 rows");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOneToOneRightJoinWithoutWhere() throws QException
{
CountInput countInput = initCountRequest();
countInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withType(QueryJoin.Type.RIGHT));
CountOutput countOutput = new CountAction().execute(countInput);
assertEquals(6, countOutput.getCount(), "Right Join count should find 6 rows");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOneToOneInnerJoinWithWhere() throws QException
{
CountInput countInput = initCountRequest();
countInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSON, TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true));
countInput.setFilter(new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber", QCriteriaOperator.STARTS_WITH, "1980")));
CountOutput countOutput = new CountAction().execute(countInput);
assertEquals(2, countOutput.getCount(), "Right Join count should find 2 rows");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRecordSecurity() throws QException
{
CountInput countInput = new CountInput();
countInput.setTableName(TestUtils.TABLE_NAME_ORDER);
QContext.setQSession(new QSession());
assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(0);
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(8);
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2).withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3));
assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(5);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRecordSecurityWithLockFromJoinTableWhereTheKeyIsOnTheManySide() throws QException
{
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
CountInput countInput = new CountInput();
countInput.setTableName(TestUtils.TABLE_NAME_WAREHOUSE);
assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(4);
}
}

View File

@ -0,0 +1,322 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.sqlite.actions;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSDeleteAction;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
import com.kingsrook.qqq.backend.module.rdbms.strategy.BaseRDBMSActionStrategy;
import com.kingsrook.qqq.backend.module.sqlite.BaseTest;
import com.kingsrook.qqq.backend.module.sqlite.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
**
*******************************************************************************/
public class SQLiteDeleteActionTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testDeleteAll() throws Exception
{
DeleteInput deleteInput = initStandardPersonDeleteRequest();
deleteInput.setPrimaryKeys(List.of(1, 2, 3, 4, 5));
DeleteOutput deleteResult = new DeleteAction().execute(deleteInput);
assertEquals(5, deleteResult.getDeletedRecordCount(), "Unfiltered delete should return all rows");
assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors");
runTestSql("SELECT id FROM person", (rs -> assertFalse(rs.next())));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testDeleteOne() throws Exception
{
DeleteInput deleteInput = initStandardPersonDeleteRequest();
deleteInput.setPrimaryKeys(List.of(1));
DeleteOutput deleteResult = new DeleteAction().execute(deleteInput);
assertEquals(1, deleteResult.getDeletedRecordCount(), "Should delete one row");
assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors");
runTestSql("SELECT id FROM person WHERE id = 1", (rs -> assertFalse(rs.next())));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testDeleteSome() throws Exception
{
DeleteInput deleteInput = initStandardPersonDeleteRequest();
deleteInput.setPrimaryKeys(List.of(1, 3, 5));
DeleteOutput deleteResult = new DeleteAction().execute(deleteInput);
assertEquals(3, deleteResult.getDeletedRecordCount(), "Should delete one row");
assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors");
runTestSql("SELECT id FROM person", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
assertTrue(rs.getInt(1) == 2 || rs.getInt(1) == 4);
}
assertEquals(2, rowsFound);
}));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDeleteSomeIdsThatExistAndSomeThatDoNot() throws Exception
{
DeleteInput deleteInput = initStandardPersonDeleteRequest();
deleteInput.setPrimaryKeys(List.of(1, -1));
DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput);
assertEquals(1, deleteResult.getDeletedRecordCount(), "Should delete one row");
/////////////////////////////////////////////////////////////////////////////////////
// note - that if we went to the top-level DeleteAction, then it would have pre- //
// checked that the ids existed, and it WOULD give us an error for the -1 row here //
/////////////////////////////////////////////////////////////////////////////////////
assertEquals(0, deleteResult.getRecordsWithErrors().size(), "should have no errors (the one not found is just noop)");
}
/*******************************************************************************
**
*******************************************************************************/
private DeleteInput initStandardPersonDeleteRequest()
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(TestUtils.defineTablePerson().getName());
return deleteInput;
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testDeleteWhereForeignKeyBlocksSome() throws Exception
{
//////////////////////////////////////////////////////////////////
// load the parent-child tables, with foreign keys and instance //
//////////////////////////////////////////////////////////////////
TestUtils.primeTestDatabase("prime-test-database-parent-child-tables.sql");
DeleteInput deleteInput = initChildTableInstanceAndDeleteRequest();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// try to delete all of the child records - 2 should fail, because they are referenced by parent_table.child_id //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
deleteInput.setPrimaryKeys(List.of(1, 2, 3, 4, 5));
BaseRDBMSActionStrategy actionStrategy = getBaseRDBMSActionStrategyAndActivateCollectingStatistics();
DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput);
////////////////////////////////////////////////////////////////////////////////////////
// assert that 6 queries ran - the initial delete (which failed), then 5 more deletes //
////////////////////////////////////////////////////////////////////////////////////////
actionStrategy.setCollectStatistics(false);
Map<String, Integer> queryStats = actionStrategy.getStatistics();
assertEquals(6, queryStats.get(BaseRDBMSActionStrategy.STAT_QUERIES_RAN), "Number of queries ran");
assertEquals(2, deleteResult.getRecordsWithErrors().size(), "Should get back the 2 records with errors");
assertTrue(deleteResult.getRecordsWithErrors().stream().noneMatch(r -> r.getErrors().isEmpty()), "All we got back should have errors");
assertEquals(3, deleteResult.getDeletedRecordCount(), "Should get back that 3 were deleted");
runTestSql("SELECT id FROM child_table", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
///////////////////////////////////////////
// child_table rows 1 & 3 should survive //
///////////////////////////////////////////
assertTrue(rs.getInt(1) == 1 || rs.getInt(1) == 3);
}
assertEquals(2, rowsFound);
}));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testDeleteByFilterThatJustWorks() throws Exception
{
//////////////////////////////////////////////////////////////////
// load the parent-child tables, with foreign keys and instance //
//////////////////////////////////////////////////////////////////
TestUtils.primeTestDatabase("prime-test-database-parent-child-tables.sql");
DeleteInput deleteInput = initChildTableInstanceAndDeleteRequest();
////////////////////////////////////////////////////////////////////////
// try to delete the records without a foreign key that'll block them //
////////////////////////////////////////////////////////////////////////
deleteInput.setQueryFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, List.of(2, 4, 5))));
BaseRDBMSActionStrategy actionStrategy = getBaseRDBMSActionStrategyAndActivateCollectingStatistics();
DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput);
//////////////////////////////////
// assert that just 1 query ran //
//////////////////////////////////
actionStrategy.setCollectStatistics(false);
Map<String, Integer> queryStats = actionStrategy.getStatistics();
assertEquals(1, queryStats.get(BaseRDBMSActionStrategy.STAT_QUERIES_RAN), "Number of queries ran");
assertEquals(3, deleteResult.getDeletedRecordCount(), "Should get back that 3 were deleted");
runTestSql("SELECT id FROM child_table", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
///////////////////////////////////////////
// child_table rows 1 & 3 should survive //
///////////////////////////////////////////
assertTrue(rs.getInt(1) == 1 || rs.getInt(1) == 3);
}
assertEquals(2, rowsFound);
}));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testDeleteByFilterWhereForeignKeyBlocksSome() throws Exception
{
//////////////////////////////////////////////////////////////////
// load the parent-child tables, with foreign keys and instance //
//////////////////////////////////////////////////////////////////
TestUtils.primeTestDatabase("prime-test-database-parent-child-tables.sql");
DeleteInput deleteInput = initChildTableInstanceAndDeleteRequest();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// try to delete all of the child records - 2 should fail, because they are referenced by parent_table.child_id //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
deleteInput.setQueryFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, List.of(1, 2, 3, 4, 5))));
BaseRDBMSActionStrategy actionStrategy = getBaseRDBMSActionStrategyAndActivateCollectingStatistics();
DeleteOutput deleteResult = new RDBMSDeleteAction().execute(deleteInput);
///////////////////////////////////////////////////////////////////////////////////////////////////////
// assert that 8 queries ran - the initial delete (which failed), then 1 to look up the ids //
// from that query, another to try to delete all those ids (also fails), and finally 5 deletes by id //
// todo - maybe we shouldn't do that 2nd "try to delete 'em all by id"... why would it ever work, //
// but the original filter query didn't (other than malformed SQL)? //
///////////////////////////////////////////////////////////////////////////////////////////////////////
actionStrategy.setCollectStatistics(false);
Map<String, Integer> queryStats = actionStrategy.getStatistics();
assertEquals(8, queryStats.get(BaseRDBMSActionStrategy.STAT_QUERIES_RAN), "Number of queries ran");
assertEquals(2, deleteResult.getRecordsWithErrors().size(), "Should get back the 2 records with errors");
assertTrue(deleteResult.getRecordsWithErrors().stream().noneMatch(r -> r.getErrors().isEmpty()), "All we got back should have errors");
assertEquals(3, deleteResult.getDeletedRecordCount(), "Should get back that 3 were deleted");
runTestSql("SELECT id FROM child_table", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
///////////////////////////////////////////
// child_table rows 1 & 3 should survive //
///////////////////////////////////////////
assertTrue(rs.getInt(1) == 1 || rs.getInt(1) == 3);
}
assertEquals(2, rowsFound);
}));
}
/*******************************************************************************
**
*******************************************************************************/
private DeleteInput initChildTableInstanceAndDeleteRequest()
{
QInstance qInstance = TestUtils.defineInstance();
String childTableName = "childTable";
qInstance.addTable(new QTableMetaData()
.withName(childTableName)
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("name", QFieldType.STRING))
.withBackendDetails(new RDBMSTableBackendDetails()
.withTableName("child_table")));
qInstance.addTable(new QTableMetaData()
.withName("parentTable")
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("name", QFieldType.STRING))
.withField(new QFieldMetaData("childId", QFieldType.INTEGER).withBackendName("child_id"))
.withBackendDetails(new RDBMSTableBackendDetails()
.withTableName("parent_table")));
reInitInstanceInContext(qInstance);
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(childTableName);
return deleteInput;
}
}

View File

@ -0,0 +1,219 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.sqlite.actions;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.module.rdbms.strategy.BaseRDBMSActionStrategy;
import com.kingsrook.qqq.backend.module.sqlite.BaseTest;
import com.kingsrook.qqq.backend.module.sqlite.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
**
*******************************************************************************/
public class SQLiteInsertActionTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testInsertNullList() throws QException
{
InsertInput insertInput = initInsertRequest();
insertInput.setRecords(null);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals(0, insertOutput.getRecords().size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testInsertEmptyList() throws QException
{
InsertInput insertInput = initInsertRequest();
insertInput.setRecords(Collections.emptyList());
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals(0, insertOutput.getRecords().size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testInsertOne() throws Exception
{
InsertInput insertInput = initInsertRequest();
QRecord record = new QRecord().withTableName("person")
.withValue("firstName", "James")
.withValue("lastName", "Kirk")
.withValue("email", "jamestk@starfleet.net")
.withValue("birthDate", "2210-05-20");
insertInput.setRecords(List.of(record));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals(1, insertOutput.getRecords().size(), "Should return 1 row");
assertNotNull(insertOutput.getRecords().get(0).getValue("id"), "Should have an id in the row");
// todo - add errors to QRecord? assertTrue(insertResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors");
assertAnInsertedPersonRecord("James", "Kirk", 6);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testInsertMany() throws Exception
{
getBaseRDBMSActionStrategyAndActivateCollectingStatistics()
.setPageSize(2);
InsertInput insertInput = initInsertRequest();
QRecord record1 = new QRecord().withTableName("person")
.withValue("firstName", "Jean-Luc")
.withValue("lastName", "Picard")
.withValue("email", "jl@starfleet.net")
.withValue("birthDate", "2310-05-20");
QRecord record2 = new QRecord().withTableName("person")
.withValue("firstName", "William")
.withValue("lastName", "Riker")
.withValue("email", "notthomas@starfleet.net")
.withValue("birthDate", "2320-05-20");
QRecord record3 = new QRecord().withTableName("person")
.withValue("firstName", "Beverly")
.withValue("lastName", "Crusher")
.withValue("email", "doctor@starfleet.net")
.withValue("birthDate", "2320-06-26");
insertInput.setRecords(List.of(record1, record2, record3));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
assertEquals(3, insertOutput.getRecords().size(), "Should return right # of rows");
assertEquals(6, insertOutput.getRecords().get(0).getValue("id"), "Should have next id in the row");
assertEquals(7, insertOutput.getRecords().get(1).getValue("id"), "Should have next id in the row");
assertEquals(8, insertOutput.getRecords().get(2).getValue("id"), "Should have next id in the row");
Map<String, Integer> statistics = getBaseRDBMSActionStrategy().getStatistics();
assertEquals(2, statistics.get(BaseRDBMSActionStrategy.STAT_QUERIES_RAN));
assertAnInsertedPersonRecord("Jean-Luc", "Picard", 6);
assertAnInsertedPersonRecord("William", "Riker", 7);
assertAnInsertedPersonRecord("Beverly", "Crusher", 8);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInsertAssociations() throws QException
{
QContext.getQSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1);
int originalNoOfOrderLineExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).size();
int originalNoOfOrderLines = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER_LINE).size();
int originalNoOfOrders = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size();
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(
new QRecord().withValue("storeId", 1).withValue("billToPersonId", 100).withValue("shipToPersonId", 200)
.withAssociatedRecord("orderLine", new QRecord().withValue("storeId", 1).withValue("sku", "BASIC1").withValue("quantity", 1)
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-1.1").withValue("value", "LINE-VAL-1")))
.withAssociatedRecord("orderLine", new QRecord().withValue("storeId", 1).withValue("sku", "BASIC2").withValue("quantity", 2)
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.1").withValue("value", "LINE-VAL-2"))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.2").withValue("value", "LINE-VAL-3")))
));
new InsertAction().execute(insertInput);
List<QRecord> orders = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER);
assertEquals(originalNoOfOrders + 1, orders.size());
assertTrue(orders.stream().anyMatch(r -> Objects.equals(r.getValue("billToPersonId"), 100) && Objects.equals(r.getValue("shipToPersonId"), 200)));
List<QRecord> orderLines = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER_LINE);
assertEquals(originalNoOfOrderLines + 2, orderLines.size());
assertTrue(orderLines.stream().anyMatch(r -> Objects.equals(r.getValue("sku"), "BASIC1") && Objects.equals(r.getValue("quantity"), 1)));
assertTrue(orderLines.stream().anyMatch(r -> Objects.equals(r.getValue("sku"), "BASIC2") && Objects.equals(r.getValue("quantity"), 2)));
List<QRecord> lineItemExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
assertEquals(originalNoOfOrderLineExtrinsics + 3, lineItemExtrinsics.size());
assertTrue(lineItemExtrinsics.stream().anyMatch(r -> Objects.equals(r.getValue("key"), "LINE-EXT-1.1") && Objects.equals(r.getValue("value"), "LINE-VAL-1")));
assertTrue(lineItemExtrinsics.stream().anyMatch(r -> Objects.equals(r.getValue("key"), "LINE-EXT-2.1") && Objects.equals(r.getValue("value"), "LINE-VAL-2")));
assertTrue(lineItemExtrinsics.stream().anyMatch(r -> Objects.equals(r.getValue("key"), "LINE-EXT-2.2") && Objects.equals(r.getValue("value"), "LINE-VAL-3")));
}
/***************************************************************************
**
***************************************************************************/
private void assertAnInsertedPersonRecord(String firstName, String lastName, Integer id) throws Exception
{
runTestSql("SELECT * FROM person WHERE last_name = '" + lastName + "'", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
assertEquals(id, rs.getInt("id"));
assertEquals(firstName, rs.getString("first_name"));
assertNotNull(rs.getString("create_date"));
assertNotNull(rs.getString("modify_date"));
}
assertEquals(1, rowsFound);
}));
}
/*******************************************************************************
**
*******************************************************************************/
private InsertInput initInsertRequest()
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON);
return insertInput;
}
}

View File

@ -0,0 +1,443 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.sqlite.actions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.module.rdbms.strategy.BaseRDBMSActionStrategy;
import com.kingsrook.qqq.backend.module.sqlite.BaseTest;
import com.kingsrook.qqq.backend.module.sqlite.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/*******************************************************************************
**
*******************************************************************************/
public class SQLiteUpdateActionTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
public void beforeEach() throws Exception
{
getBaseRDBMSActionStrategyAndActivateCollectingStatistics();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testUpdateNullList() throws QException
{
UpdateInput updateInput = initUpdateRequest();
updateInput.setRecords(null);
UpdateOutput updateResult = new UpdateAction().execute(updateInput);
assertEquals(0, updateResult.getRecords().size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testUpdateEmptyList() throws QException
{
UpdateInput updateInput = initUpdateRequest();
updateInput.setRecords(Collections.emptyList());
new UpdateAction().execute(updateInput);
UpdateOutput updateResult = new UpdateAction().execute(updateInput);
assertEquals(0, updateResult.getRecords().size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testUpdateOne() throws Exception
{
UpdateInput updateInput = initUpdateRequest();
QRecord record = new QRecord()
.withValue("id", 2)
.withValue("firstName", "James")
.withValue("lastName", "Kirk")
.withValue("email", "jamestk@starfleet.net")
.withValue("birthDate", "2210-05-20");
updateInput.setRecords(List.of(record));
UpdateOutput updateResult = new UpdateAction().execute(updateInput);
Map<String, Integer> statistics = getBaseRDBMSActionStrategy().getStatistics();
assertEquals(2, statistics.get(BaseRDBMSActionStrategy.STAT_QUERIES_RAN));
assertEquals(1, updateResult.getRecords().size(), "Should return 1 row");
assertEquals(2, updateResult.getRecords().get(0).getValue("id"), "Should have id=2 in the row");
// todo - add errors to QRecord? assertTrue(updateResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors");
runTestSql("SELECT * FROM person WHERE last_name = 'Kirk'", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
assertEquals(2, rs.getInt("id"));
assertEquals("James", rs.getString("first_name"));
assertEquals("2210-05-20", rs.getString("birth_date"));
}
assertEquals(1, rowsFound);
}));
runTestSql("SELECT * FROM person WHERE last_name = 'Maes'", (rs ->
{
if(rs.next())
{
fail("Should not have found Maes any more.");
}
}));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testUpdateManyWithDifferentColumnsAndValues() throws Exception
{
UpdateInput updateInput = initUpdateRequest();
QRecord record1 = new QRecord()
.withValue("id", 1)
.withValue("firstName", "Darren")
.withValue("lastName", "From Bewitched")
.withValue("birthDate", "1900-01-01");
QRecord record2 = new QRecord()
.withValue("id", 3)
.withValue("firstName", "Wilt")
.withValue("birthDate", null);
QRecord record3 = new QRecord()
.withValue("id", 5)
.withValue("firstName", "Richard")
.withValue("birthDate", null);
updateInput.setRecords(List.of(record1, record2, record3));
UpdateOutput updateResult = new UpdateAction().execute(updateInput);
// this test runs one batch and one regular query
Map<String, Integer> statistics = getBaseRDBMSActionStrategy().getStatistics();
assertEquals(1, statistics.get(BaseRDBMSActionStrategy.STAT_BATCHES_RAN));
assertEquals(2, statistics.get(BaseRDBMSActionStrategy.STAT_QUERIES_RAN));
assertEquals(3, updateResult.getRecords().size(), "Should return 3 rows");
assertEquals(1, updateResult.getRecords().get(0).getValue("id"), "Should have expected ids in the row");
assertEquals(3, updateResult.getRecords().get(1).getValue("id"), "Should have expected ids in the row");
assertEquals(5, updateResult.getRecords().get(2).getValue("id"), "Should have expected ids in the row");
// todo - add errors to QRecord? assertTrue(updateResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors");
runTestSql("SELECT * FROM person WHERE last_name = 'From Bewitched'", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
assertEquals(1, rs.getInt("id"));
assertEquals("Darren", rs.getString("first_name"));
assertEquals("From Bewitched", rs.getString("last_name"));
assertEquals("1900-01-01", rs.getString("birth_date"));
}
assertEquals(1, rowsFound);
}));
runTestSql("SELECT * FROM person WHERE last_name = 'Chamberlain'", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
assertEquals(3, rs.getInt("id"));
assertEquals("Wilt", rs.getString("first_name"));
assertNull(rs.getString("birth_date"));
}
assertEquals(1, rowsFound);
}));
runTestSql("SELECT * FROM person WHERE last_name = 'Richardson'", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
assertEquals(5, rs.getInt("id"));
assertEquals("Richard", rs.getString("first_name"));
assertNull(rs.getString("birth_date"));
}
assertEquals(1, rowsFound);
}));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testUpdateManyWithSameColumnsDifferentValues() throws Exception
{
UpdateInput updateInput = initUpdateRequest();
QRecord record1 = new QRecord()
.withValue("id", 1)
.withValue("firstName", "Darren")
.withValue("lastName", "From Bewitched")
.withValue("birthDate", "1900-01-01");
QRecord record2 = new QRecord()
.withValue("id", 3)
.withValue("firstName", "Wilt")
.withValue("lastName", "Tim's Uncle")
.withValue("birthDate", null);
updateInput.setRecords(List.of(record1, record2));
UpdateOutput updateResult = new UpdateAction().execute(updateInput);
Map<String, Integer> statistics = getBaseRDBMSActionStrategy().getStatistics();
assertEquals(1, statistics.get(BaseRDBMSActionStrategy.STAT_BATCHES_RAN));
assertEquals(2, updateResult.getRecords().size(), "Should return 2 rows");
assertEquals(1, updateResult.getRecords().get(0).getValue("id"), "Should have expected ids in the row");
assertEquals(3, updateResult.getRecords().get(1).getValue("id"), "Should have expected ids in the row");
// todo - add errors to QRecord? assertTrue(updateResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors");
runTestSql("SELECT * FROM person WHERE last_name = 'From Bewitched'", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
assertEquals(1, rs.getInt("id"));
assertEquals("Darren", rs.getString("first_name"));
assertEquals("From Bewitched", rs.getString("last_name"));
assertEquals("1900-01-01", rs.getString("birth_date"));
}
assertEquals(1, rowsFound);
}));
runTestSql("SELECT * FROM person WHERE last_name = 'Tim''s Uncle'", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
assertEquals(3, rs.getInt("id"));
assertEquals("Wilt", rs.getString("first_name"));
assertEquals("Tim's Uncle", rs.getString("last_name"));
assertNull(rs.getString("birth_date"));
}
assertEquals(1, rowsFound);
}));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testUpdateManyWithSameColumnsSameValues() throws Exception
{
UpdateInput updateInput = initUpdateRequest();
List<QRecord> records = new ArrayList<>();
for(int i = 1; i <= 5; i++)
{
records.add(new QRecord()
.withValue("id", i)
.withValue("birthDate", "1999-09-09"));
}
updateInput.setRecords(records);
UpdateOutput updateResult = new UpdateAction().execute(updateInput);
Map<String, Integer> statistics = getBaseRDBMSActionStrategy().getStatistics();
assertEquals(2, statistics.get(BaseRDBMSActionStrategy.STAT_QUERIES_RAN));
assertEquals(5, updateResult.getRecords().size(), "Should return 5 rows");
// todo - add errors to QRecord? assertTrue(updateResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors");
runTestSql("SELECT * FROM person WHERE id <= 5", (rs ->
{
int rowsFound = 0;
while(rs.next())
{
rowsFound++;
assertEquals("1999-09-09", rs.getString("birth_date"));
}
assertEquals(5, rowsFound);
}));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testModifyDateGetsUpdated() throws Exception
{
String originalModifyDate = selectModifyDate(1);
UpdateInput updateInput = initUpdateRequest();
List<QRecord> records = new ArrayList<>();
records.add(new QRecord()
.withValue("id", 1)
.withValue("firstName", "Johnny Updated"));
updateInput.setRecords(records);
new UpdateAction().execute(updateInput);
String updatedModifyDate = selectModifyDate(1);
assertTrue(StringUtils.hasContent(originalModifyDate));
assertTrue(StringUtils.hasContent(updatedModifyDate));
assertNotEquals(originalModifyDate, updatedModifyDate);
}
/*******************************************************************************
** This situation - fails in a real mysql, but not in h2... anyway, because mysql
** didn't want to convert the date-time string format to a date-time.
*******************************************************************************/
@Test
void testDateTimesCanBeModifiedFromIsoStrings() throws Exception
{
UpdateInput updateInput = initUpdateRequest();
List<QRecord> records = new ArrayList<>();
records.add(new QRecord()
.withValue("id", 1)
.withValue("createDate", "2022-10-03T10:29:35Z")
.withValue("firstName", "Johnny Updated"));
updateInput.setRecords(records);
new UpdateAction().execute(updateInput);
}
/*******************************************************************************
** Make sure that records without a primary key come back with error.
*******************************************************************************/
@Test
void testWithoutPrimaryKeyErrors() throws Exception
{
{
UpdateInput updateInput = initUpdateRequest();
List<QRecord> records = new ArrayList<>();
records.add(new QRecord()
.withValue("firstName", "Johnny Updated"));
updateInput.setRecords(records);
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertFalse(updateOutput.getRecords().get(0).getErrors().isEmpty());
assertEquals("Missing value in primary key field", updateOutput.getRecords().get(0).getErrors().get(0).getMessage());
}
{
UpdateInput updateInput = initUpdateRequest();
List<QRecord> records = new ArrayList<>();
records.add(new QRecord()
.withValue("id", null)
.withValue("firstName", "Johnny Updated"));
updateInput.setRecords(records);
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertFalse(updateOutput.getRecords().get(0).getErrors().isEmpty());
assertEquals("Missing value in primary key field", updateOutput.getRecords().get(0).getErrors().get(0).getMessage());
}
{
UpdateInput updateInput = initUpdateRequest();
List<QRecord> records = new ArrayList<>();
records.add(new QRecord()
.withValue("id", null)
.withValue("firstName", "Johnny Not Updated"));
records.add(new QRecord()
.withValue("id", 2)
.withValue("firstName", "Johnny Updated"));
updateInput.setRecords(records);
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
assertFalse(updateOutput.getRecords().get(0).getErrors().isEmpty());
assertEquals("Missing value in primary key field", updateOutput.getRecords().get(0).getErrors().get(0).getMessage());
assertTrue(updateOutput.getRecords().get(1).getErrors().isEmpty());
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON);
getInput.setPrimaryKey(2);
GetOutput getOutput = new GetAction().execute(getInput);
assertEquals("Johnny Updated", getOutput.getRecord().getValueString("firstName"));
}
}
/*******************************************************************************
**
*******************************************************************************/
private String selectModifyDate(Integer id) throws Exception
{
StringBuilder modifyDate = new StringBuilder();
runTestSql("SELECT modify_date FROM person WHERE id = " + id, (rs ->
{
if(rs.next())
{
modifyDate.append(rs.getString("modify_date"));
}
}));
return (modifyDate.toString());
}
/*******************************************************************************
**
*******************************************************************************/
private UpdateInput initUpdateRequest()
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON);
return updateInput;
}
}

View File

@ -0,0 +1,49 @@
--
-- QQQ - Low-code Application Framework for Engineers.
-- Copyright (C) 2021-2025. Kingsrook, LLC
-- 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
-- contact@kingsrook.com
-- https://github.com/Kingsrook/
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
--
DROP TABLE IF EXISTS parent_table;
DROP TABLE IF EXISTS child_table;
CREATE TABLE child_table
(
id INT AUTO_INCREMENT primary key,
name VARCHAR(80) NOT NULL
);
INSERT INTO child_table (id, name) VALUES (1, 'Timmy');
INSERT INTO child_table (id, name) VALUES (2, 'Jimmy');
INSERT INTO child_table (id, name) VALUES (3, 'Johnny');
INSERT INTO child_table (id, name) VALUES (4, 'Gracie');
INSERT INTO child_table (id, name) VALUES (5, 'Suzie');
CREATE TABLE parent_table
(
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL,
child_id INT,
foreign key (child_id) references child_table(id)
);
INSERT INTO parent_table (id, name, child_id) VALUES (1, 'Tim''s Dad', 1);
INSERT INTO parent_table (id, name, child_id) VALUES (2, 'Tim''s Mom', 1);
INSERT INTO parent_table (id, name, child_id) VALUES (3, 'Childless Man', null);
INSERT INTO parent_table (id, name, child_id) VALUES (4, 'Childless Woman', null);
INSERT INTO parent_table (id, name, child_id) VALUES (5, 'Johny''s Single Dad', 3);

View File

@ -0,0 +1,215 @@
--
-- QQQ - Low-code Application Framework for Engineers.
-- Copyright (C) 2021-2025. Kingsrook, LLC
-- 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
-- contact@kingsrook.com
-- https://github.com/Kingsrook/
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as
-- published by the Free Software Foundation, either version 3 of the
-- License, or (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
--
DROP TABLE IF EXISTS person;
CREATE TABLE person
(
id INTEGER PRIMARY KEY,
create_date TIMESTAMP, -- DEFAULT datetime('now'), -- can't get this to work!
modify_date TIMESTAMP, -- DEFAULT datetime('now'),
first_name VARCHAR(80) NOT NULL,
last_name VARCHAR(80) NOT NULL,
birth_date DATE,
email VARCHAR(250) NOT NULL,
is_employed BOOLEAN,
annual_salary DECIMAL(12,2),
days_worked INTEGER,
home_town VARCHAR(80),
start_time TIME
);
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked, home_town) VALUES (1, 'Darin', 'Kelkhoff', '1980-05-31', 'darin.kelkhoff@gmail.com', 1, 25000, 27, 'Chester');
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked, home_town) VALUES (2, 'James', 'Maes', '1980-05-15', 'jmaes@mmltholdings.com', 1, 26000, 124, 'Chester');
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked, home_town) VALUES (3, 'Tim', 'Chamberlain', '1976-05-28', 'tchamberlain@mmltholdings.com', 0, null, 0, 'Decatur');
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked, home_town) VALUES (4, 'Tyler', 'Samples', NULL, 'tsamples@mmltholdings.com', 1, 30000, 99, 'Texas');
INSERT INTO person (id, first_name, last_name, birth_date, email, is_employed, annual_salary, days_worked, home_town) VALUES (5, 'Garret', 'Richardson', '1981-01-01', 'grichardson@mmltholdings.com', 1, 1000000, 232, null);
DROP TABLE IF EXISTS personal_id_card;
CREATE TABLE personal_id_card
(
id INTEGER PRIMARY KEY,
create_date TIMESTAMP, -- DEFAULT date(),
modify_date TIMESTAMP, -- DEFAULT date(),
person_id INTEGER,
id_number VARCHAR(250)
);
INSERT INTO personal_id_card (person_id, id_number) VALUES (1, '19800531');
INSERT INTO personal_id_card (person_id, id_number) VALUES (2, '19800515');
INSERT INTO personal_id_card (person_id, id_number) VALUES (3, '19760528');
INSERT INTO personal_id_card (person_id, id_number) VALUES (6, '123123123');
INSERT INTO personal_id_card (person_id, id_number) VALUES (null, '987987987');
INSERT INTO personal_id_card (person_id, id_number) VALUES (null, '456456456');
DROP TABLE IF EXISTS carrier;
CREATE TABLE carrier
(
id INTEGER PRIMARY KEY,
name VARCHAR(80) NOT NULL,
company_code VARCHAR(80) NOT NULL,
service_level VARCHAR(80) NOT NULL
);
INSERT INTO carrier (id, name, company_code, service_level) VALUES (1, 'UPS Ground', 'UPS', 'G');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (2, 'UPS 2Day', 'UPS', '2');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (3, 'UPS International', 'UPS', 'I');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (4, 'Fedex Ground', 'FEDEX', 'G');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (5, 'Fedex Next Day', 'UPS', '1');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (6, 'Will Call', 'WILL_CALL', 'W');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (7, 'USPS Priority', 'USPS', '1');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (8, 'USPS Super Slow', 'USPS', '4');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (9, 'USPS Super Fast', 'USPS', '0');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (10, 'DHL International', 'DHL', 'I');
INSERT INTO carrier (id, name, company_code, service_level) VALUES (11, 'GSO', 'GSO', 'G');
DROP TABLE IF EXISTS line_item_extrinsic;
DROP TABLE IF EXISTS order_line;
DROP TABLE IF EXISTS item;
DROP TABLE IF EXISTS `order`;
DROP TABLE IF EXISTS order_instructions;
DROP TABLE IF EXISTS warehouse_store_int;
DROP TABLE IF EXISTS store;
DROP TABLE IF EXISTS warehouse;
CREATE TABLE store
(
id INTEGER PRIMARY KEY,
name VARCHAR(80) NOT NULL
);
-- define 3 stores
INSERT INTO store (id, name) VALUES (1, 'Q-Mart');
INSERT INTO store (id, name) VALUES (2, 'QQQ ''R'' Us');
INSERT INTO store (id, name) VALUES (3, 'QDepot');
CREATE TABLE item
(
id INTEGER PRIMARY KEY,
sku VARCHAR(80) NOT NULL,
description VARCHAR(80),
store_id INT NOT NULL REFERENCES store
);
-- three items for each store
INSERT INTO item (id, sku, description, store_id) VALUES (1, 'QM-1', 'Q-Mart Item 1', 1);
INSERT INTO item (id, sku, description, store_id) VALUES (2, 'QM-2', 'Q-Mart Item 2', 1);
INSERT INTO item (id, sku, description, store_id) VALUES (3, 'QM-3', 'Q-Mart Item 3', 1);
INSERT INTO item (id, sku, description, store_id) VALUES (4, 'QRU-1', 'QQQ R Us Item 4', 2);
INSERT INTO item (id, sku, description, store_id) VALUES (5, 'QRU-2', 'QQQ R Us Item 5', 2);
INSERT INTO item (id, sku, description, store_id) VALUES (6, 'QRU-3', 'QQQ R Us Item 6', 2);
INSERT INTO item (id, sku, description, store_id) VALUES (7, 'QD-1', 'QDepot Item 7', 3);
INSERT INTO item (id, sku, description, store_id) VALUES (8, 'QD-2', 'QDepot Item 8', 3);
INSERT INTO item (id, sku, description, store_id) VALUES (9, 'QD-3', 'QDepot Item 9', 3);
CREATE TABLE `order`
(
id INTEGER PRIMARY KEY,
store_id INT REFERENCES store,
bill_to_person_id INT,
ship_to_person_id INT,
current_order_instructions_id INT -- f-key to order_instructions, which also has an f-key back here!
);
-- variable orders
INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (1, 1, 1, 1);
INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (2, 1, 1, 2);
INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (3, 1, 2, 3);
INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (4, 2, 4, 5);
INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (5, 2, 5, 4);
INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (6, 3, 5, null);
INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (7, 3, null, 5);
INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (8, 3, null, 5);
CREATE TABLE order_instructions
(
id INTEGER PRIMARY KEY,
order_id INT,
instructions VARCHAR(250)
);
-- give orders 1 & 2 multiple versions of the instruction record
INSERT INTO order_instructions (id, order_id, instructions) VALUES (1, 1, 'order 1 v1');
INSERT INTO order_instructions (id, order_id, instructions) VALUES (2, 1, 'order 1 v2');
UPDATE `order` SET current_order_instructions_id = 2 WHERE id=1;
INSERT INTO order_instructions (id, order_id, instructions) VALUES (3, 2, 'order 2 v1');
INSERT INTO order_instructions (id, order_id, instructions) VALUES (4, 2, 'order 2 v2');
INSERT INTO order_instructions (id, order_id, instructions) VALUES (5, 2, 'order 2 v3');
UPDATE `order` SET current_order_instructions_id = 5 WHERE id=2;
-- give all other orders just 1 instruction
INSERT INTO order_instructions (order_id, instructions) SELECT id, concat('order ', id, ' v1') FROM `order` WHERE current_order_instructions_id IS NULL;
UPDATE `order` SET current_order_instructions_id = (SELECT MIN(id) FROM order_instructions WHERE order_id = `order`.id) WHERE current_order_instructions_id is null;
CREATE TABLE order_line
(
id INTEGER PRIMARY KEY,
order_id INT REFERENCES `order`,
sku VARCHAR(80),
store_id INT REFERENCES store,
quantity INT
);
-- various lines
INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (1, 'QM-1', 1, 10);
INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (1, 'QM-2', 1, 1);
INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (1, 'QM-3', 1, 1);
INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (2, 'QRU-1', 2, 1); -- this line has an item from a different store than its order.
INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (3, 'QM-1', 1, 20);
INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (4, 'QRU-1', 2, 1);
INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (4, 'QRU-2', 2, 2);
INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (5, 'QRU-1', 2, 1);
INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (6, 'QD-1', 3, 1);
INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (7, 'QD-1', 3, 2);
INSERT INTO order_line (order_id, sku, store_id, quantity) VALUES (8, 'QD-1', 3, 3);
CREATE TABLE warehouse
(
id INTEGER PRIMARY KEY,
name VARCHAR(80)
);
INSERT INTO warehouse (name) VALUES ('Patterson');
INSERT INTO warehouse (name) VALUES ('Edison');
INSERT INTO warehouse (name) VALUES ('Stockton');
INSERT INTO warehouse (name) VALUES ('Somewhere in Texas');
CREATE TABLE warehouse_store_int
(
id INTEGER PRIMARY KEY,
warehouse_id INT REFERENCES `warehouse`,
store_id INT REFERENCES `store`
);
INSERT INTO warehouse_store_int (warehouse_id, store_id) VALUES (1, 1);
INSERT INTO warehouse_store_int (warehouse_id, store_id) VALUES (1, 2);
INSERT INTO warehouse_store_int (warehouse_id, store_id) VALUES (1, 3);
CREATE TABLE line_item_extrinsic
(
id INTEGER PRIMARY KEY,
order_line_id INT REFERENCES order_line,
`key` VARCHAR(80),
`value` VARCHAR(80)
);