Refactor caching out of GetAction - namely, to support initial use-cases in QueryAction.

This commit is contained in:
2023-06-30 12:36:15 -05:00
parent 75ae848afd
commit c086874e64
9 changed files with 2099 additions and 431 deletions

View File

@ -22,29 +22,15 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
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.delete.DeleteInput;
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.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.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.AfterEach;
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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
@ -85,166 +71,4 @@ class GetActionTest extends BaseTest
assertNotNull(result.getRecord());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUniqueKeyCache() throws QException
{
QInstance qInstance = QContext.getQInstance();
/////////////////////////////////////
// insert rows in the source table //
/////////////////////////////////////
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
new QRecord().withValue("id", 1).withValue("firstName", "George").withValue("lastName", "Washington").withValue("noOfShoes", 5),
new QRecord().withValue("id", 2).withValue("firstName", "John").withValue("lastName", "Adams"),
new QRecord().withValue("id", 3).withValue("firstName", "Thomas").withValue("lastName", "Jefferson"),
new QRecord().withValue("id", 4).withValue("firstName", "Thomas 503").withValue("lastName", "Jefferson"),
new QRecord().withValue("id", 5).withValue("firstName", "Thomas 999").withValue("lastName", "Jefferson")
));
/////////////////////////////////////////////////////////////////////////////
// get from the table which caches it - confirm they are (magically) found //
/////////////////////////////////////////////////////////////////////////////
{
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
assertNotNull(getOutput.getRecord().getValue("cachedDate"));
assertEquals(5, getOutput.getRecord().getValue("noOfShoes"));
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// try to get from the table which caches it - but should not find because use case should filter out because of matching 503 //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
getInput.setUniqueKey(Map.of("firstName", "Thomas 503", "lastName", "Jefferson"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNull(getOutput.getRecord());
getInput.setUniqueKey(Map.of("firstName", "Thomas 999", "lastName", "Jefferson"));
getOutput = new GetAction().execute(getInput);
assertNull(getOutput.getRecord());
}
///////////////////////////////////////////////////////////////////////////
// request a row that doesn't exist in cache or source, should miss both //
///////////////////////////////////////////////////////////////////////////
{
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
getInput.setUniqueKey(Map.of("firstName", "John", "lastName", "McCain"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNull(getOutput.getRecord());
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
// update the record in the source table - then re-get from cache table - shouldn't see new value. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfShoes", 6)));
new UpdateAction().execute(updateInput);
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
assertNotNull(getOutput.getRecord().getValue("cachedDate"));
assertEquals(5, getOutput.getRecord().getValue("noOfShoes"));
}
///////////////////////////////////////////////////////////////////////////
// delete the cached record; re-get, and we should see the updated value //
///////////////////////////////////////////////////////////////////////////
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
deleteInput.setQueryFilter(new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, "George")));
new DeleteAction().execute(deleteInput);
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
assertNotNull(getOutput.getRecord().getValue("cachedDate"));
assertEquals(6, getOutput.getRecord().getValue("noOfShoes"));
}
///////////////////////////////////////////////////////////////////
// update the source record; see that it isn't updated in cache. //
///////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfShoes", 7)));
new UpdateAction().execute(updateInput);
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
assertNotNull(getOutput.getRecord().getValue("cachedDate"));
assertEquals(6, getOutput.getRecord().getValue("noOfShoes"));
///////////////////////////////////////////////////////////////////////
// then artificially move back the cachedDate in the cache table. //
// then re-get from cache table, and we should see the updated value //
///////////////////////////////////////////////////////////////////////
updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
updateInput.setRecords(List.of(getOutput.getRecord().withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
new UpdateAction().execute(updateInput);
getOutput = new GetAction().execute(getInput);
assertEquals(7, getOutput.getRecord().getValue("noOfShoes"));
}
/////////////////////////////////////////////////
// should only be 1 cache record at this point //
/////////////////////////////////////////////////
assertEquals(1, TestUtils.queryTable(QContext.getQInstance(), TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE).size());
//////////////////////////////////////////////////////////////////////
// delete the source record - it will still be in the cache though. //
//////////////////////////////////////////////////////////////////////
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
deleteInput.setPrimaryKeys(List.of(1));
new DeleteAction().execute(deleteInput);
GetInput getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
////////////////////////////////////////////////////////////////////
// then artificially move back the cachedDate in the cache table. //
// then re-get from cache table, and now it should go away //
////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
updateInput.setRecords(List.of(getOutput.getRecord().withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
new UpdateAction().execute(updateInput);
getInput = new GetInput();
getInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
getOutput = new GetAction().execute(getInput);
assertNull(getOutput.getRecord());
}
}
}

View File

@ -0,0 +1,276 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
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.delete.DeleteInput;
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.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.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.utils.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.assertNull;
/*******************************************************************************
** Unit test for GetActionCacheHelper
*******************************************************************************/
class GetActionCacheHelperTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUniqueKeyCache() throws QException
{
QInstance qInstance = QContext.getQInstance();
String sourceTableName = TestUtils.TABLE_NAME_PERSON_MEMORY;
String cacheTableName = TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE;
/////////////////////////////////////
// insert rows in the source table //
/////////////////////////////////////
TestUtils.insertRecords(qInstance, qInstance.getTable(sourceTableName), List.of(
new QRecord().withValue("id", 1).withValue("firstName", "George").withValue("lastName", "Washington").withValue("noOfShoes", 5),
new QRecord().withValue("id", 2).withValue("firstName", "John").withValue("lastName", "Adams"),
new QRecord().withValue("id", 3).withValue("firstName", "Thomas").withValue("lastName", "Jefferson"),
new QRecord().withValue("id", 4).withValue("firstName", "James").withValue("lastName", "Garfield").withValue("noOfShoes", 503),
new QRecord().withValue("id", 5).withValue("firstName", "Abraham").withValue("lastName", "Lincoln").withValue("noOfShoes", 999),
new QRecord().withValue("id", 6).withValue("firstName", "Bill").withValue("lastName", "Clinton")
));
/////////////////////////////////////////////////////////////////////////////
// get from the table which caches it - confirm they are (magically) found //
/////////////////////////////////////////////////////////////////////////////
{
GetInput getInput = new GetInput();
getInput.setTableName(cacheTableName);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
assertNotNull(getOutput.getRecord().getValue("cachedDate"));
assertEquals(5, getOutput.getRecord().getValue("noOfShoes"));
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// try to get records through the cache table, which meet the conditions that cause them to not be cached. //
// so we should get results from the Get request - but - then let's go directly to the backend to confirm the records are not cached. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
GetInput getInput = new GetInput();
getInput.setTableName(cacheTableName);
getInput.setUniqueKey(Map.of("firstName", "James", "lastName", "Garfield"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
getInput.setUniqueKey(Map.of("firstName", "Abraham", "lastName", "Lincoln"));
getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.IN, "Abraham", "James"))));
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// fetch a record through the cache, so it gets cached. //
// then update the source record so that it meets the condition that doesn't allow it to be cached. //
// then expire the cached record. //
// then re-fetch through cache - which should see the expiration, re-fetch from source, and delete from cache. //
// assert record is no longer in cache. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
GetInput getInput = new GetInput();
getInput.setTableName(cacheTableName);
getInput.setUniqueKey(Map.of("firstName", "Bill", "lastName", "Clinton"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
assertNotNull(getOutput.getRecord().getValue("cachedDate"));
assertEquals(1, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Clinton"))));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(sourceTableName);
updateInput.setRecords(List.of(new QRecord().withValue("id", 6).withValue("noOfShoes", 503)));
new UpdateAction().execute(updateInput);
updateInput = new UpdateInput();
updateInput.setTableName(cacheTableName);
updateInput.setRecords(List.of(getOutput.getRecord().withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
new UpdateAction().execute(updateInput);
getInput = new GetInput();
getInput.setTableName(cacheTableName);
getInput.setUniqueKey(Map.of("firstName", "Bill", "lastName", "Clinton"));
getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
assertEquals(503, getOutput.getRecord().getValue("noOfShoes"));
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Clinton"))));
}
///////////////////////////////////////////////////////////////////////////
// request a row that doesn't exist in cache or source, should miss both //
///////////////////////////////////////////////////////////////////////////
{
GetInput getInput = new GetInput();
getInput.setTableName(cacheTableName);
getInput.setUniqueKey(Map.of("firstName", "John", "lastName", "McCain"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNull(getOutput.getRecord());
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
// update the record in the source table - then re-get from cache table - shouldn't see new value. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(sourceTableName);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfShoes", 6)));
new UpdateAction().execute(updateInput);
GetInput getInput = new GetInput();
getInput.setTableName(cacheTableName);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
assertNotNull(getOutput.getRecord().getValue("cachedDate"));
assertEquals(5, getOutput.getRecord().getValue("noOfShoes"));
}
///////////////////////////////////////////////////////////////////////////
// delete the cached record; re-get, and we should see the updated value //
///////////////////////////////////////////////////////////////////////////
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(cacheTableName);
deleteInput.setQueryFilter(new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, "George")));
new DeleteAction().execute(deleteInput);
GetInput getInput = new GetInput();
getInput.setTableName(cacheTableName);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
assertNotNull(getOutput.getRecord().getValue("cachedDate"));
assertEquals(6, getOutput.getRecord().getValue("noOfShoes"));
}
///////////////////////////////////////////////////////////////////
// update the source record; see that it isn't updated in cache. //
///////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(sourceTableName);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfShoes", 7)));
new UpdateAction().execute(updateInput);
GetInput getInput = new GetInput();
getInput.setTableName(cacheTableName);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
assertNotNull(getOutput.getRecord().getValue("cachedDate"));
assertEquals(6, getOutput.getRecord().getValue("noOfShoes"));
///////////////////////////////////////////////////////////////////////
// then artificially move back the cachedDate in the cache table. //
// then re-get from cache table, and we should see the updated value //
///////////////////////////////////////////////////////////////////////
updateInput = new UpdateInput();
updateInput.setTableName(cacheTableName);
updateInput.setRecords(List.of(getOutput.getRecord().withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
new UpdateAction().execute(updateInput);
getOutput = new GetAction().execute(getInput);
assertEquals(7, getOutput.getRecord().getValue("noOfShoes"));
}
/////////////////////////////////////////////////
// should only be 1 cache record at this point //
/////////////////////////////////////////////////
assertEquals(1, TestUtils.queryTable(QContext.getQInstance(), cacheTableName).size());
//////////////////////////////////////////////////////////////////////
// delete the source record - it will still be in the cache though. //
//////////////////////////////////////////////////////////////////////
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(sourceTableName);
deleteInput.setPrimaryKeys(List.of(1));
new DeleteAction().execute(deleteInput);
GetInput getInput = new GetInput();
getInput.setTableName(cacheTableName);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
GetOutput getOutput = new GetAction().execute(getInput);
assertNotNull(getOutput.getRecord());
////////////////////////////////////////////////////////////////////
// then artificially move back the cachedDate in the cache table. //
// then re-get from cache table, and now it should go away //
////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(cacheTableName);
updateInput.setRecords(List.of(getOutput.getRecord().withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
new UpdateAction().execute(updateInput);
getInput = new GetInput();
getInput.setTableName(cacheTableName);
getInput.setUniqueKey(Map.of("firstName", "George", "lastName", "Washington"));
getOutput = new GetAction().execute(getInput);
assertNull(getOutput.getRecord());
}
}
/*******************************************************************************
**
*******************************************************************************/
private static int countCachedRecordsDirectlyInBackend(String tableName, QQueryFilter filter) throws QException
{
List<QRecord> cachedRecords = MemoryRecordStore.getInstance().query(new QueryInput()
.withTableName(tableName)
.withFilter(filter));
return cachedRecords.size();
}
}

View File

@ -0,0 +1,850 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.time.Instant;
import java.util.List;
import java.util.stream.Stream;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
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.delete.DeleteInput;
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.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
** Unit test for QueryActionCacheHelper
*******************************************************************************/
class QueryActionCacheHelperTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUniqueKeyCacheSingleFieldUniqueKeySingleRecordUseCases() throws QException
{
QInstance qInstance = QContext.getQInstance();
String sourceTableName = TestUtils.TABLE_NAME_SHAPE;
String cacheTableName = TestUtils.TABLE_NAME_SHAPE_CACHE;
/////////////////////////////////////
// insert rows in the source table //
/////////////////////////////////////
TestUtils.insertRecords(qInstance, qInstance.getTable(sourceTableName), List.of(
new QRecord().withValue("id", 1).withValue("name", "Triangle").withValue("noOfSides", 3),
new QRecord().withValue("id", 2).withValue("name", "Square").withValue("noOfSides", 4),
new QRecord().withValue("id", 3).withValue("name", "Pentagon").withValue("noOfSides", 5),
new QRecord().withValue("id", 4).withValue("name", "ServerErrorGon").withValue("noOfSides", 503),
new QRecord().withValue("id", 5).withValue("name", "ManyGon").withValue("noOfSides", 999)
));
/////////////////////////////////////////////////////////////////////////////
// get from the table which caches it - confirm they are (magically) found //
/////////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertNotEquals(0, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(3, queryOutput.getRecords().get(0).getValue("noOfSides"));
}
////////////////////////////////////////////////////////////////////////////////////
// try to get from the table which caches it - it should be found, but not cached //
// because use case should filter out because of matching 503 //
////////////////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "ServerErrorGon")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "ManyGon")));
queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
}
///////////////////////////////////////////////////////////////////////////
// request a row that doesn't exist in cache or source, should miss both //
///////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Line"))); // lines aren't shapes :)
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
// update the record in the source table - then re-get from cache table - shouldn't see new value. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(sourceTableName);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfSides", 6)));
new UpdateAction().execute(updateInput);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertNotEquals(0, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(3, queryOutput.getRecords().get(0).getValue("noOfSides"));
}
///////////////////////////////////////////////////////////////////////////
// delete the cached record; re-get, and we should see the updated value //
///////////////////////////////////////////////////////////////////////////
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(cacheTableName);
deleteInput.setQueryFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle")));
new DeleteAction().execute(deleteInput);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertNotEquals(0, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(6, queryOutput.getRecords().get(0).getValue("noOfSides"));
}
///////////////////////////////////////////////////////////////////
// update the source record; see that it isn't updated in cache. //
///////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(sourceTableName);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfSides", 7)));
new UpdateAction().execute(updateInput);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertNotEquals(0, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(6, queryOutput.getRecords().get(0).getValue("noOfSides"));
///////////////////////////////////////////////////////////////////////
// then artificially move back the cachedDate in the cache table. //
// then re-get from cache table, and we should see the updated value //
///////////////////////////////////////////////////////////////////////
updateInput = new UpdateInput();
updateInput.setTableName(cacheTableName);
updateInput.setRecords(List.of(queryOutput.getRecords().get(0).withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
new UpdateAction().execute(updateInput);
queryOutput = new QueryAction().execute(queryInput);
assertEquals(7, queryOutput.getRecords().get(0).getValue("noOfSides"));
}
/////////////////////////////////////////////////
// should only be 1 cache record at this point //
/////////////////////////////////////////////////
assertEquals(1, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
//////////////////////////////////////////////////////////////////////
// delete the source record - it will still be in the cache though. //
//////////////////////////////////////////////////////////////////////
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(sourceTableName);
deleteInput.setPrimaryKeys(List.of(1));
new DeleteAction().execute(deleteInput);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
assertEquals(1, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
////////////////////////////////////////////////////////////////////
// then artificially move back the cachedDate in the cache table. //
// then re-get from cache table, and now it should go away //
////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(cacheTableName);
updateInput.setRecords(List.of(queryOutput.getRecords().get(0).withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
new UpdateAction().execute(updateInput);
queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle")));
queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUniqueKeyCacheSingleFieldUniqueKeyMultiRecordUseCases() throws QException
{
QInstance qInstance = QContext.getQInstance();
String sourceTableName = TestUtils.TABLE_NAME_SHAPE;
String cacheTableName = TestUtils.TABLE_NAME_SHAPE_CACHE;
/////////////////////////////////////
// insert rows in the source table //
/////////////////////////////////////
TestUtils.insertRecords(qInstance.getTable(sourceTableName), List.of(
new QRecord().withValue("id", 1).withValue("name", "Triangle").withValue("noOfSides", 3),
new QRecord().withValue("id", 2).withValue("name", "Square").withValue("noOfSides", 4),
new QRecord().withValue("id", 3).withValue("name", "Pentagon").withValue("noOfSides", 5),
new QRecord().withValue("id", 4).withValue("name", "ServerErrorGon").withValue("noOfSides", 503),
new QRecord().withValue("id", 5).withValue("name", "ManyGon").withValue("noOfSides", 999)
));
/////////////////////////////////////////////////////////////////////////////
// get from the table which caches it - confirm they are (magically) found //
/////////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.IN, "Triangle", "Square", "Pentagon")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(3, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// try to get records through the cache table, which meet the conditions that cause them to not be cached. //
// so we should get results from the Query request - but - then let's go directly to the backend to confirm the records are not cached. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
assertEquals(3, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "ServerErrorGon")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.IN, "ManyGon", "ServerErrorGon")));
queryOutput = new QueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.IN, "ManyGon", "Square")));
queryOutput = new QueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size());
assertEquals(3, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
}
///////////////////////////////////////////////////////////////////////////
// request a row that doesn't exist in cache or source, should miss both //
///////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Line"))); // lines aren't shapes :)
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// update one source record; delete another - query and should still find the previously cached //
//////////////////////////////////////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(sourceTableName);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfSides", 6)));
new UpdateAction().execute(updateInput);
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(sourceTableName);
deleteInput.setPrimaryKeys(List.of(2)); // delete Square
new DeleteAction().execute(deleteInput);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.IN, "Triangle", "Square", "Pentagon", "ServerErrorGon")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(4, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(3, queryOutput.getRecords().get(0).getValue("noOfSides"));
/////////////////////////////////////////////////////////////////////////////////////////////
// then artificially move back the cachedDate in the cache table. //
// then re-get from cache table, and we should see the updated value (and the deleted one) //
/////////////////////////////////////////////////////////////////////////////////////////////
updateInput = new UpdateInput();
updateInput.setTableName(cacheTableName);
updateInput.setRecords(Stream.of(1, 2, 3).map(id -> new QRecord().withValue("id", id).withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))).toList());
new UpdateAction().execute(updateInput);
queryOutput = new QueryAction().execute(queryInput);
assertEquals(3, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(6, queryOutput.getRecords().stream().filter(r -> r.getValueString("name").equals("Triangle")).findFirst().get().getValue("noOfSides"));
assertEquals(2, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUniqueKeyCacheMultiFieldUniqueKeySingleRecordUseCases() throws QException
{
QInstance qInstance = QContext.getQInstance();
String sourceTableName = TestUtils.TABLE_NAME_PERSON_MEMORY;
String cacheTableName = TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE;
/////////////////////////////////////
// insert rows in the source table //
/////////////////////////////////////
TestUtils.insertRecords(qInstance.getTable(sourceTableName), List.of(
new QRecord().withValue("id", 1).withValue("firstName", "George").withValue("lastName", "Washington").withValue("noOfShoes", 5),
new QRecord().withValue("id", 2).withValue("firstName", "John").withValue("lastName", "Adams"),
new QRecord().withValue("id", 3).withValue("firstName", "Thomas").withValue("lastName", "Jefferson"),
new QRecord().withValue("id", 4).withValue("firstName", "James").withValue("lastName", "Garfield").withValue("noOfShoes", 503),
new QRecord().withValue("id", 5).withValue("firstName", "Abraham").withValue("lastName", "Lincoln").withValue("noOfShoes", 999),
new QRecord().withValue("id", 6).withValue("firstName", "Bill").withValue("lastName", "Clinton")
));
/////////////////////////////////////////////////////////////////////////////
// get from the table which caches it - confirm they are (magically) found //
/////////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPerson("George", "Washington"));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(5, queryOutput.getRecords().get(0).getValue("noOfShoes"));
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// try to get records through the cache table, which meet the conditions that cause them to not be cached. //
// so we should get results from the Get request - but - then let's go directly to the backend to confirm the records are not cached. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPerson("James", "Garfield"));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
queryInput.setFilter(getFilterForPerson("Abraham", "Lincoln"));
queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.IN, "Abraham", "James"))));
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// fetch a record through the cache, so it gets cached. //
// then update the source record so that it meets the condition that doesn't allow it to be cached. //
// then expire the cached record. //
// then re-fetch through cache - which should see the expiration, re-fetch from source, and delete from cache. //
// assert record is no longer in cache. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPerson("Bill", "Clinton"));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(1, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Clinton"))));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(sourceTableName);
updateInput.setRecords(List.of(new QRecord().withValue("id", 6).withValue("noOfShoes", 503)));
new UpdateAction().execute(updateInput);
updateInput = new UpdateInput();
updateInput.setTableName(cacheTableName);
updateInput.setRecords(List.of(queryOutput.getRecords().get(0).withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
new UpdateAction().execute(updateInput);
queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(503, queryOutput.getRecords().get(0).getValue("noOfShoes"));
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Clinton"))));
}
///////////////////////////////////////////////////////////////////////////
// request a row that doesn't exist in cache or source, should miss both //
///////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPerson("John", "McCain"));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
// update the record in the source table - then re-get from cache table - shouldn't see new value. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(sourceTableName);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfShoes", 6)));
new UpdateAction().execute(updateInput);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPerson("George", "Washington"));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(5, queryOutput.getRecords().get(0).getValue("noOfShoes"));
}
///////////////////////////////////////////////////////////////////////////
// delete the cached record; re-get, and we should see the updated value //
///////////////////////////////////////////////////////////////////////////
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(cacheTableName);
deleteInput.setQueryFilter(new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, "George")));
new DeleteAction().execute(deleteInput);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPerson("George", "Washington"));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(6, queryOutput.getRecords().get(0).getValue("noOfShoes"));
}
///////////////////////////////////////////////////////////////////
// update the source record; see that it isn't updated in cache. //
///////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(sourceTableName);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfShoes", 7)));
new UpdateAction().execute(updateInput);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPerson("George", "Washington"));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(6, queryOutput.getRecords().get(0).getValue("noOfShoes"));
///////////////////////////////////////////////////////////////////////
// then artificially move back the cachedDate in the cache table. //
// then re-get from cache table, and we should see the updated value //
///////////////////////////////////////////////////////////////////////
updateInput = new UpdateInput();
updateInput.setTableName(cacheTableName);
updateInput.setRecords(List.of(queryOutput.getRecords().get(0).withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
new UpdateAction().execute(updateInput);
queryOutput = new QueryAction().execute(queryInput);
assertEquals(7, queryOutput.getRecords().get(0).getValue("noOfShoes"));
}
/////////////////////////////////////////////////
// should only be 1 cache record at this point //
/////////////////////////////////////////////////
assertEquals(1, TestUtils.queryTable(QContext.getQInstance(), cacheTableName).size());
//////////////////////////////////////////////////////////////////////
// delete the source record - it will still be in the cache though. //
//////////////////////////////////////////////////////////////////////
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(sourceTableName);
deleteInput.setPrimaryKeys(List.of(1));
new DeleteAction().execute(deleteInput);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPerson("George", "Washington"));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
////////////////////////////////////////////////////////////////////
// then artificially move back the cachedDate in the cache table. //
// then re-get from cache table, and now it should go away //
////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(cacheTableName);
updateInput.setRecords(List.of(queryOutput.getRecords().get(0).withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))));
new UpdateAction().execute(updateInput);
queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPerson("George", "Washington"));
queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUniqueKeyCacheMultiFieldUniqueKeyMultiRecordUseCases() throws QException
{
QInstance qInstance = QContext.getQInstance();
String sourceTableName = TestUtils.TABLE_NAME_PERSON_MEMORY;
String cacheTableName = TestUtils.TABLE_NAME_PERSON_MEMORY_CACHE;
/////////////////////////////////////
// insert rows in the source table //
/////////////////////////////////////
TestUtils.insertRecords(qInstance.getTable(sourceTableName), List.of(
new QRecord().withValue("id", 1).withValue("firstName", "George").withValue("lastName", "Washington").withValue("noOfShoes", 5),
new QRecord().withValue("id", 2).withValue("firstName", "John").withValue("lastName", "Adams"),
new QRecord().withValue("id", 3).withValue("firstName", "Thomas").withValue("lastName", "Jefferson"),
new QRecord().withValue("id", 4).withValue("firstName", "James").withValue("lastName", "Garfield").withValue("noOfShoes", 503),
new QRecord().withValue("id", 5).withValue("firstName", "Abraham").withValue("lastName", "Lincoln").withValue("noOfShoes", 999),
new QRecord().withValue("id", 6).withValue("firstName", "Bill").withValue("lastName", "Clinton")
));
/////////////////////////////////////////////////////////////////////////////
// get from the table which caches it - confirm they are (magically) found //
/////////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPersons(getFilterForPerson("George", "Washington"), getFilterForPerson("John", "Adams"), getFilterForPerson("Thomas", "Jefferson")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(3, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// try to get records through the cache table, which meet the conditions that cause them to not be cached. //
// so we should get results from the Query request - but - then let's go directly to the backend to confirm the records are not cached. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
assertEquals(3, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPersons(getFilterForPerson("James", "Garfield"), getFilterForPerson("Abraham", "Lincoln"), getFilterForPerson("Thomas", "Jefferson")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(3, queryOutput.getRecords().size());
assertEquals(3, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
}
///////////////////////////////////////////////////////////////////////////
// request a row that doesn't exist in cache or source, should miss both //
///////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPersons(getFilterForPerson("John", "McCain"), getFilterForPerson("John", "Kerry")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// fetch a record through the cache, so it gets cached. //
// then update the source record so that it meets the condition that doesn't allow it to be cached. //
// and delete another one. //
// then expire the cached records. //
// then re-fetch through cache - which should see the expiration, re-fetch from source, and delete from cache. //
// assert record is no longer in cache. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(getFilterForPersons(getFilterForPerson("George", "Washington"), getFilterForPerson("Bill", "Clinton")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(1, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Clinton"))));
assertEquals(1, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Washington"))));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(sourceTableName);
updateInput.setRecords(List.of(new QRecord().withValue("id", 6).withValue("noOfShoes", 503)));
new UpdateAction().execute(updateInput);
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(sourceTableName);
deleteInput.setPrimaryKeys(List.of(1)); // delete Washington
new DeleteAction().execute(deleteInput);
updateInput = new UpdateInput();
updateInput.setTableName(cacheTableName);
updateInput.setRecords(Stream.of(1, 2, 3, 4).map(id -> new QRecord().withValue("id", id).withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))).toList());
new UpdateAction().execute(updateInput);
queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(503, queryOutput.getRecords().stream().filter(r -> r.getValueString("lastName").equals("Clinton")).findFirst().get().getValue("noOfShoes"));
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Clinton"))));
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, "Washington"))));
}
/*
//////////////////////////////////////////////////////////////////////////////////////////////////
// update one source record; delete another - query and should still find the previously cached //
//////////////////////////////////////////////////////////////////////////////////////////////////
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(sourceTableName);
updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("noOfSides", 6)));
new UpdateAction().execute(updateInput);
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(sourceTableName);
deleteInput.setPrimaryKeys(List.of(2)); // delete Square
new DeleteAction().execute(deleteInput);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.IN, "Triangle", "Square", "Pentagon", "ServerErrorGon")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(4, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(3, queryOutput.getRecords().get(0).getValue("noOfSides"));
/////////////////////////////////////////////////////////////////////////////////////////////
// then artificially move back the cachedDate in the cache table. //
// then re-get from cache table, and we should see the updated value (and the deleted one) //
/////////////////////////////////////////////////////////////////////////////////////////////
updateInput = new UpdateInput();
updateInput.setTableName(cacheTableName);
updateInput.setRecords(Stream.of(1, 2, 3).map(id -> new QRecord().withValue("id", id).withValue("cachedDate", Instant.parse("2001-01-01T00:00:00Z"))).toList());
new UpdateAction().execute(updateInput);
queryOutput = new QueryAction().execute(queryInput);
assertEquals(3, queryOutput.getRecords().size());
assertNotNull(queryOutput.getRecords().get(0).getValue("cachedDate"));
assertEquals(6, queryOutput.getRecords().stream().filter(r -> r.getValueString("name").equals("Triangle")).findFirst().get().getValue("noOfSides"));
assertEquals(2, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
}
*/
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUniqueKeyCacheNonCachingUseCases() throws QException
{
QInstance qInstance = QContext.getQInstance();
String sourceTableName = TestUtils.TABLE_NAME_SHAPE;
String cacheTableName = TestUtils.TABLE_NAME_SHAPE_CACHE;
/////////////////////////////////////
// insert rows in the source table //
/////////////////////////////////////
TestUtils.insertRecords(qInstance.getTable(sourceTableName), List.of(
new QRecord().withValue("id", 1).withValue("name", "Triangle").withValue("noOfSides", 3),
new QRecord().withValue("id", 2).withValue("name", "Square").withValue("noOfSides", 4),
new QRecord().withValue("id", 3).withValue("name", "Pentagon").withValue("noOfSides", 5),
new QRecord().withValue("id", 4).withValue("name", "ServerErrorGon").withValue("noOfSides", 503),
new QRecord().withValue("id", 5).withValue("name", "ManyGon").withValue("noOfSides", 999)));
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// do queries on the cache table that we aren't allowed to do caching with - confirm that cache remains empty //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
///////////////
// no filter //
///////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
}
{
//////////////////////////////
// unique key not in filter //
//////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("noOfSides", QCriteriaOperator.LESS_THAN_OR_EQUALS, 5)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
}
{
////////////////////////////////////////////////
// unsupported operator in filter on UK field //
////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.STARTS_WITH, "T")));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
}
{
///////////////////////////////////////////////////////////////////////////
// an AND sub-filter //
// (technically we could do this, since only 1 sub-filter, but we don't) //
///////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter().withSubFilters(List.of(
new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle"))
)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
}
{
//////////////////////////////////////////////
// an OR sub-filter, but unsupported fields //
//////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR).withSubFilters(List.of(
new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle")),
new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 3))
)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
}
{
////////////////////////////////////////////////////////////////////////////////////////////////////////
// an OR sub-filter, but with unsupported operator (IN - supported w/o subqueries, but not like this) //
// (technically we could do this, since only 1 sub-filter, but we don't) //
////////////////////////////////////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR).withSubFilters(List.of(
new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.IN, "Triangle", "Square"))
)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size());
assertEquals(0, countCachedRecordsDirectlyInBackend(cacheTableName, new QQueryFilter()));
}
///////////////////////////////////////////////////////////////////////////////////////////
// finally - queries that DO hit cache (so note, cache will stop being empty after here) //
///////////////////////////////////////////////////////////////////////////////////////////
{
///////////////////////////////////////////////////////////
// an OR sub-filter, with supported ops, and UKey fields //
///////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(cacheTableName);
queryInput.setFilter(new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR).withSubFilters(List.of(
new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Triangle")),
new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "Square"))
)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size());
}
}
/*******************************************************************************
**
*******************************************************************************/
private static QQueryFilter getFilterForPerson(String firstName, String lastName)
{
return new QQueryFilter(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, firstName), new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, lastName));
}
/*******************************************************************************
**
*******************************************************************************/
private QQueryFilter getFilterForPersons(QQueryFilter... subFilters)
{
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
for(QQueryFilter subFilter : subFilters)
{
filter.addSubFilter(subFilter);
}
return (filter);
}
/*******************************************************************************
**
*******************************************************************************/
private static int countCachedRecordsDirectlyInBackend(String tableName, QQueryFilter filter) throws QException
{
List<QRecord> cachedRecords = MemoryRecordStore.getInstance().query(new QueryInput()
.withTableName(tableName)
.withFilter(filter));
return cachedRecords.size();
}
}

