Merge branch 'feature/query-stats' into feature/CTLE-507-custom-ct-live-packing-slips

This commit is contained in:
2023-06-26 12:09:52 -05:00
43 changed files with 3563 additions and 68 deletions

View File

@ -10,7 +10,7 @@ The bundle contains all of the sub-jars. It is named:
```qqq-${version}.jar``` ```qqq-${version}.jar```
You can also use fine grained jars: You can also use fine-grained jars:
- `qqq-backend-core`: The core module. Useful if you're developing other modules. - `qqq-backend-core`: The core module. Useful if you're developing other modules.
- `qqq-backend-module-rdbms`: Backend module for working with Relational Databases. - `qqq-backend-module-rdbms`: Backend module for working with Relational Databases.
- `qqq-backend-module-filesystem`: Backend module for working with Filesystems (including AWS S3). - `qqq-backend-module-filesystem`: Backend module for working with Filesystems (including AWS S3).
@ -35,4 +35,3 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License 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/>. along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@ -31,7 +31,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOu
** Interface for the Aggregate action. ** Interface for the Aggregate action.
** **
*******************************************************************************/ *******************************************************************************/
public interface AggregateInterface public interface AggregateInterface extends BaseQueryInterface
{ {
/******************************************************************************* /*******************************************************************************
** **

View File

@ -0,0 +1,70 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.interfaces;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
/*******************************************************************************
** Base class for "query" (e.g., read-operations) action interfaces (query, count, aggregate).
** Initially just here for the QueryStat methods - if we expand those to apply
** to insert/update/delete, well, then rename this maybe to BaseActionInterface?
*******************************************************************************/
public interface BaseQueryInterface
{
/*******************************************************************************
**
*******************************************************************************/
default void setQueryStat(QueryStat queryStat)
{
//////////
// noop //
//////////
}
/*******************************************************************************
**
*******************************************************************************/
default QueryStat getQueryStat()
{
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
default void setQueryStatFirstResultTime()
{
QueryStat queryStat = getQueryStat();
if(queryStat != null)
{
if(queryStat.getFirstResultTimestamp() == null)
{
queryStat.setFirstResultTimestamp(Instant.now());
}
}
}
}

View File

@ -31,7 +31,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
** Interface for the Count action. ** Interface for the Count action.
** **
*******************************************************************************/ *******************************************************************************/
public interface CountInterface public interface CountInterface extends BaseQueryInterface
{ {
/******************************************************************************* /*******************************************************************************
** **

View File

@ -31,10 +31,11 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
** Interface for the Query action. ** Interface for the Query action.
** **
*******************************************************************************/ *******************************************************************************/
public interface QueryInterface public interface QueryInterface extends BaseQueryInterface
{ {
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
QueryOutput execute(QueryInput queryInput) throws QException; QueryOutput execute(QueryInput queryInput) throws QException;
} }

View File

@ -23,9 +23,14 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -43,11 +48,20 @@ public class AggregateAction
{ {
ActionHelper.validateSession(aggregateInput); ActionHelper.validateSession(aggregateInput);
QTableMetaData table = aggregateInput.getTable();
QBackendMetaData backend = aggregateInput.getBackend();
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, aggregateInput.getFilter());
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(aggregateInput.getBackend()); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(aggregateInput.getBackend());
// todo pre-customization - just get to modify the request?
AggregateOutput aggregateOutput = qModule.getAggregateInterface().execute(aggregateInput); AggregateInterface aggregateInterface = qModule.getAggregateInterface();
// todo post-customization - can do whatever w/ the result if you want aggregateInterface.setQueryStat(queryStat);
AggregateOutput aggregateOutput = aggregateInterface.execute(aggregateInput);
QueryStatManager.getInstance().add(queryStat);
return aggregateOutput; return aggregateOutput;
} }
} }

View File

@ -23,9 +23,14 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -43,11 +48,20 @@ public class CountAction
{ {
ActionHelper.validateSession(countInput); ActionHelper.validateSession(countInput);
QTableMetaData table = countInput.getTable();
QBackendMetaData backend = countInput.getBackend();
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, countInput.getFilter());
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(countInput.getBackend()); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(countInput.getBackend());
// todo pre-customization - just get to modify the request?
CountOutput countOutput = qModule.getCountInterface().execute(countInput); CountInterface countInterface = qModule.getCountInterface();
// todo post-customization - can do whatever w/ the result if you want countInterface.setQueryStat(queryStat);
CountOutput countOutput = countInterface.execute(countInput);
QueryStatManager.getInstance().add(queryStat);
return countOutput; return countOutput;
} }
} }

View File

