diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java
index c57afd5b..19161379 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java
@@ -72,6 +72,7 @@ public class QBackendModuleDispatcher
// todo - let modules somehow "export" their types here?
// e.g., backend-core shouldn't need to "know" about the modules.
"com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule",
+ "com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule",
"com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule",
"com.kingsrook.qqq.backend.module.filesystem.local.FilesystemBackendModule",
"com.kingsrook.qqq.backend.module.filesystem.s3.S3BackendModule"
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java
new file mode 100644
index 00000000..84852cb9
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java
@@ -0,0 +1,116 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
+
+
+import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
+
+
+/*******************************************************************************
+ ** A simple (probably only valid for testing?) implementation of the QModuleInterface,
+ ** that just stores its records in-memory.
+ **
+ *******************************************************************************/
+public class MemoryBackendModule implements QBackendModuleInterface
+{
+ /*******************************************************************************
+ ** Method where a backend module must be able to provide its type (name).
+ *******************************************************************************/
+ @Override
+ public String getBackendType()
+ {
+ return ("memory");
+ }
+
+
+
+ /*******************************************************************************
+ ** Method to identify the class used for backend meta data for this module.
+ *******************************************************************************/
+ @Override
+ public Class extends QBackendMetaData> getBackendMetaDataClass()
+ {
+ return (QBackendMetaData.class);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public CountInterface getCountInterface()
+ {
+ return new MemoryCountAction();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QueryInterface getQueryInterface()
+ {
+ return new MemoryQueryAction();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public InsertInterface getInsertInterface()
+ {
+ return (new MemoryInsertAction());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public UpdateInterface getUpdateInterface()
+ {
+ return (new MemoryUpdateAction());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public DeleteInterface getDeleteInterface()
+ {
+ return (new MemoryDeleteAction());
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryCountAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryCountAction.java
new file mode 100644
index 00000000..4f5e0d67
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryCountAction.java
@@ -0,0 +1,54 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
+
+
+import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
+
+
+/*******************************************************************************
+ ** In-memory version of count action.
+ **
+ *******************************************************************************/
+public class MemoryCountAction implements CountInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public CountOutput execute(CountInput countInput) throws QException
+ {
+ try
+ {
+ CountOutput countOutput = new CountOutput();
+ countOutput.setCount(MemoryRecordStore.getInstance().count(countInput));
+ return (countOutput);
+ }
+ catch(Exception e)
+ {
+ throw new QException("Error executing count", e);
+ }
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryDeleteAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryDeleteAction.java
new file mode 100644
index 00000000..50c5e239
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryDeleteAction.java
@@ -0,0 +1,55 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
+
+
+import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
+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.delete.DeleteOutput;
+
+
+/*******************************************************************************
+ ** In-memory version of delete action.
+ **
+ *******************************************************************************/
+public class MemoryDeleteAction implements DeleteInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public DeleteOutput execute(DeleteInput deleteInput) throws QException
+ {
+ try
+ {
+ DeleteOutput deleteOutput = new DeleteOutput();
+ deleteOutput.setDeletedRecordCount(MemoryRecordStore.getInstance().delete(deleteInput));
+ return (deleteOutput);
+ }
+ catch(Exception e)
+ {
+ throw new QException("Error executing delete: " + e.getMessage(), e);
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryInsertAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryInsertAction.java
new file mode 100644
index 00000000..01839359
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryInsertAction.java
@@ -0,0 +1,55 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
+
+
+import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
+
+
+/*******************************************************************************
+ ** In-memory version of insert action.
+ **
+ *******************************************************************************/
+public class MemoryInsertAction implements InsertInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public InsertOutput execute(InsertInput insertInput) throws QException
+ {
+ try
+ {
+ InsertOutput insertOutput = new InsertOutput();
+ insertOutput.setRecords(MemoryRecordStore.getInstance().insert(insertInput, true));
+ return (insertOutput);
+ }
+ catch(Exception e)
+ {
+ throw new QException("Error executing insert: " + e.getMessage(), e);
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java
new file mode 100644
index 00000000..cd0e8bf7
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryQueryAction.java
@@ -0,0 +1,55 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
+
+
+import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+
+
+/*******************************************************************************
+ ** In-memory version of query action.
+ **
+ *******************************************************************************/
+public class MemoryQueryAction implements QueryInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QueryOutput execute(QueryInput queryInput) throws QException
+ {
+ try
+ {
+ QueryOutput queryOutput = new QueryOutput(queryInput);
+ queryOutput.addRecords(MemoryRecordStore.getInstance().query(queryInput));
+ return (queryOutput);
+ }
+ catch(Exception e)
+ {
+ throw new QException("Error executing query", e);
+ }
+ }
+
+}
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
new file mode 100644
index 00000000..8fc7a02b
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
@@ -0,0 +1,238 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
+
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+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.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+
+
+/*******************************************************************************
+ ** Storage provider for the MemoryBackendModule
+ *******************************************************************************/
+public class MemoryRecordStore
+{
+ private static MemoryRecordStore instance;
+
+ private Map> data;
+ private Map nextSerials;
+
+
+
+ /*******************************************************************************
+ ** private singleton constructor
+ *******************************************************************************/
+ private MemoryRecordStore()
+ {
+ data = new HashMap<>();
+ nextSerials = new HashMap<>();
+ }
+
+
+
+ /*******************************************************************************
+ ** Forget all data in the memory store...
+ *******************************************************************************/
+ public void reset()
+ {
+ data.clear();
+ nextSerials.clear();
+ }
+
+
+
+ /*******************************************************************************
+ ** singleton accessor
+ *******************************************************************************/
+ public static MemoryRecordStore getInstance()
+ {
+ if(instance == null)
+ {
+ instance = new MemoryRecordStore();
+ }
+ return (instance);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private Map getTableData(QTableMetaData table)
+ {
+ if(!data.containsKey(table.getName()))
+ {
+ data.put(table.getName(), new HashMap<>());
+ }
+ return (data.get(table.getName()));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public List query(QueryInput input)
+ {
+ Map tableData = getTableData(input.getTable());
+ List records = new ArrayList<>(tableData.values());
+ // todo - filtering
+ return (records);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Integer count(CountInput input)
+ {
+ Map tableData = getTableData(input.getTable());
+ List records = new ArrayList<>(tableData.values());
+ // todo - filtering
+ return (records.size());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public List insert(InsertInput input, boolean returnInsertedRecords)
+ {
+ if(input.getRecords() == null)
+ {
+ return (new ArrayList<>());
+ }
+
+ QTableMetaData table = input.getTable();
+ Map tableData = getTableData(table);
+ Integer nextSerial = nextSerials.get(table.getName());
+ if(nextSerial == null)
+ {
+ nextSerial = 1;
+ while(tableData.containsKey(nextSerial))
+ {
+ nextSerial++;
+ }
+ }
+
+ List outputRecords = new ArrayList<>();
+ QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
+ for(QRecord record : input.getRecords())
+ {
+ if(record.getValue(primaryKeyField.getName()) == null && primaryKeyField.getType().equals(QFieldType.INTEGER))
+ {
+ record.setValue(primaryKeyField.getName(), nextSerial++);
+ }
+
+ tableData.put(record.getValue(primaryKeyField.getName()), record);
+ if(returnInsertedRecords)
+ {
+ outputRecords.add(record);
+ }
+ }
+
+ return (outputRecords);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public List update(UpdateInput input, boolean returnUpdatedRecords)
+ {
+ if(input.getRecords() == null)
+ {
+ return (new ArrayList<>());
+ }
+
+ QTableMetaData table = input.getTable();
+ Map tableData = getTableData(table);
+
+ List outputRecords = new ArrayList<>();
+ QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
+ for(QRecord record : input.getRecords())
+ {
+ Serializable primaryKeyValue = record.getValue(primaryKeyField.getName());
+ if(tableData.containsKey(primaryKeyValue))
+ {
+ QRecord recordToUpdate = tableData.get(primaryKeyValue);
+ for(Map.Entry valueEntry : record.getValues().entrySet())
+ {
+ recordToUpdate.setValue(valueEntry.getKey(), valueEntry.getValue());
+ }
+
+ if(returnUpdatedRecords)
+ {
+ outputRecords.add(record);
+ }
+ }
+ else
+ {
+ outputRecords.add(record);
+ }
+ }
+
+ return (outputRecords);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public int delete(DeleteInput input)
+ {
+ if(input.getPrimaryKeys() == null)
+ {
+ return (0);
+ }
+
+ QTableMetaData table = input.getTable();
+ Map tableData = getTableData(table);
+ int rowsDeleted = 0;
+ for(Serializable primaryKeyValue : input.getPrimaryKeys())
+ {
+ if(tableData.containsKey(primaryKeyValue))
+ {
+ tableData.remove(primaryKeyValue);
+ rowsDeleted++;
+ }
+ }
+
+ return (rowsDeleted);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryUpdateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryUpdateAction.java
new file mode 100644
index 00000000..97793ce6
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryUpdateAction.java
@@ -0,0 +1,55 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
+
+
+import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
+
+
+/*******************************************************************************
+ ** In-memory version of update action.
+ **
+ *******************************************************************************/
+public class MemoryUpdateAction implements UpdateInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public UpdateOutput execute(UpdateInput updateInput) throws QException
+ {
+ try
+ {
+ UpdateOutput updateOutput = new UpdateOutput();
+ updateOutput.setRecords(MemoryRecordStore.getInstance().update(updateInput, true));
+ return (updateOutput);
+ }
+ catch(Exception e)
+ {
+ throw new QException("Error executing update: " + e.getMessage(), e);
+ }
+ }
+
+}
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
new file mode 100644
index 00000000..f07e1b9a
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
@@ -0,0 +1,185 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
+import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
+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.actions.tables.update.UpdateOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Unit test for MemoryBackendModule
+ *******************************************************************************/
+class MemoryBackendModuleTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @AfterEach
+ void afterEach()
+ {
+ MemoryRecordStore.getInstance().reset();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testFullCRUD() throws QException
+ {
+ QInstance qInstance = TestUtils.defineInstance();
+ QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_SHAPE);
+ QSession session = new QSession();
+
+ /////////////////////////
+ // do an initial count //
+ /////////////////////////
+ CountInput countInput = new CountInput(qInstance);
+ countInput.setSession(session);
+ countInput.setTableName(table.getName());
+ assertEquals(0, new CountAction().execute(countInput).getCount());
+
+ //////////////////
+ // do an insert //
+ //////////////////
+ InsertInput insertInput = new InsertInput(qInstance);
+ insertInput.setSession(session);
+ insertInput.setTableName(table.getName());
+ insertInput.setRecords(List.of(
+ new QRecord()
+ .withTableName(table.getName())
+ .withValue("name", "My Triangle")
+ .withValue("type", "triangle")
+ .withValue("noOfSides", 3)
+ .withValue("isPolygon", true),
+ new QRecord()
+ .withTableName(table.getName())
+ .withValue("name", "Your Square")
+ .withValue("type", "square")
+ .withValue("noOfSides", 4)
+ .withValue("isPolygon", true),
+ new QRecord()
+ .withTableName(table.getName())
+ .withValue("name", "Some Circle")
+ .withValue("type", "circle")
+ .withValue("noOfSides", null)
+ .withValue("isPolygon", false)
+ ));
+ InsertOutput insertOutput = new InsertAction().execute(insertInput);
+ assertEquals(insertOutput.getRecords().size(), 3);
+ assertTrue(insertOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null));
+ assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1)));
+ assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2)));
+ assertTrue(insertOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3)));
+
+ ////////////////
+ // do a query //
+ ////////////////
+ QueryInput queryInput = new QueryInput(qInstance);
+ queryInput.setSession(session);
+ queryInput.setTableName(table.getName());
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(queryOutput.getRecords().size(), 3);
+ assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("id") != null));
+ assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(1)));
+ assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(2)));
+ assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3)));
+ assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("My Triangle")));
+ assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Your Square")));
+ assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Some Circle")));
+
+ assertEquals(3, new CountAction().execute(countInput).getCount());
+
+ //////////////////
+ // do an update //
+ //////////////////
+ UpdateInput updateInput = new UpdateInput(qInstance);
+ updateInput.setSession(session);
+ updateInput.setTableName(table.getName());
+ updateInput.setRecords(List.of(
+ new QRecord()
+ .withTableName(table.getName())
+ .withValue("id", 1)
+ .withValue("name", "Not My Triangle any more"),
+ new QRecord()
+ .withTableName(table.getName())
+ .withValue("id", 3)
+ .withValue("type", "ellipse")
+ ));
+ UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
+ assertEquals(updateOutput.getRecords().size(), 2);
+
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(queryOutput.getRecords().size(), 3);
+ assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("name").equals("My Triangle")));
+ assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("name").equals("Not My Triangle any more")));
+ assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueString("type").equals("ellipse")));
+ assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("type").equals("circle")));
+
+ assertEquals(3, new CountAction().execute(countInput).getCount());
+
+ /////////////////
+ // do a delete //
+ /////////////////
+ DeleteInput deleteInput = new DeleteInput(qInstance);
+ deleteInput.setSession(session);
+ deleteInput.setTableName(table.getName());
+ deleteInput.setPrimaryKeys(List.of(1, 2));
+ DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
+ assertEquals(deleteOutput.getDeletedRecordCount(), 2);
+
+ assertEquals(1, new CountAction().execute(countInput).getCount());
+
+ queryOutput = new QueryAction().execute(queryInput);
+ assertEquals(queryOutput.getRecords().size(), 1);
+ assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(1)));
+ assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueInteger("id").equals(2)));
+ assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValueInteger("id").equals(3)));
+ }
+
+}
\ No newline at end of file
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 a0711f87..aa537948 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
@@ -26,7 +26,6 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge;
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
-import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
@@ -52,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule;
import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
@@ -65,12 +65,14 @@ import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackend
public class TestUtils
{
public static final String DEFAULT_BACKEND_NAME = "default";
+ public static final String MEMORY_BACKEND_NAME = "memory";
public static final String APP_NAME_GREETINGS = "greetingsApp";
public static final String APP_NAME_PEOPLE = "peopleApp";
public static final String APP_NAME_MISCELLANEOUS = "miscellaneous";
public static final String TABLE_NAME_PERSON = "person";
+ public static final String TABLE_NAME_SHAPE = "shape";
public static final String PROCESS_NAME_GREET_PEOPLE = "greet";
public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
@@ -89,10 +91,12 @@ public class TestUtils
QInstance qInstance = new QInstance();
qInstance.setAuthentication(defineAuthentication());
qInstance.addBackend(defineBackend());
+ qInstance.addBackend(defineMemoryBackend());
qInstance.addTable(defineTablePerson());
qInstance.addTable(definePersonFileTable());
qInstance.addTable(defineTableIdAndNameOnly());
+ qInstance.addTable(defineTableShape());
qInstance.addPossibleValueSource(defineStatesPossibleValueSource());
@@ -104,8 +108,6 @@ public class TestUtils
defineApps(qInstance);
- System.out.println(new QInstanceAdapter().qInstanceToJson(qInstance));
-
return (qInstance);
}
@@ -174,6 +176,18 @@ public class TestUtils
+ /*******************************************************************************
+ ** Define the in-memory backend used in standard tests
+ *******************************************************************************/
+ public static QBackendMetaData defineMemoryBackend()
+ {
+ return new QBackendMetaData()
+ .withName(MEMORY_BACKEND_NAME)
+ .withBackendType(MemoryBackendModule.class);
+ }
+
+
+
/*******************************************************************************
** Define the 'person' table used in standard tests.
*******************************************************************************/
@@ -196,6 +210,27 @@ public class TestUtils
+ /*******************************************************************************
+ ** Define the 'shape' table used in standard tests.
+ *******************************************************************************/
+ public static QTableMetaData defineTableShape()
+ {
+ return new QTableMetaData()
+ .withName(TABLE_NAME_SHAPE)
+ .withBackendName(MEMORY_BACKEND_NAME)
+ .withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
+ .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
+ .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
+ .withField(new QFieldMetaData("name", QFieldType.STRING))
+ .withField(new QFieldMetaData("type", QFieldType.STRING)) // todo PVS
+ .withField(new QFieldMetaData("noOfSides", QFieldType.INTEGER))
+ .withField(new QFieldMetaData("isPolygon", QFieldType.BOOLEAN)) // mmm, should be derived from type, no?
+ ;
+ }
+
+
+
/*******************************************************************************
** Define a 2nd version of the 'person' table for this test (pretend it's backed by a file)
*******************************************************************************/
diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java
index 1ea36990..8464bb22 100644
--- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java
+++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncStep.java
@@ -91,7 +91,7 @@ public class FilesystemSyncStep implements BackendStep
String sourceFileName = sourceEntry.getKey();
if(!archiveFiles.contains(sourceFileName))
{
- LOG.info("Syncing file [" + sourceFileName + "] to [" + archiveTable + "] and [" + processingTable + "]");
+ LOG.info("Syncing file [" + sourceFileName + "] to [" + archiveTable.getName() + "] and [" + processingTable.getName() + "]");
InputStream inputStream = sourceActionBase.readFile(sourceEntry.getValue());
byte[] bytes = inputStream.readAllBytes();