diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStat.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStat.java new file mode 100644 index 00000000..2c35f9f4 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStat.java @@ -0,0 +1,397 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats; + + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +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.QRecordEntity; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QueryStat extends QRecordEntity +{ + public static final String TABLE_NAME = "queryStat"; + + @QField() + private Integer id; + + @QField() + private String tableName; + + @QField() + private Instant startTimestamp; + + @QField() + private Instant firstResultTimestamp; + + @QField() + private Integer firstResultMillis; + + @QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE) + private String joinTables; + + @QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE) + private String orderBys; + + @QAssociation(name = "queryStatFilterCriteria") + private List queryStatFilterCriteriaList; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setJoinTables(Collection joinTableNames) + { + if(CollectionUtils.nullSafeIsEmpty(joinTableNames)) + { + setJoinTables((String) null); + } + + setJoinTables(joinTableNames.stream().sorted().collect(Collectors.joining(","))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setQQueryFilter(QQueryFilter filter) + { + if(filter == null) + { + setQueryStatFilterCriteriaList(null); + setOrderBys(null); + } + else + { + ///////////////////////////////////////////// + // manage list of sub-records for criteria // + ///////////////////////////////////////////// + if(CollectionUtils.nullSafeIsEmpty(filter.getCriteria()) && CollectionUtils.nullSafeIsEmpty(filter.getSubFilters())) + { + setQueryStatFilterCriteriaList(null); + } + else + { + ArrayList criteriaList = new ArrayList<>(); + setQueryStatFilterCriteriaList(criteriaList); + processFilterCriteria(filter, criteriaList); + } + + ////////////////////////////////////////////////////////////// + // set orderBys (comma-delimited concatenated string field) // + ////////////////////////////////////////////////////////////// + if(CollectionUtils.nullSafeIsEmpty(filter.getOrderBys())) + { + setOrderBys(null); + } + else + { + setOrderBys(filter.getOrderBys().stream().map(ob -> ob.getFieldName()).collect(Collectors.joining(","))); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void processFilterCriteria(QQueryFilter filter, ArrayList criteriaList) + { + for(QFilterCriteria criterion : CollectionUtils.nonNullList(filter.getCriteria())) + { + criteriaList.add(new QueryStatFilterCriteria() + .withFieldName(criterion.getFieldName()) + .withOperator(criterion.getOperator().name()) + .withValues(CollectionUtils.nonNullList(criterion.getValues()).stream().map(v -> ValueUtils.getValueAsString(v)).collect(Collectors.joining(",")))); + } + + for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters())) + { + processFilterCriteria(subFilter, criteriaList); + } + } + + + + /******************************************************************************* + ** 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 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 joinTables + *******************************************************************************/ + public String getJoinTables() + { + return (this.joinTables); + } + + + + /******************************************************************************* + ** Setter for joinTables + *******************************************************************************/ + public void setJoinTables(String joinTables) + { + this.joinTables = joinTables; + } + + + + /******************************************************************************* + ** Fluent setter for joinTables + *******************************************************************************/ + public QueryStat withJoinTables(String joinTables) + { + this.joinTables = joinTables; + return (this); + } + + + + /******************************************************************************* + ** Getter for queryStatFilterCriteriaList + *******************************************************************************/ + public List getQueryStatFilterCriteriaList() + { + return (this.queryStatFilterCriteriaList); + } + + + + /******************************************************************************* + ** Setter for queryStatFilterCriteriaList + *******************************************************************************/ + public void setQueryStatFilterCriteriaList(List queryStatFilterCriteriaList) + { + this.queryStatFilterCriteriaList = queryStatFilterCriteriaList; + } + + + + /******************************************************************************* + ** Fluent setter for queryStatFilterCriteriaList + *******************************************************************************/ + public QueryStat withQueryStatFilterCriteriaList(List queryStatFilterCriteriaList) + { + this.queryStatFilterCriteriaList = queryStatFilterCriteriaList; + return (this); + } + + + + /******************************************************************************* + ** Getter for orderBys + *******************************************************************************/ + public String getOrderBys() + { + return (this.orderBys); + } + + + + /******************************************************************************* + ** Setter for orderBys + *******************************************************************************/ + public void setOrderBys(String orderBys) + { + this.orderBys = orderBys; + } + + + + /******************************************************************************* + ** Fluent setter for orderBys + *******************************************************************************/ + public QueryStat withOrderBys(String orderBys) + { + this.orderBys = orderBys; + return (this); + } + + + + /******************************************************************************* + ** 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); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatFilterCriteria.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatFilterCriteria.java new file mode 100644 index 00000000..ffb29a8b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatFilterCriteria.java @@ -0,0 +1,162 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats; + + +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QueryStatFilterCriteria extends QRecordEntity +{ + private Integer queryStatId; + private String fieldName; + private String operator; + private String values; + + + + /******************************************************************************* + ** 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 QueryStatFilterCriteria withQueryStatId(Integer queryStatId) + { + this.queryStatId = queryStatId; + return (this); + } + + + + /******************************************************************************* + ** Getter for fieldName + *******************************************************************************/ + public String getFieldName() + { + return (this.fieldName); + } + + + + /******************************************************************************* + ** Setter for fieldName + *******************************************************************************/ + public void setFieldName(String fieldName) + { + this.fieldName = fieldName; + } + + + + /******************************************************************************* + ** Fluent setter for fieldName + *******************************************************************************/ + public QueryStatFilterCriteria withFieldName(String fieldName) + { + this.fieldName = fieldName; + 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 QueryStatFilterCriteria 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 QueryStatFilterCriteria withValues(String values) + { + this.values = values; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatManager.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatManager.java new file mode 100644 index 00000000..eda40a98 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatManager.java @@ -0,0 +1,217 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats; + + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +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.InsertAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QueryStatManager +{ + private static QueryStatManager queryStatManager = null; + + // todo - support multiple qInstances? + private QInstance qInstance; + private Supplier sessionSupplier; + + private boolean active = false; + private List queryStats = new ArrayList<>(); + + private ScheduledExecutorService executorService; + + + + /******************************************************************************* + ** Singleton constructor + *******************************************************************************/ + private QueryStatManager() + { + + } + + + + /******************************************************************************* + ** Singleton accessor + *******************************************************************************/ + public static QueryStatManager getInstance() + { + if(queryStatManager == null) + { + queryStatManager = new QueryStatManager(); + } + return (queryStatManager); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void start(Supplier sessionSupplier) + { + qInstance = QContext.getQInstance(); + this.sessionSupplier = sessionSupplier; + + active = true; + queryStats = new ArrayList<>(); + + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.scheduleAtFixedRate(new QueryStatManagerInsertJob(), 60, 60, TimeUnit.SECONDS); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void stop() + { + active = false; + queryStats.clear(); + + if(executorService != null) + { + executorService.shutdown(); + executorService = null; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void add(QueryStat queryStat) + { + if(active) + { + synchronized(this) + { + if(queryStat.getFirstResultTimestamp() == null) + { + //////////////////////////////////////////////// + // in case it didn't get set in the interface // + //////////////////////////////////////////////// + queryStat.setFirstResultTimestamp(Instant.now()); + } + + /////////////////////////////////////////////// + // compute the millis (so you don't have to) // + /////////////////////////////////////////////// + if(queryStat.getStartTimestamp() != null && queryStat.getFirstResultTimestamp() != null && queryStat.getFirstResultMillis() == null) + { + long millis = queryStat.getFirstResultTimestamp().toEpochMilli() - queryStat.getStartTimestamp().toEpochMilli(); + queryStat.setFirstResultMillis((int) millis); + } + + queryStats.add(queryStat); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List getListAndReset() + { + if(queryStats.isEmpty()) + { + return Collections.emptyList(); + } + + synchronized(this) + { + List returnList = queryStats; + queryStats = new ArrayList<>(); + return (returnList); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + 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()); + + List list = getInstance().getListAndReset(); + LOG.info(logPair("queryStatListSize", list.size())); + + if(list.isEmpty()) + { + return; + } + + try + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(QueryStat.TABLE_NAME); + insertInput.setRecords(list.stream().map(qs -> qs.toQRecord()).toList()); + new InsertAction().execute(insertInput); + } + catch(Exception e) + { + LOG.error("Error inserting query stats", e); + } + } + finally + { + QContext.clear(); + } + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java index 7092b3d1..c5123721 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java @@ -118,6 +118,24 @@ public abstract class QRecordEntity qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList); } } + + for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass())) + { + List associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name()); + if(associatedRecords == null) + { + qRecordEntityAssociation.getSetter().invoke(this, (Object) null); + } + else + { + List associatedEntityList = new ArrayList<>(); + for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords)) + { + associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord)); + } + qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList); + } + } } catch(Exception e) { @@ -179,8 +197,7 @@ public abstract class QRecordEntity { QRecord qRecord = new QRecord(); - List fieldList = getFieldList(this.getClass()); - for(QRecordEntityField qRecordEntityField : fieldList) + for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass())) { Serializable thisValue = (Serializable) qRecordEntityField.getGetter().invoke(this); Serializable originalValue = null; @@ -195,6 +212,25 @@ public abstract class QRecordEntity } } + for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass())) + { + List associatedEntities = (List) 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); } catch(Exception e) diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index 21c09d8e..95d61458 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -37,6 +37,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats.QueryStat; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;