@ -34,8 +34,10 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe; import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipeBufferedWrapper; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipeBufferedWrapper;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
@ -47,12 +49,14 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; 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.fields.AdornmentType; import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; 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.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -87,12 +91,14 @@ public class QueryAction
throw (new QException("Table name was not specified in query input")); throw (new QException("Table name was not specified in query input"));
} }
if(queryInput.getTable() == null) QTableMetaData table = queryInput.getTable();
if(table == null)
{ {
throw (new QException("A table named [" + queryInput.getTableName() + "] was not found in the active QInstance")); throw (new QException("A table named [" + queryInput.getTableName() + "] was not found in the active QInstance"));
} }
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole()); QBackendMetaData backend = queryInput.getBackend();
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
this.queryInput = queryInput; this.queryInput = queryInput;
if(queryInput.getRecordPipe() != null) if(queryInput.getRecordPipe() != null)
@ -109,11 +115,16 @@ public class QueryAction
} }
} }
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, queryInput.getFilter());
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
// todo pre-customization - just get to modify the request?
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput); QueryInterface queryInterface = qModule.getQueryInterface();
// todo post-customization - can do whatever w/ the result if you want queryInterface.setQueryStat(queryStat);
QueryOutput queryOutput = queryInterface.execute(queryInput);
QueryStatManager.getInstance().add(queryStat);
if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe) if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe)
{ {

View File

@ -0,0 +1,640 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
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.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatCriteriaField;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatJoinTable;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatOrderByField;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Singleton, which starts a thread, to store query stats into a table.
**
** Supports these systemProperties or ENV_VARS:
** qqq.queryStatManager.enabled / QQQ_QUERY_STAT_MANAGER_ENABLED
** qqq.queryStatManager.minMillisToStore / QQQ_QUERY_STAT_MANAGER_MIN_MILLIS_TO_STORE
** qqq.queryStatManager.jobPeriodSeconds / QQQ_QUERY_STAT_MANAGER_JOB_PERIOD_SECONDS
** qqq.queryStatManager.jobInitialDelay / QQQ_QUERY_STAT_MANAGER_JOB_INITIAL_DELAY
*******************************************************************************/
public class QueryStatManager
{
private static final QLogger LOG = QLogger.getLogger(QueryStatManager.class);
private static QueryStatManager queryStatManager = null;
// todo - support multiple qInstances?
private QInstance qInstance;
private Supplier<QSession> sessionSupplier;
private boolean active = false;
private List<QueryStat> queryStats = new ArrayList<>();
private ScheduledExecutorService executorService;
private int jobPeriodSeconds = 60;
private int jobInitialDelay = 60;
private int minMillisToStore = 0;
/*******************************************************************************
** Singleton constructor
*******************************************************************************/
private QueryStatManager()
{
}
/*******************************************************************************
** Singleton accessor
*******************************************************************************/
public static QueryStatManager getInstance()
{
if(queryStatManager == null)
{
queryStatManager = new QueryStatManager();
QMetaDataVariableInterpreter interpreter = new QMetaDataVariableInterpreter();
Integer propertyMinMillisToStore = interpreter.getIntegerFromPropertyOrEnvironment("qqq.queryStatManager.minMillisToStore", "QQQ_QUERY_STAT_MANAGER_MIN_MILLIS_TO_STORE", null);
if(propertyMinMillisToStore != null)
{
queryStatManager.setMinMillisToStore(propertyMinMillisToStore);
}
Integer propertyJobPeriodSeconds = interpreter.getIntegerFromPropertyOrEnvironment("qqq.queryStatManager.jobPeriodSeconds", "QQQ_QUERY_STAT_MANAGER_JOB_PERIOD_SECONDS", null);
if(propertyJobPeriodSeconds != null)
{
queryStatManager.setJobPeriodSeconds(propertyJobPeriodSeconds);
}
Integer propertyJobInitialDelay = interpreter.getIntegerFromPropertyOrEnvironment("qqq.queryStatManager.jobInitialDelay", "QQQ_QUERY_STAT_MANAGER_JOB_INITIAL_DELAY", null);
if(propertyJobInitialDelay != null)
{
queryStatManager.setJobInitialDelay(propertyJobInitialDelay);
}
}
return (queryStatManager);
}
/*******************************************************************************
**
*******************************************************************************/
public static QueryStat newQueryStat(QBackendMetaData backend, QTableMetaData table, QQueryFilter filter)
{
QueryStat queryStat = null;
if(table.isCapabilityEnabled(backend, Capability.QUERY_STATS))
{
queryStat = new QueryStat();
queryStat.setTableName(table.getName());
queryStat.setQueryFilter(Objects.requireNonNullElse(filter, new QQueryFilter()));
queryStat.setStartTimestamp(Instant.now());
}
return (queryStat);
}
/*******************************************************************************
**
*******************************************************************************/
public void start(QInstance qInstance, Supplier<QSession> sessionSupplier)
{
if(!isEnabled())
{
LOG.info("Not starting QueryStatManager per settings.");
return;
}
LOG.info("Starting QueryStatManager");
this.qInstance = qInstance;
this.sessionSupplier = sessionSupplier;
active = true;
queryStats = new ArrayList<>();
executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleAtFixedRate(new QueryStatManagerInsertJob(), jobInitialDelay, jobPeriodSeconds, TimeUnit.SECONDS);
}
/*******************************************************************************
**
*******************************************************************************/
private static boolean isEnabled()
{
return new QMetaDataVariableInterpreter().getBooleanFromPropertyOrEnvironment("qqq.queryStatManager.enabled", "QQQ_QUERY_STAT_MANAGER_ENABLED", true);
}
/*******************************************************************************
**
*******************************************************************************/
public void stop()
{
active = false;
queryStats.clear();
if(executorService != null)
{
executorService.shutdown();
executorService = null;
}
}
/*******************************************************************************
**
*******************************************************************************/
public void add(QueryStat queryStat)
{
if(queryStat == null)
{
return;
}
if(active)
{
////////////////////////////////////////////////////////////////////////////////////////
// set fields that we need to capture now (rather than when the thread to store runs) //
////////////////////////////////////////////////////////////////////////////////////////
if(queryStat.getFirstResultTimestamp() == null)
{
queryStat.setFirstResultTimestamp(Instant.now());
}
if(queryStat.getStartTimestamp() != null && queryStat.getFirstResultTimestamp() != null && queryStat.getFirstResultMillis() == null)
{
long millis = queryStat.getFirstResultTimestamp().toEpochMilli() - queryStat.getStartTimestamp().toEpochMilli();
queryStat.setFirstResultMillis((int) millis);
}
if(queryStat.getFirstResultMillis() != null && queryStat.getFirstResultMillis() < minMillisToStore)
{
//////////////////////////////////////////////////////////////
// discard this record if it's under the min millis setting //
//////////////////////////////////////////////////////////////
return;
}
if(queryStat.getSessionId() == null && QContext.getQSession() != null)
{
queryStat.setSessionId(QContext.getQSession().getUuid());
}
if(queryStat.getAction() == null)
{
if(!QContext.getActionStack().isEmpty())
{
queryStat.setAction(QContext.getActionStack().peek().getActionIdentity());
}
else
{
boolean expected = false;
Exception e = new Exception("Unexpected empty action stack");
for(StackTraceElement stackTraceElement : e.getStackTrace())
{
String className = stackTraceElement.getClassName();
if(className.contains(QueryStatManagerInsertJob.class.getName()))
{
expected = true;
break;
}
}
if(!expected)
{
LOG.debug(e);
}
}
}
synchronized(this)
{
queryStats.add(queryStat);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private List<QueryStat> getListAndReset()
{
if(queryStats.isEmpty())
{
return Collections.emptyList();
}
synchronized(this)
{
List<QueryStat> returnList = queryStats;
queryStats = new ArrayList<>();
return (returnList);
}
}
/*******************************************************************************
** force stats to be stored right now (rather than letting the scheduled job do it)
*******************************************************************************/
public void storeStatsNow()
{
new QueryStatManagerInsertJob().run();
}
/*******************************************************************************
** Runnable that gets scheduled to periodically reset and store the list of collected stats
*******************************************************************************/
private static class QueryStatManagerInsertJob implements Runnable
{
private static final QLogger LOG = QLogger.getLogger(QueryStatManagerInsertJob.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run()
{
try
{
QContext.init(getInstance().qInstance, getInstance().sessionSupplier.get());
/////////////////////////////////////////////////////////////////////////////////////
// every time we re-run, check if we've been turned off - if so, stop the service. //
/////////////////////////////////////////////////////////////////////////////////////
if(!isEnabled())
{
LOG.info("Stopping QueryStatManager.");
getInstance().stop();
return;
}
List<QueryStat> list = getInstance().getListAndReset();
LOG.info(logPair("queryStatListSize", list.size()));
if(list.isEmpty())
{
return;
}
////////////////////////////////////
// prime the entities for storing //
////////////////////////////////////
List<QRecord> queryStatQRecordsToInsert = new ArrayList<>();
for(QueryStat queryStat : list)
{
try
{
//////////////////////
// set the table id //
//////////////////////
Integer qqqTableId = getQQQTableId(queryStat.getTableName());
queryStat.setQqqTableId(qqqTableId);
//////////////////////////////
// build join-table records //
//////////////////////////////
if(CollectionUtils.nullSafeHasContents(queryStat.getJoinTableNames()))
{
List<QueryStatJoinTable> queryStatJoinTableList = new ArrayList<>();
for(String joinTableName : queryStat.getJoinTableNames())
{
queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(getQQQTableId(joinTableName)));
}
queryStat.setQueryStatJoinTableList(queryStatJoinTableList);
}
////////////////////////////
// build criteria records //
////////////////////////////
if(queryStat.getQueryFilter() != null && queryStat.getQueryFilter().hasAnyCriteria())
{
List<QueryStatCriteriaField> queryStatCriteriaFieldList = new ArrayList<>();
processCriteriaFromFilter(qqqTableId, queryStatCriteriaFieldList, queryStat.getQueryFilter());
queryStat.setQueryStatCriteriaFieldList(queryStatCriteriaFieldList);
}
if(CollectionUtils.nullSafeHasContents(queryStat.getQueryFilter().getOrderBys()))
{
List<QueryStatOrderByField> queryStatOrderByFieldList = new ArrayList<>();
processOrderByFromFilter(qqqTableId, queryStatOrderByFieldList, queryStat.getQueryFilter());
queryStat.setQueryStatOrderByFieldList(queryStatOrderByFieldList);
}
queryStatQRecordsToInsert.add(queryStat.toQRecord());
}
catch(Exception e)
{
//////////////////////
// skip this record //
//////////////////////
LOG.warn("Error priming a query stat for storing", e);
}
}
try
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(QueryStat.TABLE_NAME);
insertInput.setRecords(queryStatQRecordsToInsert);
new InsertAction().execute(insertInput);
}
catch(Exception e)
{
LOG.error("Error inserting query stats", e);
}
}
catch(Exception e)
{
LOG.warn("Error storing query stats", e);
}
finally
{
QContext.clear();
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void processCriteriaFromFilter(Integer qqqTableId, List<QueryStatCriteriaField> queryStatCriteriaFieldList, QQueryFilter queryFilter) throws QException
{
for(QFilterCriteria criteria : CollectionUtils.nonNullList(queryFilter.getCriteria()))
{
String fieldName = criteria.getFieldName();
QueryStatCriteriaField queryStatCriteriaField = new QueryStatCriteriaField();
queryStatCriteriaField.setOperator(String.valueOf(criteria.getOperator()));
if(criteria.getValues() != null)
{
queryStatCriteriaField.setValues(StringUtils.join(",", criteria.getValues()));
}
if(fieldName.contains("."))
{
String[] parts = fieldName.split("\\.");
if(parts.length > 1)
{
queryStatCriteriaField.setQqqTableId(getQQQTableId(parts[0]));
queryStatCriteriaField.setName(parts[1]);
}
}
else
{
queryStatCriteriaField.setQqqTableId(qqqTableId);
queryStatCriteriaField.setName(fieldName);
}
queryStatCriteriaFieldList.add(queryStatCriteriaField);
}
for(QQueryFilter subFilter : CollectionUtils.nonNullList(queryFilter.getSubFilters()))
{
processCriteriaFromFilter(qqqTableId, queryStatCriteriaFieldList, subFilter);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void processOrderByFromFilter(Integer qqqTableId, List<QueryStatOrderByField> queryStatOrderByFieldList, QQueryFilter queryFilter) throws QException
{
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys()))
{
String fieldName = orderBy.getFieldName();
QueryStatOrderByField queryStatOrderByField = new QueryStatOrderByField();
if(fieldName != null)
{
if(fieldName.contains("."))
{
String[] parts = fieldName.split("\\.");
if(parts.length > 1)
{
queryStatOrderByField.setQqqTableId(getQQQTableId(parts[0]));
queryStatOrderByField.setName(parts[1]);
}
}
else
{
queryStatOrderByField.setQqqTableId(qqqTableId);
queryStatOrderByField.setName(fieldName);
}
queryStatOrderByFieldList.add(queryStatOrderByField);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static Integer getQQQTableId(String tableName) throws QException
{
/////////////////////////////
// look in the cache table //
/////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME);
getInput.setUniqueKey(MapBuilder.of("name", tableName));
GetOutput getOutput = new GetAction().execute(getInput);
////////////////////////
// upon cache miss... //
////////////////////////
if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////
// insert the record (into the table, not the cache) //
///////////////////////////////////////////////////////
QTableMetaData tableMetaData = getInstance().qInstance.getTable(tableName);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(QQQTable.TABLE_NAME);
insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel())));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
///////////////////////////////////
// repeat the get from the cache //
///////////////////////////////////
getOutput = new GetAction().execute(getInput);
}
return getOutput.getRecord().getValueInteger("id");
}
}
/*******************************************************************************
** Getter for jobPeriodSeconds
*******************************************************************************/
public int getJobPeriodSeconds()
{
return (this.jobPeriodSeconds);
}
/*******************************************************************************
** Setter for jobPeriodSeconds
*******************************************************************************/
public void setJobPeriodSeconds(int jobPeriodSeconds)
{
this.jobPeriodSeconds = jobPeriodSeconds;
}
/*******************************************************************************
** Fluent setter for jobPeriodSeconds
*******************************************************************************/
public QueryStatManager withJobPeriodSeconds(int jobPeriodSeconds)
{
this.jobPeriodSeconds = jobPeriodSeconds;
return (this);
}
/*******************************************************************************
** Getter for jobInitialDelay
*******************************************************************************/
public int getJobInitialDelay()
{
return (this.jobInitialDelay);
}
/*******************************************************************************
** Setter for jobInitialDelay
*******************************************************************************/
public void setJobInitialDelay(int jobInitialDelay)
{
this.jobInitialDelay = jobInitialDelay;
}
/*******************************************************************************
** Fluent setter for jobInitialDelay
*******************************************************************************/
public QueryStatManager withJobInitialDelay(int jobInitialDelay)
{
this.jobInitialDelay = jobInitialDelay;
return (this);
}
/*******************************************************************************
** Getter for minMillisToStore
*******************************************************************************/
public int getMinMillisToStore()
{
return (this.minMillisToStore);
}
/*******************************************************************************
** Setter for minMillisToStore
*******************************************************************************/
public void setMinMillisToStore(int minMillisToStore)
{
this.minMillisToStore = minMillisToStore;
}
/*******************************************************************************
** Fluent setter for minMillisToStore
*******************************************************************************/
public QueryStatManager withMinMillisToStore(int minMillisToStore)
{
this.minMillisToStore = minMillisToStore;
return (this);
}
}

View File

@ -30,6 +30,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.Dotenv;
import io.github.cdimascio.dotenv.DotenvEntry; import io.github.cdimascio.dotenv.DotenvEntry;
@ -266,4 +267,111 @@ public class QMetaDataVariableInterpreter
valueMaps.put(name, values); valueMaps.put(name, values);
} }
/*******************************************************************************
** First look for a boolean ("true" or "false") in the specified system property -
** Next look for a boolean in the specified env var name -
** Finally return the default.
*******************************************************************************/
public boolean getBooleanFromPropertyOrEnvironment(String systemPropertyName, String environmentVariableName, boolean defaultIfNotSet)
{
String propertyValue = System.getProperty(systemPropertyName);
if(StringUtils.hasContent(propertyValue))
{
if("false".equalsIgnoreCase(propertyValue))
{
LOG.info("Read system property [" + systemPropertyName + "] as boolean false.");
return (false);
}
else if("true".equalsIgnoreCase(propertyValue))
{
LOG.info("Read system property [" + systemPropertyName + "] as boolean true.");
return (true);
}
else
{
LOG.warn("Unrecognized boolean value [" + propertyValue + "] for system property [" + systemPropertyName + "].");
}
}
String envValue = interpret("${env." + environmentVariableName + "}");
if(StringUtils.hasContent(envValue))
{
if("false".equalsIgnoreCase(envValue))
{
LOG.info("Read env var [" + environmentVariableName + "] as boolean false.");
return (false);
}
else if("true".equalsIgnoreCase(envValue))
{
LOG.info("Read env var [" + environmentVariableName + "] as boolean true.");
return (true);
}
else
{
LOG.warn("Unrecognized boolean value [" + envValue + "] for env var [" + environmentVariableName + "].");
}
}
return defaultIfNotSet;
}
/*******************************************************************************
** First look for an Integer in the specified system property -
** Next look for an Integer in the specified env var name -
** Finally return the default (null allowed as default!)
*******************************************************************************/
public Integer getIntegerFromPropertyOrEnvironment(String systemPropertyName, String environmentVariableName, Integer defaultIfNotSet)
{
String propertyValue = System.getProperty(systemPropertyName);
if(StringUtils.hasContent(propertyValue))
{
if(canParseAsInteger(propertyValue))
{
LOG.info("Read system property [" + systemPropertyName + "] as integer " + propertyValue);
return (Integer.parseInt(propertyValue));
}
else
{
LOG.warn("Unrecognized integer value [" + propertyValue + "] for system property [" + systemPropertyName + "].");
}
}
String envValue = interpret("${env." + environmentVariableName + "}");
if(StringUtils.hasContent(envValue))
{
if(canParseAsInteger(envValue))
{
LOG.info("Read env var [" + environmentVariableName + "] as integer " + environmentVariableName);
return (Integer.parseInt(propertyValue));
}
else
{
LOG.warn("Unrecognized integer value [" + envValue + "] for env var [" + environmentVariableName + "].");
}
}
return defaultIfNotSet;
}
/*******************************************************************************
** we'd use NumberUtils.isDigits, but that doesn't allow negatives, or
** numberUtils.isParseable, but that allows decimals, so...
*******************************************************************************/
private boolean canParseAsInteger(String value)
{
if(value == null)
{
return (false);
}
return (value.matches("^-?[0-9]+$"));
}
} }

View File

@ -56,6 +56,16 @@ public class AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
public String getActionIdentity()
{
return (getClass().getSimpleName());
}
/******************************************************************************* /*******************************************************************************
** performance instance validation (if not previously done). ** performance instance validation (if not previously done).
* // todo - verify this is happening (e.g., when context is set i guess) * // todo - verify this is happening (e.g., when context is set i guess)

View File

@ -46,6 +46,17 @@ public class AbstractTableActionInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getActionIdentity()
{
return (getClass().getSimpleName() + ":" + getTableName());
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -76,6 +76,17 @@ public class RunProcessInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getActionIdentity()
{
return (getClass().getSimpleName() + ":" + getProcessName());
}
/******************************************************************************* /*******************************************************************************
** e.g., for steps after the first step in a process, seed the data in a run ** e.g., for steps after the first step in a process, seed the data in a run
** function request from a process state. ** function request from a process state.

View File

@ -50,6 +50,17 @@ public class RenderWidgetInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getActionIdentity()
{
return (getClass().getSimpleName() + ":" + widgetMetaData.getName());
}
/******************************************************************************* /*******************************************************************************
** Getter for widgetMetaData ** Getter for widgetMetaData
** **

View File

@ -0,0 +1,45 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*******************************************************************************
** Annotation to place onto fields in a QRecordEntity, to mark them as associated
** record lists
**
*******************************************************************************/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface QAssociation
{
/*******************************************************************************
**
*******************************************************************************/
String name();
}

View File

@ -23,8 +23,12 @@ package com.kingsrook.qqq.backend.core.model.data;
import java.io.Serializable; import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedParameterizedType;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
@ -40,7 +44,9 @@ import java.util.Optional;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException; import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash; import com.kingsrook.qqq.backend.core.utils.ListingHash;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/******************************************************************************* /*******************************************************************************
@ -50,7 +56,8 @@ public abstract class QRecordEntity
{ {
private static final QLogger LOG = QLogger.getLogger(QRecordEntity.class); private static final QLogger LOG = QLogger.getLogger(QRecordEntity.class);
private static final ListingHash<Class<? extends QRecordEntity>, QRecordEntityField> fieldMapping = new ListingHash<>(); private static final ListingHash<Class<? extends QRecordEntity>, QRecordEntityField> fieldMapping = new ListingHash<>();
private static final ListingHash<Class<? extends QRecordEntity>, QRecordEntityAssociation> associationMapping = new ListingHash<>();
private Map<String, Serializable> originalRecordValues; private Map<String, Serializable> originalRecordValues;
@ -92,7 +99,7 @@ public abstract class QRecordEntity
** Build an entity of this QRecord type from a QRecord ** Build an entity of this QRecord type from a QRecord
** **
*******************************************************************************/ *******************************************************************************/
protected <T extends QRecordEntity> void populateFromQRecord(QRecord qRecord) throws QRuntimeException protected void populateFromQRecord(QRecord qRecord) throws QRuntimeException
{ {
populateFromQRecord(qRecord, ""); populateFromQRecord(qRecord, "");
} }
@ -123,6 +130,42 @@ public abstract class QRecordEntity
qRecordEntityField.getSetter().invoke(this, typedValue); qRecordEntityField.getSetter().invoke(this, typedValue);
originalRecordValues.put(qRecordEntityField.getFieldName(), value); originalRecordValues.put(qRecordEntityField.getFieldName(), value);
} }
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
List<QRecord> associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name());
if(associatedRecords == null)
{
qRecordEntityAssociation.getSetter().invoke(this, (Object) null);
}
else
{
List<QRecordEntity> associatedEntityList = new ArrayList<>();
for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords))
{
associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord));
}
qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList);
}
}
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
List<QRecord> associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name());
if(associatedRecords == null)
{
qRecordEntityAssociation.getSetter().invoke(this, (Object) null);
}
else
{
List<QRecordEntity> associatedEntityList = new ArrayList<>();
for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords))
{
associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord));
}
qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList);
}
}
} }
catch(Exception e) catch(Exception e)
{ {
@ -142,12 +185,30 @@ public abstract class QRecordEntity
{ {
QRecord qRecord = new QRecord(); QRecord qRecord = new QRecord();
List<QRecordEntityField> fieldList = getFieldList(this.getClass()); for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
for(QRecordEntityField qRecordEntityField : fieldList)
{ {
qRecord.setValue(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this)); qRecord.setValue(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this));
} }
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
if(associatedEntities != null)
{
/////////////////////////////////////////////////////////////////////////////////
// do this so an empty list in the entity becomes an empty list in the QRecord //
/////////////////////////////////////////////////////////////////////////////////
qRecord.withAssociatedRecords(associationName, new ArrayList<>());
}
for(QRecordEntity associatedEntity : CollectionUtils.nonNullList(associatedEntities))
{
qRecord.withAssociatedRecord(associationName, associatedEntity.toQRecord());
}
}
return (qRecord); return (qRecord);
} }
catch(Exception e) catch(Exception e)
@ -157,7 +218,6 @@ public abstract class QRecordEntity
} }
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -167,8 +227,7 @@ public abstract class QRecordEntity
{ {
QRecord qRecord = new QRecord(); QRecord qRecord = new QRecord();
List<QRecordEntityField> fieldList = getFieldList(this.getClass()); for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
for(QRecordEntityField qRecordEntityField : fieldList)
{ {
Serializable thisValue = (Serializable) qRecordEntityField.getGetter().invoke(this); Serializable thisValue = (Serializable) qRecordEntityField.getGetter().invoke(this);
Serializable originalValue = null; Serializable originalValue = null;
@ -183,6 +242,25 @@ public abstract class QRecordEntity
} }
} }
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
if(associatedEntities != null)
{
/////////////////////////////////////////////////////////////////////////////////
// do this so an empty list in the entity becomes an empty list in the QRecord //
/////////////////////////////////////////////////////////////////////////////////
qRecord.withAssociatedRecords(associationName, new ArrayList<>());
}
for(QRecordEntity associatedEntity : CollectionUtils.nonNullList(associatedEntities))
{
qRecord.withAssociatedRecord(associationName, associatedEntity.toQRecord());
}
}
return (qRecord); return (qRecord);
} }
catch(Exception e) catch(Exception e)
@ -211,7 +289,15 @@ public abstract class QRecordEntity
{ {
String fieldName = getFieldNameFromGetter(possibleGetter); String fieldName = getFieldNameFromGetter(possibleGetter);
Optional<QField> fieldAnnotation = getQFieldAnnotation(c, fieldName); Optional<QField> fieldAnnotation = getQFieldAnnotation(c, fieldName);
fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), fieldAnnotation.orElse(null)));
if(fieldAnnotation.isPresent())
{
fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), fieldAnnotation.orElse(null)));
}
else
{
LOG.debug("Skipping field without @QField annotation", logPair("class", c.getSimpleName()), logPair("fieldName", fieldName));
}
} }
else else
{ {
@ -226,15 +312,73 @@ public abstract class QRecordEntity
/*******************************************************************************
**
*******************************************************************************/
public static List<QRecordEntityAssociation> getAssociationList(Class<? extends QRecordEntity> c)
{
if(!associationMapping.containsKey(c))
{
List<QRecordEntityAssociation> associationList = new ArrayList<>();
for(Method possibleGetter : c.getMethods())
{
if(isGetter(possibleGetter))
{
Optional<Method> setter = getSetterForGetter(c, possibleGetter);
if(setter.isPresent())
{
String fieldName = getFieldNameFromGetter(possibleGetter);
Optional<QAssociation> associationAnnotation = getQAssociationAnnotation(c, fieldName);
if(associationAnnotation.isPresent())
{
Class<? extends QRecordEntity> listTypeParam = (Class<? extends QRecordEntity>) getListTypeParam(possibleGetter.getReturnType(), possibleGetter.getAnnotatedReturnType());
associationList.add(new QRecordEntityAssociation(fieldName, possibleGetter, setter.get(), listTypeParam, associationAnnotation.orElse(null)));
}
}
else
{
LOG.info("Getter method [" + possibleGetter.getName() + "] does not have a corresponding setter.");
}
}
}
associationMapping.put(c, associationList);
}
return (associationMapping.get(c));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public static Optional<QField> getQFieldAnnotation(Class<? extends QRecordEntity> c, String fieldName) public static Optional<QField> getQFieldAnnotation(Class<? extends QRecordEntity> c, String fieldName)
{
return (getAnnotationOnField(c, QField.class, fieldName));
}
/*******************************************************************************
**
*******************************************************************************/
public static Optional<QAssociation> getQAssociationAnnotation(Class<? extends QRecordEntity> c, String fieldName)
{
return (getAnnotationOnField(c, QAssociation.class, fieldName));
}
/*******************************************************************************
**
*******************************************************************************/
public static <A extends Annotation> Optional<A> getAnnotationOnField(Class<? extends QRecordEntity> c, Class<A> annotationClass, String fieldName)
{ {
try try
{ {
Field field = c.getDeclaredField(fieldName); Field field = c.getDeclaredField(fieldName);
return (Optional.ofNullable(field.getAnnotation(QField.class))); return (Optional.ofNullable(field.getAnnotation(annotationClass)));
} }
catch(NoSuchFieldException e) catch(NoSuchFieldException e)
{ {
@ -269,7 +413,7 @@ public abstract class QRecordEntity
{ {
if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*")) if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*"))
{ {
if(isSupportedFieldType(method.getReturnType())) if(isSupportedFieldType(method.getReturnType()) || isSupportedAssociation(method.getReturnType(), method.getAnnotatedReturnType()))
{ {
return (true); return (true);
} }
@ -334,4 +478,41 @@ public abstract class QRecordEntity
///////////////////////////////////////////// /////////////////////////////////////////////
} }
/*******************************************************************************
**
*******************************************************************************/
private static boolean isSupportedAssociation(Class<?> returnType, AnnotatedType annotatedType)
{
Class<?> listTypeParam = getListTypeParam(returnType, annotatedType);
return (listTypeParam != null && QRecordEntity.class.isAssignableFrom(listTypeParam));
}
/*******************************************************************************
**
*******************************************************************************/
private static Class<?> getListTypeParam(Class<?> listType, AnnotatedType annotatedType)
{
if(listType.equals(List.class))
{
if(annotatedType instanceof AnnotatedParameterizedType apt)
{
AnnotatedType[] annotatedActualTypeArguments = apt.getAnnotatedActualTypeArguments();
for(AnnotatedType annotatedActualTypeArgument : annotatedActualTypeArguments)
{
Type type = annotatedActualTypeArgument.getType();
if(type instanceof Class<?> c)
{
return (c);
}
}
}
}
return (null);
}
} }

View File

@ -0,0 +1,110 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data;
import java.lang.reflect.Method;
/*******************************************************************************
** Reflective information about an association in a QRecordEntity
*******************************************************************************/
public class QRecordEntityAssociation
{
private final String fieldName;
private final Method getter;
private final Method setter;
private final Class<? extends QRecordEntity> associatedType;
private final QAssociation associationAnnotation;
/*******************************************************************************
** Constructor.
*******************************************************************************/
public QRecordEntityAssociation(String fieldName, Method getter, Method setter, Class<? extends QRecordEntity> associatedType, QAssociation associationAnnotation)
{
this.fieldName = fieldName;
this.getter = getter;
this.setter = setter;
this.associatedType = associatedType;
this.associationAnnotation = associationAnnotation;
}
/*******************************************************************************
** Getter for fieldName
**
*******************************************************************************/
public String getFieldName()
{
return fieldName;
}
/*******************************************************************************
** Getter for getter
**
*******************************************************************************/
public Method getGetter()
{
return getter;
}
/*******************************************************************************
** Getter for setter
**
*******************************************************************************/
public Method getSetter()
{
return setter;
}
/*******************************************************************************
** Getter for associatedType
**
*******************************************************************************/
public Class<? extends QRecordEntity> getAssociatedType()
{
return associatedType;
}
/*******************************************************************************
** Getter for associationAnnotation
**
*******************************************************************************/
public QAssociation getAssociationAnnotation()
{
return associationAnnotation;
}
}

View File

@ -22,7 +22,6 @@
package com.kingsrook.qqq.backend.core.model.metadata; package com.kingsrook.qqq.backend.core.model.metadata;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@ -59,6 +58,10 @@ public class QBackendMetaData
*******************************************************************************/ *******************************************************************************/
public QBackendMetaData() public QBackendMetaData()
{ {
/////////////////////////////////////////////////////////////////////////////
// by default, we will turn off the query stats capability on all backends //
/////////////////////////////////////////////////////////////////////////////
withoutCapability(Capability.QUERY_STATS);
} }
@ -199,6 +202,10 @@ public class QBackendMetaData
public void setEnabledCapabilities(Set<Capability> enabledCapabilities) public void setEnabledCapabilities(Set<Capability> enabledCapabilities)
{ {
this.enabledCapabilities = enabledCapabilities; this.enabledCapabilities = enabledCapabilities;
if(this.disabledCapabilities != null)
{
this.disabledCapabilities.removeAll(enabledCapabilities);
}
} }
@ -209,7 +216,7 @@ public class QBackendMetaData
*******************************************************************************/ *******************************************************************************/
public QBackendMetaData withEnabledCapabilities(Set<Capability> enabledCapabilities) public QBackendMetaData withEnabledCapabilities(Set<Capability> enabledCapabilities)
{ {
this.enabledCapabilities = enabledCapabilities; setEnabledCapabilities(enabledCapabilities);
return (this); return (this);
} }
@ -221,7 +228,10 @@ public class QBackendMetaData
*******************************************************************************/ *******************************************************************************/
public QBackendMetaData withCapabilities(Set<Capability> enabledCapabilities) public QBackendMetaData withCapabilities(Set<Capability> enabledCapabilities)
{ {
this.enabledCapabilities = enabledCapabilities; for(Capability enabledCapability : enabledCapabilities)
{
withCapability(enabledCapability);
}
return (this); return (this);
} }
@ -238,6 +248,7 @@ public class QBackendMetaData
this.enabledCapabilities = new HashSet<>(); this.enabledCapabilities = new HashSet<>();
} }
this.enabledCapabilities.add(capability); this.enabledCapabilities.add(capability);
this.disabledCapabilities.remove(capability);
return (this); return (this);
} }
@ -249,11 +260,10 @@ public class QBackendMetaData
*******************************************************************************/ *******************************************************************************/
public QBackendMetaData withCapabilities(Capability... enabledCapabilities) public QBackendMetaData withCapabilities(Capability... enabledCapabilities)
{ {
if(this.enabledCapabilities == null) for(Capability enabledCapability : enabledCapabilities)
{ {
this.enabledCapabilities = new HashSet<>(); withCapability(enabledCapability);
} }
this.enabledCapabilities.addAll(Arrays.stream(enabledCapabilities).toList());
return (this); return (this);
} }
@ -277,6 +287,10 @@ public class QBackendMetaData
public void setDisabledCapabilities(Set<Capability> disabledCapabilities) public void setDisabledCapabilities(Set<Capability> disabledCapabilities)
{ {
this.disabledCapabilities = disabledCapabilities; this.disabledCapabilities = disabledCapabilities;
if(this.enabledCapabilities != null)
{
this.enabledCapabilities.removeAll(disabledCapabilities);
}
} }
@ -287,7 +301,7 @@ public class QBackendMetaData
*******************************************************************************/ *******************************************************************************/
public QBackendMetaData withDisabledCapabilities(Set<Capability> disabledCapabilities) public QBackendMetaData withDisabledCapabilities(Set<Capability> disabledCapabilities)
{ {
this.disabledCapabilities = disabledCapabilities; setDisabledCapabilities(disabledCapabilities);
return (this); return (this);
} }
@ -299,11 +313,10 @@ public class QBackendMetaData
*******************************************************************************/ *******************************************************************************/
public QBackendMetaData withoutCapabilities(Capability... disabledCapabilities) public QBackendMetaData withoutCapabilities(Capability... disabledCapabilities)
{ {
if(this.disabledCapabilities == null) for(Capability disabledCapability : disabledCapabilities)
{ {
this.disabledCapabilities = new HashSet<>(); withoutCapability(disabledCapability);
} }
this.disabledCapabilities.addAll(Arrays.stream(disabledCapabilities).toList());
return (this); return (this);
} }
@ -315,7 +328,10 @@ public class QBackendMetaData
*******************************************************************************/ *******************************************************************************/
public QBackendMetaData withoutCapabilities(Set<Capability> disabledCapabilities) public QBackendMetaData withoutCapabilities(Set<Capability> disabledCapabilities)
{ {
this.disabledCapabilities = disabledCapabilities; for(Capability disabledCapability : disabledCapabilities)
{
withCapability(disabledCapability);
}
return (this); return (this);
} }
@ -332,6 +348,7 @@ public class QBackendMetaData
this.disabledCapabilities = new HashSet<>(); this.disabledCapabilities = new HashSet<>();
} }
this.disabledCapabilities.add(capability); this.disabledCapabilities.add(capability);
this.enabledCapabilities.remove(capability);
return (this); return (this);
} }

