diff --git a/pom.xml b/pom.xml index 68110a6b..c3f02707 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,7 @@ qqq-backend-module-api qqq-backend-module-filesystem qqq-backend-module-rdbms + qqq-backend-module-sqlite qqq-backend-module-mongodb qqq-language-support-javascript qqq-openapi diff --git a/qqq-backend-module-sqlite/pom.xml b/qqq-backend-module-sqlite/pom.xml new file mode 100644 index 00000000..106dede0 --- /dev/null +++ b/qqq-backend-module-sqlite/pom.xml @@ -0,0 +1,113 @@ + + + + + 4.0.0 + + qqq-backend-module-sqlite + + + com.kingsrook.qqq + qqq-parent-project + ${revision} + + + + + + + + + + + com.kingsrook.qqq + qqq-backend-module-rdbms + ${revision} + + + + + org.xerial + sqlite-jdbc + 3.47.1.0 + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.3 + + false + + + *:* + + META-INF/* + + + + + + + ${plugin.shade.phase} + + shade + + + + + + + + diff --git a/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/SQLiteBackendModule.java b/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/SQLiteBackendModule.java new file mode 100644 index 00000000..b733f796 --- /dev/null +++ b/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/SQLiteBackendModule.java @@ -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 getBackendMetaDataClass() + { + return (SQLiteBackendMetaData.class); + } + + + + /******************************************************************************* + ** Method to identify the class used for table-backend details for this module. + *******************************************************************************/ + @Override + public Class getTableBackendDetailsClass() + { + return (SQLiteTableBackendDetails.class); + } + +} diff --git a/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/model/metadata/SQLiteBackendMetaData.java b/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/model/metadata/SQLiteBackendMetaData.java new file mode 100644 index 00000000..25f1af26 --- /dev/null +++ b/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/model/metadata/SQLiteBackendMetaData.java @@ -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()); + } +} diff --git a/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/model/metadata/SQLiteTableBackendDetails.java b/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/model/metadata/SQLiteTableBackendDetails.java new file mode 100644 index 00000000..98430d91 --- /dev/null +++ b/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/model/metadata/SQLiteTableBackendDetails.java @@ -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); + } + +} diff --git a/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/strategy/SQLiteRDBMSActionStrategy.java b/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/strategy/SQLiteRDBMSActionStrategy.java new file mode 100644 index 00000000..ee948e1d --- /dev/null +++ b/qqq-backend-module-sqlite/src/main/java/com/kingsrook/qqq/backend/module/sqlite/strategy/SQLiteRDBMSActionStrategy.java @@ -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 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 executeInsertForGeneratedIds(Connection connection, String sql, List 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 rs = new ArrayList<>(); + while(generatedKeys.next()) + { + rs.add(getFieldValueFromResultSet(primaryKeyField.getType(), generatedKeys, 1)); + } + return (rs); + } + } + +} diff --git a/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/BaseTest.java b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/BaseTest.java new file mode 100644 index 00000000..86228aea --- /dev/null +++ b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/BaseTest.java @@ -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 . + */ + +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(); + } +} diff --git a/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/TestUtils.java b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/TestUtils.java new file mode 100644 index 00000000..bdf26b24 --- /dev/null +++ b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/TestUtils.java @@ -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 . + */ + +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 lines = (List) 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 queryTable(String tableName) throws QException + { + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(tableName); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + return (queryOutput.getRecords()); + } +} diff --git a/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteCountActionTest.java b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteCountActionTest.java new file mode 100644 index 00000000..c28c4c9c --- /dev/null +++ b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteCountActionTest.java @@ -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 . + */ + +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); + } + +} diff --git a/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteDeleteActionTest.java b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteDeleteActionTest.java new file mode 100644 index 00000000..86d2cadf --- /dev/null +++ b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteDeleteActionTest.java @@ -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 . + */ + +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 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 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 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; + } +} \ No newline at end of file diff --git a/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteInsertActionTest.java b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteInsertActionTest.java new file mode 100644 index 00000000..4c59d024 --- /dev/null +++ b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteInsertActionTest.java @@ -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 . + */ + +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 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 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 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 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; + } + +} \ No newline at end of file diff --git a/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteQueryActionTest.java b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteQueryActionTest.java new file mode 100644 index 00000000..da415b84 --- /dev/null +++ b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteQueryActionTest.java @@ -0,0 +1,1136 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.sqlite.actions; + + +import java.io.Serializable; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +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.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +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.actions.tables.query.expressions.Now; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.ThisOrLastPeriod; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction; +import com.kingsrook.qqq.backend.module.rdbms.actions.AbstractRDBMSAction; +import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSQueryAction; +import com.kingsrook.qqq.backend.module.sqlite.BaseTest; +import com.kingsrook.qqq.backend.module.sqlite.TestUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +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 SQLiteQueryActionTest extends BaseTest +{ + + + /******************************************************************************* + ** + *******************************************************************************/ + @AfterEach + void afterEach() + { + AbstractRDBMSAction.setLogSQL(false); + QContext.getQSession().removeValue(QSession.VALUE_KEY_USER_TIMEZONE); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testUnfilteredQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size(), "Unfiltered query should find all rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testTrueQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.TRUE))); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size(), "'TRUE' query should find all rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testFalseQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("email", QCriteriaOperator.FALSE))); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(0, queryOutput.getRecords().size(), "'FALSE' query should find no rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testEqualsQuery() throws QException + { + String email = "darin.kelkhoff@gmail.com"; + + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.EQUALS) + .withValues(List.of(email))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertEquals(email, queryOutput.getRecords().get(0).getValueString("email"), "Should find expected email address"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testNotEqualsQuery() throws QException + { + String email = "darin.kelkhoff@gmail.com"; + + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.NOT_EQUALS) + .withValues(List.of(email))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").equals(email)), "Should NOT find expected email address"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testNotEqualsOrIsNullQuery() throws QException + { + ///////////////////////////////////////////////////////////////////////////// + // 5 rows, 1 has a null salary, 1 has 1,000,000. // + // first confirm that query for != returns 3 (the null does NOT come back) // + // then, confirm that != or is null gives the (more humanly expected) 4. // + ///////////////////////////////////////////////////////////////////////////// + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("annualSalary") + .withOperator(QCriteriaOperator.NOT_EQUALS) + .withValues(List.of(1_000_000)))); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(3, queryOutput.getRecords().size(), "Expected # of rows"); + + queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("annualSalary") + .withOperator(QCriteriaOperator.NOT_EQUALS_OR_IS_NULL) + .withValues(List.of(1_000_000)))); + queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> Objects.equals(1_000_000, r.getValueInteger("annualSalary"))), "Should NOT find expected salary"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testInQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("id") + .withOperator(QCriteriaOperator.IN) + .withValues(List.of(2, 4))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(2) || r.getValueInteger("id").equals(4)), "Should find expected ids"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testNotInQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("id") + .withOperator(QCriteriaOperator.NOT_IN) + .withValues(List.of(2, 3, 4))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(5)), "Should find expected ids"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testStartsWith() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.STARTS_WITH) + .withValues(List.of("darin"))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches("darin.*")), "Should find matching email address"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testContains() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.CONTAINS) + .withValues(List.of("kelkhoff"))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testLike() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.LIKE) + .withValues(List.of("%kelk%"))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testNotLike() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.NOT_LIKE) + .withValues(List.of("%kelk%"))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testEndsWith() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.ENDS_WITH) + .withValues(List.of("gmail.com"))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*gmail.com")), "Should find matching email address"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testNotStartsWith() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.NOT_STARTS_WITH) + .withValues(List.of("darin"))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches("darin.*")), "Should find matching email address"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testNotContains() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.NOT_CONTAINS) + .withValues(List.of("kelkhoff"))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testNotEndsWith() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("email") + .withOperator(QCriteriaOperator.NOT_ENDS_WITH) + .withValues(List.of("gmail.com"))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*gmail.com")), "Should find matching email address"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testLessThanQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("id") + .withOperator(QCriteriaOperator.LESS_THAN) + .withValues(List.of(3))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(2)), "Should find expected ids"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testLessThanOrEqualsQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("id") + .withOperator(QCriteriaOperator.LESS_THAN_OR_EQUALS) + .withValues(List.of(2))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(2)), "Should find expected ids"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testGreaterThanQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("id") + .withOperator(QCriteriaOperator.GREATER_THAN) + .withValues(List.of(3))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(4) || r.getValueInteger("id").equals(5)), "Should find expected ids"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testGreaterThanOrEqualsQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("id") + .withOperator(QCriteriaOperator.GREATER_THAN_OR_EQUALS) + .withValues(List.of(4))) + ); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(4) || r.getValueInteger("id").equals(5)), "Should find expected ids"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testIsBlankQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("birthDate") + .withOperator(QCriteriaOperator.IS_BLANK) + )); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("birthDate") == null), "Should find expected row"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testIsNotBlankQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("firstName") + .withOperator(QCriteriaOperator.IS_NOT_BLANK) + )); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("firstName") != null), "Should find expected rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testBetweenQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("id") + .withOperator(QCriteriaOperator.BETWEEN) + .withValues(List.of(2, 4)) + )); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(3, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(2) || r.getValueInteger("id").equals(3) || r.getValueInteger("id").equals(4)), "Should find expected ids"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testNotBetweenQuery() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName("id") + .withOperator(QCriteriaOperator.NOT_BETWEEN) + .withValues(List.of(2, 4)) + )); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(5)), "Should find expected ids"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testFilterExpressions() throws QException + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_PERSON); + insertInput.setRecords(List.of( + new QRecord().withValue("email", "-").withValue("firstName", "past").withValue("lastName", "ExpressionTest").withValue("birthDate", Instant.now().minus(3, ChronoUnit.DAYS)), + new QRecord().withValue("email", "-").withValue("firstName", "future").withValue("lastName", "ExpressionTest").withValue("birthDate", Instant.now().plus(3, ChronoUnit.DAYS)) + )); + new InsertAction().execute(insertInput); + + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest"))) + .withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withValues(List.of(new Now())))); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); + } + + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest"))) + .withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withValues(List.of(NowWithOffset.plus(2, ChronoUnit.DAYS))))); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); + } + + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest"))) + .withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.GREATER_THAN).withValues(List.of(NowWithOffset.minus(5, ChronoUnit.DAYS))))); + QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("future")), "Should find expected row"); + } + } + + + + /******************************************************************************* + ** Adding additional test conditions, specifically for DATE-precision + *******************************************************************************/ + @ParameterizedTest() + @ValueSource(strings = { "UTC", "US/Eastern", "UTC+12" }) + void testMoreFilterExpressions(String userTimezone) throws QException + { + QContext.getQSession().setValue(QSession.VALUE_KEY_USER_TIMEZONE, userTimezone); + + LocalDate today = Instant.now().atZone(ZoneId.of(userTimezone)).toLocalDate(); + LocalDate yesterday = today.minusDays(1); + LocalDate tomorrow = today.plusDays(1); + + new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON).withRecords(List.of( + new QRecord().withValue("email", "-").withValue("firstName", "yesterday").withValue("lastName", "ExpressionTest").withValue("birthDate", yesterday), + new QRecord().withValue("email", "-").withValue("firstName", "today").withValue("lastName", "ExpressionTest").withValue("birthDate", today), + new QRecord().withValue("email", "-").withValue("firstName", "tomorrow").withValue("lastName", "ExpressionTest").withValue("birthDate", tomorrow)) + )); + + UnsafeFunction, List, QException> testFunction = (filterConsumer) -> + { + QQueryFilter filter = new QQueryFilter().withCriteria("lastName", QCriteriaOperator.EQUALS, "ExpressionTest"); + filter.withOrderBy(new QFilterOrderBy("birthDate")); + filterConsumer.accept(filter); + + return QueryAction.execute(TestUtils.TABLE_NAME_PERSON, filter); + }; + + assertOneRecordWithFirstName("today", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.EQUALS, new Now())))); + assertOneRecordWithFirstName("tomorrow", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, new Now())))); + assertOneRecordWithFirstName("yesterday", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, new Now())))); + assertTwoRecordsWithFirstNames("yesterday", "today", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN_OR_EQUALS, new Now())))); + assertTwoRecordsWithFirstNames("today", "tomorrow", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN_OR_EQUALS, new Now())))); + + assertNoOfRecords(0, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, NowWithOffset.minus(1, ChronoUnit.DAYS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN_OR_EQUALS, NowWithOffset.plus(1, ChronoUnit.DAYS))))); + assertOneRecordWithFirstName("yesterday", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.EQUALS, NowWithOffset.minus(1, ChronoUnit.DAYS))))); + assertOneRecordWithFirstName("tomorrow", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.EQUALS, NowWithOffset.plus(1, ChronoUnit.DAYS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, NowWithOffset.plus(1, ChronoUnit.WEEKS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, NowWithOffset.plus(1, ChronoUnit.MONTHS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, NowWithOffset.plus(1, ChronoUnit.YEARS))))); + + assertThatThrownBy(() -> testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, NowWithOffset.plus(1, ChronoUnit.HOURS))))) + .hasRootCauseMessage("Unsupported unit: Hours"); + + assertOneRecordWithFirstName("today", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.EQUALS, ThisOrLastPeriod.this_(ChronoUnit.DAYS))))); + assertOneRecordWithFirstName("yesterday", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.EQUALS, ThisOrLastPeriod.last(ChronoUnit.DAYS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, ThisOrLastPeriod.last(ChronoUnit.WEEKS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, ThisOrLastPeriod.last(ChronoUnit.MONTHS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, ThisOrLastPeriod.last(ChronoUnit.YEARS))))); + assertNoOfRecords(0, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, ThisOrLastPeriod.last(ChronoUnit.WEEKS))))); + assertNoOfRecords(0, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, ThisOrLastPeriod.last(ChronoUnit.MONTHS))))); + assertNoOfRecords(0, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, ThisOrLastPeriod.last(ChronoUnit.YEARS))))); + + assertThatThrownBy(() -> testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, ThisOrLastPeriod.this_(ChronoUnit.HOURS))))) + .hasRootCauseMessage("Unsupported unit: Hours"); + assertThatThrownBy(() -> testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, ThisOrLastPeriod.last(ChronoUnit.MINUTES))))) + .hasRootCauseMessage("Unsupported unit: Minutes"); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void assertNoOfRecords(Integer expectedSize, List actualRecords) + { + assertEquals(expectedSize, actualRecords.size()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void assertOneRecordWithFirstName(String expectedFirstName, List actualRecords) + { + assertEquals(1, actualRecords.size()); + assertEquals(expectedFirstName, actualRecords.get(0).getValueString("firstName")); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void assertTwoRecordsWithFirstNames(String expectedFirstName0, String expectedFirstName1, List actualRecords) + { + assertEquals(2, actualRecords.size()); + assertEquals(expectedFirstName0, actualRecords.get(0).getValueString("firstName")); + assertEquals(expectedFirstName1, actualRecords.get(1).getValueString("firstName")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QueryInput initQueryRequest() + { + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_PERSON); + return queryInput; + } + + + + /******************************************************************************* + ** This doesn't really test any RDBMS code, but is a checkpoint that the core + ** module is populating displayValues when it performs the system-level query action + ** (if so requested by input field). + *******************************************************************************/ + @Test + public void testThatDisplayValuesGetSetGoingThroughQueryAction() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setShouldGenerateDisplayValues(true); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size(), "Unfiltered query should find all rows"); + + for(QRecord record : queryOutput.getRecords()) + { + assertThat(record.getValues()).isNotEmpty(); + assertThat(record.getDisplayValues()).isNotEmpty(); + assertThat(record.getErrors()).isEmpty(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testLookInsideTransaction() throws QException + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_PERSON); + + InsertAction insertAction = new InsertAction(); + QBackendTransaction transaction = QBackendTransaction.openFor(insertInput); + + insertInput.setTransaction(transaction); + insertInput.setRecords(List.of( + new QRecord().withValue("firstName", "George").withValue("lastName", "Washington").withValue("email", "gw@kingsrook.com") + )); + + insertAction.execute(insertInput); + + QueryInput queryInput = initQueryRequest(); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size(), "Query without the transaction should not see the new row."); + + queryInput = initQueryRequest(); + queryInput.setTransaction(transaction); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(6, queryOutput.getRecords().size(), "Query with the transaction should see the new row."); + + transaction.rollback(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testEmptyInList() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.IN, List.of()))); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(0, queryOutput.getRecords().size(), "IN empty list should find nothing."); + + queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.NOT_IN, List.of()))); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size(), "NOT_IN empty list should find everything."); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOr() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Darin"))) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Tim"))) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size(), "OR should find 2 rows"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNestedFilterAndOrOr() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withSubFilters(List.of( + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("James"))) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Maes"))), + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Darin"))) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Kelkhoff"))) + )) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(2, queryOutput.getRecords().size(), "Complex query should find 2 rows"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("lastName").equals("Maes")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("lastName").equals("Kelkhoff")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNestedFilterOrAndAnd() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withSubFilters(List.of( + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("James"))) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Tim"))), + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Kelkhoff"))) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Chamberlain"))) + )) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "Complex query should find 1 row"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("lastName").equals("Chamberlain")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNestedFilterAndTopLevelFilter() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 3)) + .withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withSubFilters(List.of( + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("James"))) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Tim"))), + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Kelkhoff"))) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Chamberlain"))) + )) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "Complex query should find 1 row"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueInteger("id").equals(3) && r.getValueString("firstName").equals("Tim") && r.getValueString("lastName").equals("Chamberlain")); + + queryInput.getFilter().setCriteria(List.of(new QFilterCriteria("id", QCriteriaOperator.NOT_EQUALS, 3))); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(0, queryOutput.getRecords().size(), "Next complex query should find 0 rows"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRecordSecurityWithOrQueries() throws QException + { + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + queryInput.setFilter(new QQueryFilter( + new QFilterCriteria("billToPersonId", QCriteriaOperator.EQUALS, List.of(1)), + new QFilterCriteria("shipToPersonId", QCriteriaOperator.EQUALS, List.of(5)) + ).withBooleanOperator(QQueryFilter.BooleanOperator.OR)); + QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(5) + .allMatch(r -> Objects.equals(r.getValueInteger("billToPersonId"), 1) || Objects.equals(r.getValueInteger("shipToPersonId"), 5)); + + queryInput.setFilter(new QQueryFilter( + new QFilterCriteria("billToPersonId", QCriteriaOperator.EQUALS, List.of(1)), + new QFilterCriteria("shipToPersonId", QCriteriaOperator.EQUALS, List.of(5)) + ).withBooleanOperator(QQueryFilter.BooleanOperator.OR)); + QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(1) + .allMatch(r -> r.getValueInteger("storeId").equals(2)) + .allMatch(r -> Objects.equals(r.getValueInteger("billToPersonId"), 1) || Objects.equals(r.getValueInteger("shipToPersonId"), 5)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRecordSecurityWithSubFilters() throws QException + { + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + queryInput.setFilter(new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withSubFilters(List.of( + new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, 2), new QFilterCriteria("billToPersonId", QCriteriaOperator.EQUALS, 1)), + new QQueryFilter(new QFilterCriteria("billToPersonId", QCriteriaOperator.IS_BLANK), new QFilterCriteria("shipToPersonId", QCriteriaOperator.IS_BLANK)).withBooleanOperator(QQueryFilter.BooleanOperator.OR) + ))); + Predicate p = r -> r.getValueInteger("billToPersonId") == null || r.getValueInteger("shipToPersonId") == null || (r.getValueInteger("id") >= 2 && r.getValueInteger("billToPersonId") == 1); + + QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(4) + .allMatch(p); + + QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(1) + .allMatch(r -> r.getValueInteger("storeId").equals(1)) + .allMatch(p); + + QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(3) + .allMatch(r -> r.getValueInteger("storeId").equals(3)) + .allMatch(p); + + QContext.setQSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRecordSecurityNullValues() throws Exception + { + runTestSql("INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (9, NULL, 1, 6)", null); + runTestSql("INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (10, NULL, 6, 5)", null); + + QInstance qInstance = TestUtils.defineInstance(); + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + reInitInstanceInContext(qInstance); + + Predicate hasNullStoreId = r -> r.getValueInteger("storeId") == null; + + //////////////////////////////////////////// + // all-access user should get all 10 rows // + //////////////////////////////////////////// + QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(10) + .anyMatch(hasNullStoreId); + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // no-values user should get 0 rows (given that default null-behavior on this key type is DENY) // + ////////////////////////////////////////////////////////////////////////////////////////////////// + QContext.setQSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // user with list of all ids shouldn't see the nulls (given that default null-behavior on this key type is DENY) // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + { + QSession qSession = new QSession(); + for(Integer i : List.of(1, 2, 3, 4, 5)) + { + qSession.withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, i); + } + QContext.setQSession(qSession); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(8) + .noneMatch(hasNullStoreId); + } + + ////////////////////////////////////////////////////////////////////////// + // specifically set the null behavior to deny - repeat the last 2 tests // + ////////////////////////////////////////////////////////////////////////// + qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.DENY); + + QContext.setQSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + { + QSession qSession = new QSession(); + for(Integer i : List.of(1, 2, 3, 4, 5)) + { + qSession.withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, i); + } + QContext.setQSession(qSession); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(8) + .noneMatch(hasNullStoreId); + } + + /////////////////////////////////// + // change null behavior to ALLOW // + /////////////////////////////////// + qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW); + + ///////////////////////////////////////////// + // all-access user should still get all 10 // + ///////////////////////////////////////////// + QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(10) + .anyMatch(hasNullStoreId); + + ///////////////////////////////////////////////////// + // no-values user should only get the rows w/ null // + ///////////////////////////////////////////////////// + QContext.setQSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(2) + .allMatch(hasNullStoreId); + + //////////////////////////////////////////////////// + // user with list of all ids should see the nulls // + //////////////////////////////////////////////////// + { + QSession qSession = new QSession(); + for(Integer i : List.of(1, 2, 3, 4, 5)) + { + qSession.withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, i); + } + QContext.setQSession(qSession); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(10) + .anyMatch(hasNullStoreId); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + @SuppressWarnings("unchecked") + void testHeavyFields() throws QException + { + ////////////////////////////////////////////////////////// + // set homeTown field as heavy - so it won't be fetched // + ////////////////////////////////////////////////////////// + QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON) + .getField("homeTown") + .withIsHeavy(true); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.TABLE_NAME_PERSON); + List records = new QueryAction().execute(queryInput).getRecords(); + assertThat(records).describedAs("No records should have the heavy homeTown field set").noneMatch(r -> r.getValue("homeTown") != null); + assertThat(records).describedAs("Some records should have a homeTown length backend detail set").anyMatch(r -> ((Map) r.getBackendDetail(QRecord.BACKEND_DETAILS_TYPE_HEAVY_FIELD_LENGTHS)).get("homeTown") != null); + assertThat(records).describedAs("Some records should have a null homeTown length backend").anyMatch(r -> ((Map) r.getBackendDetail(QRecord.BACKEND_DETAILS_TYPE_HEAVY_FIELD_LENGTHS)).get("homeTown") == null); + + ////////////////////////////////////////////// + // re-do the query, requesting heavy fields // + ////////////////////////////////////////////// + queryInput.setShouldFetchHeavyFields(true); + records = new QueryAction().execute(queryInput).getRecords(); + assertThat(records).describedAs("Some records should have the heavy homeTown field set when heavies are requested").anyMatch(r -> r.getValue("homeTown") != null); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldNamesToInclude() throws QException + { + QQueryFilter filter = new QQueryFilter().withCriteria("id", QCriteriaOperator.EQUALS, 1); + QueryInput queryInput = new QueryInput(TestUtils.TABLE_NAME_PERSON).withFilter(filter); + + QRecord record = new QueryAction().execute(queryInput.withFieldNamesToInclude(null)).getRecords().get(0); + assertTrue(record.getValues().containsKey("id")); + assertTrue(record.getValues().containsKey("firstName")); + assertTrue(record.getValues().containsKey("createDate")); + assertEquals(QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON).getFields().size(), record.getValues().size()); + + record = new QueryAction().execute(queryInput.withFieldNamesToInclude(Set.of("id", "firstName"))).getRecords().get(0); + assertTrue(record.getValues().containsKey("id")); + assertTrue(record.getValues().containsKey("firstName")); + assertFalse(record.getValues().containsKey("createDate")); + assertEquals(2, record.getValues().size()); + + record = new QueryAction().execute(queryInput.withFieldNamesToInclude(Set.of("homeTown"))).getRecords().get(0); + assertFalse(record.getValues().containsKey("id")); + assertFalse(record.getValues().containsKey("firstName")); + assertFalse(record.getValues().containsKey("createDate")); + assertEquals(1, record.getValues().size()); + } + +} diff --git a/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteUpdateActionTest.java b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteUpdateActionTest.java new file mode 100644 index 00000000..29d6540c --- /dev/null +++ b/qqq-backend-module-sqlite/src/test/java/com/kingsrook/qqq/backend/module/sqlite/actions/SQLiteUpdateActionTest.java @@ -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 . + */ + +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 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 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 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 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 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 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 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 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 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 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; + } + +} \ No newline at end of file diff --git a/qqq-backend-module-sqlite/src/test/resources/prime-test-database-parent-child-tables.sql b/qqq-backend-module-sqlite/src/test/resources/prime-test-database-parent-child-tables.sql new file mode 100644 index 00000000..e512a5d4 --- /dev/null +++ b/qqq-backend-module-sqlite/src/test/resources/prime-test-database-parent-child-tables.sql @@ -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 . +-- + +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); diff --git a/qqq-backend-module-sqlite/src/test/resources/prime-test-database.sql b/qqq-backend-module-sqlite/src/test/resources/prime-test-database.sql new file mode 100644 index 00000000..874f7583 --- /dev/null +++ b/qqq-backend-module-sqlite/src/test/resources/prime-test-database.sql @@ -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 . +-- + +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) +); +