From 1062f00ed48dc244d1d9e1e4b1e77b60f45d8d8e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 12 Mar 2024 12:02:36 -0500 Subject: [PATCH 01/10] Add c3p0 connection pooling to RDBMS module (ConnectionManager) --- qqq-backend-module-rdbms/pom.xml | 5 + .../rdbms/actions/RDBMSInsertAction.java | 145 ++--- .../module/rdbms/jdbc/ConnectionManager.java | 136 ++++- .../model/metadata/RDBMSBackendMetaData.java | 32 ++ .../module/rdbms/actions/RDBMSActionTest.java | 1 + .../module/rdbms/jdbc/QueryManagerTest.java | 501 +++++++++--------- 6 files changed, 496 insertions(+), 324 deletions(-) diff --git a/qqq-backend-module-rdbms/pom.xml b/qqq-backend-module-rdbms/pom.xml index 19d778c3..3e9be513 100644 --- a/qqq-backend-module-rdbms/pom.xml +++ b/qqq-backend-module-rdbms/pom.xml @@ -50,6 +50,11 @@ mysql-connector-java 8.0.30 + + com.mchange + c3p0 + 0.10.0 + com.h2database h2 diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java index 2a88d43e..101d96f4 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSInsertAction.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.io.Serializable; import java.sql.Connection; +import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -53,9 +54,12 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte *******************************************************************************/ public InsertOutput execute(InsertInput insertInput) throws QException { - InsertOutput rs = new InsertOutput(); + InsertOutput rs = new InsertOutput(); QTableMetaData table = insertInput.getTable(); + Connection connection = null; + boolean needToCloseConnection = false; + try { List insertableFields = table.getFields().values().stream() @@ -72,8 +76,6 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte List outputRecords = new ArrayList<>(); rs.setRecords(outputRecords); - Connection connection; - boolean needToCloseConnection = false; if(insertInput.getTransaction() != null && insertInput.getTransaction() instanceof RDBMSTransaction rdbmsTransaction) { connection = rdbmsTransaction.getConnection(); @@ -84,87 +86,77 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte needToCloseConnection = true; } - try + for(List page : CollectionUtils.getPages(insertInput.getRecords(), QueryManager.PAGE_SIZE)) { - for(List page : CollectionUtils.getPages(insertInput.getRecords(), QueryManager.PAGE_SIZE)) + String tableName = escapeIdentifier(getTableName(table)); + StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append("(").append(columns).append(") VALUES"); + List params = new ArrayList<>(); + int recordIndex = 0; + + ////////////////////////////////////////////////////// + // for each record in the page: // + // - if it has errors, skip it // + // - else add a "(?,?,...,?)," clause to the INSERT // + // - then add all fields into the params list // + ////////////////////////////////////////////////////// + for(QRecord record : page) { - String tableName = escapeIdentifier(getTableName(table)); - StringBuilder sql = new StringBuilder("INSERT INTO ").append(tableName).append("(").append(columns).append(") VALUES"); - List params = new ArrayList<>(); - int recordIndex = 0; - - ////////////////////////////////////////////////////// - // for each record in the page: // - // - if it has errors, skip it // - // - else add a "(?,?,...,?)," clause to the INSERT // - // - then add all fields into the params list // - ////////////////////////////////////////////////////// - for(QRecord record : page) + if(CollectionUtils.nullSafeHasContents(record.getErrors())) { - if(CollectionUtils.nullSafeHasContents(record.getErrors())) - { - continue; - } - - if(recordIndex++ > 0) - { - sql.append(","); - } - sql.append("(").append(questionMarks).append(")"); - - for(QFieldMetaData field : insertableFields) - { - Serializable value = record.getValue(field.getName()); - value = scrubValue(field, value); - params.add(value); - } - } - - //////////////////////////////////////////////////////////////////////////////////////// - // if all records had errors, copy them to the output, and continue w/o running query // - //////////////////////////////////////////////////////////////////////////////////////// - if(recordIndex == 0) - { - for(QRecord record : page) - { - QRecord outputRecord = new QRecord(record); - outputRecords.add(outputRecord); - } continue; } - Long mark = System.currentTimeMillis(); + if(recordIndex++ > 0) + { + sql.append(","); + } + sql.append("(").append(questionMarks).append(")"); - /////////////////////////////////////////////////////////// - // execute the insert, then foreach record in the input, // - // add it to the output, and set its generated id too. // - /////////////////////////////////////////////////////////// - // todo sql customization - can edit sql and/or param list - // todo - non-serial-id style tables - // todo - other generated values, e.g., createDate... maybe need to re-select? - List idList = QueryManager.executeInsertForGeneratedIds(connection, sql.toString(), params); - int index = 0; + for(QFieldMetaData field : insertableFields) + { + Serializable value = record.getValue(field.getName()); + value = scrubValue(field, value); + params.add(value); + } + } + + //////////////////////////////////////////////////////////////////////////////////////// + // if all records had errors, copy them to the output, and continue w/o running query // + //////////////////////////////////////////////////////////////////////////////////////// + if(recordIndex == 0) + { for(QRecord record : page) { QRecord outputRecord = new QRecord(record); outputRecords.add(outputRecord); - - if(CollectionUtils.nullSafeIsEmpty(record.getErrors())) - { - Integer id = idList.get(index++); - outputRecord.setValue(table.getPrimaryKeyField(), id); - } } + continue; + } - logSQL(sql, params, mark); - } - } - finally - { - if(needToCloseConnection) + Long mark = System.currentTimeMillis(); + + /////////////////////////////////////////////////////////// + // execute the insert, then foreach record in the input, // + // add it to the output, and set its generated id too. // + /////////////////////////////////////////////////////////// + // todo sql customization - can edit sql and/or param list + // todo - non-serial-id style tables + // todo - other generated values, e.g., createDate... maybe need to re-select? + List idList = QueryManager.executeInsertForGeneratedIds(connection, sql.toString(), params); + int index = 0; + for(QRecord record : page) { - connection.close(); + QRecord outputRecord = new QRecord(record); + outputRecords.add(outputRecord); + + if(CollectionUtils.nullSafeIsEmpty(record.getErrors())) + { + Integer id = idList.get(index++); + outputRecord.setValue(table.getPrimaryKeyField(), id); + } } + + logSQL(sql, params, mark); } return rs; @@ -173,6 +165,21 @@ public class RDBMSInsertAction extends AbstractRDBMSAction implements InsertInte { throw new QException("Error executing insert: " + e.getMessage(), e); } + finally + { + if(needToCloseConnection && connection != null) + { + try + { + connection.close(); + } + catch(SQLException se) + { + LOG.error("Error closing database connection", se); + } + } + } + } } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java index 6714979b..3800810f 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java @@ -25,8 +25,12 @@ package com.kingsrook.qqq.backend.module.rdbms.jdbc; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import com.mchange.v2.c3p0.ComboPooledDataSource; /******************************************************************************* @@ -34,32 +38,134 @@ import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaDat *******************************************************************************/ public class ConnectionManager { + private boolean mayUseConnectionPool = true; + + private static Map initedConnectionPool = new HashMap<>(); + private static Map connectionPoolMap = new HashMap<>(); + + private static int usageCounter = 0; + /******************************************************************************* ** *******************************************************************************/ public Connection getConnection(RDBMSBackendMetaData backend) throws SQLException { - String jdbcURL; + usageCounter++; - if(StringUtils.hasContent(backend.getJdbcUrl())) + if(mayUseConnectionPool) { - jdbcURL = backend.getJdbcUrl(); - } - else - { - switch(backend.getVendor()) - { - // TODO aws-mysql-jdbc driver not working when running on AWS - // jdbcURL = "jdbc:mysql:aws://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=CONVERT_TO_NULL"; - case "aurora" -> jdbcURL = "jdbc:mysql://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=convertToNull&useSSL=false"; - case "mysql" -> jdbcURL = "jdbc:mysql://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=convertToNull"; - case "h2" -> jdbcURL = "jdbc:h2:" + backend.getHostName() + ":" + backend.getDatabaseName() + ";MODE=MySQL;DB_CLOSE_DELAY=-1"; - default -> throw new IllegalArgumentException("Unsupported rdbms backend vendor: " + backend.getVendor()); - } + return (getConnectionFromPool(backend)); } + String jdbcURL = getJdbcUrl(backend); return DriverManager.getConnection(jdbcURL, backend.getUsername(), backend.getPassword()); } + + /******************************************************************************* + ** + *******************************************************************************/ + public static void checkPools() + { + try + { + System.out.println("Usages: " + usageCounter); + + for(Map.Entry entry : CollectionUtils.nonNullMap(connectionPoolMap).entrySet()) + { + System.out.println("POOL USAGE: " + entry.getKey() + ": " + entry.getValue().getNumBusyConnections()); + if(entry.getValue().getNumBusyConnections() > 2) + { + System.out.println("break!"); + } + } + } + catch(Exception e) + { + e.printStackTrace(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Connection getConnectionFromPool(RDBMSBackendMetaData backend) throws SQLException + { + try + { + if(!initedConnectionPool.getOrDefault(backend.getName(), false)) + { + // todo - some syncrhonized + ComboPooledDataSource connectionPool = new ComboPooledDataSource(); + connectionPool.setDriverClass(getJdbcDriverClassName(backend)); + connectionPool.setJdbcUrl(getJdbcUrl(backend)); + connectionPool.setUser(backend.getUsername()); + connectionPool.setPassword(backend.getPassword()); + + connectionPool.setTestConnectionOnCheckout(true); + + ////////////////////////////////////////////////////////////////////////// + // useful to debug leaking connections - meant for tests only though... // + ////////////////////////////////////////////////////////////////////////// + // connectionPool.setDebugUnreturnedConnectionStackTraces(true); + // connectionPool.setUnreturnedConnectionTimeout(10); + + connectionPoolMap.put(backend.getName(), connectionPool); + initedConnectionPool.put(backend.getName(), true); + } + + return (connectionPoolMap.get(backend.getName()).getConnection()); + } + catch(Exception e) + { + throw (new SQLException("Error getting connection from pool", e)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getJdbcDriverClassName(RDBMSBackendMetaData backend) + { + if(StringUtils.hasContent(backend.getJdbcDriverClassName())) + { + return backend.getJdbcDriverClassName(); + } + + return switch(backend.getVendor()) + { + case "mysql", "aurora" -> "com.mysql.cj.jdbc.Driver"; + case "h2" -> "org.h2.Driver"; + default -> throw (new IllegalStateException("We do not know what jdbc driver to use for vendor name [" + backend.getVendor() + "]. Try setting jdbcDriverClassName in your backend meta data.")); + }; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getJdbcUrl(RDBMSBackendMetaData backend) + { + if(StringUtils.hasContent(backend.getJdbcUrl())) + { + return backend.getJdbcUrl(); + } + + return switch(backend.getVendor()) + { + // TODO aws-mysql-jdbc driver not working when running on AWS + // jdbcURL = "jdbc:mysql:aws://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=CONVERT_TO_NULL"; + case "aurora" -> "jdbc:mysql://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=convertToNull&useSSL=false"; + case "mysql" -> "jdbc:mysql://" + backend.getHostName() + ":" + backend.getPort() + "/" + backend.getDatabaseName() + "?rewriteBatchedStatements=true&zeroDateTimeBehavior=convertToNull"; + case "h2" -> "jdbc:h2:" + backend.getHostName() + ":" + backend.getDatabaseName() + ";MODE=MySQL;DB_CLOSE_DELAY=-1"; + default -> throw new IllegalArgumentException("Unsupported rdbms backend vendor: " + backend.getVendor()); + }; + } + } 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 6ecc6e8c..a86a6f45 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 @@ -40,6 +40,7 @@ public class RDBMSBackendMetaData extends QBackendMetaData private String password; private String jdbcUrl; + private String jdbcDriverClassName; @@ -314,4 +315,35 @@ public class RDBMSBackendMetaData extends QBackendMetaData return (this); } + + /******************************************************************************* + ** Getter for jdbcDriverClassName + *******************************************************************************/ + public String getJdbcDriverClassName() + { + return (this.jdbcDriverClassName); + } + + + + /******************************************************************************* + ** Setter for jdbcDriverClassName + *******************************************************************************/ + public void setJdbcDriverClassName(String jdbcDriverClassName) + { + this.jdbcDriverClassName = jdbcDriverClassName; + } + + + + /******************************************************************************* + ** Fluent setter for jdbcDriverClassName + *******************************************************************************/ + public RDBMSBackendMetaData withJdbcDriverClassName(String jdbcDriverClassName) + { + this.jdbcDriverClassName = jdbcDriverClassName; + return (this); + } + + } 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 e9b490fc..08ebe9a9 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 @@ -67,5 +67,6 @@ public class RDBMSActionTest extends BaseTest ConnectionManager connectionManager = new ConnectionManager(); Connection connection = connectionManager.getConnection(TestUtils.defineBackend()); QueryManager.executeStatement(connection, sql, resultSetProcessor); + connection.close(); } } diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java index 33934721..232be3b7 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java @@ -64,18 +64,20 @@ class QueryManagerTest extends BaseTest @BeforeEach void beforeEach() throws SQLException { - Connection connection = getConnection(); - QueryManager.executeUpdate(connection, """ - CREATE TABLE test_table - ( - int_col INTEGER, - datetime_col DATETIME, - char_col CHAR(1), - date_col DATE, - time_col TIME, - long_col LONG - ) - """); + try(Connection connection = getConnection()) + { + QueryManager.executeUpdate(connection, """ + CREATE TABLE test_table + ( + int_col INTEGER, + datetime_col DATETIME, + char_col CHAR(1), + date_col DATE, + time_col TIME, + long_col LONG + ) + """); + } } @@ -86,8 +88,10 @@ class QueryManagerTest extends BaseTest @AfterEach void afterEach() throws SQLException { - Connection connection = getConnection(); - QueryManager.executeUpdate(connection, "DROP TABLE test_table"); + try(Connection connection = getConnection()) + { + QueryManager.executeUpdate(connection, "DROP TABLE test_table"); + } } @@ -109,56 +113,58 @@ class QueryManagerTest extends BaseTest @Test void testBindParams() throws SQLException { - long ctMillis = System.currentTimeMillis(); - Connection connection = getConnection(); - PreparedStatement ps = connection.prepareStatement("UPDATE test_table SET int_col = ? WHERE int_col > 0"); - - /////////////////////////////////////////////////////////////////////////////// - // these calls - we just want to assert that they don't throw any exceptions // - /////////////////////////////////////////////////////////////////////////////// - QueryManager.bindParamObject(ps, 1, (short) 1); - QueryManager.bindParamObject(ps, 1, (long) 1); - QueryManager.bindParamObject(ps, 1, true); - QueryManager.bindParamObject(ps, 1, BigDecimal.ONE); - QueryManager.bindParamObject(ps, 1, "hello".getBytes(StandardCharsets.UTF_8)); - QueryManager.bindParamObject(ps, 1, new Timestamp(ctMillis)); - QueryManager.bindParamObject(ps, 1, new Date(ctMillis)); - QueryManager.bindParamObject(ps, 1, new GregorianCalendar()); - QueryManager.bindParamObject(ps, 1, LocalDate.now()); - QueryManager.bindParamObject(ps, 1, OffsetDateTime.now()); - QueryManager.bindParamObject(ps, 1, LocalDateTime.now()); - QueryManager.bindParamObject(ps, 1, AutomationStatus.PENDING_INSERT_AUTOMATIONS); - - assertThrows(SQLException.class, () -> + try(Connection connection = getConnection()) { - QueryManager.bindParamObject(ps, 1, new Object()); - }); + long ctMillis = System.currentTimeMillis(); + PreparedStatement ps = connection.prepareStatement("UPDATE test_table SET int_col = ? WHERE int_col > 0"); - QueryManager.bindParam(ps, 1, (Integer) null); - QueryManager.bindParam(ps, 1, (Boolean) null); - QueryManager.bindParam(ps, 1, (BigDecimal) null); - QueryManager.bindParam(ps, 1, (byte[]) null); - QueryManager.bindParam(ps, 1, (Timestamp) null); - QueryManager.bindParam(ps, 1, (String) null); - QueryManager.bindParam(ps, 1, (Date) null); - QueryManager.bindParam(ps, 1, (GregorianCalendar) null); - QueryManager.bindParam(ps, 1, (LocalDate) null); - QueryManager.bindParam(ps, 1, (LocalDateTime) null); + /////////////////////////////////////////////////////////////////////////////// + // these calls - we just want to assert that they don't throw any exceptions // + /////////////////////////////////////////////////////////////////////////////// + QueryManager.bindParamObject(ps, 1, (short) 1); + QueryManager.bindParamObject(ps, 1, (long) 1); + QueryManager.bindParamObject(ps, 1, true); + QueryManager.bindParamObject(ps, 1, BigDecimal.ONE); + QueryManager.bindParamObject(ps, 1, "hello".getBytes(StandardCharsets.UTF_8)); + QueryManager.bindParamObject(ps, 1, new Timestamp(ctMillis)); + QueryManager.bindParamObject(ps, 1, new Date(ctMillis)); + QueryManager.bindParamObject(ps, 1, new GregorianCalendar()); + QueryManager.bindParamObject(ps, 1, LocalDate.now()); + QueryManager.bindParamObject(ps, 1, OffsetDateTime.now()); + QueryManager.bindParamObject(ps, 1, LocalDateTime.now()); + QueryManager.bindParamObject(ps, 1, AutomationStatus.PENDING_INSERT_AUTOMATIONS); - QueryManager.bindParam(ps, 1, 1); - QueryManager.bindParam(ps, 1, true); - QueryManager.bindParam(ps, 1, BigDecimal.ONE); - QueryManager.bindParam(ps, 1, "hello".getBytes(StandardCharsets.UTF_8)); - QueryManager.bindParam(ps, 1, new Timestamp(ctMillis)); - QueryManager.bindParam(ps, 1, "hello"); - QueryManager.bindParam(ps, 1, new Date(ctMillis)); - QueryManager.bindParam(ps, 1, new GregorianCalendar()); - QueryManager.bindParam(ps, 1, LocalDate.now()); - QueryManager.bindParam(ps, 1, LocalDateTime.now()); + assertThrows(SQLException.class, () -> + { + QueryManager.bindParamObject(ps, 1, new Object()); + }); - //////////////////////////////////////////////////////////////////////////////////////////////// - // originally longs were being downgraded to int when binding, so, verify that doesn't happen // - //////////////////////////////////////////////////////////////////////////////////////////////// + QueryManager.bindParam(ps, 1, (Integer) null); + QueryManager.bindParam(ps, 1, (Boolean) null); + QueryManager.bindParam(ps, 1, (BigDecimal) null); + QueryManager.bindParam(ps, 1, (byte[]) null); + QueryManager.bindParam(ps, 1, (Timestamp) null); + QueryManager.bindParam(ps, 1, (String) null); + QueryManager.bindParam(ps, 1, (Date) null); + QueryManager.bindParam(ps, 1, (GregorianCalendar) null); + QueryManager.bindParam(ps, 1, (LocalDate) null); + QueryManager.bindParam(ps, 1, (LocalDateTime) null); + + QueryManager.bindParam(ps, 1, 1); + QueryManager.bindParam(ps, 1, true); + QueryManager.bindParam(ps, 1, BigDecimal.ONE); + QueryManager.bindParam(ps, 1, "hello".getBytes(StandardCharsets.UTF_8)); + QueryManager.bindParam(ps, 1, new Timestamp(ctMillis)); + QueryManager.bindParam(ps, 1, "hello"); + QueryManager.bindParam(ps, 1, new Date(ctMillis)); + QueryManager.bindParam(ps, 1, new GregorianCalendar()); + QueryManager.bindParam(ps, 1, LocalDate.now()); + QueryManager.bindParam(ps, 1, LocalDateTime.now()); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // originally longs were being downgraded to int when binding, so, verify that doesn't happen // + //////////////////////////////////////////////////////////////////////////////////////////////// + } } @@ -169,19 +175,21 @@ class QueryManagerTest extends BaseTest @Test void testLongBinding() throws SQLException { - Long biggerThanMaxInteger = 2147483648L; + try(Connection connection = getConnection()) + { + Long biggerThanMaxInteger = 2147483648L; - Connection connection = getConnection(); - PreparedStatement ps = connection.prepareStatement("INSERT INTO test_table (long_col) VALUES (?)"); - QueryManager.bindParam(ps, 1, biggerThanMaxInteger); - ps.execute(); + PreparedStatement ps = connection.prepareStatement("INSERT INTO test_table (long_col) VALUES (?)"); + QueryManager.bindParam(ps, 1, biggerThanMaxInteger); + ps.execute(); - ps = connection.prepareStatement("SELECT long_col FROM test_table WHERE long_col = ?"); - QueryManager.bindParam(ps, 1, biggerThanMaxInteger); - ps.execute(); - ResultSet rs = ps.getResultSet(); - assertTrue(rs.next()); - assertEquals(biggerThanMaxInteger, QueryManager.getLong(rs, "long_col")); + ps = connection.prepareStatement("SELECT long_col FROM test_table WHERE long_col = ?"); + QueryManager.bindParam(ps, 1, biggerThanMaxInteger); + ps.execute(); + ResultSet rs = ps.getResultSet(); + assertTrue(rs.next()); + assertEquals(biggerThanMaxInteger, QueryManager.getLong(rs, "long_col")); + } } @@ -192,43 +200,45 @@ class QueryManagerTest extends BaseTest @Test void testGetValueMethods() throws SQLException { - Long biggerThanMaxInteger = 2147483648L; + try(Connection connection = getConnection()) + { + Long biggerThanMaxInteger = 2147483648L; - Connection connection = getConnection(); - QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, datetime_col, char_col, long_col) VALUES (1, now(), 'A', " + biggerThanMaxInteger + ")"); - PreparedStatement preparedStatement = connection.prepareStatement("SELECT int_col, datetime_col, char_col, long_col from test_table"); - preparedStatement.execute(); - ResultSet rs = preparedStatement.getResultSet(); - rs.next(); + QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, datetime_col, char_col, long_col) VALUES (1, now(), 'A', " + biggerThanMaxInteger + ")"); + PreparedStatement preparedStatement = connection.prepareStatement("SELECT int_col, datetime_col, char_col, long_col from test_table"); + preparedStatement.execute(); + ResultSet rs = preparedStatement.getResultSet(); + rs.next(); - assertEquals(1, QueryManager.getInteger(rs, "int_col")); - assertEquals(1, QueryManager.getInteger(rs, 1)); - assertEquals(1L, QueryManager.getLong(rs, "int_col")); - assertEquals(1L, QueryManager.getLong(rs, 1)); - assertArrayEquals(new byte[] { 0, 0, 0, 1 }, QueryManager.getByteArray(rs, "int_col")); - assertArrayEquals(new byte[] { 0, 0, 0, 1 }, QueryManager.getByteArray(rs, 1)); - assertEquals(1, QueryManager.getObject(rs, "int_col")); - assertEquals(1, QueryManager.getObject(rs, 1)); - assertEquals(BigDecimal.ONE, QueryManager.getBigDecimal(rs, "int_col")); - assertEquals(BigDecimal.ONE, QueryManager.getBigDecimal(rs, 1)); - assertEquals(true, QueryManager.getBoolean(rs, "int_col")); - assertEquals(true, QueryManager.getBoolean(rs, 1)); - assertNotNull(QueryManager.getDate(rs, "datetime_col")); - assertNotNull(QueryManager.getDate(rs, 2)); - assertNotNull(QueryManager.getCalendar(rs, "datetime_col")); - assertNotNull(QueryManager.getCalendar(rs, 2)); - assertNotNull(QueryManager.getLocalDate(rs, "datetime_col")); - assertNotNull(QueryManager.getLocalDate(rs, 2)); - assertNotNull(QueryManager.getLocalDateTime(rs, "datetime_col")); - assertNotNull(QueryManager.getLocalDateTime(rs, 2)); - assertNotNull(QueryManager.getOffsetDateTime(rs, "datetime_col")); - assertNotNull(QueryManager.getOffsetDateTime(rs, 2)); - assertNotNull(QueryManager.getTimestamp(rs, "datetime_col")); - assertNotNull(QueryManager.getTimestamp(rs, 2)); - assertEquals("A", QueryManager.getObject(rs, "char_col")); - assertEquals("A", QueryManager.getObject(rs, 3)); - assertEquals(biggerThanMaxInteger, QueryManager.getLong(rs, "long_col")); - assertEquals(biggerThanMaxInteger, QueryManager.getLong(rs, 4)); + assertEquals(1, QueryManager.getInteger(rs, "int_col")); + assertEquals(1, QueryManager.getInteger(rs, 1)); + assertEquals(1L, QueryManager.getLong(rs, "int_col")); + assertEquals(1L, QueryManager.getLong(rs, 1)); + assertArrayEquals(new byte[] { 0, 0, 0, 1 }, QueryManager.getByteArray(rs, "int_col")); + assertArrayEquals(new byte[] { 0, 0, 0, 1 }, QueryManager.getByteArray(rs, 1)); + assertEquals(1, QueryManager.getObject(rs, "int_col")); + assertEquals(1, QueryManager.getObject(rs, 1)); + assertEquals(BigDecimal.ONE, QueryManager.getBigDecimal(rs, "int_col")); + assertEquals(BigDecimal.ONE, QueryManager.getBigDecimal(rs, 1)); + assertEquals(true, QueryManager.getBoolean(rs, "int_col")); + assertEquals(true, QueryManager.getBoolean(rs, 1)); + assertNotNull(QueryManager.getDate(rs, "datetime_col")); + assertNotNull(QueryManager.getDate(rs, 2)); + assertNotNull(QueryManager.getCalendar(rs, "datetime_col")); + assertNotNull(QueryManager.getCalendar(rs, 2)); + assertNotNull(QueryManager.getLocalDate(rs, "datetime_col")); + assertNotNull(QueryManager.getLocalDate(rs, 2)); + assertNotNull(QueryManager.getLocalDateTime(rs, "datetime_col")); + assertNotNull(QueryManager.getLocalDateTime(rs, 2)); + assertNotNull(QueryManager.getOffsetDateTime(rs, "datetime_col")); + assertNotNull(QueryManager.getOffsetDateTime(rs, 2)); + assertNotNull(QueryManager.getTimestamp(rs, "datetime_col")); + assertNotNull(QueryManager.getTimestamp(rs, 2)); + assertEquals("A", QueryManager.getObject(rs, "char_col")); + assertEquals("A", QueryManager.getObject(rs, 3)); + assertEquals(biggerThanMaxInteger, QueryManager.getLong(rs, "long_col")); + assertEquals(biggerThanMaxInteger, QueryManager.getLong(rs, 4)); + } } @@ -239,39 +249,41 @@ class QueryManagerTest extends BaseTest @Test void testGetValueMethodsReturningNull() throws SQLException { - Connection connection = getConnection(); - QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, datetime_col, char_col) VALUES (null, null, null)"); - PreparedStatement preparedStatement = connection.prepareStatement("SELECT * from test_table"); - preparedStatement.execute(); - ResultSet rs = preparedStatement.getResultSet(); - rs.next(); + try(Connection connection = getConnection()) + { + QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, datetime_col, char_col) VALUES (null, null, null)"); + PreparedStatement preparedStatement = connection.prepareStatement("SELECT * from test_table"); + preparedStatement.execute(); + ResultSet rs = preparedStatement.getResultSet(); + rs.next(); - assertNull(QueryManager.getInteger(rs, "int_col")); - assertNull(QueryManager.getInteger(rs, 1)); - assertNull(QueryManager.getLong(rs, "int_col")); - assertNull(QueryManager.getLong(rs, 1)); - assertNull(QueryManager.getByteArray(rs, "int_col")); - assertNull(QueryManager.getByteArray(rs, 1)); - assertNull(QueryManager.getObject(rs, "int_col")); - assertNull(QueryManager.getObject(rs, 1)); - assertNull(QueryManager.getBigDecimal(rs, "int_col")); - assertNull(QueryManager.getBigDecimal(rs, 1)); - assertNull(QueryManager.getBoolean(rs, "int_col")); - assertNull(QueryManager.getBoolean(rs, 1)); - assertNull(QueryManager.getDate(rs, "datetime_col")); - assertNull(QueryManager.getDate(rs, 2)); - assertNull(QueryManager.getCalendar(rs, "datetime_col")); - assertNull(QueryManager.getCalendar(rs, 2)); - assertNull(QueryManager.getLocalDate(rs, "datetime_col")); - assertNull(QueryManager.getLocalDate(rs, 2)); - assertNull(QueryManager.getLocalDateTime(rs, "datetime_col")); - assertNull(QueryManager.getLocalDateTime(rs, 2)); - assertNull(QueryManager.getOffsetDateTime(rs, "datetime_col")); - assertNull(QueryManager.getOffsetDateTime(rs, 2)); - assertNull(QueryManager.getTimestamp(rs, "datetime_col")); - assertNull(QueryManager.getTimestamp(rs, 2)); - assertNull(QueryManager.getObject(rs, "char_col")); - assertNull(QueryManager.getObject(rs, 3)); + assertNull(QueryManager.getInteger(rs, "int_col")); + assertNull(QueryManager.getInteger(rs, 1)); + assertNull(QueryManager.getLong(rs, "int_col")); + assertNull(QueryManager.getLong(rs, 1)); + assertNull(QueryManager.getByteArray(rs, "int_col")); + assertNull(QueryManager.getByteArray(rs, 1)); + assertNull(QueryManager.getObject(rs, "int_col")); + assertNull(QueryManager.getObject(rs, 1)); + assertNull(QueryManager.getBigDecimal(rs, "int_col")); + assertNull(QueryManager.getBigDecimal(rs, 1)); + assertNull(QueryManager.getBoolean(rs, "int_col")); + assertNull(QueryManager.getBoolean(rs, 1)); + assertNull(QueryManager.getDate(rs, "datetime_col")); + assertNull(QueryManager.getDate(rs, 2)); + assertNull(QueryManager.getCalendar(rs, "datetime_col")); + assertNull(QueryManager.getCalendar(rs, 2)); + assertNull(QueryManager.getLocalDate(rs, "datetime_col")); + assertNull(QueryManager.getLocalDate(rs, 2)); + assertNull(QueryManager.getLocalDateTime(rs, "datetime_col")); + assertNull(QueryManager.getLocalDateTime(rs, 2)); + assertNull(QueryManager.getOffsetDateTime(rs, "datetime_col")); + assertNull(QueryManager.getOffsetDateTime(rs, 2)); + assertNull(QueryManager.getTimestamp(rs, "datetime_col")); + assertNull(QueryManager.getTimestamp(rs, 2)); + assertNull(QueryManager.getObject(rs, "char_col")); + assertNull(QueryManager.getObject(rs, 3)); + } } @@ -283,37 +295,39 @@ class QueryManagerTest extends BaseTest @Test void testLocalDate() throws SQLException { - Connection connection = getConnection(); - QueryManager.executeUpdate(connection, "INSERT INTO test_table (date_col) VALUES (?)", LocalDate.of(2013, Month.OCTOBER, 1)); + try(Connection connection = getConnection()) + { + QueryManager.executeUpdate(connection, "INSERT INTO test_table (date_col) VALUES (?)", LocalDate.of(2013, Month.OCTOBER, 1)); - PreparedStatement preparedStatement = connection.prepareStatement("SELECT date_col from test_table"); - preparedStatement.execute(); - ResultSet rs = preparedStatement.getResultSet(); - rs.next(); + PreparedStatement preparedStatement = connection.prepareStatement("SELECT date_col from test_table"); + preparedStatement.execute(); + ResultSet rs = preparedStatement.getResultSet(); + rs.next(); - Date date = QueryManager.getDate(rs, 1); - assertEquals(1, date.getDate(), "Date value"); - assertEquals(Month.OCTOBER.getValue(), date.getMonth() + 1, "Month value"); - assertEquals(2013, date.getYear() + 1900, "Year value"); + Date date = QueryManager.getDate(rs, 1); + assertEquals(1, date.getDate(), "Date value"); + assertEquals(Month.OCTOBER.getValue(), date.getMonth() + 1, "Month value"); + assertEquals(2013, date.getYear() + 1900, "Year value"); - LocalDate localDate = QueryManager.getLocalDate(rs, 1); - assertEquals(1, localDate.getDayOfMonth(), "Date value"); - assertEquals(Month.OCTOBER, localDate.getMonth(), "Month value"); - assertEquals(2013, localDate.getYear(), "Year value"); + LocalDate localDate = QueryManager.getLocalDate(rs, 1); + assertEquals(1, localDate.getDayOfMonth(), "Date value"); + assertEquals(Month.OCTOBER, localDate.getMonth(), "Month value"); + assertEquals(2013, localDate.getYear(), "Year value"); - LocalDateTime localDateTime = QueryManager.getLocalDateTime(rs, 1); - assertEquals(1, localDateTime.getDayOfMonth(), "Date value"); - assertEquals(Month.OCTOBER, localDateTime.getMonth(), "Month value"); - assertEquals(2013, localDateTime.getYear(), "Year value"); - assertEquals(0, localDateTime.getHour(), "Hour value"); - assertEquals(0, localDateTime.getMinute(), "Minute value"); + LocalDateTime localDateTime = QueryManager.getLocalDateTime(rs, 1); + assertEquals(1, localDateTime.getDayOfMonth(), "Date value"); + assertEquals(Month.OCTOBER, localDateTime.getMonth(), "Month value"); + assertEquals(2013, localDateTime.getYear(), "Year value"); + assertEquals(0, localDateTime.getHour(), "Hour value"); + assertEquals(0, localDateTime.getMinute(), "Minute value"); - OffsetDateTime offsetDateTime = QueryManager.getOffsetDateTime(rs, 1); - assertEquals(1, offsetDateTime.getDayOfMonth(), "Date value"); - assertEquals(Month.OCTOBER, offsetDateTime.getMonth(), "Month value"); - assertEquals(2013, offsetDateTime.getYear(), "Year value"); - assertEquals(0, offsetDateTime.getHour(), "Hour value"); - assertEquals(0, offsetDateTime.getMinute(), "Minute value"); + OffsetDateTime offsetDateTime = QueryManager.getOffsetDateTime(rs, 1); + assertEquals(1, offsetDateTime.getDayOfMonth(), "Date value"); + assertEquals(Month.OCTOBER, offsetDateTime.getMonth(), "Month value"); + assertEquals(2013, offsetDateTime.getYear(), "Year value"); + assertEquals(0, offsetDateTime.getHour(), "Hour value"); + assertEquals(0, offsetDateTime.getMinute(), "Minute value"); + } } @@ -324,47 +338,48 @@ class QueryManagerTest extends BaseTest @Test void testLocalTime() throws SQLException { - Connection connection = getConnection(); + try(Connection connection = getConnection()) + { + //////////////////////////////////// + // insert one just hour & minutes // + //////////////////////////////////// + QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, time_col) VALUES (?, ?)", 1, LocalTime.of(10, 42)); - //////////////////////////////////// - // insert one just hour & minutes // - //////////////////////////////////// - QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, time_col) VALUES (?, ?)", 1, LocalTime.of(10, 42)); + PreparedStatement preparedStatement = connection.prepareStatement("SELECT time_col from test_table where int_col=1"); + preparedStatement.execute(); + ResultSet rs = preparedStatement.getResultSet(); + rs.next(); - PreparedStatement preparedStatement = connection.prepareStatement("SELECT time_col from test_table where int_col=1"); - preparedStatement.execute(); - ResultSet rs = preparedStatement.getResultSet(); - rs.next(); + LocalTime localTime = QueryManager.getLocalTime(rs, 1); + assertEquals(10, localTime.getHour(), "Hour value"); + assertEquals(42, localTime.getMinute(), "Minute value"); + assertEquals(0, localTime.getSecond(), "Second value"); - LocalTime localTime = QueryManager.getLocalTime(rs, 1); - assertEquals(10, localTime.getHour(), "Hour value"); - assertEquals(42, localTime.getMinute(), "Minute value"); - assertEquals(0, localTime.getSecond(), "Second value"); + localTime = QueryManager.getLocalTime(rs, "time_col"); + assertEquals(10, localTime.getHour(), "Hour value"); + assertEquals(42, localTime.getMinute(), "Minute value"); + assertEquals(0, localTime.getSecond(), "Second value"); - localTime = QueryManager.getLocalTime(rs, "time_col"); - assertEquals(10, localTime.getHour(), "Hour value"); - assertEquals(42, localTime.getMinute(), "Minute value"); - assertEquals(0, localTime.getSecond(), "Second value"); + ///////////////////////////////// + // now insert one with seconds // + ///////////////////////////////// + QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, time_col) VALUES (?, ?)", 2, LocalTime.of(10, 42, 59)); - ///////////////////////////////// - // now insert one with seconds // - ///////////////////////////////// - QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, time_col) VALUES (?, ?)", 2, LocalTime.of(10, 42, 59)); + preparedStatement = connection.prepareStatement("SELECT time_col from test_table where int_col=2"); + preparedStatement.execute(); + rs = preparedStatement.getResultSet(); + rs.next(); - preparedStatement = connection.prepareStatement("SELECT time_col from test_table where int_col=2"); - preparedStatement.execute(); - rs = preparedStatement.getResultSet(); - rs.next(); + localTime = QueryManager.getLocalTime(rs, 1); + assertEquals(10, localTime.getHour(), "Hour value"); + assertEquals(42, localTime.getMinute(), "Minute value"); + assertEquals(59, localTime.getSecond(), "Second value"); - localTime = QueryManager.getLocalTime(rs, 1); - assertEquals(10, localTime.getHour(), "Hour value"); - assertEquals(42, localTime.getMinute(), "Minute value"); - assertEquals(59, localTime.getSecond(), "Second value"); - - localTime = QueryManager.getLocalTime(rs, "time_col"); - assertEquals(10, localTime.getHour(), "Hour value"); - assertEquals(42, localTime.getMinute(), "Minute value"); - assertEquals(59, localTime.getSecond(), "Second value"); + localTime = QueryManager.getLocalTime(rs, "time_col"); + assertEquals(10, localTime.getHour(), "Hour value"); + assertEquals(42, localTime.getMinute(), "Minute value"); + assertEquals(59, localTime.getSecond(), "Second value"); + } } @@ -375,27 +390,29 @@ class QueryManagerTest extends BaseTest @Test void testExecuteStatementForSingleValue() throws SQLException { - Connection connection = getConnection(); - QueryManager.executeUpdate(connection, """ - INSERT INTO test_table - ( int_col, datetime_col, char_col, date_col, time_col ) - VALUES - ( 47, '2022-08-10 19:22:08', 'Q', '2022-08-10', '19:22:08') - """); - assertEquals(null, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT int_col FROM test_table WHERE int_col = -1")); - assertEquals(1, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT COUNT(*) FROM test_table")); - assertEquals(47, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT int_col FROM test_table")); - assertEquals("Q", QueryManager.executeStatementForSingleValue(connection, String.class, "SELECT char_col FROM test_table")); - assertEquals(new BigDecimal("1.1"), QueryManager.executeStatementForSingleValue(connection, BigDecimal.class, "SELECT 1.1 FROM test_table")); - assertEquals(1, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT 1.1 FROM test_table")); + try(Connection connection = getConnection()) + { + QueryManager.executeUpdate(connection, """ + INSERT INTO test_table + ( int_col, datetime_col, char_col, date_col, time_col ) + VALUES + ( 47, '2022-08-10 19:22:08', 'Q', '2022-08-10', '19:22:08') + """); + assertEquals(null, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT int_col FROM test_table WHERE int_col = -1")); + assertEquals(1, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT COUNT(*) FROM test_table")); + assertEquals(47, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT int_col FROM test_table")); + assertEquals("Q", QueryManager.executeStatementForSingleValue(connection, String.class, "SELECT char_col FROM test_table")); + assertEquals(new BigDecimal("1.1"), QueryManager.executeStatementForSingleValue(connection, BigDecimal.class, "SELECT 1.1 FROM test_table")); + assertEquals(1, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT 1.1 FROM test_table")); - QueryManager.executeUpdate(connection, """ - INSERT INTO test_table - ( int_col, datetime_col, char_col, date_col, time_col ) - VALUES - ( null, null, null, null, null) - """); - assertEquals(null, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT int_col FROM test_table WHERE int_col IS NULL")); + QueryManager.executeUpdate(connection, """ + INSERT INTO test_table + ( int_col, datetime_col, char_col, date_col, time_col ) + VALUES + ( null, null, null, null, null) + """); + assertEquals(null, QueryManager.executeStatementForSingleValue(connection, Integer.class, "SELECT int_col FROM test_table WHERE int_col IS NULL")); + } } @@ -406,17 +423,19 @@ class QueryManagerTest extends BaseTest @Test void testQueryForSimpleEntity() throws SQLException { - Connection connection = getConnection(); - QueryManager.executeUpdate(connection, """ - INSERT INTO test_table - ( int_col, datetime_col, char_col, date_col, time_col ) - VALUES - ( 47, '2022-08-10 19:22:08', 'Q', '2022-08-10', '19:22:08') - """); - SimpleEntity simpleEntity = QueryManager.executeStatementForSimpleEntity(connection, "SELECT * FROM test_table"); - assertNotNull(simpleEntity); - assertEquals(47, simpleEntity.get("INT_COL")); - assertEquals("Q", simpleEntity.get("CHAR_COL")); + try(Connection connection = getConnection()) + { + QueryManager.executeUpdate(connection, """ + INSERT INTO test_table + ( int_col, datetime_col, char_col, date_col, time_col ) + VALUES + ( 47, '2022-08-10 19:22:08', 'Q', '2022-08-10', '19:22:08') + """); + SimpleEntity simpleEntity = QueryManager.executeStatementForSimpleEntity(connection, "SELECT * FROM test_table"); + assertNotNull(simpleEntity); + assertEquals(47, simpleEntity.get("INT_COL")); + assertEquals("Q", simpleEntity.get("CHAR_COL")); + } } @@ -427,17 +446,19 @@ class QueryManagerTest extends BaseTest @Test void testQueryForRows() throws SQLException { - Connection connection = getConnection(); - QueryManager.executeUpdate(connection, """ - INSERT INTO test_table - ( int_col, datetime_col, char_col, date_col, time_col ) - VALUES - ( 47, '2022-08-10 19:22:08', 'Q', '2022-08-10', '19:22:08') - """); - List> rows = QueryManager.executeStatementForRows(connection, "SELECT * FROM test_table"); - assertNotNull(rows); - assertEquals(47, rows.get(0).get("INT_COL")); - assertEquals("Q", rows.get(0).get("CHAR_COL")); + try(Connection connection = getConnection()) + { + QueryManager.executeUpdate(connection, """ + INSERT INTO test_table + ( int_col, datetime_col, char_col, date_col, time_col ) + VALUES + ( 47, '2022-08-10 19:22:08', 'Q', '2022-08-10', '19:22:08') + """); + List> rows = QueryManager.executeStatementForRows(connection, "SELECT * FROM test_table"); + assertNotNull(rows); + assertEquals(47, rows.get(0).get("INT_COL")); + assertEquals("Q", rows.get(0).get("CHAR_COL")); + } } } \ No newline at end of file From 7e34b97998f89509eb7d67795ee0d6dfce54347c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 12 Mar 2024 13:53:47 -0500 Subject: [PATCH 02/10] jdbc Connections in try-with-resources (so they close and return to connection pool) --- .../qqq/backend/javalin/TestUtils.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index 430f72ae..12c1c194 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -106,16 +106,19 @@ public class TestUtils @SuppressWarnings("unchecked") public static void primeTestDatabase() throws Exception { - ConnectionManager connectionManager = new ConnectionManager(); - Connection connection = connectionManager.getConnection(TestUtils.defineDefaultH2Backend()); - InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql"); - assertNotNull(primeTestDatabaseSqlStream); - List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); - lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList(); - String joinedSQL = String.join("\n", lines); - for(String sql : joinedSQL.split(";")) + ConnectionManager connectionManager = new ConnectionManager(); + + try(Connection connection = connectionManager.getConnection(TestUtils.defineDefaultH2Backend())) { - QueryManager.executeUpdate(connection, sql); + InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql"); + assertNotNull(primeTestDatabaseSqlStream); + List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); + lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList(); + String joinedSQL = String.join("\n", lines); + for(String sql : joinedSQL.split(";")) + { + QueryManager.executeUpdate(connection, sql); + } } } @@ -128,8 +131,10 @@ public class TestUtils public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception { ConnectionManager connectionManager = new ConnectionManager(); - Connection connection = connectionManager.getConnection(defineDefaultH2Backend()); - QueryManager.executeStatement(connection, sql, resultSetProcessor); + try(Connection connection = connectionManager.getConnection(defineDefaultH2Backend())) + { + QueryManager.executeStatement(connection, sql, resultSetProcessor); + } } From 71ecde74df7fe897637ae6cd20f9cef9eea98a64 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 12 Mar 2024 14:28:46 -0500 Subject: [PATCH 03/10] jdbc Connections in try-with-resources (so they close and return to connection pool) --- .../qqq/frontend/picocli/TestUtils.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java index 847dc9d8..cb226925 100644 --- a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java +++ b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java @@ -60,16 +60,18 @@ public class TestUtils @SuppressWarnings("unchecked") public static void primeTestDatabase() throws Exception { - ConnectionManager connectionManager = new ConnectionManager(); - Connection connection = connectionManager.getConnection(TestUtils.defineBackend()); - InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql"); - assertNotNull(primeTestDatabaseSqlStream); - List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); - lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList(); - String joinedSQL = String.join("\n", lines); - for(String sql : joinedSQL.split(";")) + ConnectionManager connectionManager = new ConnectionManager(); + try(Connection connection = connectionManager.getConnection(TestUtils.defineBackend())) { - QueryManager.executeUpdate(connection, sql); + InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql"); + assertNotNull(primeTestDatabaseSqlStream); + List lines = (List) IOUtils.readLines(primeTestDatabaseSqlStream); + lines = lines.stream().filter(line -> !line.startsWith("-- ")).toList(); + String joinedSQL = String.join("\n", lines); + for(String sql : joinedSQL.split(";")) + { + QueryManager.executeUpdate(connection, sql); + } } } @@ -82,8 +84,10 @@ public class TestUtils public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception { ConnectionManager connectionManager = new ConnectionManager(); - Connection connection = connectionManager.getConnection(defineBackend()); - QueryManager.executeStatement(connection, sql, resultSetProcessor); + try(Connection connection = connectionManager.getConnection(defineBackend())) + { + QueryManager.executeStatement(connection, sql, resultSetProcessor); + } } From f9cca885ed8b18de339f26f91181f7d6ad48009f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 5 Jun 2024 15:23:02 -0500 Subject: [PATCH 04/10] checkpoint - working version of c3p0 connection pooling, and read-only database meta-data connections (per query hint) --- .../core/actions/reporting/ExportAction.java | 4 +- .../reporting/GenerateReportAction.java | 4 +- .../values/QPossibleValueTranslator.java | 44 +-- .../core/model/actions/tables/QueryHint.java | 38 ++ .../tables/aggregate/AggregateInput.java | 78 +++++ .../actions/tables/count/CountInput.java | 78 +++++ .../actions/tables/query/QueryInput.java | 35 +- .../GarbageCollectorExtractStep.java | 3 +- .../rdbms/actions/AbstractRDBMSAction.java | 33 +- .../rdbms/actions/RDBMSQueryAction.java | 4 +- .../jdbc/C3P0PooledConnectionProvider.java | 182 ++++++++++ .../module/rdbms/jdbc/ConnectionManager.java | 126 ++++--- .../jdbc/ConnectionProviderInterface.java | 58 ++++ .../rdbms/jdbc/SimpleConnectionProvider.java | 63 ++++ .../metadata/ConnectionPoolSettings.java | 326 ++++++++++++++++++ .../model/metadata/RDBMSBackendMetaData.java | 100 ++++++ .../C3P0PooledConnectionProviderTest.java | 237 +++++++++++++ .../javalin/QJavalinImplementation.java | 3 + 18 files changed, 1294 insertions(+), 122 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/QueryHint.java create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProvider.java create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionProviderInterface.java create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/SimpleConnectionProvider.java create mode 100644 qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/ConnectionPoolSettings.java create mode 100644 qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java index e14de682..9c5628f9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java @@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; 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.query.QQueryFilter; @@ -216,7 +217,8 @@ public class ExportAction } queryInput.getFilter().setLimit(exportInput.getLimit()); queryInput.setShouldTranslatePossibleValues(true); - queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS); + queryInput.withQueryHint(QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS); + queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); ///////////////////////////////////////////////////////////////// // tell this query that it needs to put its output into a pipe // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index 3c2ace5c..a984bed3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -59,6 +59,7 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; 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.query.JoinsContext; @@ -417,7 +418,8 @@ public class GenerateReportAction extends AbstractQActionFunction transactionsPerTable = new HashMap<>(); - - // todo not commit - remove instance & session - use Context - - - boolean useTransactionsAsConnectionPool = false; - - - - /******************************************************************************* - ** - *******************************************************************************/ - private QBackendTransaction getTransaction(String tableName) - { - ///////////////////////////////////////////////////////////// - // mmm, this does cut down on connections used - // - // especially seems helpful in big exports. // - // but, let's just start using connection pools instead... // - ///////////////////////////////////////////////////////////// - if(useTransactionsAsConnectionPool) - { - try - { - if(!transactionsPerTable.containsKey(tableName)) - { - transactionsPerTable.put(tableName, QBackendTransaction.openFor(new InsertInput(tableName))); - } - - return (transactionsPerTable.get(tableName)); - } - catch(Exception e) - { - LOG.warn("Error opening transaction for table", logPair("tableName", tableName)); - } - } - - return null; - } - /******************************************************************************* @@ -601,7 +561,7 @@ public class QPossibleValueTranslator QueryInput queryInput = new QueryInput(); queryInput.setTableName(tableName); queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(idField, QCriteriaOperator.IN, page))); - queryInput.setTransaction(getTransaction(tableName)); + queryInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // when querying for possible values, we do want to generate their display values, which makes record labels, which are usually used as PVS labels // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/QueryHint.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/QueryHint.java new file mode 100644 index 00000000..8c7050f2 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/QueryHint.java @@ -0,0 +1,38 @@ +/* + * 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.core.model.actions.tables; + + +/******************************************************************************* + ** Information about the query that an application (or qqq service) may know and + ** want to tell the backend, that can help influence how the backend processes + ** query. + ** + ** For example, a query with potentially a large result set, for MySQL backend, + ** we may want to configure the result set to stream results rather than do its + ** default in-memory thing. See RDBMSQueryAction for usage. + *******************************************************************************/ +public enum QueryHint +{ + POTENTIALLY_LARGE_NUMBER_OF_RESULTS, + MAY_USE_READ_ONLY_BACKEND +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateInput.java index 862ebcf6..9d69e5ac 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/aggregate/AggregateInput.java @@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.aggregate; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; @@ -44,6 +46,8 @@ public class AggregateInput extends AbstractTableActionInput private List queryJoins = null; + private EnumSet queryHints = EnumSet.noneOf(QueryHint.class); + /******************************************************************************* @@ -302,4 +306,78 @@ public class AggregateInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for queryHints + *******************************************************************************/ + public EnumSet getQueryHints() + { + return (this.queryHints); + } + + + + /******************************************************************************* + ** Setter for queryHints + *******************************************************************************/ + public void setQueryHints(EnumSet queryHints) + { + this.queryHints = queryHints; + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public AggregateInput withQueryHints(EnumSet queryHints) + { + this.queryHints = queryHints; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public AggregateInput withQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + this.queryHints = EnumSet.noneOf(QueryHint.class); + } + this.queryHints.add(queryHint); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public AggregateInput withoutQueryHint(QueryHint queryHint) + { + if(this.queryHints != null) + { + this.queryHints.remove(queryHint); + } + return (this); + } + + + + /******************************************************************************* + ** null-safely check if query hints map contains the specified hint + *******************************************************************************/ + public boolean hasQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + return (false); + } + + return (queryHints.contains(queryHint)); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java index e1994d8b..a4f47090 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/count/CountInput.java @@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.count; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; @@ -42,6 +44,8 @@ public class CountInput extends AbstractTableActionInput private List queryJoins = null; private Boolean includeDistinctCount = false; + private EnumSet queryHints = EnumSet.noneOf(QueryHint.class); + /******************************************************************************* @@ -207,4 +211,78 @@ public class CountInput extends AbstractTableActionInput return (this); } + + + /******************************************************************************* + ** Getter for queryHints + *******************************************************************************/ + public EnumSet getQueryHints() + { + return (this.queryHints); + } + + + + /******************************************************************************* + ** Setter for queryHints + *******************************************************************************/ + public void setQueryHints(EnumSet queryHints) + { + this.queryHints = queryHints; + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public CountInput withQueryHints(EnumSet queryHints) + { + this.queryHints = queryHints; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public CountInput withQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + this.queryHints = EnumSet.noneOf(QueryHint.class); + } + this.queryHints.add(queryHint); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public CountInput withoutQueryHint(QueryHint queryHint) + { + if(this.queryHints != null) + { + this.queryHints.remove(queryHint); + } + return (this); + } + + + + /******************************************************************************* + ** null-safely check if query hints map contains the specified hint + *******************************************************************************/ + public boolean hasQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + return (false); + } + + return (queryHints.contains(queryHint)); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java index a0e71e19..5a00b1a3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java @@ -31,12 +31,16 @@ import java.util.Set; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface; /******************************************************************************* ** Input data for the Query action ** + ** Todo - maybe make a class between AbstractTableActionInput and {QueryInput, + ** CountInput, and AggregateInput}, with common attributes for all of these + ** "read" operations (like, queryHints, *******************************************************************************/ public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface, Cloneable { @@ -74,22 +78,6 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn - /******************************************************************************* - ** Information about the query that an application (or qqq service) may know and - ** want to tell the backend, that can help influence how the backend processes - ** query. - ** - ** For example, a query with potentially a large result set, for MySQL backend, - ** we may want to configure the result set to stream results rather than do its - ** default in-memory thing. See RDBMSQueryAction for usage. - *******************************************************************************/ - public enum QueryHint - { - POTENTIALLY_LARGE_NUMBER_OF_RESULTS - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -683,4 +671,19 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn return (this); } + + + /******************************************************************************* + ** null-safely check if query hints map contains the specified hint + *******************************************************************************/ + public boolean hasQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + return (false); + } + + return (queryHints.contains(queryHint)); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/garbagecollector/GarbageCollectorExtractStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/garbagecollector/GarbageCollectorExtractStep.java index 3f4ee4c0..171b4c33 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/garbagecollector/GarbageCollectorExtractStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/garbagecollector/GarbageCollectorExtractStep.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.garbagecollecto import java.time.Instant; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; 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.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; @@ -66,7 +67,7 @@ public class GarbageCollectorExtractStep extends ExtractViaQueryStep @Override protected void customizeInputPreQuery(QueryInput queryInput) { - queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS); + queryInput.withQueryHint(QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS); } } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index afb3b5d6..3940f31b 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -47,14 +47,18 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; 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.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -135,11 +139,34 @@ public abstract class AbstractRDBMSAction /******************************************************************************* ** Get a database connection, per the backend in the request. + ** + ** Note that it may be a connection to a read-only backend, per query-hints, + ** and backend settings. *******************************************************************************/ - public static Connection getConnection(AbstractTableActionInput qTableRequest) throws SQLException + public static Connection getConnection(AbstractTableActionInput tableActionInput) throws SQLException { - ConnectionManager connectionManager = new ConnectionManager(); - return connectionManager.getConnection((RDBMSBackendMetaData) qTableRequest.getBackend()); + RDBMSBackendMetaData backend = (RDBMSBackendMetaData) tableActionInput.getBackend(); + + boolean useReadOnly = false; + if(tableActionInput instanceof QueryInput queryInput) + { + useReadOnly = queryInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); + } + else if(tableActionInput instanceof CountInput countInput) + { + useReadOnly = countInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); + } + else if(tableActionInput instanceof AggregateInput aggregateInput) + { + useReadOnly = aggregateInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); + } + + if(useReadOnly && backend.getReadOnlyBackendMetaData() != null) + { + return ConnectionManager.getConnection(backend.getReadOnlyBackendMetaData()); + } + + return ConnectionManager.getConnection(backend); } 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 86f119fb..8bcb81b2 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 @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; 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.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; @@ -80,7 +81,6 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf } } - /******************************************************************************* ** *******************************************************************************/ @@ -361,7 +361,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if we're allowed to use the mysqlResultSetOptimization, and we have the query hint of "expected large result set", then do it. // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(mysqlResultSetOptimizationEnabled && queryInput.getQueryHints() != null && queryInput.getQueryHints().contains(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS)) + if(mysqlResultSetOptimizationEnabled && queryInput.getQueryHints() != null && queryInput.getQueryHints().contains(QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS)) { ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // mysql "optimization", presumably here - from Result Set section of https://dev.mysql.com/doc/connector-j/en/connector-j-reference-implementation-notes.html // diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProvider.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProvider.java new file mode 100644 index 00000000..1a4740e4 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProvider.java @@ -0,0 +1,182 @@ +/* + * 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.jdbc; + + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.LinkedHashMap; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.ConnectionPoolSettings; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import com.mchange.v2.c3p0.ComboPooledDataSource; +import org.json.JSONObject; +import static com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager.getJdbcUrl; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class C3P0PooledConnectionProvider implements ConnectionProviderInterface +{ + private RDBMSBackendMetaData backend; + private ComboPooledDataSource connectionPool; + + private long usageCount = 0; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void init(RDBMSBackendMetaData backend) throws QException + { + this.backend = backend; + + try + { + ComboPooledDataSource pool = new ComboPooledDataSource(); + pool.setDriverClass(ConnectionManager.getJdbcDriverClassName(backend)); + pool.setJdbcUrl(getJdbcUrl(backend)); + pool.setUser(backend.getUsername()); + pool.setPassword(backend.getPassword()); + + ConnectionPoolSettings poolSettings = backend.getConnectionPoolSettings(); + if(poolSettings != null) + { + if(poolSettings.getInitialPoolSize() != null) + { + pool.setInitialPoolSize(poolSettings.getInitialPoolSize()); + } + + if(poolSettings.getMinPoolSize() != null) + { + pool.setMinPoolSize(poolSettings.getMinPoolSize()); + } + + if(poolSettings.getMaxPoolSize() != null) + { + pool.setMaxPoolSize(poolSettings.getMaxPoolSize()); + } + + if(poolSettings.getAcquireIncrement() != null) + { + pool.setAcquireIncrement(poolSettings.getAcquireIncrement()); + } + + if(poolSettings.getMaxConnectionAgeSeconds() != null) + { + pool.setMaxConnectionAge(poolSettings.getMaxConnectionAgeSeconds()); + } + + if(poolSettings.getMaxIdleTimeSeconds() != null) + { + pool.setMaxIdleTime(poolSettings.getMaxIdleTimeSeconds()); + } + + if(poolSettings.getMaxIdleTimeExcessConnectionsSeconds() != null) + { + pool.setMaxIdleTimeExcessConnections(poolSettings.getMaxIdleTimeExcessConnectionsSeconds()); + } + + if(poolSettings.getCheckoutTimeoutSeconds() != null) + { + pool.setCheckoutTimeout(poolSettings.getCheckoutTimeoutSeconds() * 1000); + } + + if(poolSettings.getTestConnectionOnCheckout() != null) + { + pool.setTestConnectionOnCheckout(poolSettings.getTestConnectionOnCheckout()); + } + } + + customizePool(pool); + + this.connectionPool = pool; + } + catch(Exception e) + { + throw (new QException("Error Initializing C3P0PooledConnectionProvider for backend [" + backend.getName() + "]", e)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected void customizePool(ComboPooledDataSource pool) + { + //////////////////////// + // noop in base class // + //////////////////////// + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Connection getConnection() throws SQLException + { + usageCount++; + return (this.connectionPool.getConnection()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public JSONObject dumpDebug() throws SQLException + { + JSONObject rs = new JSONObject(new LinkedHashMap<>()); + + JSONObject settings = new JSONObject(new LinkedHashMap<>()); + rs.put("settings", settings); + settings.put("initialPoolSize", connectionPool.getInitialPoolSize()); + settings.put("minPoolSize", connectionPool.getMinPoolSize()); + settings.put("maxPoolSize", connectionPool.getMaxPoolSize()); + settings.put("acquireIncrement", connectionPool.getAcquireIncrement()); + settings.put("maxConnectionAge", connectionPool.getMaxConnectionAge()); + settings.put("maxIdleTime", connectionPool.getMaxIdleTime()); + settings.put("maxIdleTimeExcessConnections", connectionPool.getMaxIdleTimeExcessConnections()); + settings.put("checkoutTimeout", connectionPool.getCheckoutTimeout()); + settings.put("testConnectionOnCheckout", connectionPool.isTestConnectionOnCheckout()); + + JSONObject state = new JSONObject(new LinkedHashMap<>()); + rs.put("state", state); + state.put("numUsages", usageCount); + state.put("numConnections", connectionPool.getNumConnections()); + state.put("numBusyConnections", connectionPool.getNumBusyConnections()); + state.put("numIdleConnections", connectionPool.getNumIdleConnections()); + state.put("numFailedCheckins", connectionPool.getNumFailedCheckinsDefaultUser()); + state.put("numFailedCheckouts", connectionPool.getNumFailedCheckoutsDefaultUser()); + state.put("numFailedIdleTests", connectionPool.getNumFailedIdleTestsDefaultUser()); + state.put("numThreadsAwaitingCheckout", connectionPool.getNumThreadsAwaitingCheckoutDefaultUser()); + return (rs); + } + +} diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java index 3800810f..688e231b 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java @@ -23,68 +23,81 @@ package com.kingsrook.qqq.backend.module.rdbms.jdbc; import java.sql.Connection; -import java.sql.DriverManager; import java.sql.SQLException; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import java.util.concurrent.ConcurrentHashMap; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; -import com.mchange.v2.c3p0.ComboPooledDataSource; +import org.json.JSONArray; +import org.json.JSONObject; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* + ** Class to manage access to JDBC Connections. ** + ** Relies heavily on RDBMSBackendMetaData. *******************************************************************************/ public class ConnectionManager { - private boolean mayUseConnectionPool = true; + private static final QLogger LOG = QLogger.getLogger(ConnectionManager.class); - private static Map initedConnectionPool = new HashMap<>(); - private static Map connectionPoolMap = new HashMap<>(); + private static final Map connectionProviderMap = new ConcurrentHashMap<>(); - private static int usageCounter = 0; /******************************************************************************* ** *******************************************************************************/ - public Connection getConnection(RDBMSBackendMetaData backend) throws SQLException - { - usageCounter++; - - if(mayUseConnectionPool) - { - return (getConnectionFromPool(backend)); - } - - String jdbcURL = getJdbcUrl(backend); - return DriverManager.getConnection(jdbcURL, backend.getUsername(), backend.getPassword()); - } - - - /******************************************************************************* - ** - *******************************************************************************/ - public static void checkPools() + public static Connection getConnection(RDBMSBackendMetaData backend) throws SQLException { try { - System.out.println("Usages: " + usageCounter); + ConnectionProviderInterface connectionProvider = getConnectionProvider(backend); + return connectionProvider.getConnection(); + } + catch(QException qe) + { + throw (new SQLException("Error getting connection", qe)); + } + } - for(Map.Entry entry : CollectionUtils.nonNullMap(connectionPoolMap).entrySet()) + + + /******************************************************************************* + ** + *******************************************************************************/ + private static ConnectionProviderInterface getConnectionProvider(RDBMSBackendMetaData backend) throws QException + { + if(!connectionProviderMap.containsKey(backend.getName())) + { + synchronized(connectionProviderMap) { - System.out.println("POOL USAGE: " + entry.getKey() + ": " + entry.getValue().getNumBusyConnections()); - if(entry.getValue().getNumBusyConnections() > 2) + if(!connectionProviderMap.containsKey(backend.getName())) { - System.out.println("break!"); + QCodeReference connectionProviderReference = backend.getConnectionProvider(); + boolean usingDefaultSimpleProvider = false; + if(connectionProviderReference == null) + { + connectionProviderReference = new QCodeReference(SimpleConnectionProvider.class); + usingDefaultSimpleProvider = true; + } + + LOG.info("Initializing connection provider for RDBMS backend", logPair("backendName", backend.getName()), logPair("connectionProvider", connectionProviderReference.getName()), logPair("usingDefaultSimpleProvider", usingDefaultSimpleProvider)); + ConnectionProviderInterface connectionProvider = QCodeLoader.getAdHoc(ConnectionProviderInterface.class, connectionProviderReference); + connectionProvider.init(backend); + + connectionProviderMap.put(backend.getName(), connectionProvider); } } } - catch(Exception e) - { - e.printStackTrace(); - } + + return (connectionProviderMap.get(backend.getName())); } @@ -92,36 +105,27 @@ public class ConnectionManager /******************************************************************************* ** *******************************************************************************/ - private Connection getConnectionFromPool(RDBMSBackendMetaData backend) throws SQLException + public static JSONArray dumpConnectionProviderDebug() { try { - if(!initedConnectionPool.getOrDefault(backend.getName(), false)) + JSONArray rs = new JSONArray(); + for(Map.Entry entry : connectionProviderMap.entrySet()) { - // todo - some syncrhonized - ComboPooledDataSource connectionPool = new ComboPooledDataSource(); - connectionPool.setDriverClass(getJdbcDriverClassName(backend)); - connectionPool.setJdbcUrl(getJdbcUrl(backend)); - connectionPool.setUser(backend.getUsername()); - connectionPool.setPassword(backend.getPassword()); - - connectionPool.setTestConnectionOnCheckout(true); - - ////////////////////////////////////////////////////////////////////////// - // useful to debug leaking connections - meant for tests only though... // - ////////////////////////////////////////////////////////////////////////// - // connectionPool.setDebugUnreturnedConnectionStackTraces(true); - // connectionPool.setUnreturnedConnectionTimeout(10); - - connectionPoolMap.put(backend.getName(), connectionPool); - initedConnectionPool.put(backend.getName(), true); + JSONObject jsonObject = new JSONObject(new LinkedHashMap<>()); + jsonObject.put("backendName", entry.getKey()); + jsonObject.put("connectionProviderClass", entry.getValue().getClass().getName()); + jsonObject.put("values", entry.getValue().dumpDebug()); + rs.put(jsonObject); } - return (connectionPoolMap.get(backend.getName()).getConnection()); + return (rs); } catch(Exception e) { - throw (new SQLException("Error getting connection from pool", e)); + String message = "Error dumping debug data for connection providers"; + LOG.warn(message, e); + return (new JSONArray(new JSONObject(Map.of("error", e.getMessage())))); } } @@ -168,4 +172,14 @@ public class ConnectionManager }; } + + + /******************************************************************************* + ** reset the map of connection providers - not necessarily meant to be useful + ** in production code - written for use in qqq tests. + *******************************************************************************/ + static void resetConnectionProviders() + { + connectionProviderMap.clear(); + } } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionProviderInterface.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionProviderInterface.java new file mode 100644 index 00000000..6e80f5d5 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionProviderInterface.java @@ -0,0 +1,58 @@ +/* + * 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.jdbc; + + +import java.sql.Connection; +import java.sql.SQLException; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import org.json.JSONObject; + + +/******************************************************************************* + ** interface for classes that can provide jdbc Connections for an RDBMS backend. + *******************************************************************************/ +public interface ConnectionProviderInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void init(RDBMSBackendMetaData backend) throws QException; + + /******************************************************************************* + ** + *******************************************************************************/ + Connection getConnection() throws SQLException; + + /******************************************************************************* + ** + *******************************************************************************/ + default JSONObject dumpDebug() throws SQLException + { + JSONObject rs = new JSONObject(); + rs.put("nothingToReport", true); + return (rs); + } +} + diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/SimpleConnectionProvider.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/SimpleConnectionProvider.java new file mode 100644 index 00000000..264f5528 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/SimpleConnectionProvider.java @@ -0,0 +1,63 @@ +/* + * 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.jdbc; + + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import static com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager.getJdbcUrl; + + +/******************************************************************************* + ** Simple connection provider - no pooling, just opens a new connection for + ** every request. + *******************************************************************************/ +public class SimpleConnectionProvider implements ConnectionProviderInterface +{ + private RDBMSBackendMetaData backend; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void init(RDBMSBackendMetaData backend) + { + this.backend = backend; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Connection getConnection() throws SQLException + { + String jdbcURL = getJdbcUrl(backend); + return DriverManager.getConnection(jdbcURL, backend.getUsername(), backend.getPassword()); + } + +} diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/ConnectionPoolSettings.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/ConnectionPoolSettings.java new file mode 100644 index 00000000..63a0d650 --- /dev/null +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/model/metadata/ConnectionPoolSettings.java @@ -0,0 +1,326 @@ +/* + * 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.model.metadata; + + +/******************************************************************************* + ** Settings for a connection pool (if your backend is configured to use one). + ** Originally based on the most common settings for C3P0 - see + ** https://www.mchange.com/projects/c3p0/#configuration + ** + ** If you want more - you'll be looking at defining your own subclass of + ** C3P0PooledConnectionProvider and possibly this class. + ** + ** If using a pool other than C3P0 - some of these may apply others may not. + *******************************************************************************/ +public class ConnectionPoolSettings +{ + private Integer initialPoolSize; + private Integer minPoolSize; + private Integer maxPoolSize; + private Integer acquireIncrement; + private Integer maxConnectionAgeSeconds; + private Integer maxIdleTimeSeconds; + private Integer maxIdleTimeExcessConnectionsSeconds; + private Integer checkoutTimeoutSeconds; + private Boolean testConnectionOnCheckout; + + + + /******************************************************************************* + ** Getter for initialPoolSize + *******************************************************************************/ + public Integer getInitialPoolSize() + { + return (this.initialPoolSize); + } + + + + /******************************************************************************* + ** Setter for initialPoolSize + *******************************************************************************/ + public void setInitialPoolSize(Integer initialPoolSize) + { + this.initialPoolSize = initialPoolSize; + } + + + + /******************************************************************************* + ** Fluent setter for initialPoolSize + *******************************************************************************/ + public ConnectionPoolSettings withInitialPoolSize(Integer initialPoolSize) + { + this.initialPoolSize = initialPoolSize; + return (this); + } + + + + /******************************************************************************* + ** Getter for minPoolSize + *******************************************************************************/ + public Integer getMinPoolSize() + { + return (this.minPoolSize); + } + + + + /******************************************************************************* + ** Setter for minPoolSize + *******************************************************************************/ + public void setMinPoolSize(Integer minPoolSize) + { + this.minPoolSize = minPoolSize; + } + + + + /******************************************************************************* + ** Fluent setter for minPoolSize + *******************************************************************************/ + public ConnectionPoolSettings withMinPoolSize(Integer minPoolSize) + { + this.minPoolSize = minPoolSize; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxPoolSize + *******************************************************************************/ + public Integer getMaxPoolSize() + { + return (this.maxPoolSize); + } + + + + /******************************************************************************* + ** Setter for maxPoolSize + *******************************************************************************/ + public void setMaxPoolSize(Integer maxPoolSize) + { + this.maxPoolSize = maxPoolSize; + } + + + + /******************************************************************************* + ** Fluent setter for maxPoolSize + *******************************************************************************/ + public ConnectionPoolSettings withMaxPoolSize(Integer maxPoolSize) + { + this.maxPoolSize = maxPoolSize; + return (this); + } + + + + /******************************************************************************* + ** Getter for acquireIncrement + *******************************************************************************/ + public Integer getAcquireIncrement() + { + return (this.acquireIncrement); + } + + + + /******************************************************************************* + ** Setter for acquireIncrement + *******************************************************************************/ + public void setAcquireIncrement(Integer acquireIncrement) + { + this.acquireIncrement = acquireIncrement; + } + + + + /******************************************************************************* + ** Fluent setter for acquireIncrement + *******************************************************************************/ + public ConnectionPoolSettings withAcquireIncrement(Integer acquireIncrement) + { + this.acquireIncrement = acquireIncrement; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxConnectionAgeSeconds + *******************************************************************************/ + public Integer getMaxConnectionAgeSeconds() + { + return (this.maxConnectionAgeSeconds); + } + + + + /******************************************************************************* + ** Setter for maxConnectionAgeSeconds + *******************************************************************************/ + public void setMaxConnectionAgeSeconds(Integer maxConnectionAgeSeconds) + { + this.maxConnectionAgeSeconds = maxConnectionAgeSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for maxConnectionAgeSeconds + *******************************************************************************/ + public ConnectionPoolSettings withMaxConnectionAgeSeconds(Integer maxConnectionAgeSeconds) + { + this.maxConnectionAgeSeconds = maxConnectionAgeSeconds; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxIdleTimeSeconds + *******************************************************************************/ + public Integer getMaxIdleTimeSeconds() + { + return (this.maxIdleTimeSeconds); + } + + + + /******************************************************************************* + ** Setter for maxIdleTimeSeconds + *******************************************************************************/ + public void setMaxIdleTimeSeconds(Integer maxIdleTimeSeconds) + { + this.maxIdleTimeSeconds = maxIdleTimeSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for maxIdleTimeSeconds + *******************************************************************************/ + public ConnectionPoolSettings withMaxIdleTimeSeconds(Integer maxIdleTimeSeconds) + { + this.maxIdleTimeSeconds = maxIdleTimeSeconds; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxIdleTimeExcessConnectionsSeconds + *******************************************************************************/ + public Integer getMaxIdleTimeExcessConnectionsSeconds() + { + return (this.maxIdleTimeExcessConnectionsSeconds); + } + + + + /******************************************************************************* + ** Setter for maxIdleTimeExcessConnectionsSeconds + *******************************************************************************/ + public void setMaxIdleTimeExcessConnectionsSeconds(Integer maxIdleTimeExcessConnectionsSeconds) + { + this.maxIdleTimeExcessConnectionsSeconds = maxIdleTimeExcessConnectionsSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for maxIdleTimeExcessConnectionsSeconds + *******************************************************************************/ + public ConnectionPoolSettings withMaxIdleTimeExcessConnectionsSeconds(Integer maxIdleTimeExcessConnectionsSeconds) + { + this.maxIdleTimeExcessConnectionsSeconds = maxIdleTimeExcessConnectionsSeconds; + return (this); + } + + + + /******************************************************************************* + ** Getter for testConnectionOnCheckout + *******************************************************************************/ + public Boolean getTestConnectionOnCheckout() + { + return (this.testConnectionOnCheckout); + } + + + + /******************************************************************************* + ** Setter for testConnectionOnCheckout + *******************************************************************************/ + public void setTestConnectionOnCheckout(Boolean testConnectionOnCheckout) + { + this.testConnectionOnCheckout = testConnectionOnCheckout; + } + + + + /******************************************************************************* + ** Fluent setter for testConnectionOnCheckout + *******************************************************************************/ + public ConnectionPoolSettings withTestConnectionOnCheckout(Boolean testConnectionOnCheckout) + { + this.testConnectionOnCheckout = testConnectionOnCheckout; + return (this); + } + + + + /******************************************************************************* + ** Getter for checkoutTimeoutSeconds + *******************************************************************************/ + public Integer getCheckoutTimeoutSeconds() + { + return (this.checkoutTimeoutSeconds); + } + + + + /******************************************************************************* + ** Setter for checkoutTimeoutSeconds + *******************************************************************************/ + public void setCheckoutTimeoutSeconds(Integer checkoutTimeoutSeconds) + { + this.checkoutTimeoutSeconds = checkoutTimeoutSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for checkoutTimeoutSeconds + *******************************************************************************/ + public ConnectionPoolSettings withCheckoutTimeoutSeconds(Integer checkoutTimeoutSeconds) + { + this.checkoutTimeoutSeconds = checkoutTimeoutSeconds; + return (this); + } + +} 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 a86a6f45..aa93d363 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 @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.module.rdbms.model.metadata; 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; @@ -42,6 +43,12 @@ public class RDBMSBackendMetaData extends QBackendMetaData private String jdbcUrl; private String jdbcDriverClassName; + private QCodeReference connectionProvider; + + private ConnectionPoolSettings connectionPoolSettings; + + private RDBMSBackendMetaData readOnlyBackendMetaData; + /******************************************************************************* @@ -316,6 +323,7 @@ public class RDBMSBackendMetaData extends QBackendMetaData } + /******************************************************************************* ** Getter for jdbcDriverClassName *******************************************************************************/ @@ -346,4 +354,96 @@ public class RDBMSBackendMetaData extends QBackendMetaData } + + /******************************************************************************* + ** Getter for connectionProvider + *******************************************************************************/ + public QCodeReference getConnectionProvider() + { + return (this.connectionProvider); + } + + + + /******************************************************************************* + ** Setter for connectionProvider + *******************************************************************************/ + public void setConnectionProvider(QCodeReference connectionProvider) + { + this.connectionProvider = connectionProvider; + } + + + + /******************************************************************************* + ** Fluent setter for connectionProvider + *******************************************************************************/ + public RDBMSBackendMetaData withConnectionProvider(QCodeReference connectionProvider) + { + this.connectionProvider = connectionProvider; + return (this); + } + + + + /******************************************************************************* + ** Getter for readOnlyBackendMetaData + *******************************************************************************/ + public RDBMSBackendMetaData getReadOnlyBackendMetaData() + { + return (this.readOnlyBackendMetaData); + } + + + + /******************************************************************************* + ** Setter for readOnlyBackendMetaData + *******************************************************************************/ + public void setReadOnlyBackendMetaData(RDBMSBackendMetaData readOnlyBackendMetaData) + { + this.readOnlyBackendMetaData = readOnlyBackendMetaData; + } + + + + /******************************************************************************* + ** Fluent setter for readOnlyBackendMetaData + *******************************************************************************/ + public RDBMSBackendMetaData withReadOnlyBackendMetaData(RDBMSBackendMetaData readOnlyBackendMetaData) + { + this.readOnlyBackendMetaData = readOnlyBackendMetaData; + return (this); + } + + + + /******************************************************************************* + ** Getter for connectionPoolSettings + *******************************************************************************/ + public ConnectionPoolSettings getConnectionPoolSettings() + { + return (this.connectionPoolSettings); + } + + + + /******************************************************************************* + ** Setter for connectionPoolSettings + *******************************************************************************/ + public void setConnectionPoolSettings(ConnectionPoolSettings connectionPoolSettings) + { + this.connectionPoolSettings = connectionPoolSettings; + } + + + + /******************************************************************************* + ** Fluent setter for connectionPoolSettings + *******************************************************************************/ + public RDBMSBackendMetaData withConnectionPoolSettings(ConnectionPoolSettings connectionPoolSettings) + { + this.connectionPoolSettings = connectionPoolSettings; + return (this); + } + } diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java new file mode 100644 index 00000000..23b0e7e0 --- /dev/null +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java @@ -0,0 +1,237 @@ +/* + * 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.jdbc; + + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import com.kingsrook.qqq.backend.module.rdbms.BaseTest; +import com.kingsrook.qqq.backend.module.rdbms.TestUtils; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.ConnectionPoolSettings; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import com.mchange.v2.resourcepool.TimeoutException; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +/******************************************************************************* + ** Unit test for C3P0PooledConnectionProvider + *******************************************************************************/ +class C3P0PooledConnectionProviderTest extends BaseTest +{ + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() throws Exception + { + TestUtils.primeTestDatabase("prime-test-database.sql"); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // must call this after the primeTestDatabase call (as i uses a raw version of the backend, w/o our updated settings) // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ConnectionManager.resetConnectionProviders(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @AfterEach + void afterEach() + { + //////////////////////////////////////////////////////////// + // just for good measure, do this after each test in here // + //////////////////////////////////////////////////////////// + ConnectionManager.resetConnectionProviders(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + // @RepeatedTest(100) + void test() throws Exception + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // change the default database backend to use the class under test here - the C3PL connection pool provider // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + QInstance qInstance = TestUtils.defineInstance(); + RDBMSBackendMetaData backend = (RDBMSBackendMetaData) qInstance.getBackend(TestUtils.DEFAULT_BACKEND_NAME); + backend.setConnectionProvider(new QCodeReference(C3P0PooledConnectionProvider.class)); + QContext.init(qInstance, new QSession()); + + for(int i = 0; i < 5; i++) + { + new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON)); + } + + JSONObject debugValues = getDebugStateValues(true); + assertThat(debugValues.getInt("numConnections")).isEqualTo(3); // one time (in a @RepeatedTest(100) we saw a 3 != 6 here...) + + //////////////////////////////////////////////////////////////////// + // open up 4 transactions - confirm the pool opens some new conns // + //////////////////////////////////////////////////////////////////// + List transactions = new ArrayList<>(); + for(int i = 0; i < 5; i++) + { + transactions.add(QBackendTransaction.openFor(new InsertInput(TestUtils.TABLE_NAME_PERSON))); + } + + debugValues = getDebugStateValues(true); + assertThat(debugValues.getInt("numConnections")).isGreaterThan(3); + + transactions.forEach(transaction -> transaction.close()); + + ///////////////////////////////////////////////////////////////////////// + // might take a second for the pool to re-claim the closed connections // + ///////////////////////////////////////////////////////////////////////// + boolean foundMatch = false; + for(int i = 0; i < 5; i++) + { + debugValues = getDebugStateValues(true); + if(debugValues.getInt("numConnections") == debugValues.getInt("numIdleConnections")) + { + foundMatch = true; + break; + } + System.out.println("oops!"); + SleepUtils.sleep(250, TimeUnit.MILLISECONDS); + } + + assertTrue(foundMatch, "The pool didn't re-claim all connections..."); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPoolSettings() throws Exception + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // change the default database backend to use the class under test here - the C3PL connection pool provider // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + QInstance qInstance = TestUtils.defineInstance(); + RDBMSBackendMetaData backend = (RDBMSBackendMetaData) qInstance.getBackend(TestUtils.DEFAULT_BACKEND_NAME); + backend.setConnectionProvider(new QCodeReference(C3P0PooledConnectionProvider.class)); + backend.setConnectionPoolSettings(new ConnectionPoolSettings() + .withInitialPoolSize(2) + .withAcquireIncrement(1) + .withMinPoolSize(1) + .withMaxPoolSize(4) + .withCheckoutTimeoutSeconds(1)); + QContext.init(qInstance, new QSession()); + + ///////////////////////// + // assert initial size // + ///////////////////////// + new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON)); + JSONObject debugValues = getDebugStateValues(true); + assertThat(debugValues.getInt("numConnections")).isEqualTo(2); + + /////////////////////////////////////////////////////////////////////// + // open (and close) 5 conns - shouldn't get bigger than initial size // + /////////////////////////////////////////////////////////////////////// + for(int i = 0; i < 5; i++) + { + new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON)); + } + debugValues = getDebugStateValues(true); + assertThat(debugValues.getInt("numConnections")).isEqualTo(2); // one time (in a @RepeatedTest(100) we saw a 3 != 6 here...) + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // open up 4 transactions - confirm the pool opens some new conns, but stops at the max, and throws based on checkoutTimeout setting // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + List transactions = new ArrayList<>(); + for(int i = 0; i < 5; i++) + { + if(i == 4) + { + ////////////////////////////////////////// + // expect this one to fail - full pool! // + ////////////////////////////////////////// + assertThatThrownBy(() -> QBackendTransaction.openFor(new InsertInput(TestUtils.TABLE_NAME_PERSON))) + .hasRootCauseInstanceOf(TimeoutException.class); + } + else + { + transactions.add(QBackendTransaction.openFor(new InsertInput(TestUtils.TABLE_NAME_PERSON))); + } + } + + debugValues = getDebugStateValues(true); + assertThat(debugValues.getInt("numConnections")).isEqualTo(4); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static JSONObject getDebugStateValues(boolean printIt) + { + JSONArray debugArray = ConnectionManager.dumpConnectionProviderDebug(); + for(int i = 0; i < debugArray.length(); i++) + { + JSONObject object = debugArray.getJSONObject(i); + if(TestUtils.DEFAULT_BACKEND_NAME.equals(object.optString("backendName"))) + { + JSONObject values = object.getJSONObject("values"); + if(printIt) + { + System.out.println(values.toString(3)); + } + + JSONObject state = values.getJSONObject("state"); + return state; + } + } + + fail("Didn't find debug values..."); + return (null); + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index ba87ccf1..a6afe0ca 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -82,6 +82,7 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; 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.delete.DeleteInput; @@ -1195,6 +1196,7 @@ public class QJavalinImplementation countInput.setTimeoutSeconds(DEFAULT_COUNT_TIMEOUT_SECONDS); countInput.setQueryJoins(processQueryJoinsParam(context)); countInput.setIncludeDistinctCount(QJavalinUtils.queryParamIsTrue(context, "includeDistinct")); + countInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); CountAction countAction = new CountAction(); CountOutput countOutput = countAction.execute(countInput); @@ -1250,6 +1252,7 @@ public class QJavalinImplementation queryInput.setShouldGenerateDisplayValues(true); queryInput.setShouldTranslatePossibleValues(true); queryInput.setTimeoutSeconds(DEFAULT_QUERY_TIMEOUT_SECONDS); + queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ); From 64380dd84960520b0dbd1dc8438344d77bbd0d45 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 6 Jun 2024 16:41:55 -0500 Subject: [PATCH 05/10] Exclude mchange-commons-java from backend-core -- prefer our own. --- qqq-backend-module-rdbms/pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qqq-backend-module-rdbms/pom.xml b/qqq-backend-module-rdbms/pom.xml index 3e9be513..a7ba512e 100644 --- a/qqq-backend-module-rdbms/pom.xml +++ b/qqq-backend-module-rdbms/pom.xml @@ -42,6 +42,13 @@ com.kingsrook.qqq qqq-backend-core ${revision} + + + + com.mchange + mchange-commons-java + + From 9e9d1960c8086730a284ca6d5400de007fad7534 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 6 Jun 2024 19:45:12 -0500 Subject: [PATCH 06/10] rather than exclusion for mchange-common (lib for c3p0), import the version that rdbms will want directly in backend-core --- qqq-backend-core/pom.xml | 7 +++++++ qqq-backend-module-rdbms/pom.xml | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/qqq-backend-core/pom.xml b/qqq-backend-core/pom.xml index 60561f25..c9a3c8f5 100644 --- a/qqq-backend-core/pom.xml +++ b/qqq-backend-core/pom.xml @@ -192,6 +192,13 @@ 2.3.2 + + + com.mchange + mchange-commons-java + 0.3.0 + + org.apache.logging.log4j diff --git a/qqq-backend-module-rdbms/pom.xml b/qqq-backend-module-rdbms/pom.xml index a7ba512e..3e9be513 100644 --- a/qqq-backend-module-rdbms/pom.xml +++ b/qqq-backend-module-rdbms/pom.xml @@ -42,13 +42,6 @@ com.kingsrook.qqq qqq-backend-core ${revision} - - - - com.mchange - mchange-commons-java - - From 0fd24308664d1022a1eac410be1c8345e6f1ff30 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 7 Jun 2024 12:43:26 -0500 Subject: [PATCH 07/10] Update to avoid NPE for null backend names (used by some non-standard flows) --- .../module/rdbms/jdbc/ConnectionManager.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java index 688e231b..1e5eeea1 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java @@ -26,6 +26,7 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -74,11 +75,16 @@ public class ConnectionManager *******************************************************************************/ private static ConnectionProviderInterface getConnectionProvider(RDBMSBackendMetaData backend) throws QException { - if(!connectionProviderMap.containsKey(backend.getName())) + ////////////////////////////////////////////////////////////////////////////////// + // some non-standard use-cases use a backend without a name... avoid NPE in map // + ////////////////////////////////////////////////////////////////////////////////// + String name = Objects.requireNonNullElse(backend.getName(), ""); + + if(!connectionProviderMap.containsKey(name)) { synchronized(connectionProviderMap) { - if(!connectionProviderMap.containsKey(backend.getName())) + if(!connectionProviderMap.containsKey(name)) { QCodeReference connectionProviderReference = backend.getConnectionProvider(); boolean usingDefaultSimpleProvider = false; @@ -88,16 +94,16 @@ public class ConnectionManager usingDefaultSimpleProvider = true; } - LOG.info("Initializing connection provider for RDBMS backend", logPair("backendName", backend.getName()), logPair("connectionProvider", connectionProviderReference.getName()), logPair("usingDefaultSimpleProvider", usingDefaultSimpleProvider)); + LOG.info("Initializing connection provider for RDBMS backend", logPair("backendName", name), logPair("connectionProvider", connectionProviderReference.getName()), logPair("usingDefaultSimpleProvider", usingDefaultSimpleProvider)); ConnectionProviderInterface connectionProvider = QCodeLoader.getAdHoc(ConnectionProviderInterface.class, connectionProviderReference); connectionProvider.init(backend); - connectionProviderMap.put(backend.getName(), connectionProvider); + connectionProviderMap.put(name, connectionProvider); } } } - return (connectionProviderMap.get(backend.getName())); + return (connectionProviderMap.get(name)); } From 98031b53cb810abb039ce7cb01cab97c9e5e0819 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 7 Jun 2024 12:53:26 -0500 Subject: [PATCH 08/10] Give these tests a little room for timing-based instabilitiy --- .../module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java index 23b0e7e0..417fd6cc 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/C3P0PooledConnectionProviderTest.java @@ -108,7 +108,7 @@ class C3P0PooledConnectionProviderTest extends BaseTest } JSONObject debugValues = getDebugStateValues(true); - assertThat(debugValues.getInt("numConnections")).isEqualTo(3); // one time (in a @RepeatedTest(100) we saw a 3 != 6 here...) + assertThat(debugValues.getInt("numConnections")).isBetween(3, 6); // due to potential timing issues, sometimes pool will acquire another 3 conns, so 3 or 6 seems ok. //////////////////////////////////////////////////////////////////// // open up 4 transactions - confirm the pool opens some new conns // @@ -180,7 +180,7 @@ class C3P0PooledConnectionProviderTest extends BaseTest new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON)); } debugValues = getDebugStateValues(true); - assertThat(debugValues.getInt("numConnections")).isEqualTo(2); // one time (in a @RepeatedTest(100) we saw a 3 != 6 here...) + assertThat(debugValues.getInt("numConnections")).isBetween(2, 4); // due to potential timing issues, sometimes pool will acquire 1 or 2 more, so, this seems ok /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // open up 4 transactions - confirm the pool opens some new conns, but stops at the max, and throws based on checkoutTimeout setting // From 27682870a15a2047df6d6393d5381a097ea78d4d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 7 Jun 2024 16:22:56 -0500 Subject: [PATCH 09/10] Update API query & get with MAY_USE_READ_ONLY query hint (adding that option to Get's) --- .../model/actions/tables/get/GetInput.java | 79 +++++++++++++++++++ .../rdbms/actions/AbstractRDBMSAction.java | 5 ++ .../qqq/api/actions/ApiImplementation.java | 8 +- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java index 2cf7872f..711c3280 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/get/GetInput.java @@ -25,10 +25,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; +import java.util.EnumSet; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; @@ -60,6 +62,8 @@ public class GetInput extends AbstractTableActionInput implements QueryOrGetInpu private boolean includeAssociations = false; private Collection associationNamesToInclude = null; + private EnumSet queryHints = EnumSet.noneOf(QueryHint.class); + /******************************************************************************* @@ -462,4 +466,79 @@ public class GetInput extends AbstractTableActionInput implements QueryOrGetInpu return (this); } + + + /******************************************************************************* + ** Getter for queryHints + *******************************************************************************/ + public EnumSet getQueryHints() + { + return (this.queryHints); + } + + + + /******************************************************************************* + ** Setter for queryHints + *******************************************************************************/ + public void setQueryHints(EnumSet queryHints) + { + this.queryHints = queryHints; + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public GetInput withQueryHints(EnumSet queryHints) + { + this.queryHints = queryHints; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public GetInput withQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + this.queryHints = EnumSet.noneOf(QueryHint.class); + } + this.queryHints.add(queryHint); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for queryHints + *******************************************************************************/ + public GetInput withoutQueryHint(QueryHint queryHint) + { + if(this.queryHints != null) + { + this.queryHints.remove(queryHint); + } + return (this); + } + + + + /******************************************************************************* + ** null-safely check if query hints map contains the specified hint + *******************************************************************************/ + public boolean hasQueryHint(QueryHint queryHint) + { + if(this.queryHints == null) + { + return (false); + } + + return (queryHints.contains(queryHint)); + } + } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index 3940f31b..082759bc 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -54,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; @@ -156,6 +157,10 @@ public abstract class AbstractRDBMSAction { useReadOnly = countInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); } + else if(tableActionInput instanceof GetInput getInput) + { + useReadOnly = getInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); + } else if(tableActionInput instanceof AggregateInput aggregateInput) { useReadOnly = aggregateInput.hasQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java index 84137b7d..b56c7542 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/ApiImplementation.java @@ -76,6 +76,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; +import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint; 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.delete.DeleteInput; @@ -160,6 +161,7 @@ public class ApiImplementation queryInput.setTableName(tableName); queryInput.setIncludeAssociations(true); queryInput.setShouldFetchHeavyFields(true); + queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ); @@ -394,6 +396,7 @@ public class ApiImplementation CountInput countInput = new CountInput(); countInput.setTableName(tableName); countInput.setFilter(filter); + countInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); CountOutput countOutput = new CountAction().execute(countInput); output.put("count", countOutput.getCount()); } @@ -595,6 +598,7 @@ public class ApiImplementation GetInput getInput = new GetInput(); getInput.setTableName(tableName); + getInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND); PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ); @@ -1144,8 +1148,8 @@ public class ApiImplementation ApiProcessOutputInterface output = apiProcessMetaData.getOutput(); if(output != null) { - Serializable outputForProcess = output.getOutputForProcess(runProcessInput, runProcessOutput); - HttpApiResponse httpApiResponse = new HttpApiResponse(output.getSuccessStatusCode(runProcessInput, runProcessOutput), outputForProcess); + Serializable outputForProcess = output.getOutputForProcess(runProcessInput, runProcessOutput); + HttpApiResponse httpApiResponse = new HttpApiResponse(output.getSuccessStatusCode(runProcessInput, runProcessOutput), outputForProcess); output.customizeHttpApiResponse(httpApiResponse, runProcessInput, runProcessOutput); return httpApiResponse; } From 35c1150f8054bcb13e0dc39a9c1dc52b75ad1a8d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 19 Jun 2024 15:59:02 -0500 Subject: [PATCH 10/10] Throw explicit exception if backend is missing a name --- .../qqq/backend/module/rdbms/jdbc/ConnectionManager.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java index 1e5eeea1..8068e725 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManager.java @@ -75,10 +75,11 @@ public class ConnectionManager *******************************************************************************/ private static ConnectionProviderInterface getConnectionProvider(RDBMSBackendMetaData backend) throws QException { - ////////////////////////////////////////////////////////////////////////////////// - // some non-standard use-cases use a backend without a name... avoid NPE in map // - ////////////////////////////////////////////////////////////////////////////////// String name = Objects.requireNonNullElse(backend.getName(), ""); + if(!StringUtils.hasContent(name)) + { + throw (new QException("RDBMSBackendMetaData is missing a name")); + } if(!connectionProviderMap.containsKey(name)) {