From dc6d37aad3e8cd8b71ab69fdce4300784dd8eb9f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 3 Jan 2025 16:49:09 -0600 Subject: [PATCH] Introduce the concept of RDBMSActionStrategyInterface - to use strategy pattern for refinement of how different RDBMS sub-backends may need to behave (e.g., to support SQLite, and FULLTEXT INDEX in MySQL). --- .../model/metadata/RDBMSBackendMetaData.java | 84 ++ .../model/metadata/RDBMSFieldMetaData.java | 166 ++++ .../strategy/BaseRDBMSActionStrategy.java | 873 ++++++++++++++++++ .../MySQLFullTextIndexFieldStrategy.java | 62 ++ .../RDBMSActionStrategyInterface.java | 103 +++ .../module/rdbms/actions/RDBMSActionTest.java | 35 +- 6 files changed, 1320 insertions(+), 3 deletions(-) create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSFieldMetaData.java create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/BaseRDBMSActionStrategy.java create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/MySQLFullTextIndexFieldStrategy.java create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/RDBMSActionStrategyInterface.java diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java index 28d7dc7a..277502ed 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSBackendMetaData.java @@ -23,10 +23,14 @@ package com.kingsrook.qqq.backend.module.rdbms.model.metadata; import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule; +import com.kingsrook.qqq.backend.module.rdbms.strategy.BaseRDBMSActionStrategy; +import com.kingsrook.qqq.backend.module.rdbms.strategy.RDBMSActionStrategyInterface; /******************************************************************************* @@ -50,6 +54,9 @@ public class RDBMSBackendMetaData extends QBackendMetaData private RDBMSBackendMetaData readOnlyBackendMetaData; + private QCodeReference actionStrategyCodeReference; + private RDBMSActionStrategyInterface actionStrategy; + private List queriesForNewConnections = null; /////////////////////////////////////////////////////////// @@ -466,6 +473,83 @@ public class RDBMSBackendMetaData extends QBackendMetaData return null; } + + + /******************************************************************************* + ** Getter for actionStrategyCodeReference + *******************************************************************************/ + public QCodeReference getActionStrategyCodeReference() + { + return (this.actionStrategyCodeReference); + } + + + + /******************************************************************************* + ** Setter for actionStrategyCodeReference + *******************************************************************************/ + public void setActionStrategyCodeReference(QCodeReference actionStrategyCodeReference) + { + this.actionStrategyCodeReference = actionStrategyCodeReference; + } + + + + /******************************************************************************* + ** Fluent setter for actionStrategyCodeReference + *******************************************************************************/ + public RDBMSBackendMetaData withActionStrategyCodeReference(QCodeReference actionStrategyCodeReference) + { + this.actionStrategyCodeReference = actionStrategyCodeReference; + return (this); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @JsonIgnore + public RDBMSActionStrategyInterface getActionStrategy() + { + if(actionStrategy == null) + { + if(actionStrategyCodeReference != null) + { + actionStrategy = QCodeLoader.getAdHoc(RDBMSActionStrategyInterface.class, actionStrategyCodeReference); + } + else + { + actionStrategy = new BaseRDBMSActionStrategy(); + } + } + + return (actionStrategy); + } + + + + /*************************************************************************** + * note - protected - meant for sub-classes to use in their implementation of + * getActionStrategy, but not for public use. + ***************************************************************************/ + protected RDBMSActionStrategyInterface getActionStrategyField() + { + return (actionStrategy); + } + + + + /*************************************************************************** + * note - protected - meant for sub-classes to use in their implementation of + * getActionStrategy, but not for public use. + ***************************************************************************/ + protected void setActionStrategyField(RDBMSActionStrategyInterface actionStrategy) + { + this.actionStrategy = actionStrategy; + } + + /******************************************************************************* ** Getter for queriesForNewConnections *******************************************************************************/ diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSFieldMetaData.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSFieldMetaData.java new file mode 100644 index 00000000..58e6fca4 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/RDBMSFieldMetaData.java @@ -0,0 +1,166 @@ +/* + * 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.module.rdbms.model.metadata; + + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData; +import com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule; +import com.kingsrook.qqq.backend.module.rdbms.strategy.RDBMSActionStrategyInterface; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RDBMSFieldMetaData extends QSupplementalFieldMetaData +{ + private QCodeReference actionStrategyCodeReference; + private RDBMSActionStrategyInterface actionStrategy; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public RDBMSFieldMetaData() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static RDBMSFieldMetaData of(QFieldMetaData field) + { + return ((RDBMSFieldMetaData) field.getSupplementalMetaData(RDBMSBackendModule.NAME)); + } + + + + /******************************************************************************* + ** either get the object attached to a field - or create a new one and attach + ** it to the field, and return that. + *******************************************************************************/ + public static RDBMSFieldMetaData ofOrWithNew(QFieldMetaData field) + { + RDBMSFieldMetaData rdbmsFieldMetaData = of(field); + if(rdbmsFieldMetaData == null) + { + rdbmsFieldMetaData = new RDBMSFieldMetaData(); + field.withSupplementalMetaData(rdbmsFieldMetaData); + } + return (rdbmsFieldMetaData); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String getType() + { + return (RDBMSBackendModule.NAME); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @JsonIgnore + public RDBMSActionStrategyInterface getActionStrategy() + { + if(actionStrategy == null) + { + if(actionStrategyCodeReference != null) + { + actionStrategy = QCodeLoader.getAdHoc(RDBMSActionStrategyInterface.class, actionStrategyCodeReference); + } + else + { + return (null); + } + } + + return (actionStrategy); + } + + + + /******************************************************************************* + ** Getter for actionStrategyCodeReference + *******************************************************************************/ + public QCodeReference getActionStrategyCodeReference() + { + return (this.actionStrategyCodeReference); + } + + + + /******************************************************************************* + ** Setter for actionStrategyCodeReference + *******************************************************************************/ + public void setActionStrategyCodeReference(QCodeReference actionStrategyCodeReference) + { + this.actionStrategyCodeReference = actionStrategyCodeReference; + } + + + + /******************************************************************************* + ** Fluent setter for actionStrategyCodeReference + *******************************************************************************/ + public RDBMSFieldMetaData withActionStrategyCodeReference(QCodeReference actionStrategyCodeReference) + { + this.actionStrategyCodeReference = actionStrategyCodeReference; + return (this); + } + + + + /*************************************************************************** + * note - protected - meant for sub-classes to use in their implementation of + * getActionStrategy, but not for public use. + ***************************************************************************/ + protected RDBMSActionStrategyInterface getActionStrategyField() + { + return (actionStrategy); + } + + + + /*************************************************************************** + * note - protected - meant for sub-classes to use in their implementation of + * getActionStrategy, but not for public use. + ***************************************************************************/ + protected void setActionStrategyField(RDBMSActionStrategyInterface actionStrategy) + { + this.actionStrategy = actionStrategy; + } + +} diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/BaseRDBMSActionStrategy.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/BaseRDBMSActionStrategy.java new file mode 100644 index 00000000..08309cd3 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/BaseRDBMSActionStrategy.java @@ -0,0 +1,873 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. 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.module.rdbms.strategy; + + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +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.possiblevalues.PossibleValueEnum; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class BaseRDBMSActionStrategy implements RDBMSActionStrategyInterface +{ + private static final QLogger LOG = QLogger.getLogger(BaseRDBMSActionStrategy.class); + + private static final int MILLIS_PER_SECOND = 1000; + + public static final int DEFAULT_PAGE_SIZE = 2000; + public static int PAGE_SIZE = DEFAULT_PAGE_SIZE; + + private boolean collectStatistics = false; + private final Map statistics = Collections.synchronizedMap(new HashMap<>()); + + public static final String STAT_QUERIES_RAN = "queriesRan"; + public static final String STAT_BATCHES_RAN = "batchesRan"; + + + + /*************************************************************************** + * + ***************************************************************************/ + public Integer appendCriterionToWhereClause(QFilterCriteria criterion, StringBuilder clause, String column, List values, QFieldMetaData field) + { + clause.append(column); + + switch(criterion.getOperator()) + { + case EQUALS -> + { + clause.append(" = ?"); + return (1); + } + case NOT_EQUALS -> + { + clause.append(" != ?"); + return (1); + } + case NOT_EQUALS_OR_IS_NULL -> + { + clause.append(" != ? OR ").append(column).append(" IS NULL "); + return (1); + } + case IN -> + { + if(values.isEmpty()) + { + /////////////////////////////////////////////////////// + // if there are no values, then we want a false here // + /////////////////////////////////////////////////////// + clause.delete(0, clause.length()); + clause.append(" 0 = 1 "); + return (0); + } + else + { + clause.append(" IN (").append(values.stream().map(x -> "?").collect(Collectors.joining(","))).append(")"); + return (values.size()); + } + } + case IS_NULL_OR_IN -> + { + clause.append(" IS NULL "); + + if(!values.isEmpty()) + { + clause.append(" OR ").append(column).append(" IN (").append(values.stream().map(x -> "?").collect(Collectors.joining(","))).append(")"); + return (values.size()); + } + else + { + return (0); + } + } + case NOT_IN -> + { + if(values.isEmpty()) + { + ////////////////////////////////////////////////////// + // if there are no values, then we want a true here // + ////////////////////////////////////////////////////// + clause.delete(0, clause.length()); + clause.append(" 1 = 1 "); + return (0); + } + else + { + clause.append(" NOT IN (").append(values.stream().map(x -> "?").collect(Collectors.joining(","))).append(")"); + return (values.size()); + } + } + case LIKE -> + { + clause.append(" LIKE ?"); + return (1); + } + case NOT_LIKE -> + { + clause.append(" NOT LIKE ?"); + return (1); + } + case STARTS_WITH -> + { + clause.append(" LIKE ?"); + ActionHelper.editFirstValue(values, (s -> s + "%")); + return (1); + } + case ENDS_WITH -> + { + clause.append(" LIKE ?"); + ActionHelper.editFirstValue(values, (s -> "%" + s)); + return (1); + } + case CONTAINS -> + { + clause.append(" LIKE ?"); + ActionHelper.editFirstValue(values, (s -> "%" + s + "%")); + return (1); + } + case NOT_STARTS_WITH -> + { + clause.append(" NOT LIKE ?"); + ActionHelper.editFirstValue(values, (s -> s + "%")); + return (1); + } + case NOT_ENDS_WITH -> + { + clause.append(" NOT LIKE ?"); + ActionHelper.editFirstValue(values, (s -> "%" + s)); + return (1); + } + case NOT_CONTAINS -> + { + clause.append(" NOT LIKE ?"); + ActionHelper.editFirstValue(values, (s -> "%" + s + "%")); + return (1); + } + case LESS_THAN -> + { + clause.append(" < ?"); + return (1); + } + case LESS_THAN_OR_EQUALS -> + { + clause.append(" <= ?"); + return (1); + } + case GREATER_THAN -> + { + clause.append(" > ?"); + return (1); + } + case GREATER_THAN_OR_EQUALS -> + { + clause.append(" >= ?"); + return (1); + } + case IS_BLANK -> + { + clause.append(" IS NULL"); + if(field.getType().isStringLike()) + { + clause.append(" OR ").append(column).append(" = ''"); + } + return (0); + } + case IS_NOT_BLANK -> + { + clause.append(" IS NOT NULL"); + if(field.getType().isStringLike()) + { + clause.append(" AND ").append(column).append(" != ''"); + } + return (0); + } + case BETWEEN -> + { + clause.append(" BETWEEN ? AND ?"); + return (2); + } + case NOT_BETWEEN -> + { + clause.append(" NOT BETWEEN ? AND ?"); + return (2); + } + case TRUE -> + { + clause.delete(0, clause.length()); + clause.append(" 1 = 1 "); + return (0); + } + case FALSE -> + { + clause.delete(0, clause.length()); + clause.append(" 0 = 1 "); + return (0); + } + default -> throw new IllegalStateException("Unexpected operator: " + criterion.getOperator()); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public Serializable getFieldValueFromResultSet(QFieldType type, ResultSet resultSet, int i) throws SQLException + { + return switch(type) + { + case STRING, TEXT, HTML, PASSWORD -> (QueryManager.getString(resultSet, i)); + case INTEGER -> (QueryManager.getInteger(resultSet, i)); + case LONG -> (QueryManager.getLong(resultSet, i)); + case DECIMAL -> (QueryManager.getBigDecimal(resultSet, i)); + case DATE -> (QueryManager.getDate(resultSet, i));// todo - queryManager.getLocalDate? + case TIME -> (QueryManager.getLocalTime(resultSet, i)); + case DATE_TIME -> (QueryManager.getInstant(resultSet, i)); + case BOOLEAN -> (QueryManager.getBoolean(resultSet, i)); + case BLOB -> (QueryManager.getByteArray(resultSet, i)); + default -> throw new IllegalStateException("Unexpected field type: " + type); + }; + } + + + + /*************************************************************************** + * + ***************************************************************************/ + @Override + public PreparedStatement executeUpdate(Connection connection, String sql, List params) throws SQLException + { + try(PreparedStatement statement = prepareStatementAndBindParams(connection, sql, new List[] { params })) + { + incrementStatistic(STAT_QUERIES_RAN); + statement.executeUpdate(); + return (statement); + } + catch(SQLException e) + { + LOG.warn("SQLException", e, logPair("sql", sql)); + throw (e); + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + @Override + public void executeBatchUpdate(Connection connection, String updateSQL, List> values) throws SQLException + { + for(List> page : CollectionUtils.getPages(values, PAGE_SIZE)) + { + PreparedStatement updatePS = connection.prepareStatement(updateSQL); + for(List row : page) + { + Object[] params = new Object[row.size()]; + for(int i = 0; i < row.size(); i++) + { + params[i] = row.get(i); + } + + bindParams(params, updatePS); + updatePS.addBatch(); + } + incrementStatistic(STAT_BATCHES_RAN); + updatePS.executeBatch(); + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + @Override + public List executeInsertForGeneratedIds(Connection connection, String sql, List params, QFieldMetaData primaryKeyField) throws SQLException + { + try(PreparedStatement statement = connection.prepareStatement(sql, new String[] { getColumnName(primaryKeyField) })) + { + bindParams(params.toArray(), statement); + incrementStatistic(STAT_QUERIES_RAN); + statement.executeUpdate(); + + ResultSet generatedKeys = statement.getGeneratedKeys(); + List rs = new ArrayList<>(); + while(generatedKeys.next()) + { + rs.add(getFieldValueFromResultSet(primaryKeyField.getType(), generatedKeys, 1)); + } + return (rs); + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + @Override + public Integer executeUpdateForRowCount(Connection connection, String sql, Object... params) throws SQLException + { + try(PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params)) + { + incrementStatistic(STAT_QUERIES_RAN); + int rowCount = statement.executeUpdate(); + return (rowCount); + } + catch(SQLException e) + { + LOG.warn("SQLException", e, logPair("sql", sql)); + throw (e); + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + @Override + public void executeStatement(PreparedStatement statement, CharSequence sql, ResultSetProcessor processor, Object... params) throws SQLException, QException + { + ResultSet resultSet = null; + + try + { + bindParams(params, statement); + incrementStatistic(STAT_QUERIES_RAN); + statement.execute(); + resultSet = statement.getResultSet(); + + if(processor != null) + { + processor.processResultSet(resultSet); + } + } + catch(SQLException e) + { + if(sql != null) + { + LOG.warn("SQLException", e, logPair("sql", sql)); + } + throw (e); + } + finally + { + if(resultSet != null) + { + resultSet.close(); + } + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + @Override + public Integer getPageSize(AbstractActionInput actionInput) + { + return PAGE_SIZE; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected PreparedStatement prepareStatementAndBindParams(Connection connection, String sql, Object[] params) throws SQLException + { + PreparedStatement statement = connection.prepareStatement(sql); + bindParams(params, statement); + return statement; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void bindParams(Object[] params, PreparedStatement statement) throws SQLException + { + int paramIndex = 0; + if(params != null) + { + for(Object param : params) + { + int paramsBound = bindParamObject(statement, (paramIndex + 1), param); + paramIndex += paramsBound; + } + } + } + + + + /******************************************************************************* + * index is 1-based!! + *******************************************************************************/ + protected int bindParamObject(PreparedStatement statement, int index, Object value) throws SQLException + { + if(value instanceof Integer i) + { + bindParam(statement, index, i); + return (1); + } + else if(value instanceof Short s) + { + bindParam(statement, index, s.intValue()); + return (1); + } + else if(value instanceof Long l) + { + bindParam(statement, index, l); + return (1); + } + else if(value instanceof Double d) + { + bindParam(statement, index, d); + return (1); + } + else if(value instanceof String s) + { + bindParam(statement, index, s); + return (1); + } + else if(value instanceof Boolean b) + { + bindParam(statement, index, b); + return (1); + } + else if(value instanceof Timestamp ts) + { + bindParam(statement, index, ts); + return (1); + } + else if(value instanceof Date) + { + bindParam(statement, index, (Date) value); + return (1); + } + else if(value instanceof Calendar c) + { + bindParam(statement, index, c); + return (1); + } + else if(value instanceof BigDecimal bd) + { + bindParam(statement, index, bd); + return (1); + } + else if(value == null) + { + statement.setNull(index, Types.CHAR); + return (1); + } + else if(value instanceof Collection c) + { + int paramsBound = 0; + for(Object o : c) + { + paramsBound += bindParamObject(statement, (index + paramsBound), o); + } + return (paramsBound); + } + else if(value instanceof byte[] ba) + { + statement.setBytes(index, ba); + return (1); + } + else if(value instanceof Instant i) + { + statement.setObject(index, i); + return (1); + } + else if(value instanceof LocalDate ld) + { + @SuppressWarnings("deprecation") + Date date = new Date(ld.getYear() - 1900, ld.getMonthValue() - 1, ld.getDayOfMonth()); + statement.setDate(index, date); + return (1); + } + else if(value instanceof LocalTime lt) + { + @SuppressWarnings("deprecation") + Time time = new Time(lt.getHour(), lt.getMinute(), lt.getSecond()); + statement.setTime(index, time); + return (1); + } + else if(value instanceof OffsetDateTime odt) + { + long epochMillis = odt.toEpochSecond() * MILLIS_PER_SECOND; + Timestamp timestamp = new Timestamp(epochMillis); + statement.setTimestamp(index, timestamp); + return (1); + } + else if(value instanceof LocalDateTime ldt) + { + ZoneOffset offset = OffsetDateTime.now().getOffset(); + long epochMillis = ldt.toEpochSecond(offset) * MILLIS_PER_SECOND; + Timestamp timestamp = new Timestamp(epochMillis); + statement.setTimestamp(index, timestamp); + return (1); + } + else if(value instanceof PossibleValueEnum pve) + { + return (bindParamObject(statement, index, pve.getPossibleValueId())); + } + else + { + throw (new SQLException("Unexpected value type [" + value.getClass().getSimpleName() + "] in bindParamObject.")); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, Integer value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.INTEGER); + } + else + { + statement.setInt(index, value); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, Long value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.INTEGER); + } + else + { + statement.setLong(index, value); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, Double value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.DOUBLE); + } + else + { + statement.setDouble(index, value); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, String value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.CHAR); + } + else + { + statement.setString(index, value); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, Boolean value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.BOOLEAN); + } + else + { + statement.setBoolean(index, value); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, Date value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.DATE); + } + else + { + statement.setDate(index, new Date(value.getTime())); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, Timestamp value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.TIMESTAMP); + } + else + { + statement.setTimestamp(index, value); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, Calendar value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.DATE); + } + else + { + statement.setTimestamp(index, new Timestamp(value.getTimeInMillis())); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, LocalDate value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.DATE); + } + else + { + LocalDateTime localDateTime = value.atTime(0, 0); + Timestamp timestamp = new Timestamp(localDateTime.atZone(ZoneId.systemDefault()).toEpochSecond() * MILLIS_PER_SECOND); // TimeStamp expects millis, not seconds, after epoch + statement.setTimestamp(index, timestamp); + } + } + + + + /******************************************************************************* + * + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, LocalDateTime value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.TIMESTAMP); + } + else + { + Timestamp timestamp = new Timestamp(value.atZone(ZoneId.systemDefault()).toEpochSecond() * MILLIS_PER_SECOND); // TimeStamp expects millis, not seconds, after epoch + statement.setTimestamp(index, timestamp); + } + } + + + + /******************************************************************************* + ** + ** + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, BigDecimal value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.DECIMAL); + } + else + { + statement.setBigDecimal(index, value); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void bindParam(PreparedStatement statement, int index, byte[] value) throws SQLException + { + if(value == null) + { + statement.setNull(index, Types.ARRAY); + } + else + { + statement.setBytes(index, value); + } + } + + + + /******************************************************************************* + ** + ** + *******************************************************************************/ + protected void bindParamNull(PreparedStatement statement, int index) throws SQLException + { + statement.setNull(index, Types.NULL); + } + + + + /******************************************************************************* + ** Increment a statistic + ** + *******************************************************************************/ + protected void incrementStatistic(String statName) + { + if(collectStatistics) + { + statistics.putIfAbsent(statName, 0); + statistics.put(statName, statistics.get(statName) + 1); + } + } + + + + /******************************************************************************* + ** Setter for collectStatistics + ** + *******************************************************************************/ + public void setCollectStatistics(boolean collectStatistics) + { + this.collectStatistics = collectStatistics; + } + + + + /******************************************************************************* + ** clear the map of statistics + ** + *******************************************************************************/ + public void resetStatistics() + { + statistics.clear(); + } + + + + /******************************************************************************* + ** Getter for statistics + ** + *******************************************************************************/ + public Map getStatistics() + { + return statistics; + } + + + + /******************************************************************************* + ** Setter for pageSize + ** + *******************************************************************************/ + public void setPageSize(int pageSize) + { + BaseRDBMSActionStrategy.PAGE_SIZE = pageSize; + } + + /******************************************************************************* + ** Get the column name to use for a field in the RDBMS, from the fieldMetaData. + ** + ** That is, field.backendName if set -- else, field.name + *******************************************************************************/ + protected String getColumnName(QFieldMetaData field) + { + if(field.getBackendName() != null) + { + return (field.getBackendName()); + } + return (field.getName()); + } + +} diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/MySQLFullTextIndexFieldStrategy.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/MySQLFullTextIndexFieldStrategy.java new file mode 100644 index 00000000..f49ccf04 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/MySQLFullTextIndexFieldStrategy.java @@ -0,0 +1,62 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.rdbms.strategy; + + +import java.io.Serializable; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; + + +/******************************************************************************* + ** RDBMS action strategy for a field with a FULLTEXT INDEX on it in a MySQL + ** database. Makes a LIKE or CONTAINS (or NOT those) query use the special + ** syntax that hits the FULLTEXT INDEX. + *******************************************************************************/ +public class MySQLFullTextIndexFieldStrategy extends BaseRDBMSActionStrategy +{ + /*************************************************************************** + * + ***************************************************************************/ + @Override + public Integer appendCriterionToWhereClause(QFilterCriteria criterion, StringBuilder clause, String column, List values, QFieldMetaData field) + { + switch(criterion.getOperator()) + { + case LIKE, CONTAINS -> + { + clause.append(" MATCH (").append(column).append(") AGAINST (?) "); + return (1); + } + case NOT_LIKE, NOT_CONTAINS -> + { + clause.append(" NOT MATCH (").append(column).append(") AGAINST (?) "); + return (1); + } + default -> + { + return super.appendCriterionToWhereClause(criterion, clause, column, values, field); + } + } + } +} diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/RDBMSActionStrategyInterface.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/RDBMSActionStrategyInterface.java new file mode 100644 index 00000000..892d0974 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/strategy/RDBMSActionStrategyInterface.java @@ -0,0 +1,103 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. 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.module.rdbms.strategy; + + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface RDBMSActionStrategyInterface +{ + + /*************************************************************************** + * modifies the clause StringBuilder (appending to it) + * returning the number of expected number of params to bind + ***************************************************************************/ + Integer appendCriterionToWhereClause(QFilterCriteria criterion, StringBuilder clause, String column, List values, QFieldMetaData field); + + /*************************************************************************** + * + ***************************************************************************/ + Serializable getFieldValueFromResultSet(QFieldType type, ResultSet resultSet, int i) throws SQLException; + + + /*************************************************************************** + * + ***************************************************************************/ + PreparedStatement executeUpdate(Connection connection, String sql, List params) throws SQLException; + + + /*************************************************************************** + * + ***************************************************************************/ + void executeBatchUpdate(Connection connection, String updateSQL, List> values) throws SQLException; + + + /*************************************************************************** + * + ***************************************************************************/ + List executeInsertForGeneratedIds(Connection connection, String sql, List params, QFieldMetaData primaryKeyField) throws SQLException; + + + /*************************************************************************** + * + ***************************************************************************/ + Integer executeUpdateForRowCount(Connection connection, String sql, Object... params) throws SQLException; + + + /*************************************************************************** + * + ***************************************************************************/ + void executeStatement(PreparedStatement statement, CharSequence sql, ResultSetProcessor processor, Object... params) throws SQLException, QException; + + + /*************************************************************************** + * + ***************************************************************************/ + Integer getPageSize(AbstractActionInput actionInput); + + + /******************************************************************************* + ** + *******************************************************************************/ + @FunctionalInterface + interface ResultSetProcessor + { + /******************************************************************************* + ** + *******************************************************************************/ + void processResultSet(ResultSet rs) throws SQLException, QException; + } +} diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSActionTest.java index 08ebe9a9..9e12c9b5 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSActionTest.java @@ -23,10 +23,13 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.sql.Connection; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.module.rdbms.BaseTest; import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import com.kingsrook.qqq.backend.module.rdbms.strategy.BaseRDBMSActionStrategy; import org.junit.jupiter.api.AfterEach; @@ -42,9 +45,10 @@ public class RDBMSActionTest extends BaseTest @AfterEach void afterEachRDBMSActionTest() { - QueryManager.resetPageSize(); - QueryManager.resetStatistics(); - QueryManager.setCollectStatistics(false); + BaseRDBMSActionStrategy actionStrategy = getBaseRDBMSActionStrategy(); + actionStrategy.setPageSize(BaseRDBMSActionStrategy.DEFAULT_PAGE_SIZE); + actionStrategy.resetStatistics(); + actionStrategy.setCollectStatistics(false); } @@ -59,6 +63,31 @@ public class RDBMSActionTest extends BaseTest + /*************************************************************************** + * + ***************************************************************************/ + protected static BaseRDBMSActionStrategy getBaseRDBMSActionStrategy() + { + RDBMSBackendMetaData backend = (RDBMSBackendMetaData) QContext.getQInstance().getBackend(TestUtils.DEFAULT_BACKEND_NAME); + BaseRDBMSActionStrategy actionStrategy = (BaseRDBMSActionStrategy) backend.getActionStrategy(); + return actionStrategy; + } + + + + /*************************************************************************** + * + ***************************************************************************/ + protected static BaseRDBMSActionStrategy getBaseRDBMSActionStrategyAndActivateCollectingStatistics() + { + BaseRDBMSActionStrategy actionStrategy = getBaseRDBMSActionStrategy(); + actionStrategy.setCollectStatistics(true); + actionStrategy.resetStatistics(); + return actionStrategy; + } + + + /******************************************************************************* ** *******************************************************************************/