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)) + ); + } + + + /******************************************************************************* ** *******************************************************************************/