diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryModuleBackendVariantSetting.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryModuleBackendVariantSetting.java
new file mode 100644
index 00000000..2d646a39
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryModuleBackendVariantSetting.java
@@ -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 .
+ */
+
+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
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
index b53d69a2..255654ac 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
@@ -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> data;
- private Map nextSerials;
+ //////////////////////////////////////////////////////////
+ // these maps are: BackendIdentifier > tableName > data //
+ //////////////////////////////////////////////////////////
+ private Map>> data;
+ private Map> nextSerials;
private static boolean collectStatistics = false;
@@ -150,13 +156,30 @@ public class MemoryRecordStore
/*******************************************************************************
**
*******************************************************************************/
- private Map getTableData(QTableMetaData table)
+ private Map getTableData(QTableMetaData table) throws QException
{
- if(!data.containsKey(table.getName()))
+ BackendIdentifier backendIdentifier = getBackendIdentifier(table);
+ Map> 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 insert(InsertInput input, boolean returnInsertedRecords)
+ public List 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 nextSerialsForBackend = nextSerials.computeIfAbsent(backendIdentifier, (k) -> new HashMap<>());
+ nextSerialsForBackend.put(table.getName(), nextSerial);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private Integer getNextSerial(QTableMetaData table) throws QException
+ {
+ BackendIdentifier backendIdentifier = getBackendIdentifier(table);
+ Map nextSerialsForBackend = nextSerials.computeIfAbsent(backendIdentifier, (k) -> new HashMap<>());
+ return (nextSerialsForBackend.get(table.getName()));
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
- public List update(UpdateInput input, boolean returnUpdatedRecords)
+ public List 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
+ {
+ }
+
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
index d9f6df9c..09c826ed 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
@@ -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());
+
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
index 731b7235..7b81bc23 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
@@ -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))
+ );
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/