View File

@ -33,9 +33,10 @@ public enum Capability
TABLE_COUNT, TABLE_COUNT,
TABLE_INSERT, TABLE_INSERT,
TABLE_UPDATE, TABLE_UPDATE,
TABLE_DELETE TABLE_DELETE,
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
// keep these values in sync with Capability.ts in qqq-frontend-core // // keep these values in sync with Capability.ts in qqq-frontend-core //
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
QUERY_STATS
} }

View File

@ -862,6 +862,10 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
public void setEnabledCapabilities(Set<Capability> enabledCapabilities) public void setEnabledCapabilities(Set<Capability> enabledCapabilities)
{ {
this.enabledCapabilities = enabledCapabilities; this.enabledCapabilities = enabledCapabilities;
if(this.disabledCapabilities != null)
{
this.disabledCapabilities.removeAll(enabledCapabilities);
}
} }
@ -872,7 +876,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
*******************************************************************************/ *******************************************************************************/
public QTableMetaData withEnabledCapabilities(Set<Capability> enabledCapabilities) public QTableMetaData withEnabledCapabilities(Set<Capability> enabledCapabilities)
{ {
this.enabledCapabilities = enabledCapabilities; setEnabledCapabilities(enabledCapabilities);
return (this); return (this);
} }
@ -884,7 +888,10 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
*******************************************************************************/ *******************************************************************************/
public QTableMetaData withCapabilities(Set<Capability> enabledCapabilities) public QTableMetaData withCapabilities(Set<Capability> enabledCapabilities)
{ {
this.enabledCapabilities = enabledCapabilities; for(Capability enabledCapability : enabledCapabilities)
{
withCapability(enabledCapability);
}
return (this); return (this);
} }
@ -901,6 +908,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
this.enabledCapabilities = new HashSet<>(); this.enabledCapabilities = new HashSet<>();
} }
this.enabledCapabilities.add(capability); this.enabledCapabilities.add(capability);
this.disabledCapabilities.remove(capability);
return (this); return (this);
} }
@ -912,11 +920,10 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
*******************************************************************************/ *******************************************************************************/
public QTableMetaData withCapabilities(Capability... enabledCapabilities) public QTableMetaData withCapabilities(Capability... enabledCapabilities)
{ {
if(this.enabledCapabilities == null) for(Capability enabledCapability : enabledCapabilities)
{ {
this.enabledCapabilities = new HashSet<>(); withCapability(enabledCapability);
} }
this.enabledCapabilities.addAll(Arrays.stream(enabledCapabilities).toList());
return (this); return (this);
} }
@ -940,6 +947,10 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
public void setDisabledCapabilities(Set<Capability> disabledCapabilities) public void setDisabledCapabilities(Set<Capability> disabledCapabilities)
{ {
this.disabledCapabilities = disabledCapabilities; this.disabledCapabilities = disabledCapabilities;
if(this.enabledCapabilities != null)
{
this.enabledCapabilities.removeAll(disabledCapabilities);
}
} }
@ -950,7 +961,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
*******************************************************************************/ *******************************************************************************/
public QTableMetaData withDisabledCapabilities(Set<Capability> disabledCapabilities) public QTableMetaData withDisabledCapabilities(Set<Capability> disabledCapabilities)
{ {
this.disabledCapabilities = disabledCapabilities; setDisabledCapabilities(disabledCapabilities);
return (this); return (this);
} }
@ -962,11 +973,10 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
*******************************************************************************/ *******************************************************************************/
public QTableMetaData withoutCapabilities(Capability... disabledCapabilities) public QTableMetaData withoutCapabilities(Capability... disabledCapabilities)
{ {
if(this.disabledCapabilities == null) for(Capability disabledCapability : disabledCapabilities)
{ {
this.disabledCapabilities = new HashSet<>(); withoutCapability(disabledCapability);
} }
this.disabledCapabilities.addAll(Arrays.stream(disabledCapabilities).toList());
return (this); return (this);
} }
@ -978,7 +988,10 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
*******************************************************************************/ *******************************************************************************/
public QTableMetaData withoutCapabilities(Set<Capability> disabledCapabilities) public QTableMetaData withoutCapabilities(Set<Capability> disabledCapabilities)
{ {
this.disabledCapabilities = disabledCapabilities; for(Capability disabledCapability : disabledCapabilities)
{
withCapability(disabledCapability);
}
return (this); return (this);
} }
@ -995,6 +1008,7 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
this.disabledCapabilities = new HashSet<>(); this.disabledCapabilities = new HashSet<>();
} }
this.disabledCapabilities.add(capability); this.disabledCapabilities.add(capability);
this.enabledCapabilities.remove(capability);
return (this); return (this);
} }

