diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QueryInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QueryInterface.java index 674203dd..d700a2e3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QueryInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QueryInterface.java @@ -24,10 +24,10 @@ package com.kingsrook.qqq.backend.core.actions.interfaces; import java.time.Instant; import java.util.Set; -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.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.querystats.QueryStat; /******************************************************************************* @@ -67,7 +67,7 @@ public interface QueryInterface QueryStat queryStat = getQueryStat(); if(queryStat != null) { - queryStat.setJoinTables(joinTableNames); + queryStat.setJoinTableNames(joinTableNames); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index af6575dc..01e3d4ae 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -39,8 +39,7 @@ 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.RecordPipeBufferedWrapper; -import com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats.QueryStat; -import com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats.QueryStatManager; +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.QValueFormatter; import com.kingsrook.qqq.backend.core.context.QContext; @@ -52,12 +51,15 @@ 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.QueryOutput; 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.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; +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.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -92,12 +94,14 @@ public class QueryAction 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")); } - 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; if(queryInput.getRecordPipe() != null) @@ -114,13 +118,17 @@ public class QueryAction } } - QueryStat queryStat = new QueryStat(); - queryStat.setTableName(queryInput.getTableName()); - queryStat.setQQueryFilter(Objects.requireNonNullElse(queryInput.getFilter(), new QQueryFilter())); - queryStat.setStartTimestamp(Instant.now()); + QueryStat queryStat = null; + if(table.isCapabilityEnabled(backend, Capability.QUERY_STATS)) + { + queryStat = new QueryStat(); + queryStat.setTableName(queryInput.getTableName()); + queryStat.setQueryFilter(Objects.requireNonNullElse(queryInput.getFilter(), new QQueryFilter())); + queryStat.setStartTimestamp(Instant.now()); + } QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); - QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); + QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend); // todo pre-customization - just get to modify the request? QueryInterface queryInterface = qModule.getQueryInterface(); @@ -129,7 +137,10 @@ public class QueryAction // todo post-customization - can do whatever w/ the result if you want? - QueryStatManager.getInstance().add(queryStat); + if(queryStat != null) + { + QueryStatManager.getInstance().add(queryStat); + } if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe) { 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 deleted file mode 100644 index eda40a98..00000000 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatManager.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * 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-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 1e778d80..21c09d8e 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 @@ -30,12 +30,13 @@ import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; 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; @@ -48,6 +49,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.querystats.QueryStat; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; @@ -142,12 +144,29 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf ////////////////////////////////////////////// QueryOutput queryOutput = new QueryOutput(queryInput); + if(queryStat != null) + { + queryStat.setQueryText(sql.toString()); + + if(CollectionUtils.nullSafeHasContents(joinsContext.getQueryJoins())) + { + Set joinTableNames = new HashSet<>(); + for(QueryJoin queryJoin : joinsContext.getQueryJoins()) + { + joinTableNames.add(queryJoin.getJoinTable()); + } + setQueryStatJoinTables(joinTableNames); + } + } + PreparedStatement statement = createStatement(connection, sql.toString(), queryInput); QueryManager.executeStatement(statement, ((ResultSet resultSet) -> { ResultSetMetaData metaData = resultSet.getMetaData(); while(resultSet.next()) { + setQueryStatFirstResultTime(); + QRecord record = new QRecord(); record.setTableName(table.getName()); LinkedHashMap values = new LinkedHashMap<>();