View File

@ -129,6 +129,7 @@ public class TestUtils
public static final String TABLE_NAME_PERSON = "person";
public static final String TABLE_NAME_SHAPE = "shape";
public static final String TABLE_NAME_SHAPE_CACHE = "shapeCache";
public static final String TABLE_NAME_ORDER = "order";
public static final String TABLE_NAME_LINE_ITEM = "orderLine";
public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic";
@ -185,6 +186,7 @@ public class TestUtils
qInstance.addTable(definePersonMemoryCacheTable());
qInstance.addTable(defineTableIdAndNameOnly());
qInstance.addTable(defineTableShape());
qInstance.addTable(defineShapeCacheTable());
qInstance.addTable(defineTableBasepull());
qInstance.addTable(defineTableOrder());
qInstance.addTable(defineTableLineItem());
@ -338,8 +340,7 @@ public class TestUtils
private static QAutomationProviderMetaData definePollingAutomationProvider()
{
return (new PollingAutomationProviderMetaData()
.withName(POLLING_AUTOMATION)
);
.withName(POLLING_AUTOMATION));
}
@ -847,8 +848,43 @@ public class TestUtils
.withCacheSourceMisses(false)
.withExcludeRecordsMatching(List.of(
new QQueryFilter(
new QFilterCriteria("firstName", QCriteriaOperator.CONTAINS, "503"),
new QFilterCriteria("firstName", QCriteriaOperator.CONTAINS, "999")
new QFilterCriteria("noOfShoes", QCriteriaOperator.EQUALS, "503"),
new QFilterCriteria("noOfShoes", QCriteriaOperator.EQUALS, "999")
).withBooleanOperator(QQueryFilter.BooleanOperator.OR)
)
))
);
}
/*******************************************************************************
** Define another version of the 'shape' table, also in-memory, and as a
** cache on the other in-memory one...
*******************************************************************************/
public static QTableMetaData defineShapeCacheTable()
{
UniqueKey uniqueKey = new UniqueKey("name");
return (new QTableMetaData()
.withName(TABLE_NAME_SHAPE_CACHE)
.withBackendName(MEMORY_BACKEND_NAME)
.withPrimaryKeyField("id")
.withUniqueKey(uniqueKey)
.withFields(TestUtils.defineTableShape().getFields()))
.withField(new QFieldMetaData("cachedDate", QFieldType.DATE_TIME))
.withCacheOf(new CacheOf()
.withSourceTable(TABLE_NAME_SHAPE)
.withCachedDateFieldName("cachedDate")
.withExpirationSeconds(60)
.withUseCase(new CacheUseCase()
.withType(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY)
.withSourceUniqueKey(uniqueKey)
.withCacheUniqueKey(uniqueKey)
.withCacheSourceMisses(false)
.withExcludeRecordsMatching(List.of(
new QQueryFilter(
new QFilterCriteria("noOfSides", QCriteriaOperator.EQUALS, 503),
new QFilterCriteria("noOfSides", QCriteriaOperator.EQUALS, 999)
).withBooleanOperator(QQueryFilter.BooleanOperator.OR)
)
))