View File

@ -0,0 +1,537 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.querystats;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
/*******************************************************************************
** QRecord Entity for QueryStat table
*******************************************************************************/
public class QueryStat extends QRecordEntity
{
public static final String TABLE_NAME = "queryStat";
@QField(isEditable = false)
private Integer id;
@QField()
private Instant startTimestamp;
@QField()
private Instant firstResultTimestamp;
@QField()
private Integer firstResultMillis;
@QField(label = "Table", possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer qqqTableId;
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String action;
@QField(maxLength = 36, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String sessionId;
@QField(maxLength = 64 * 1024 - 1, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String queryText;
@QAssociation(name = "queryStatJoinTables")
private List<QueryStatJoinTable> queryStatJoinTableList;
@QAssociation(name = "queryStatCriteriaFields")
private List<QueryStatCriteriaField> queryStatCriteriaFieldList;
@QAssociation(name = "queryStatOrderByFields")
private List<QueryStatOrderByField> queryStatOrderByFieldList;
///////////////////////////////////////////////////////////
// non-persistent fields - used to help build the record //
///////////////////////////////////////////////////////////
private String tableName;
private Set<String> joinTableNames;
private QQueryFilter queryFilter;
/*******************************************************************************
** Default constructor
*******************************************************************************/
public QueryStat()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public QueryStat(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public QueryStat withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for startTimestamp
*******************************************************************************/
public Instant getStartTimestamp()
{
return (this.startTimestamp);
}
/*******************************************************************************
** Setter for startTimestamp
*******************************************************************************/
public void setStartTimestamp(Instant startTimestamp)
{
this.startTimestamp = startTimestamp;
}
/*******************************************************************************
** Fluent setter for startTimestamp
*******************************************************************************/
public QueryStat withStartTimestamp(Instant startTimestamp)
{
this.startTimestamp = startTimestamp;
return (this);
}
/*******************************************************************************
** Getter for firstResultTimestamp
*******************************************************************************/
public Instant getFirstResultTimestamp()
{
return (this.firstResultTimestamp);
}
/*******************************************************************************
** Setter for firstResultTimestamp
*******************************************************************************/
public void setFirstResultTimestamp(Instant firstResultTimestamp)
{
this.firstResultTimestamp = firstResultTimestamp;
}
/*******************************************************************************
** Fluent setter for firstResultTimestamp
*******************************************************************************/
public QueryStat withFirstResultTimestamp(Instant firstResultTimestamp)
{
this.firstResultTimestamp = firstResultTimestamp;
return (this);
}
/*******************************************************************************
** Getter for firstResultMillis
*******************************************************************************/
public Integer getFirstResultMillis()
{
return (this.firstResultMillis);
}
/*******************************************************************************
** Setter for firstResultMillis
*******************************************************************************/
public void setFirstResultMillis(Integer firstResultMillis)
{
this.firstResultMillis = firstResultMillis;
}
/*******************************************************************************
** Fluent setter for firstResultMillis
*******************************************************************************/
public QueryStat withFirstResultMillis(Integer firstResultMillis)
{
this.firstResultMillis = firstResultMillis;
return (this);
}
/*******************************************************************************
** Getter for queryText
*******************************************************************************/
public String getQueryText()
{
return (this.queryText);
}
/*******************************************************************************
** Setter for queryText
*******************************************************************************/
public void setQueryText(String queryText)
{
this.queryText = queryText;
}
/*******************************************************************************
** Fluent setter for queryText
*******************************************************************************/
public QueryStat withQueryText(String queryText)
{
this.queryText = queryText;
return (this);
}
/*******************************************************************************
** Getter for queryStatJoinTableList
*******************************************************************************/
public List<QueryStatJoinTable> getQueryStatJoinTableList()
{
return (this.queryStatJoinTableList);
}
/*******************************************************************************
** Setter for queryStatJoinTableList
*******************************************************************************/
public void setQueryStatJoinTableList(List<QueryStatJoinTable> queryStatJoinTableList)
{
this.queryStatJoinTableList = queryStatJoinTableList;
}
/*******************************************************************************
** Fluent setter for queryStatJoinTableList
*******************************************************************************/
public QueryStat withQueryStatJoinTableList(List<QueryStatJoinTable> queryStatJoinTableList)
{
this.queryStatJoinTableList = queryStatJoinTableList;
return (this);
}
/*******************************************************************************
** Getter for queryStatCriteriaFieldList
*******************************************************************************/
public List<QueryStatCriteriaField> getQueryStatCriteriaFieldList()
{
return (this.queryStatCriteriaFieldList);
}
/*******************************************************************************
** Setter for queryStatCriteriaFieldList
*******************************************************************************/
public void setQueryStatCriteriaFieldList(List<QueryStatCriteriaField> queryStatCriteriaFieldList)
{
this.queryStatCriteriaFieldList = queryStatCriteriaFieldList;
}
/*******************************************************************************
** Fluent setter for queryStatCriteriaFieldList
*******************************************************************************/
public QueryStat withQueryStatCriteriaFieldList(List<QueryStatCriteriaField> queryStatCriteriaFieldList)
{
this.queryStatCriteriaFieldList = queryStatCriteriaFieldList;
return (this);
}
/*******************************************************************************
** Getter for queryStatOrderByFieldList
*******************************************************************************/
public List<QueryStatOrderByField> getQueryStatOrderByFieldList()
{
return (this.queryStatOrderByFieldList);
}
/*******************************************************************************
** Setter for queryStatOrderByFieldList
*******************************************************************************/
public void setQueryStatOrderByFieldList(List<QueryStatOrderByField> queryStatOrderByFieldList)
{
this.queryStatOrderByFieldList = queryStatOrderByFieldList;
}
/*******************************************************************************
** Fluent setter for queryStatOrderByFieldList
*******************************************************************************/
public QueryStat withQueryStatOrderByFieldList(List<QueryStatOrderByField> queryStatOrderByFieldList)
{
this.queryStatOrderByFieldList = queryStatOrderByFieldList;
return (this);
}
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
public QueryStat withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for queryFilter
*******************************************************************************/
public QQueryFilter getQueryFilter()
{
return (this.queryFilter);
}
/*******************************************************************************
** Setter for queryFilter
*******************************************************************************/
public void setQueryFilter(QQueryFilter queryFilter)
{
this.queryFilter = queryFilter;
}
/*******************************************************************************
** Fluent setter for queryFilter
*******************************************************************************/
public QueryStat withQueryFilter(QQueryFilter queryFilter)
{
this.queryFilter = queryFilter;
return (this);
}
/*******************************************************************************
** Getter for qqqTableId
*******************************************************************************/
public Integer getQqqTableId()
{
return (this.qqqTableId);
}
/*******************************************************************************
** Setter for qqqTableId
*******************************************************************************/
public void setQqqTableId(Integer qqqTableId)
{
this.qqqTableId = qqqTableId;
}
/*******************************************************************************
** Fluent setter for qqqTableId
*******************************************************************************/
public QueryStat withQqqTableId(Integer qqqTableId)
{
this.qqqTableId = qqqTableId;
return (this);
}
/*******************************************************************************
** Getter for joinTableNames
*******************************************************************************/
public Set<String> getJoinTableNames()
{
return (this.joinTableNames);
}
/*******************************************************************************
** Setter for joinTableNames
*******************************************************************************/
public void setJoinTableNames(Set<String> joinTableNames)
{
this.joinTableNames = joinTableNames;
}
/*******************************************************************************
** Fluent setter for joinTableNames
*******************************************************************************/
public QueryStat withJoinTableNames(Set<String> joinTableNames)
{
this.joinTableNames = joinTableNames;
return (this);
}
/*******************************************************************************
** Getter for action
*******************************************************************************/
public String getAction()
{
return (this.action);
}
/*******************************************************************************
** Setter for action
*******************************************************************************/
public void setAction(String action)
{
this.action = action;
}
/*******************************************************************************
** Fluent setter for action
*******************************************************************************/
public QueryStat withAction(String action)
{
this.action = action;
return (this);
}
/*******************************************************************************
** Getter for sessionId
*******************************************************************************/
public String getSessionId()
{
return (this.sessionId);
}
/*******************************************************************************
** Setter for sessionId
*******************************************************************************/
public void setSessionId(String sessionId)
{
this.sessionId = sessionId;
}
/*******************************************************************************
** Fluent setter for sessionId
*******************************************************************************/
public QueryStat withSessionId(String sessionId)
{
this.sessionId = sessionId;
return (this);
}
}

View File

@ -0,0 +1,262 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.querystats;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
/*******************************************************************************
** QRecord Entity for QueryStatCriteriaField table
*******************************************************************************/
public class QueryStatCriteriaField extends QRecordEntity
{
public static final String TABLE_NAME = "queryStatCriteriaField";
@QField(isEditable = false)
private Integer id;
@QField(possibleValueSourceName = QueryStat.TABLE_NAME)
private Integer queryStatId;
@QField(label = "Table", possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer qqqTableId;
@QField(maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String name;
@QField(maxLength = 30, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String operator;
@QField(maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String values;
/*******************************************************************************
** Default constructor
*******************************************************************************/
public QueryStatCriteriaField()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public QueryStatCriteriaField(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public QueryStatCriteriaField withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for queryStatId
*******************************************************************************/
public Integer getQueryStatId()
{
return (this.queryStatId);
}
/*******************************************************************************
** Setter for queryStatId
*******************************************************************************/
public void setQueryStatId(Integer queryStatId)
{
this.queryStatId = queryStatId;
}
/*******************************************************************************
** Fluent setter for queryStatId
*******************************************************************************/
public QueryStatCriteriaField withQueryStatId(Integer queryStatId)
{
this.queryStatId = queryStatId;
return (this);
}
/*******************************************************************************
** Getter for qqqTableId
*******************************************************************************/
public Integer getQqqTableId()
{
return (this.qqqTableId);
}
/*******************************************************************************
** Setter for qqqTableId
*******************************************************************************/
public void setQqqTableId(Integer qqqTableId)
{
this.qqqTableId = qqqTableId;
}
/*******************************************************************************
** Fluent setter for qqqTableId
*******************************************************************************/
public QueryStatCriteriaField withQqqTableId(Integer qqqTableId)
{
this.qqqTableId = qqqTableId;
return (this);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public QueryStatCriteriaField withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for operator
*******************************************************************************/
public String getOperator()
{
return (this.operator);
}
/*******************************************************************************
** Setter for operator
*******************************************************************************/
public void setOperator(String operator)
{
this.operator = operator;
}
/*******************************************************************************
** Fluent setter for operator
*******************************************************************************/
public QueryStatCriteriaField withOperator(String operator)
{
this.operator = operator;
return (this);
}
/*******************************************************************************
** Getter for values
*******************************************************************************/
public String getValues()
{
return (this.values);
}
/*******************************************************************************
** Setter for values
*******************************************************************************/
public void setValues(String values)
{
this.values = values;
}
/*******************************************************************************
** Fluent setter for values
*******************************************************************************/
public QueryStatCriteriaField withValues(String values)
{
this.values = values;
return (this);
}
}

View File

@ -0,0 +1,194 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.querystats;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
/*******************************************************************************
** QRecord Entity for QueryStatJoinTable table
*******************************************************************************/
public class QueryStatJoinTable extends QRecordEntity
{
public static final String TABLE_NAME = "queryStatJoinTable"; // todo - lowercase the first letter
@QField(isEditable = false)
private Integer id;
@QField(possibleValueSourceName = QueryStat.TABLE_NAME)
private Integer queryStatId;
@QField(label = "Table", possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer qqqTableId;
@QField(maxLength = 10, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String type;
/*******************************************************************************
** Default constructor
*******************************************************************************/
public QueryStatJoinTable()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public QueryStatJoinTable(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public QueryStatJoinTable withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for queryStatId
*******************************************************************************/
public Integer getQueryStatId()
{
return (this.queryStatId);
}
/*******************************************************************************
** Setter for queryStatId
*******************************************************************************/
public void setQueryStatId(Integer queryStatId)
{
this.queryStatId = queryStatId;
}
/*******************************************************************************
** Fluent setter for queryStatId
*******************************************************************************/
public QueryStatJoinTable withQueryStatId(Integer queryStatId)
{
this.queryStatId = queryStatId;
return (this);
}
/*******************************************************************************
** Getter for qqqTableId
*******************************************************************************/
public Integer getQqqTableId()
{
return (this.qqqTableId);
}
/*******************************************************************************
** Setter for qqqTableId
*******************************************************************************/
public void setQqqTableId(Integer qqqTableId)
{
this.qqqTableId = qqqTableId;
}
/*******************************************************************************
** Fluent setter for qqqTableId
*******************************************************************************/
public QueryStatJoinTable withQqqTableId(Integer qqqTableId)
{
this.qqqTableId = qqqTableId;
return (this);
}
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public QueryStatJoinTable withType(String type)
{
this.type = type;
return (this);
}
}

View File

@ -0,0 +1,189 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.querystats;
import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
/*******************************************************************************
**
*******************************************************************************/
public class QueryStatMetaDataProvider
{
/*******************************************************************************
**
*******************************************************************************/
public void defineAll(QInstance instance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
addJoins(instance);
defineQueryStatTable(instance, backendName, backendDetailEnricher);
instance.addTable(defineStandardTable(QueryStatJoinTable.TABLE_NAME, QueryStatJoinTable.class, backendName, backendDetailEnricher));
instance.addTable(defineStandardTable(QueryStatCriteriaField.TABLE_NAME, QueryStatCriteriaField.class, backendName, backendDetailEnricher)
.withExposedJoin(new ExposedJoin().withJoinTable(QueryStat.TABLE_NAME))
);
instance.addTable(defineStandardTable(QueryStatOrderByField.TABLE_NAME, QueryStatOrderByField.class, backendName, backendDetailEnricher));
instance.addPossibleValueSource(defineQueryStatPossibleValueSource());
}
/*******************************************************************************
**
*******************************************************************************/
private void addJoins(QInstance instance)
{
instance.addJoin(new QJoinMetaData()
.withLeftTable(QueryStat.TABLE_NAME)
.withRightTable(QueryStatJoinTable.TABLE_NAME)
.withInferredName()
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn("id", "queryStatId")));
instance.addJoin(new QJoinMetaData()
.withLeftTable(QueryStat.TABLE_NAME)
.withRightTable(QueryStatCriteriaField.TABLE_NAME)
.withInferredName()
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn("id", "queryStatId")));
instance.addJoin(new QJoinMetaData()
.withLeftTable(QueryStat.TABLE_NAME)
.withRightTable(QueryStatOrderByField.TABLE_NAME)
.withInferredName()
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn("id", "queryStatId")));
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineQueryStatTable(QInstance instance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
String joinTablesJoinName = QJoinMetaData.makeInferredJoinName(QueryStat.TABLE_NAME, QueryStatJoinTable.TABLE_NAME);
String criteriaFieldsJoinName = QJoinMetaData.makeInferredJoinName(QueryStat.TABLE_NAME, QueryStatCriteriaField.TABLE_NAME);
String orderByFieldsJoinName = QJoinMetaData.makeInferredJoinName(QueryStat.TABLE_NAME, QueryStatOrderByField.TABLE_NAME);
QTableMetaData table = new QTableMetaData()
.withName(QueryStat.TABLE_NAME)
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")
.withRecordLabelFields("id")
.withPrimaryKeyField("id")
.withFieldsFromEntity(QueryStat.class)
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "action", "qqqTableId", "sessionId")))
.withSection(new QFieldSection("data", new QIcon().withName("dataset"), Tier.T2, List.of("queryText", "startTimestamp", "firstResultTimestamp", "firstResultMillis")))
.withSection(new QFieldSection("joins", new QIcon().withName("merge"), Tier.T2).withWidgetName(joinTablesJoinName + "Widget"))
.withSection(new QFieldSection("criteria", new QIcon().withName("filter_alt"), Tier.T2).withWidgetName(criteriaFieldsJoinName + "Widget"))
.withSection(new QFieldSection("orderBys", new QIcon().withName("sort_by_alpha"), Tier.T2).withWidgetName(orderByFieldsJoinName + "Widget"))
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
instance.addWidget(ChildRecordListRenderer.widgetMetaDataBuilder(instance.getJoin(joinTablesJoinName)).withName(joinTablesJoinName + "Widget").withLabel("Join Tables").getWidgetMetaData());
instance.addWidget(ChildRecordListRenderer.widgetMetaDataBuilder(instance.getJoin(criteriaFieldsJoinName)).withName(criteriaFieldsJoinName + "Widget").withLabel("Criteria Fields").getWidgetMetaData());
instance.addWidget(ChildRecordListRenderer.widgetMetaDataBuilder(instance.getJoin(orderByFieldsJoinName)).withName(orderByFieldsJoinName + "Widget").withLabel("Order by Fields").getWidgetMetaData());
table.withAssociation(new Association().withName("queryStatJoinTables").withJoinName(joinTablesJoinName).withAssociatedTableName(QueryStatJoinTable.TABLE_NAME))
.withAssociation(new Association().withName("queryStatCriteriaFields").withJoinName(criteriaFieldsJoinName).withAssociatedTableName(QueryStatCriteriaField.TABLE_NAME))
.withAssociation(new Association().withName("queryStatOrderByFields").withJoinName(orderByFieldsJoinName).withAssociatedTableName(QueryStatOrderByField.TABLE_NAME));
table.getField("queryText").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("sql")));
table.getField("firstResultMillis").withDisplayFormat(DisplayFormat.COMMAS);
instance.addTable(table);
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineStandardTable(String tableName, Class<? extends QRecordEntity> entityClass, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData table = new QTableMetaData()
.withName(tableName)
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%d")
.withRecordLabelFields("id")
.withPrimaryKeyField("id")
.withFieldsFromEntity(entityClass)
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource defineQueryStatPossibleValueSource()
{
return (new QPossibleValueSource()
.withType(QPossibleValueSourceType.TABLE)
.withName(QueryStat.TABLE_NAME)
.withTableName(QueryStat.TABLE_NAME));
}
}

View File

@ -0,0 +1,194 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.querystats;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
/*******************************************************************************
** QRecord Entity for QueryStatOrderByField table
*******************************************************************************/
public class QueryStatOrderByField extends QRecordEntity
{
public static final String TABLE_NAME = "queryStatOrderByField";
@QField(isEditable = false)
private Integer id;
@QField(possibleValueSourceName = QueryStat.TABLE_NAME)
private Integer queryStatId;
@QField(label = "Table", possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer qqqTableId;
@QField(maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String name;
/*******************************************************************************
** Default constructor
*******************************************************************************/
public QueryStatOrderByField()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public QueryStatOrderByField(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public QueryStatOrderByField withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for queryStatId
*******************************************************************************/
public Integer getQueryStatId()
{
return (this.queryStatId);
}
/*******************************************************************************
** Setter for queryStatId
*******************************************************************************/
public void setQueryStatId(Integer queryStatId)
{
this.queryStatId = queryStatId;
}
/*******************************************************************************
** Fluent setter for queryStatId
*******************************************************************************/
public QueryStatOrderByField withQueryStatId(Integer queryStatId)
{
this.queryStatId = queryStatId;
return (this);
}
/*******************************************************************************
** Getter for qqqTableId
*******************************************************************************/
public Integer getQqqTableId()
{
return (this.qqqTableId);
}
/*******************************************************************************
** Setter for qqqTableId
*******************************************************************************/
public void setQqqTableId(Integer qqqTableId)
{
this.qqqTableId = qqqTableId;
}
/*******************************************************************************
** Fluent setter for qqqTableId
*******************************************************************************/
public QueryStatOrderByField withQqqTableId(Integer qqqTableId)
{
this.qqqTableId = qqqTableId;
return (this);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public QueryStatOrderByField withName(String name)
{
this.name = name;
return (this);
}
}

View File

@ -0,0 +1,228 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.tables;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
/*******************************************************************************
** QRecord Entity for QQQTable table
*******************************************************************************/
public class QQQTable extends QRecordEntity
{
public static final String TABLE_NAME = "qqqTable";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String name;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String label;
/*******************************************************************************
** Default constructor
*******************************************************************************/
public QQQTable()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public QQQTable(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public QQQTable withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public QQQTable withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public QQQTable withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public QQQTable withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for label
*******************************************************************************/
public String getLabel()
{
return (this.label);
}
/*******************************************************************************
** Setter for label
*******************************************************************************/
public void setLabel(String label)
{
this.label = label;
}
/*******************************************************************************
** Fluent setter for label
*******************************************************************************/
public QQQTable withLabel(String label)
{
this.label = label;
return (this);
}
}

View File

@ -0,0 +1,132 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.tables;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
/*******************************************************************************
**
*******************************************************************************/
public class QQQTablesMetaDataProvider
{
public static final String QQQ_TABLE_CACHE_TABLE_NAME = QQQTable.TABLE_NAME + "Cache";
/*******************************************************************************
**
*******************************************************************************/
public void defineAll(QInstance instance, String persistentBackendName, String cacheBackendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
instance.addTable(defineQQQTable(persistentBackendName, backendDetailEnricher));
instance.addTable(defineQQQTableCache(cacheBackendName, backendDetailEnricher));
instance.addPossibleValueSource(defineQQQTablePossibleValueSource());
}
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData defineQQQTable(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData table = new QTableMetaData()
.withName(QQQTable.TABLE_NAME)
.withLabel("Table")
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withPrimaryKeyField("id")
.withUniqueKey(new UniqueKey("name"))
.withFieldsFromEntity(QQQTable.class)
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData defineQQQTableCache(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData table = new QTableMetaData()
.withName(QQQ_TABLE_CACHE_TABLE_NAME)
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withPrimaryKeyField("id")
.withUniqueKey(new UniqueKey("name"))
.withFieldsFromEntity(QQQTable.class)
.withCacheOf(new CacheOf()
.withSourceTable(QQQTable.TABLE_NAME)
.withUseCase(new CacheUseCase()
.withType(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY)
.withCacheSourceMisses(false)
.withCacheUniqueKey(new UniqueKey("name"))
.withSourceUniqueKey(new UniqueKey("name"))
)
);
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource defineQQQTablePossibleValueSource()
{
return (new QPossibleValueSource()
.withType(QPossibleValueSourceType.TABLE)
.withName(QQQTable.TABLE_NAME)
.withTableName(QQQTable.TABLE_NAME));
}
}

View File

@ -110,20 +110,9 @@ public class ScheduleManager
*******************************************************************************/ *******************************************************************************/
public void start() public void start()
{ {
String propertyName = "qqq.scheduleManager.enabled"; if(!new QMetaDataVariableInterpreter().getBooleanFromPropertyOrEnvironment("qqq.scheduleManager.enabled", "QQQ_SCHEDULE_MANAGER_ENABLED", true))
String propertyValue = System.getProperty(propertyName);
if("false".equals(propertyValue))
{ {
LOG.info("Not starting ScheduleManager (per system property] [" + propertyName + "=" + propertyValue + "])."); LOG.info("Not starting ScheduleManager per settings.");
return;
}
QMetaDataVariableInterpreter qMetaDataVariableInterpreter = new QMetaDataVariableInterpreter();
String envName = "QQQ_SCHEDULE_MANAGER_ENABLED";
String envValue = qMetaDataVariableInterpreter.interpret("${env." + envName + "}");
if("false".equals(envValue))
{
LOG.info("Not starting ScheduleManager (per environment variable] [" + envName + "=" + envValue + "]).");
return; return;
} }

View File

@ -28,13 +28,21 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; 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.Capability;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockQueryAction; import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockQueryAction;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils;
@ -390,6 +398,59 @@ class QueryActionTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryManager() throws QException
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// add tables for QueryStats, and turn them on in the memory backend, then start the query-stat manager //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
QInstance qInstance = QContext.getQInstance();
qInstance.getBackend(TestUtils.MEMORY_BACKEND_NAME).withCapability(Capability.QUERY_STATS);
new QQQTablesMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null);
new QueryStatMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
QueryStatManager.getInstance().start(QContext.getQInstance(), QSession::new);
/////////////////////////////////////////////////////////////////////////////////
// insert some order "trees", then query them, so some stats will get recorded //
/////////////////////////////////////////////////////////////////////////////////
insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setIncludeAssociations(true);
queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("id")));
QContext.pushAction(queryInput);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
////////////////////////////////////////////////////////////
// run the stat manager (so we don't have to wait for it) //
////////////////////////////////////////////////////////////
QueryStatManager.getInstance().storeStatsNow();
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// stat manager expects to be ran in a thread, where it needs to clear context, so reset context after it //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
QContext.init(qInstance, new QSession());
////////////////////////////////////////////////
// query to see that some stats were inserted //
////////////////////////////////////////////////
queryInput = new QueryInput();
queryInput.setTableName(QueryStat.TABLE_NAME);
QContext.pushAction(queryInput);
queryOutput = new QueryAction().execute(queryInput);
///////////////////////////////////////////////////////////////////////////////////
// selecting all of those associations should have caused (at least?) 4 queries. //
// this is the most basic test here, but we'll take it. //
///////////////////////////////////////////////////////////////////////////////////
assertThat(queryOutput.getRecords().size()).isGreaterThanOrEqualTo(4);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -30,8 +30,10 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/******************************************************************************* /*******************************************************************************
@ -224,6 +226,53 @@ class QMetaDataVariableInterpreterTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetBooleanFromPropertyOrEnvironment()
{
QMetaDataVariableInterpreter interpreter = new QMetaDataVariableInterpreter();
//////////////////////////////////////////////////////////
// if neither prop nor env is set, get back the default //
//////////////////////////////////////////////////////////
assertFalse(interpreter.getBooleanFromPropertyOrEnvironment("notSet", "NOT_SET", false));
assertTrue(interpreter.getBooleanFromPropertyOrEnvironment("notSet", "NOT_SET", true));
/////////////////////////////////////////////
// unrecognized values are same as not set //
/////////////////////////////////////////////
System.setProperty("unrecognized", "asdf");
interpreter.setEnvironmentOverrides(Map.of("UNRECOGNIZED", "1234"));
assertFalse(interpreter.getBooleanFromPropertyOrEnvironment("unrecognized", "UNRECOGNIZED", false));
assertTrue(interpreter.getBooleanFromPropertyOrEnvironment("unrecognized", "UNRECOGNIZED", true));
/////////////////////////////////
// if only prop is set, get it //
/////////////////////////////////
assertFalse(interpreter.getBooleanFromPropertyOrEnvironment("foo.enabled", "FOO_ENABLED", false));
System.setProperty("foo.enabled", "true");
assertTrue(interpreter.getBooleanFromPropertyOrEnvironment("foo.enabled", "FOO_ENABLED", false));
////////////////////////////////
// if only env is set, get it //
////////////////////////////////
assertFalse(interpreter.getBooleanFromPropertyOrEnvironment("bar.enabled", "BAR_ENABLED", false));
interpreter.setEnvironmentOverrides(Map.of("BAR_ENABLED", "true"));
assertTrue(interpreter.getBooleanFromPropertyOrEnvironment("bar.enabled", "BAR_ENABLED", false));
///////////////////////////////////
// if both are set, get the prop //
///////////////////////////////////
System.setProperty("baz.enabled", "true");
interpreter.setEnvironmentOverrides(Map.of("BAZ_ENABLED", "false"));
assertTrue(interpreter.getBooleanFromPropertyOrEnvironment("baz.enabled", "BAZ_ENABLED", true));
assertTrue(interpreter.getBooleanFromPropertyOrEnvironment("baz.enabled", "BAZ_ENABLED", false));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -23,10 +23,14 @@ package com.kingsrook.qqq.backend.core.model.data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.testentities.Item; import com.kingsrook.qqq.backend.core.model.data.testentities.Item;
import com.kingsrook.qqq.backend.core.model.data.testentities.ItemWithPrimitives; import com.kingsrook.qqq.backend.core.model.data.testentities.ItemWithPrimitives;
import com.kingsrook.qqq.backend.core.model.data.testentities.LineItem;
import com.kingsrook.qqq.backend.core.model.data.testentities.Order;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.fields.QFieldType;
@ -34,6 +38,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -304,7 +309,103 @@ class QRecordEntityTest extends BaseTest
assertEquals(QFieldType.STRING, qTableMetaData.getField("sku").getType()); assertEquals(QFieldType.STRING, qTableMetaData.getField("sku").getType());
assertEquals(QFieldType.INTEGER, qTableMetaData.getField("quantity").getType()); assertEquals(QFieldType.INTEGER, qTableMetaData.getField("quantity").getType());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOrderWithAssociationsToQRecord() throws QException
{
Order order = new Order();
order.setOrderNo("ORD001");
order.setLineItems(List.of(
new LineItem().withSku("ABC").withQuantity(1),
new LineItem().withSku("DEF").withQuantity(2)
));
QRecord qRecord = order.toQRecord();
assertEquals("ORD001", qRecord.getValueString("orderNo"));
List<QRecord> lineItems = qRecord.getAssociatedRecords().get("lineItems");
assertNotNull(lineItems);
assertEquals(2, lineItems.size());
assertEquals("ABC", lineItems.get(0).getValueString("sku"));
assertEquals(1, lineItems.get(0).getValueInteger("quantity"));
assertEquals("DEF", lineItems.get(1).getValueString("sku"));
assertEquals(2, lineItems.get(1).getValueInteger("quantity"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOrderWithoutAssociationsToQRecord() throws QException
{
Order order = new Order();
order.setOrderNo("ORD001");
order.setLineItems(null);
QRecord qRecord = order.toQRecord();
assertEquals("ORD001", qRecord.getValueString("orderNo"));
List<QRecord> lineItems = qRecord.getAssociatedRecords().get("lineItems");
assertNull(lineItems);
order.setLineItems(new ArrayList<>());
qRecord = order.toQRecord();
lineItems = qRecord.getAssociatedRecords().get("lineItems");
assertNotNull(lineItems);
assertEquals(0, lineItems.size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQRecordWithAssociationsToOrder() throws QException
{
QRecord qRecord = new QRecord()
.withValue("orderNo", "ORD002")
.withAssociatedRecords("lineItems", List.of(
new QRecord().withValue("sku", "AB12").withValue("quantity", 42),
new QRecord().withValue("sku", "XY89").withValue("quantity", 47)
));
Order order = qRecord.toEntity(Order.class);
assertEquals("ORD002", order.getOrderNo());
assertEquals(2, order.getLineItems().size());
assertEquals("AB12", order.getLineItems().get(0).getSku());
assertEquals(42, order.getLineItems().get(0).getQuantity());
assertEquals("XY89", order.getLineItems().get(1).getSku());
assertEquals(47, order.getLineItems().get(1).getQuantity());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQRecordWithoutAssociationsToOrder() throws QException
{
QRecord qRecord = new QRecord().withValue("orderNo", "ORD002");
Order order = qRecord.toEntity(Order.class);
assertEquals("ORD002", order.getOrderNo());
assertNull(order.getLineItems());
qRecord.withAssociatedRecords("lineItems", null);
order = qRecord.toEntity(Order.class);
assertNull(order.getLineItems());
qRecord.withAssociatedRecords("lineItems", new ArrayList<>());
order = qRecord.toEntity(Order.class);
assertNotNull(order.getLineItems());
assertEquals(0, order.getLineItems().size());
} }
} }

View File

@ -43,6 +43,7 @@ public class Item extends QRecordEntity
@QField(isEditable = false, displayFormat = DisplayFormat.COMMAS) @QField(isEditable = false, displayFormat = DisplayFormat.COMMAS)
private Integer quantity; private Integer quantity;
@QField()
private BigDecimal price; private BigDecimal price;
@QField(backendName = "is_featured") @QField(backendName = "is_featured")

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.data.testentities;
import java.math.BigDecimal; import java.math.BigDecimal;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
@ -31,11 +32,20 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
*******************************************************************************/ *******************************************************************************/
public class ItemWithPrimitives extends QRecordEntity public class ItemWithPrimitives extends QRecordEntity
{ {
private String sku; @QField()
private String description; private String sku;
private int quantity;
@QField()
private String description;
@QField()
private int quantity;
@QField()
private BigDecimal price; private BigDecimal price;
private boolean featured;
@QField()
private boolean featured;

View File

@ -0,0 +1,102 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data.testentities;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/*******************************************************************************
** Sample of an entity that can be converted to & from a QRecord
*******************************************************************************/
public class LineItem extends QRecordEntity
{
@QField()
private String sku;
@QField()
private Integer quantity;
/*******************************************************************************
** Getter for sku
*******************************************************************************/
public String getSku()
{
return (this.sku);
}
/*******************************************************************************
** Setter for sku
*******************************************************************************/
public void setSku(String sku)
{
this.sku = sku;
}
/*******************************************************************************
** Fluent setter for sku
*******************************************************************************/
public LineItem withSku(String sku)
{
this.sku = sku;
return (this);
}
/*******************************************************************************
** Getter for quantity
*******************************************************************************/
public Integer getQuantity()
{
return (this.quantity);
}
/*******************************************************************************
** Setter for quantity
*******************************************************************************/
public void setQuantity(Integer quantity)
{
this.quantity = quantity;
}
/*******************************************************************************
** Fluent setter for quantity
*******************************************************************************/
public LineItem withQuantity(Integer quantity)
{
this.quantity = quantity;
return (this);
}
}

View File

@ -0,0 +1,104 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data.testentities;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/*******************************************************************************
** Sample of an entity that can be converted to & from a QRecord
*******************************************************************************/
public class Order extends QRecordEntity
{
@QField()
private String orderNo;
@QAssociation(name = "lineItems")
private List<LineItem> lineItems;
/*******************************************************************************
** Getter for orderNo
*******************************************************************************/
public String getOrderNo()
{
return (this.orderNo);
}
/*******************************************************************************
** Setter for orderNo
*******************************************************************************/
public void setOrderNo(String orderNo)
{
this.orderNo = orderNo;
}
/*******************************************************************************
** Fluent setter for orderNo
*******************************************************************************/
public Order withOrderNo(String orderNo)
{
this.orderNo = orderNo;
return (this);
}
/*******************************************************************************
** Getter for lineItems
*******************************************************************************/
public List<LineItem> getLineItems()
{
return (this.lineItems);
}
/*******************************************************************************
** Setter for lineItems
*******************************************************************************/
public void setLineItems(List<LineItem> lineItems)
{
this.lineItems = lineItems;
}
/*******************************************************************************
** Fluent setter for lineItems
*******************************************************************************/
public Order withLineItems(List<LineItem> lineItems)
{
this.lineItems = lineItems;
return (this);
}
}

View File

@ -69,6 +69,16 @@ class QTableMetaDataTest extends BaseTest
// table:false & backend:false = false // table:false & backend:false = false
assertFalse(new QTableMetaData().withoutCapability(capability).isCapabilityEnabled(new QBackendMetaData().withoutCapability(capability), capability)); assertFalse(new QTableMetaData().withoutCapability(capability).isCapabilityEnabled(new QBackendMetaData().withoutCapability(capability), capability));
// backend false, but then true = true
assertTrue(new QTableMetaData().isCapabilityEnabled(new QBackendMetaData().withoutCapability(capability).withCapability(capability), capability));
// backend true, but then false = false
assertFalse(new QTableMetaData().isCapabilityEnabled(new QBackendMetaData().withCapability(capability).withoutCapability(capability), capability));
// table true, but then false = true
assertFalse(new QTableMetaData().withCapability(capability).withoutCapability(capability).isCapabilityEnabled(new QBackendMetaData(), capability));
} }
} }

View File

@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.module.filesystem.TestUtils; import com.kingsrook.qqq.backend.module.filesystem.TestUtils;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -36,6 +37,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
/******************************************************************************* /*******************************************************************************
** Unit test for FilesystemBackendMetaData ** Unit test for FilesystemBackendMetaData
*******************************************************************************/ *******************************************************************************/
@Disabled("This concept doesn't seem right any more. We will want/need custom JSON/YAML serialization, so, let us disable this test, at least for now, and maybe permanently")
class FilesystemBackendMetaDataTest class FilesystemBackendMetaDataTest
{ {
@ -52,7 +54,7 @@ class FilesystemBackendMetaDataTest
System.out.println(JsonUtils.prettyPrint(json)); System.out.println(JsonUtils.prettyPrint(json));
System.out.println(json); System.out.println(json);
String expectToContain = """ String expectToContain = """
"local-filesystem":{"basePath":"/tmp/filesystem-tests/0","backendType":"filesystem","name":"local-filesystem","usesVariants":false}"""; "local-filesystem":{"disabledCapabilities":["QUERY_STATS"],"basePath":"/tmp/filesystem-tests/0","backendType":"filesystem","name":"local-filesystem","usesVariants":false}""";
assertTrue(json.contains(expectToContain)); assertTrue(json.contains(expectToContain));
} }

View File

@ -27,6 +27,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.module.filesystem.TestUtils; import com.kingsrook.qqq.backend.module.filesystem.TestUtils;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -35,6 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
/******************************************************************************* /*******************************************************************************
** Unit test for S3BackendMetaData ** Unit test for S3BackendMetaData
*******************************************************************************/ *******************************************************************************/
@Disabled("This concept doesn't seem right any more. We will want/need custom JSON/YAML serialization, so, let us disable this test, at least for now, and maybe permanently")
class S3BackendMetaDataTest class S3BackendMetaDataTest
{ {

View File

@ -68,6 +68,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -86,6 +87,8 @@ public abstract class AbstractRDBMSAction implements QActionInterface
{ {
private static final QLogger LOG = QLogger.getLogger(AbstractRDBMSAction.class); private static final QLogger LOG = QLogger.getLogger(AbstractRDBMSAction.class);
protected QueryStat queryStat;
/******************************************************************************* /*******************************************************************************
@ -1037,4 +1040,47 @@ public abstract class AbstractRDBMSAction implements QActionInterface
return (false); return (false);
} }
/*******************************************************************************
**
*******************************************************************************/
protected void setSqlAndJoinsInQueryStat(CharSequence sql, JoinsContext joinsContext)
{
if(queryStat != null)
{
queryStat.setQueryText(sql.toString());
if(CollectionUtils.nullSafeHasContents(joinsContext.getQueryJoins()))
{
Set<String> joinTableNames = new HashSet<>();
for(QueryJoin queryJoin : joinsContext.getQueryJoins())
{
joinTableNames.add(queryJoin.getJoinTable());
}
queryStat.setJoinTableNames(joinTableNames);
}
}
}
/*******************************************************************************
** Getter for queryStat
*******************************************************************************/
public QueryStat getQueryStat()
{
return (this.queryStat);
}
/*******************************************************************************
** Setter for queryStat
*******************************************************************************/
public void setQueryStat(QueryStat queryStat)
{
this.queryStat = queryStat;
}
} }

View File

@ -92,6 +92,8 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega
// todo sql customization - can edit sql and/or param list // todo sql customization - can edit sql and/or param list
setSqlAndJoinsInQueryStat(sql, joinsContext);
AggregateOutput rs = new AggregateOutput(); AggregateOutput rs = new AggregateOutput();
List<AggregateResult> results = new ArrayList<>(); List<AggregateResult> results = new ArrayList<>();
rs.setResults(results); rs.setResults(results);
@ -104,6 +106,8 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega
{ {
while(resultSet.next()) while(resultSet.next())
{ {
setQueryStatFirstResultTime();
AggregateResult result = new AggregateResult(); AggregateResult result = new AggregateResult();
results.add(result); results.add(result);

View File

@ -77,6 +77,8 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf
sql += " WHERE " + makeWhereClause(countInput.getInstance(), countInput.getSession(), table, joinsContext, filter, params); sql += " WHERE " + makeWhereClause(countInput.getInstance(), countInput.getSession(), table, joinsContext, filter, params);
// todo sql customization - can edit sql and/or param list // todo sql customization - can edit sql and/or param list
setSqlAndJoinsInQueryStat(sql, joinsContext);
CountOutput rs = new CountOutput(); CountOutput rs = new CountOutput();
try(Connection connection = getConnection(countInput)) try(Connection connection = getConnection(countInput))
{ {
@ -86,6 +88,8 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf
{ {
if(resultSet.next()) if(resultSet.next())
{ {
setQueryStatFirstResultTime();
rs.setCount(resultSet.getInt("record_count")); rs.setCount(resultSet.getInt("record_count"));
if(BooleanUtils.isTrue(countInput.getIncludeDistinctCount())) if(BooleanUtils.isTrue(countInput.getIncludeDistinctCount()))

View File

@ -100,6 +100,8 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
// todo sql customization - can edit sql and/or param list // todo sql customization - can edit sql and/or param list
setSqlAndJoinsInQueryStat(sql, joinsContext);
Connection connection; Connection connection;
boolean needToCloseConnection = false; boolean needToCloseConnection = false;
if(queryInput.getTransaction() != null && queryInput.getTransaction() instanceof RDBMSTransaction rdbmsTransaction) if(queryInput.getTransaction() != null && queryInput.getTransaction() instanceof RDBMSTransaction rdbmsTransaction)
@ -145,6 +147,8 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
ResultSetMetaData metaData = resultSet.getMetaData(); ResultSetMetaData metaData = resultSet.getMetaData();
while(resultSet.next()) while(resultSet.next())
{ {
setQueryStatFirstResultTime();
QRecord record = new QRecord(); QRecord record = new QRecord();
record.setTableName(table.getName()); record.setTableName(table.getName());
LinkedHashMap<String, Serializable> values = new LinkedHashMap<>(); LinkedHashMap<String, Serializable> values = new LinkedHashMap<>();

View File

@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.module.rdbms.BaseTest; import com.kingsrook.qqq.backend.module.rdbms.BaseTest;
import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -36,6 +37,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
/******************************************************************************* /*******************************************************************************
** Unit test for RDBMSBackendMetaData ** Unit test for RDBMSBackendMetaData
*******************************************************************************/ *******************************************************************************/
@Disabled("This concept doesn't seem right any more. We will want/need custom JSON/YAML serialization, so, let us disable this test, at least for now, and maybe permanently")
class RDBMSBackendMetaDataTest extends BaseTest class RDBMSBackendMetaDataTest extends BaseTest
{ {