Add support for variants to memory backend

This commit is contained in:
2025-05-23 11:30:52 -05:00
parent 5daa221ac9
commit 75fc016a4b
4 changed files with 245 additions and 11 deletions

View File

@ -0,0 +1,35 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantSetting;
/*******************************************************************************
** since some settings are required for a variant, if you're using memory backend
** with variants, this is a setting you can use.
*******************************************************************************/
public enum MemoryModuleBackendVariantSetting implements BackendVariantSetting
{
PRIMARY_KEY
}

View File

@ -63,12 +63,15 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
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.QBackendMetaData;
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.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsConfig;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsUtil;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
@ -85,8 +88,11 @@ public class MemoryRecordStore
private static MemoryRecordStore instance;
private Map<String, Map<Serializable, QRecord>> data;
private Map<String, Integer> nextSerials;
//////////////////////////////////////////////////////////
// these maps are: BackendIdentifier > tableName > data //
//////////////////////////////////////////////////////////
private Map<BackendIdentifier, Map<String, Map<Serializable, QRecord>>> data;
private Map<BackendIdentifier, Map<String, Integer>> nextSerials;
private static boolean collectStatistics = false;
@ -150,13 +156,30 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
private Map<Serializable, QRecord> getTableData(QTableMetaData table)
private Map<Serializable, QRecord> getTableData(QTableMetaData table) throws QException
{
if(!data.containsKey(table.getName()))
BackendIdentifier backendIdentifier = getBackendIdentifier(table);
Map<String, Map<Serializable, QRecord>> dataForBackend = data.computeIfAbsent(backendIdentifier, k -> new HashMap<>());
return (dataForBackend.computeIfAbsent(table.getName(), k -> new HashMap<>()));
}
/***************************************************************************
**
***************************************************************************/
private BackendIdentifier getBackendIdentifier(QTableMetaData table) throws QException
{
BackendIdentifier backendIdentifier = NonVariant.getInstance();
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(table.getBackendName());
BackendVariantsConfig backendVariantsConfig = backendMetaData.getBackendVariantsConfig();
if(backendVariantsConfig != null)
{
data.put(table.getName(), new HashMap<>());
String variantType = backendMetaData.getBackendVariantsConfig().getVariantTypeKey();
Serializable variantId = BackendVariantsUtil.getVariantId(backendMetaData);
backendIdentifier = new Variant(variantType, variantId);
}
return (data.get(table.getName()));
return backendIdentifier;
}
@ -329,7 +352,7 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
public List<QRecord> insert(InsertInput input, boolean returnInsertedRecords)
public List<QRecord> insert(InsertInput input, boolean returnInsertedRecords) throws QException
{
incrementStatistic(input);
@ -344,7 +367,7 @@ public class MemoryRecordStore
////////////////////////////////////////
// grab the next unique serial to use //
////////////////////////////////////////
Integer nextSerial = nextSerials.get(table.getName());
Integer nextSerial = getNextSerial(table);
if(nextSerial == null)
{
nextSerial = 1;
@ -407,17 +430,41 @@ public class MemoryRecordStore
}
}
nextSerials.put(table.getName(), nextSerial);
setNextSerial(table, nextSerial);
return (outputRecords);
}
/***************************************************************************
**
***************************************************************************/
private void setNextSerial(QTableMetaData table, Integer nextSerial) throws QException
{
BackendIdentifier backendIdentifier = getBackendIdentifier(table);
Map<String, Integer> nextSerialsForBackend = nextSerials.computeIfAbsent(backendIdentifier, (k) -> new HashMap<>());
nextSerialsForBackend.put(table.getName(), nextSerial);
}
/***************************************************************************
**
***************************************************************************/
private Integer getNextSerial(QTableMetaData table) throws QException
{
BackendIdentifier backendIdentifier = getBackendIdentifier(table);
Map<String, Integer> nextSerialsForBackend = nextSerials.computeIfAbsent(backendIdentifier, (k) -> new HashMap<>());
return (nextSerialsForBackend.get(table.getName()));
}
/*******************************************************************************
**
*******************************************************************************/
public List<QRecord> update(UpdateInput input, boolean returnUpdatedRecords)
public List<QRecord> update(UpdateInput input, boolean returnUpdatedRecords) throws QException
{
if(input.getRecords() == null)
{
@ -462,7 +509,7 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
public int delete(DeleteInput input)
public int delete(DeleteInput input) throws QException
{
if(input.getPrimaryKeys() == null)
{
@ -927,4 +974,58 @@ public class MemoryRecordStore
return (filter.clone());
}
}
/***************************************************************************
** key for the internal maps of this class - either for a non-variant version
** of the memory backend, or for one based on variants.
***************************************************************************/
private sealed interface BackendIdentifier permits NonVariant, Variant
{
}
/***************************************************************************
** singleton, representing non-variant instance of memory backend.
***************************************************************************/
private static final class NonVariant implements BackendIdentifier
{
private static NonVariant nonVariant = null;
/*******************************************************************************
** Singleton constructor
*******************************************************************************/
private NonVariant()
{
}
/*******************************************************************************
** Singleton accessor
*******************************************************************************/
public static NonVariant getInstance()
{
if(nonVariant == null)
{
nonVariant = new NonVariant();
}
return (nonVariant);
}
}
/***************************************************************************
** record representing a variant type & id
***************************************************************************/
private record Variant(String type, Serializable id) implements BackendIdentifier
{
}
}

View File

@ -26,6 +26,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.Month;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
@ -205,6 +206,60 @@ class MemoryBackendModuleTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testVariants() throws QException
{
////////////////////////////////////
// insert our two variant options //
////////////////////////////////////
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_OPTIONS).withRecords(List.of(
new QRecord().withValue("id", 1).withValue("name", "People"),
new QRecord().withValue("id", 2).withValue("name", "Planets")
)));
///////////////////////////////////////////////////////////////////////////////////////////
// assert we fail if no variant set in session when working with a table that needs them //
///////////////////////////////////////////////////////////////////////////////////////////
assertThatThrownBy(() -> new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_DATA).withRecords(List.of(
new QRecord().withValue("id", 1).withValue("name", "Tom")
)))).hasMessageContaining("Could not find Backend Variant information");
///////////////////////////////////////////////////////////
// set the variant in session, and assert we insert once //
///////////////////////////////////////////////////////////
QContext.getQSession().setBackendVariants(Map.of(TestUtils.TABLE_NAME_MEMORY_VARIANT_OPTIONS, 1));
Integer peopleId1 = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_DATA).withRecords(List.of(
new QRecord().withValue("name", "Tom")
))).getRecords().get(0).getValueInteger("id");
assertEquals(1, peopleId1);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// set the other variant - make sure we insert, and get the same serial (e.g., they differ per variant) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
QContext.getQSession().setBackendVariants(Map.of(TestUtils.TABLE_NAME_MEMORY_VARIANT_OPTIONS, 2));
Integer planetId1 = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_DATA).withRecords(List.of(
new QRecord().withValue("name", "Mercury"),
new QRecord().withValue("name", "Venus"),
new QRecord().withValue("name", "Earth")
))).getRecords().get(0).getValueInteger("id");
assertEquals(1, planetId1);
////////////////////////////////////////////////////////
// make sure counts return what we expect per-variant //
////////////////////////////////////////////////////////
QContext.getQSession().setBackendVariants(Map.of(TestUtils.TABLE_NAME_MEMORY_VARIANT_OPTIONS, 2));
assertEquals(3, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_DATA)).getCount());
QContext.getQSession().setBackendVariants(Map.of(TestUtils.TABLE_NAME_MEMORY_VARIANT_OPTIONS, 1));
assertEquals(1, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_MEMORY_VARIANT_DATA)).getCount());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -27,6 +27,7 @@ import java.time.LocalDate;
import java.time.Month;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarChart;
@ -114,9 +115,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAuto
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsConfig;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.MockAuthenticationModule;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryModuleBackendVariantSetting;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess;
@ -153,6 +156,10 @@ public class TestUtils
public static final String TABLE_NAME_LINE_ITEM_EXTRINSIC = "orderLineExtrinsic";
public static final String TABLE_NAME_ORDER_EXTRINSIC = "orderExtrinsic";
public static final String MEMORY_BACKEND_WITH_VARIANTS_NAME = "memoryWithVariants";
public static final String TABLE_NAME_MEMORY_VARIANT_OPTIONS = "memoryVariantOptions";
public static final String TABLE_NAME_MEMORY_VARIANT_DATA = "memoryVariantData";
public static final String PROCESS_NAME_GREET_PEOPLE = "greet";
public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
public static final String PROCESS_NAME_INCREASE_BIRTHDATE = "increaseBirthdate";
@ -255,6 +262,8 @@ public class TestUtils
qInstance.addMessagingProvider(defineEmailMessagingProvider());
qInstance.addMessagingProvider(defineSESMessagingProvider());
defineMemoryBackendVariantUseCases(qInstance);
defineWidgets(qInstance);
defineApps(qInstance);
@ -265,6 +274,40 @@ public class TestUtils
/***************************************************************************
**
***************************************************************************/
private static void defineMemoryBackendVariantUseCases(QInstance qInstance)
{
qInstance.addBackend(new QBackendMetaData()
.withName(MEMORY_BACKEND_WITH_VARIANTS_NAME)
.withBackendType(MemoryBackendModule.class)
.withUsesVariants(true)
.withBackendVariantsConfig(new BackendVariantsConfig()
.withVariantTypeKey(TABLE_NAME_MEMORY_VARIANT_OPTIONS)
.withOptionsTableName(TABLE_NAME_MEMORY_VARIANT_OPTIONS)
.withBackendSettingSourceFieldNameMap(Map.of(MemoryModuleBackendVariantSetting.PRIMARY_KEY, "id"))
));
qInstance.addTable(new QTableMetaData()
.withName(TABLE_NAME_MEMORY_VARIANT_DATA)
.withBackendName(MEMORY_BACKEND_WITH_VARIANTS_NAME)
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("name", QFieldType.STRING))
);
qInstance.addTable(new QTableMetaData()
.withName(TABLE_NAME_MEMORY_VARIANT_OPTIONS)
.withBackendName(MEMORY_BACKEND_NAME) // note, the version without variants!
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("name", QFieldType.STRING))
);
}
/*******************************************************************************
**
*******************************************************************************/