diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java index 3dc59da1..885dc6f6 100644 --- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java @@ -25,7 +25,6 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.io.Serializable; import java.sql.Connection; import java.sql.ResultSet; -import java.sql.ResultSetMetaData; import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; @@ -76,7 +75,6 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf { QueryManager.executeStatement(connection, sql, ((ResultSet resultSet) -> { - ResultSetMetaData metaData = resultSet.getMetaData(); if(resultSet.next()) { rs.setCount(resultSet.getInt("record_count")); diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index e792329c..5c8361fb 100644 --- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.io.Serializable; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; @@ -42,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -101,12 +103,12 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf try(Connection connection = getConnection(queryInput)) { - QueryManager.executeStatement(connection, sql, ((ResultSet resultSet) -> + PreparedStatement statement = createStatement(connection, sql, queryInput); + QueryManager.executeStatement(statement, ((ResultSet resultSet) -> { ResultSetMetaData metaData = resultSet.getMetaData(); while(resultSet.next()) { - // todo - should refactor this for view etc to use too. // todo - Add display values (String labels for possibleValues, formatted #'s, etc) QRecord record = new QRecord(); record.setTableName(table.getName()); @@ -137,6 +139,32 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf + /******************************************************************************* + ** + *******************************************************************************/ + private PreparedStatement createStatement(Connection connection, String sql, QueryInput queryInput) throws SQLException + { + RDBMSBackendMetaData backend = (RDBMSBackendMetaData) queryInput.getBackend(); + PreparedStatement statement; + if("mysql".equals(backend.getVendor())) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // mysql "optimization", presumably here - from Result Set section of https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-implementation-notes.html // + // without this change, we saw ~10 seconds of "wait" time, before results would start to stream out of a large query (e.g., > 1,000,000 rows). // + // with this change, we start to get results immediately, and the total runtime also seems lower... // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + statement = connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + statement.setFetchSize(Integer.MIN_VALUE); + } + else + { + statement = connection.prepareStatement(sql); + } + return (statement); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java index c694cbad..fbd634e5 100644 --- a/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java +++ b/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java @@ -87,19 +87,13 @@ public class QueryManager /******************************************************************************* ** *******************************************************************************/ - public static void executeStatement(Connection connection, String sql, ResultSetProcessor procesor, Object... params) throws SQLException + public static void executeStatement(Connection connection, String sql, ResultSetProcessor processor, Object... params) throws SQLException { PreparedStatement statement = null; - ResultSet resultSet = null; - try { statement = prepareStatementAndBindParams(connection, sql, params); - incrementStatistic(STAT_QUERIES_RAN); - statement.execute(); - resultSet = statement.getResultSet(); - - procesor.processResultSet(resultSet); + executeStatement(statement, processor, params); } finally { @@ -107,7 +101,30 @@ public class QueryManager { statement.close(); } + } + } + + + /******************************************************************************* + ** Let the caller provide their own prepared statement (e.g., possibly with some + ** customized settings/optimizations). + *******************************************************************************/ + public static void executeStatement(PreparedStatement statement, ResultSetProcessor processor, Object... params) throws SQLException + { + ResultSet resultSet = null; + + try + { + bindParams(statement, params); + incrementStatistic(STAT_QUERIES_RAN); + statement.execute(); + resultSet = statement.getResultSet(); + + processor.processResultSet(resultSet); + } + finally + { if(resultSet != null) { resultSet.close(); @@ -653,34 +670,34 @@ public class QueryManager return (1); } else*/ - if(value instanceof Integer) + if(value instanceof Integer i) { - bindParam(statement, index, (Integer) value); + bindParam(statement, index, i); return (1); } - else if(value instanceof Short) + else if(value instanceof Short s) { - bindParam(statement, index, ((Short) value).intValue()); + bindParam(statement, index, s.intValue()); return (1); } - else if(value instanceof Long) + else if(value instanceof Long l) { - bindParam(statement, index, ((Long) value).intValue()); + bindParam(statement, index, l.intValue()); return (1); } - else if(value instanceof String) + else if(value instanceof String s) { - bindParam(statement, index, (String) value); + bindParam(statement, index, s); return (1); } - else if(value instanceof Boolean) + else if(value instanceof Boolean b) { - bindParam(statement, index, (Boolean) value); + bindParam(statement, index, b); return (1); } - else if(value instanceof Timestamp) + else if(value instanceof Timestamp ts) { - bindParam(statement, index, (Timestamp) value); + bindParam(statement, index, ts); return (1); } else if(value instanceof Date) @@ -688,14 +705,14 @@ public class QueryManager bindParam(statement, index, (Date) value); return (1); } - else if(value instanceof Calendar) + else if(value instanceof Calendar c) { - bindParam(statement, index, (Calendar) value); + bindParam(statement, index, c); return (1); } - else if(value instanceof BigDecimal) + else if(value instanceof BigDecimal bd) { - bindParam(statement, index, (BigDecimal) value); + bindParam(statement, index, bd); return (1); } else if(value == null) @@ -703,42 +720,47 @@ public class QueryManager statement.setNull(index, Types.CHAR); return (1); } - else if(value instanceof Collection) + else if(value instanceof Collection c) { - Collection collection = (Collection) value; - int paramsBound = 0; - for(Object o : collection) + int paramsBound = 0; + for(Object o : c) { paramsBound += bindParamObject(statement, (index + paramsBound), o); } return (paramsBound); } - else if(value instanceof byte[]) + else if(value instanceof byte[] ba) { - statement.setBytes(index, (byte[]) value); + statement.setBytes(index, ba); return (1); } - else if(value instanceof Instant) + else if(value instanceof Instant i) { - Timestamp timestamp = new Timestamp(((Instant) value).toEpochMilli()); + long epochMillis = i.toEpochMilli(); + Timestamp timestamp = new Timestamp(epochMillis); statement.setTimestamp(index, timestamp); return (1); } - else if(value instanceof LocalDate) + else if(value instanceof LocalDate ld) { - Timestamp timestamp = new Timestamp(((LocalDate) value).atTime(0, 0).toEpochSecond(ZoneOffset.UTC) * MS_PER_SEC); + ZoneOffset offset = OffsetDateTime.now().getOffset(); + long epochMillis = ld.atStartOfDay().toEpochSecond(offset) * MS_PER_SEC; + Timestamp timestamp = new Timestamp(epochMillis); statement.setTimestamp(index, timestamp); return (1); } - else if(value instanceof OffsetDateTime) + else if(value instanceof OffsetDateTime odt) { - Timestamp timestamp = new Timestamp(((OffsetDateTime) value).toEpochSecond() * MS_PER_SEC); + long epochMillis = odt.toEpochSecond() * MS_PER_SEC; + Timestamp timestamp = new Timestamp(epochMillis); statement.setTimestamp(index, timestamp); return (1); } - else if(value instanceof LocalDateTime) + else if(value instanceof LocalDateTime ldt) { - Timestamp timestamp = new Timestamp(((LocalDateTime) value).toEpochSecond(ZoneOffset.UTC) * MS_PER_SEC); + ZoneOffset offset = OffsetDateTime.now().getOffset(); + long epochMillis = ldt.toEpochSecond(offset) * MS_PER_SEC; + Timestamp timestamp = new Timestamp(epochMillis); statement.setTimestamp(index, timestamp); return (1); } diff --git a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java index 1d0cae91..94978f2a 100644 --- a/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java @@ -32,6 +32,7 @@ import java.sql.SQLException; import java.sql.Timestamp; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.Month; import java.time.OffsetDateTime; import java.util.GregorianCalendar; import com.kingsrook.qqq.backend.module.rdbms.TestUtils; @@ -58,7 +59,7 @@ class QueryManagerTest void beforeEach() throws SQLException { Connection connection = getConnection(); - QueryManager.executeUpdate(connection, "CREATE TABLE t (i INTEGER, dt DATETIME, c CHAR(1))"); + QueryManager.executeUpdate(connection, "CREATE TABLE t (i INTEGER, dt DATETIME, c CHAR(1), d DATE)"); } @@ -86,7 +87,8 @@ class QueryManagerTest /******************************************************************************* - ** Test the various overloads that bind params + ** Test the various overloads that bind params. + ** Note, we're just confirming that these methods don't throw... *******************************************************************************/ @Test void testBindParams() throws SQLException @@ -224,4 +226,46 @@ class QueryManagerTest assertNull(QueryManager.getObject(rs, 3)); } + + + /******************************************************************************* + ** We had a bug where LocalDates weren't being properly bound. This test + ** confirms (more?) correct behavior + *******************************************************************************/ + @Test + void testLocalDate() throws SQLException + { + Connection connection = getConnection(); + QueryManager.executeUpdate(connection, "INSERT INTO t (d) VALUES (?)", LocalDate.of(2013, Month.OCTOBER, 1)); + + PreparedStatement preparedStatement = connection.prepareStatement("SELECT d from t"); + 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"); + + 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"); + + 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"); + } + } \ No newline at end of file