diff --git a/fluent-jdbc/.gitignore b/fluent-jdbc/.gitignore new file mode 100644 index 0000000..072e996 --- /dev/null +++ b/fluent-jdbc/.gitignore @@ -0,0 +1,5 @@ +/target/ +/.settings/ +/.classpath +/.project +/*.log \ No newline at end of file diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/mapper/CallableMappers.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/mapper/CallableMappers.java new file mode 100644 index 0000000..105a11d --- /dev/null +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/mapper/CallableMappers.java @@ -0,0 +1,57 @@ +package org.codejargon.fluentjdbc.api.mapper; + +import java.math.BigDecimal; + +import org.codejargon.fluentjdbc.api.query.CallableMapper; + +/** + *

A set of common mappers for convenience.

+ * @see org.codejargon.fluentjdbc.api.mapper.ObjectMappers + */ +public abstract class CallableMappers { + private static final CallableMapper singleInteger = (cs) -> cs.getInt(1); + private static final CallableMapper singleLong = (cs) -> cs.getLong(1); + private static final CallableMapper singleString = (cs) -> cs.getString(1); + private static final CallableMapper singleBigDecimal = (cs) -> cs.getBigDecimal(1); + private static final CallableMapper singleBoolean = (cs) -> cs.getBoolean(1); + + /** + * Maps the first Integer column. + * @return first Integer column + */ + public static CallableMapper singleInteger() { + return singleInteger; + } + + /** + * Maps the first Long column. + * @return first Long column + */ + public static CallableMapper singleLong() { + return singleLong; + } + + /** + * Maps the first string column + * @return first string column + */ + public static CallableMapper singleString() { + return singleString; + } + + /** + * Maps the first BigDecimal column + * @return first BigDecimal column + */ + public static CallableMapper singleBigDecimal() { + return singleBigDecimal; + } + + /** + * Maps the first Boolean column + * @return first Boolean column + */ + public static CallableMapper singleBoolean() { + return singleBoolean; + } +} diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/CallableMapper.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/CallableMapper.java new file mode 100644 index 0000000..55a34a5 --- /dev/null +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/CallableMapper.java @@ -0,0 +1,20 @@ +/* +* $Id: $ +* +* Copyright (c) 2021, CGI. All rights reserved. +* CGI PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. +*/ +package org.codejargon.fluentjdbc.api.query; + +import java.sql.CallableStatement; +import java.sql.SQLException; + +/** + * The {@code CallableResultMapper} class implements ... + * TODO write proper class description + * + * @author wiedermanna + */ +public interface CallableMapper { + T map(CallableStatement statement) throws SQLException; +} diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/CallableQuery.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/CallableQuery.java new file mode 100644 index 0000000..2f04547 --- /dev/null +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/CallableQuery.java @@ -0,0 +1,61 @@ +package org.codejargon.fluentjdbc.api.query; + +import java.sql.CallableStatement; +import java.util.List; +import java.util.Map; + +/** + * Select query for a SQL statement. A SelectQuery is mutable, non-threadsafe. + */ +public interface CallableQuery { + /** + * Adds positional query parameters. + * + * @param params additional query parameters + * @return this + */ + CallableQuery params(List params); + + /** + * Adds positional query parameters. + * + * @param params additional query parameters + * @return this + */ + CallableQuery params(Object... params); + + /** + * Adds named query paramaters. + * + * @param namedParams additional named query parameters + * @return this + */ + CallableQuery namedParams(Map namedParams); + + /** + * Adds a named query parameter + * + * @param name name of parameter + * @param parameter value of parameter + * @return this + */ + CallableQuery namedParam(String name, Object parameter); + + /** + * Sets a custom error handler + * + * @param sqlErrorHandler + * @return this + */ + CallableQuery errorHandler(SqlErrorHandler sqlErrorHandler); + + /** + * executes the callable statement and returns a single result. + * + * @param mapper {@link CallableStatement} mapper + * @param result type + * @return exactly one result + * @throws org.codejargon.fluentjdbc.api.FluentJdbcException if no result found + */ + T result(CallableMapper mapper); +} diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/Query.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/Query.java index daed639..b003106 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/Query.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/Query.java @@ -2,10 +2,6 @@ import org.codejargon.fluentjdbc.api.query.inspection.DatabaseInspection; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.function.Function; - /** *

FluentJdbc Query API to create select, update/insert, and batch update/insert queries. Immutable, thread-safe.

* @see org.codejargon.fluentjdbc.api.integration.ConnectionProvider @@ -35,6 +31,14 @@ public interface Query { */ BatchQuery batch(String sql); + /** + * Creates a callable query for SQL statement + * + * @param sql callable SQL statement + * @return callable query for SQL statement + */ + CallableQuery call(String sql); + /** * Transaction control * diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/SqlErrorHandler.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/SqlErrorHandler.java index 2d2c10a..71d9762 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/SqlErrorHandler.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/SqlErrorHandler.java @@ -3,19 +3,24 @@ import java.sql.SQLException; import java.util.Optional; +import org.codejargon.fluentjdbc.api.query.listen.QueryInfo; + public interface SqlErrorHandler { enum Action { RETRY } /** - * Handles SQL errors, may implement logging, etc. The handler should always rethrow an exception in case of - * a critical error. Otherwise retry action can be triggered. The handler is responsible to implement - * delay or limitations for the retry. + * Handles SQL errors, may implement logging, etc. The handler should always + * re-throw an exception in case of a critical error. Otherwise retry action can + * be triggered. The handler is responsible to implement delay or limitations + * for the retry. * - * @param e the error - * @param sql The sql query. Always present unless the error was thrown by direct plainConnection() usage. - * @return In case no exception is thrown, otherwise action needs to be returned ( eg retry ). + * @param e the error + * @param queryInfo Query info including the SQL query. Always present unless + * the error was thrown by direct plainConnection() usage. + * @return In case no exception is thrown, otherwise action needs to be returned + * ( eg retry ). */ - Action handle(SQLException e, Optional sql) throws SQLException; + Action handle(SQLException e, Optional queryInfo) throws SQLException; } diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/listen/ExecutionDetails.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/listen/ExecutionDetails.java index e5b96ed..e34bf1d 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/listen/ExecutionDetails.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/listen/ExecutionDetails.java @@ -5,7 +5,7 @@ public interface ExecutionDetails { Boolean success(); - String sql(); + QueryInfo queryInfo(); Long executionTimeMs(); Optional sqlException(); } diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/listen/QueryInfo.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/listen/QueryInfo.java new file mode 100644 index 0000000..6063b40 --- /dev/null +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/api/query/listen/QueryInfo.java @@ -0,0 +1,32 @@ +/** + * + */ +package org.codejargon.fluentjdbc.api.query.listen; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Information about what was executed - query + parameters. Parameters are not available for batch executions + * + * @author bwc + */ +public interface QueryInfo { + + /** + * @return the sql query which was executed + */ + String sql(); + + /** + * @return optional parameters + */ + Optional> params(); + + /** + * @return optional named parameters + */ + Optional> namedParams(); + +} diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/BatchQueryInternal.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/BatchQueryInternal.java index 5c945ef..fe8336d 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/BatchQueryInternal.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/BatchQueryInternal.java @@ -1,28 +1,34 @@ package org.codejargon.fluentjdbc.internal.query; -import org.codejargon.fluentjdbc.api.FluentJdbcException; -import org.codejargon.fluentjdbc.api.query.*; -import org.codejargon.fluentjdbc.internal.query.namedparameter.NamedTransformedSql; -import org.codejargon.fluentjdbc.internal.query.namedparameter.NamedTransformedSqlFactory; -import org.codejargon.fluentjdbc.internal.support.Ints; -import org.codejargon.fluentjdbc.internal.query.namedparameter.SqlAndParamsForNamed; -import org.codejargon.fluentjdbc.internal.support.Preconditions; +import static java.util.Optional.empty; +import static java.util.stream.Collectors.toList; +import static org.codejargon.fluentjdbc.internal.support.Iterables.stream; +import static org.codejargon.fluentjdbc.internal.support.Sneaky.consumer; -import java.awt.geom.AffineTransform; import java.sql.Connection; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; -import static java.util.Optional.empty; -import static java.util.stream.Collectors.toList; -import static org.codejargon.fluentjdbc.internal.support.Sneaky.consumer; -import static org.codejargon.fluentjdbc.internal.support.Iterables.stream; +import org.codejargon.fluentjdbc.api.FluentJdbcException; +import org.codejargon.fluentjdbc.api.query.BatchQuery; +import org.codejargon.fluentjdbc.api.query.Mapper; +import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; +import org.codejargon.fluentjdbc.api.query.UpdateResult; +import org.codejargon.fluentjdbc.api.query.UpdateResultGenKeys; +import org.codejargon.fluentjdbc.internal.query.namedparameter.NamedTransformedSql; +import org.codejargon.fluentjdbc.internal.query.namedparameter.NamedTransformedSqlFactory; +import org.codejargon.fluentjdbc.internal.query.namedparameter.SqlAndParamsForNamed; +import org.codejargon.fluentjdbc.internal.support.Ints; +import org.codejargon.fluentjdbc.internal.support.Preconditions; class BatchQueryInternal implements BatchQuery { private static final String namedSet = "Named parameters are already set."; @@ -111,7 +117,7 @@ private List run(FetchGenKey fetchGen) { Preconditions.checkArgument(params.isPresent() || namedParams.isPresent(), "Parameters must be set to run a batch query"); return query.query( connection -> params.isPresent() ? positional(connection, fetchGen) : named(connection, fetchGen), - Optional.of(sql), + Optional.of(QueryInfoInternal.of(sql)), sqlErrorHandler.get() ); } diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/CallableQueryInternal.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/CallableQueryInternal.java new file mode 100644 index 0000000..00398fe --- /dev/null +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/CallableQueryInternal.java @@ -0,0 +1,68 @@ +package org.codejargon.fluentjdbc.internal.query; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +import org.codejargon.fluentjdbc.api.FluentJdbcException; +import org.codejargon.fluentjdbc.api.query.CallableMapper; +import org.codejargon.fluentjdbc.api.query.CallableQuery; +import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; + +class CallableQueryInternal extends SingleQueryBase implements CallableQuery { + + CallableQueryInternal(String sql, QueryInternal query) { + super(query, sql); + } + + @Override + public CallableQuery params(List params) { + addParameters(params); + return this; + } + + @Override + public CallableQuery params(Object... params) { + addParameters(params); + return this; + } + + @Override + public CallableQuery namedParams(Map namedParams) { + addNamedParameters(namedParams); + return this; + } + + @Override + public CallableQuery namedParam(String name, Object parameter) { + addNamedParameter(name, parameter); + return this; + } + + @Override + public CallableQuery errorHandler(SqlErrorHandler sqlErrorHandler ) { + this.sqlErrorHandler = () -> sqlErrorHandler; + return this; + } + + + @Override + public T result(CallableMapper mapper) { + return runQuery(ps -> { + if (ps instanceof CallableStatement) { + CallableStatement cs = (CallableStatement)ps; + cs.execute(); + return mapper.map(cs); + } else { + throw new FluentJdbcException("No out parameters specified for call"); + } + }, sqlErrorHandler.get()); + } + + @Override + void customizeQuery(PreparedStatement statement, QueryConfig config) throws SQLException { + // nothing to do here for callable statements + } +} diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DatabaseInspectionInternal.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DatabaseInspectionInternal.java index fbcd1d8..ac4d428 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DatabaseInspectionInternal.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DatabaseInspectionInternal.java @@ -1,12 +1,12 @@ package org.codejargon.fluentjdbc.internal.query; +import java.util.Optional; + import org.codejargon.fluentjdbc.api.query.inspection.DatabaseInspection; import org.codejargon.fluentjdbc.api.query.inspection.MetaDataAccess; import org.codejargon.fluentjdbc.api.query.inspection.MetaDataResultSet; import org.codejargon.fluentjdbc.api.query.inspection.MetaDataSelect; -import java.util.Optional; - class DatabaseInspectionInternal implements DatabaseInspection { private final QueryInternal query; @@ -18,7 +18,7 @@ class DatabaseInspectionInternal implements DatabaseInspection { public T accessMetaData(MetaDataAccess access) { return query.query( connection -> access.access(connection.getMetaData()), - Optional.of("JDBC Database Inspection"), + Optional.of(QueryInfoInternal.of("JDBC Database Inspection")), query.config.defaultSqlErrorHandler.get() ); } diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DefaultParamSetters.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DefaultParamSetters.java index 925ac35..ede0783 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DefaultParamSetters.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DefaultParamSetters.java @@ -1,13 +1,25 @@ package org.codejargon.fluentjdbc.internal.query; -import org.codejargon.fluentjdbc.api.ParamSetter; - -import java.time.*; +import java.sql.CallableStatement; +import java.sql.JDBCType; +import java.sql.SQLType; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.OffsetDateTime; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZonedDateTime; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; +import org.codejargon.fluentjdbc.api.FluentJdbcException; +import org.codejargon.fluentjdbc.api.ParamSetter; + class DefaultParamSetters { private static final Map setters; @@ -17,6 +29,7 @@ class DefaultParamSetters { javaDate(ss); javaTime(ss); javaBinary(ss); + callableOutParams(ss); setters = Collections.unmodifiableMap(ss); } @@ -41,6 +54,28 @@ private static void javaBinary(Map ss) { reg(ss, byte[].class, (param, ps, i) -> ps.setBytes(i, param)); } + /** + * CallableStatement out parameters are indicated by specifying parameter with type of {@link JDBCType} + * + * @param ss map of setters + */ + private static void callableOutParams(Map ss) { + reg(ss, SQLType.class, (param, ps, i) -> { + if (ps instanceof CallableStatement) { + ((CallableStatement)ps).registerOutParameter(i, param); + } else { + throw new FluentJdbcException("use SQLType only for CallableStatements as out parameters"); + } + }); + reg(ss, JDBCType.class, (param, ps, i) -> { + if (ps instanceof CallableStatement) { + ((CallableStatement)ps).registerOutParameter(i, param); + } else { + throw new FluentJdbcException("use JDBCType only for CallableStatements as out parameters"); + } + }); + } + static Map setters() { return setters; } diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DefaultSqlHandler.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DefaultSqlHandler.java index 89a5d5b..372f459 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DefaultSqlHandler.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/DefaultSqlHandler.java @@ -1,13 +1,14 @@ package org.codejargon.fluentjdbc.internal.query; -import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; - import java.sql.SQLException; import java.util.Optional; +import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; +import org.codejargon.fluentjdbc.api.query.listen.QueryInfo; + public class DefaultSqlHandler implements SqlErrorHandler { @Override - public Action handle(SQLException e, Optional sql) throws SQLException { + public Action handle(SQLException e, Optional queryInfo) throws SQLException { throw e; } } diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/ExecutionDetailsInternal.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/ExecutionDetailsInternal.java index e4434e7..ba80552 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/ExecutionDetailsInternal.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/ExecutionDetailsInternal.java @@ -1,20 +1,21 @@ package org.codejargon.fluentjdbc.internal.query; -import org.codejargon.fluentjdbc.api.query.listen.ExecutionDetails; - import java.sql.SQLException; import java.util.Optional; +import org.codejargon.fluentjdbc.api.query.listen.ExecutionDetails; +import org.codejargon.fluentjdbc.api.query.listen.QueryInfo; + class ExecutionDetailsInternal implements ExecutionDetails { - private final String sql; + private final QueryInfo queryInfo; private final Long executionTimeMs; private final Optional sqlException; public ExecutionDetailsInternal( - String sql, + QueryInfo queryInfo, Long executionTimeMs, Optional sqlException) { - this.sql = sql; + this.queryInfo = queryInfo; this.executionTimeMs = executionTimeMs; this.sqlException = sqlException; } @@ -25,8 +26,8 @@ public Boolean success() { } @Override - public String sql() { - return sql; + public QueryInfo queryInfo() { + return queryInfo; } @Override diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/PreparedStatementFactory.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/PreparedStatementFactory.java index 0fbd53a..74cf1d6 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/PreparedStatementFactory.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/PreparedStatementFactory.java @@ -1,5 +1,6 @@ package org.codejargon.fluentjdbc.internal.query; +import java.sql.CallableStatement; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -14,9 +15,11 @@ class PreparedStatementFactory { this.config = config; } - PreparedStatement createSingle(Connection con, SingleQueryBase singleQueryBase, boolean fetchGenerated, String[] genColumns) throws SQLException { + PreparedStatement createSingle(Connection con, SingleQueryBase singleQueryBase, boolean fetchGenerated, + String[] genColumns) throws SQLException { SqlAndParams sqlAndParams = singleQueryBase.sqlAndParams(config); - PreparedStatement statement = prepareStatement(con, sqlAndParams.sql(), fetchGenerated, genColumns); + PreparedStatement statement = sqlAndParams.hasOutParameters() ? prepareCall(con, sqlAndParams.sql()) + : prepareStatement(con, sqlAndParams.sql(), fetchGenerated, genColumns); singleQueryBase.customizeQuery(statement, config); assignParams(statement, sqlAndParams.params()); return statement; @@ -39,7 +42,9 @@ private PreparedStatement prepareStatement(Connection con, String sql, Boolean f con.prepareStatement(sql); } - + private CallableStatement prepareCall(Connection con, String sql) throws SQLException { + return con.prepareCall(sql); + } } diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/QueryInfoInternal.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/QueryInfoInternal.java new file mode 100644 index 0000000..06f964f --- /dev/null +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/QueryInfoInternal.java @@ -0,0 +1,69 @@ +/** + * + */ +package org.codejargon.fluentjdbc.internal.query; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.codejargon.fluentjdbc.api.query.listen.QueryInfo; + +/** + * Implementation of {@link QueryInfo} + * + * @author bwc + */ +public class QueryInfoInternal implements QueryInfo { + private final String sql; + private final Optional> params; + private final Optional> namedParams; + + /** + * @param sql + * @param params + * @param namedParams + */ + public QueryInfoInternal(String sql, Optional> params, Optional> namedParams) { + this.sql = sql; + this.params = params; + this.namedParams = namedParams; + } + + public static Optional optional(String sql, List params, Map namedParams) { + return Optional.of(of(sql, params, namedParams)); + } + + public static QueryInfo of(String sql, List params, Map namedParams) { + return new QueryInfoInternal(sql, params.isEmpty() ? Optional.empty() : Optional.of(params), + namedParams.isEmpty() ? Optional.empty() : Optional.of(namedParams)); + } + + public static QueryInfo of(String sql) { + return new QueryInfoInternal(sql, Optional.empty(), Optional.empty()); + } + + /** + * @return the sql + */ + @Override + public String sql() { + return sql; + } + + /** + * @return the params + */ + @Override + public Optional> params() { + return params; + } + + /** + * @return the namedParams + */ + @Override + public Optional> namedParams() { + return namedParams; + } +} diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/QueryInternal.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/QueryInternal.java index 9f5f919..f9b2e91 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/QueryInternal.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/QueryInternal.java @@ -1,20 +1,27 @@ package org.codejargon.fluentjdbc.internal.query; -import org.codejargon.fluentjdbc.api.FluentJdbcException; -import org.codejargon.fluentjdbc.api.FluentJdbcSqlException; -import org.codejargon.fluentjdbc.api.integration.ConnectionProvider; -import org.codejargon.fluentjdbc.api.query.*; -import org.codejargon.fluentjdbc.api.query.inspection.DatabaseInspection; -import org.codejargon.fluentjdbc.internal.integration.QueryConnectionReceiverInternal; +import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkNotNull; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; import java.util.Optional; -import java.util.function.Function; -import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkNotNull; +import org.codejargon.fluentjdbc.api.FluentJdbcException; +import org.codejargon.fluentjdbc.api.FluentJdbcSqlException; +import org.codejargon.fluentjdbc.api.integration.ConnectionProvider; +import org.codejargon.fluentjdbc.api.query.BatchQuery; +import org.codejargon.fluentjdbc.api.query.CallableQuery; +import org.codejargon.fluentjdbc.api.query.PlainConnectionQuery; +import org.codejargon.fluentjdbc.api.query.Query; +import org.codejargon.fluentjdbc.api.query.SelectQuery; +import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; +import org.codejargon.fluentjdbc.api.query.Transaction; +import org.codejargon.fluentjdbc.api.query.UpdateQuery; +import org.codejargon.fluentjdbc.api.query.inspection.DatabaseInspection; +import org.codejargon.fluentjdbc.api.query.listen.QueryInfo; +import org.codejargon.fluentjdbc.internal.integration.QueryConnectionReceiverInternal; public class QueryInternal implements Query { @@ -46,6 +53,11 @@ public BatchQuery batch(String sql) { return new BatchQueryInternal(sql, this); } + @Override + public CallableQuery call(String sql) { + return new CallableQueryInternal(sql, this); + } + @Override public Transaction transaction() { return new TransactionInternal(this); @@ -57,10 +69,10 @@ public DatabaseInspection databaseInspection() { return new DatabaseInspectionInternal(this); } - T query(QueryRunnerConnection runner, Optional sql, SqlErrorHandler sqlErrorHandler) { + T query(QueryRunnerConnection runner, Optional queryInfo, SqlErrorHandler sqlErrorHandler) { AttemptResult ret = new AttemptResult<>(null, false); while(!ret.success) { - ret = attemptQuery(runner, sql, sqlErrorHandler); + ret = attemptQuery(runner, queryInfo, sqlErrorHandler); } return ret.result; } @@ -70,21 +82,22 @@ public T plainConnection(PlainConnectionQuery plainConnectionQuery) { return query(plainConnectionQuery::operation, Optional.empty(), config.defaultSqlErrorHandler.get()); } - FluentJdbcException queryException(String sql, Optional reason, Optional e) { - String message = String.format( - "Error running query" + (reason.isPresent() ? ": " + reason.get() : "") + ", %s", sql - ); + FluentJdbcException queryException(Optional queryInfo, Optional reason, Optional e) { + String sql = queryInfo.map(QueryInfo::sql).orElse(""); + String message = reason.isPresent() + ? String.format("Error running query: %s, %s", reason.get(), sql) + : String.format("Error running query, %s", sql); return e.isPresent() ? new FluentJdbcSqlException(message, e.get()) : new FluentJdbcException(message); } - private AttemptResult attemptQuery(QueryRunnerConnection runner, Optional sql, SqlErrorHandler sqlErrorHandler) { + private AttemptResult attemptQuery(QueryRunnerConnection runner, Optional queryInfo, SqlErrorHandler sqlErrorHandler) { long start = System.currentTimeMillis(); try { T returnValue = doQuery(runner); - listen(sql, start, Optional.empty()); + listen(queryInfo, start, Optional.empty()); return new AttemptResult<>(returnValue, true); } catch (SQLException e) { - handleError(sql, sqlErrorHandler, start, e); + handleError(queryInfo, sqlErrorHandler, start, e); return new AttemptResult<>(null, false); } } @@ -105,21 +118,21 @@ void assignParams(PreparedStatement statement, List params) throws SQLExcepti preparedStatementFactory.assignParams(statement, params); } - private void listen(Optional sql, long start, Optional e) { + private void listen(Optional queryInfo, long start, Optional e) { config.afterQueryListener.ifPresent( afterQueryListener -> - sql.ifPresent(sqlQuery -> afterQueryListener.listen( - new ExecutionDetailsInternal(sqlQuery, System.currentTimeMillis() - start, e) + queryInfo.ifPresent(sqlQueryInfo -> afterQueryListener.listen( + new ExecutionDetailsInternal(sqlQueryInfo, System.currentTimeMillis() - start, e) )) ); } - private void handleError(Optional sql, SqlErrorHandler sqlErrorHandler, long start, SQLException e) { + private void handleError(Optional queryInfo, SqlErrorHandler sqlErrorHandler, long start, SQLException e) { try { - checkNotNull(sqlErrorHandler.handle(e, sql), "Action in SqlErrorHandler"); + checkNotNull(sqlErrorHandler.handle(e, queryInfo), "Action in SqlErrorHandler"); } catch(SQLException sqle) { - listen(sql, start, Optional.of(e)); - throw queryException(sql.orElse(""), Optional.empty(), Optional.of(e)); + listen(queryInfo, start, Optional.of(e)); + throw queryException(queryInfo, Optional.empty(), Optional.of(e)); } } diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SelectQueryInternal.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SelectQueryInternal.java index a4368c1..87796ef 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SelectQueryInternal.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SelectQueryInternal.java @@ -1,22 +1,28 @@ package org.codejargon.fluentjdbc.internal.query; -import org.codejargon.fluentjdbc.api.FluentJdbcException; -import org.codejargon.fluentjdbc.api.query.Mapper; -import org.codejargon.fluentjdbc.api.query.SelectQuery; -import org.codejargon.fluentjdbc.api.query.SqlConsumer; -import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; -import org.codejargon.fluentjdbc.internal.support.Predicates; +import static java.util.Optional.empty; +import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkArgument; +import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkNotNull; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; -import static java.util.Optional.empty; -import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkArgument; -import static org.codejargon.fluentjdbc.internal.support.Preconditions.checkNotNull; +import org.codejargon.fluentjdbc.api.FluentJdbcException; +import org.codejargon.fluentjdbc.api.query.Mapper; +import org.codejargon.fluentjdbc.api.query.SelectQuery; +import org.codejargon.fluentjdbc.api.query.SqlConsumer; +import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; +import org.codejargon.fluentjdbc.internal.support.Predicates; class SelectQueryInternal extends SingleQueryBase implements SelectQuery { @@ -105,7 +111,7 @@ public Optional firstResult(Mapper mapper) { public T singleResult(Mapper mapper) { Optional firstResult = firstResult(mapper); if (!firstResult.isPresent()) { - throw query.queryException(sql, Optional.of("At least one result expected"), empty()); + throw query.queryException(QueryInfoInternal.optional(sql, params, namedParams), Optional.of("At least one result expected"), empty()); } return firstResult.get(); } diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SingleQueryBase.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SingleQueryBase.java index c1c347d..26b062c 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SingleQueryBase.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SingleQueryBase.java @@ -1,14 +1,18 @@ package org.codejargon.fluentjdbc.internal.query; -import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; -import org.codejargon.fluentjdbc.internal.query.namedparameter.SqlAndParamsForNamed; -import org.codejargon.fluentjdbc.internal.support.Preconditions; - import java.sql.PreparedStatement; import java.sql.SQLException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.function.Supplier; +import org.codejargon.fluentjdbc.api.query.SqlErrorHandler; +import org.codejargon.fluentjdbc.internal.query.namedparameter.SqlAndParamsForNamed; +import org.codejargon.fluentjdbc.internal.support.Preconditions; + abstract class SingleQueryBase { protected final String sql; protected final QueryInternal query; @@ -68,7 +72,7 @@ private T runQuery( try (PreparedStatement ps = query.preparedStatementFactory.createSingle(connection, this, fetchGenerated, genColumns)) { return queryRunnerPreparedStatement.run(ps); } - }, Optional.of(sql), + }, QueryInfoInternal.optional(sql, params, namedParams), sqlErrorHandler); } diff --git a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SqlAndParams.java b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SqlAndParams.java index f9cfcf9..9319107 100644 --- a/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SqlAndParams.java +++ b/fluent-jdbc/src/main/java/org/codejargon/fluentjdbc/internal/query/SqlAndParams.java @@ -1,5 +1,6 @@ package org.codejargon.fluentjdbc.internal.query; +import java.sql.SQLType; import java.util.List; public class SqlAndParams { @@ -18,4 +19,13 @@ String sql() { List params() { return params; } + + /** + * check whether parameters assume callable statement or prepared statement + * + * @return true if at least one of parameters is out parameter + */ + public boolean hasOutParameters() { + return params.parallelStream().anyMatch(param -> param instanceof SQLType); + } } diff --git a/fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/FluentJdbcCallTest.groovy b/fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/FluentJdbcCallTest.groovy new file mode 100644 index 0000000..7641902 --- /dev/null +++ b/fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/FluentJdbcCallTest.groovy @@ -0,0 +1,78 @@ +package org.codejargon.fluentjdbc.internal.query + +import org.codejargon.fluentjdbc.api.FluentJdbcException +import org.codejargon.fluentjdbc.api.query.CallableMapper; +import org.codejargon.fluentjdbc.api.FluentJdbcBuilder; +import org.codejargon.fluentjdbc.api.query.Query +import org.junit.Test +import spock.lang.Specification + +import javax.naming.OperationNotSupportedException; +import java.sql.Connection +import java.sql.ParameterMetaData; +import java.sql.CallableStatement; +import java.sql.SQLException +import java.sql.Types; +import java.util.* + +class FluentJdbcCallTest extends Specification { + + static String result1 = "res" + static String param1 = "lille" + static String param2 = "lamb" + + def connection = Mock(Connection) + def callableStatement = Mock(CallableStatement) + + Query query; + + def setup() { + query = new FluentJdbcBuilder().connectionProvider( + { q -> q.receive(connection)} + ).build().query(); + } + + def "Call with ordered parameters"() { + given: + def sql = "{? = call pack.func(?, ?)}" + connection.prepareCall(sql) >> callableStatement + mockSelectData() + def orderedParams = [java.sql.JDBCType.VARCHAR, param1, param2] + when: + query.call(sql).params(orderedParams).result(dummyMapper) + then: + 1 * callableStatement.registerOutParameter(1, java.sql.JDBCType.VARCHAR) + 1 * callableStatement.setObject(2, param1) + 1 * callableStatement.setObject(3, param2) + } + + def "Call with named parameters"() { + given: + def namedParamSql = "{:outPar = call pack.func(:param1, :param2, :param1)}" + def expectedSql = "{? = call pack.func(?, ?, ?)}" + connection.prepareCall(expectedSql) >> callableStatement + mockSelectData() + def namedParams = ["param1": param1, "outPar": java.sql.JDBCType.VARCHAR, "param2": param2] + when: + query.call(namedParamSql).namedParams(namedParams).result(dummyMapper) + then: + 1 * callableStatement.registerOutParameter(1, java.sql.JDBCType.VARCHAR) + 1 * callableStatement.setObject(2, param1) + 1 * callableStatement.setObject(3, param2) + 1 * callableStatement.setObject(4, param1) + } + + private void mockSelectData() { + callableStatement.getString(1) >> result1 + } + + static CallableMapper dummyMapper = { cs -> new Dummy(cs.getString(1)) } + + static class Dummy { + final String foo + + Dummy(String foo) { + this.foo = foo + } + } +} diff --git a/fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/QueryListenerTest.groovy b/fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/QueryListenerTest.groovy index 14b58ed..6008bd5 100644 --- a/fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/QueryListenerTest.groovy +++ b/fluent-jdbc/src/test/groovy/org/codejargon/fluentjdbc/internal/query/QueryListenerTest.groovy @@ -12,6 +12,7 @@ import java.sql.SQLException class QueryListenerTest extends Specification { protected static final def sql = "UPDATE FOO SET BAR = 'x' WHERE COL1 = ? AND COL2 = ?" + protected static final def namedSql = "UPDATE FOO SET BAR = 'x' WHERE COL1 = :name AND COL2 = :name" Query query def preparedStatement = Mock(PreparedStatement) Connection connection = Mock(Connection) @@ -29,13 +30,41 @@ class QueryListenerTest extends Specification { .query() } - def "Listener invoked"() { + def "Listener invoked without params"() { when: query.update(sql).run() then: executionDetails != null executionDetails.success() - executionDetails.sql() == sql + executionDetails.queryInfo().sql() == sql + !executionDetails.queryInfo().params().isPresent() + !executionDetails.queryInfo().namedParams().isPresent() + executionDetails.executionTimeMs() >= 0 + !executionDetails.sqlException().isPresent() + } + + def "Listener invoked with param"() { + when: + query.update(sql).params(5).run() + then: + executionDetails != null + executionDetails.success() + executionDetails.queryInfo().sql() == sql + executionDetails.queryInfo().params().isPresent() + !executionDetails.queryInfo().namedParams().isPresent() + executionDetails.executionTimeMs() >= 0 + !executionDetails.sqlException().isPresent() + } + + def "Listener invoked with named param"() { + when: + query.update(namedSql).namedParam("name", "param").run() + then: + executionDetails != null + executionDetails.success() + executionDetails.queryInfo().sql() == namedSql + !executionDetails.queryInfo().params().isPresent() + executionDetails.queryInfo().namedParams().isPresent() executionDetails.executionTimeMs() >= 0 !executionDetails.sqlException().isPresent() } @@ -49,7 +78,7 @@ class QueryListenerTest extends Specification { thrown(FluentJdbcSqlException) executionDetails != null !executionDetails.success() - executionDetails.sql() == sql + executionDetails.queryInfo().sql() == sql executionDetails.executionTimeMs() >= 0 executionDetails.sqlException().isPresent() }