diff --git a/common/src/main/java/com/moparisthebest/jdbc/TryClose.java b/common/src/main/java/com/moparisthebest/jdbc/TryClose.java index 3470745..90ecc38 100644 --- a/common/src/main/java/com/moparisthebest/jdbc/TryClose.java +++ b/common/src/main/java/com/moparisthebest/jdbc/TryClose.java @@ -2,6 +2,7 @@ package com.moparisthebest.jdbc; import javax.naming.Context; import java.io.Closeable; +import java.sql.Array; import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; @@ -48,6 +49,16 @@ public class TryClose { // tries to close certain object types } } + public static void tryClose(Array obj) { + if (obj == null) + return; + try { + obj.free(); + } catch (Throwable e) { + // ignore... + } + } + public static void tryClose(Closeable obj) { if (obj == null) return; diff --git a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java index 500aeea..a071056 100644 --- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java +++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java @@ -22,6 +22,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; //IFJAVA8_START import java.time.*; +import java.util.stream.Stream; //IFJAVA8_END import static com.moparisthebest.jdbc.TryClose.tryClose; @@ -69,7 +70,7 @@ public class JdbcMapperProcessor extends AbstractProcessor { static TypeMirror sqlExceptionType, stringType, numberType, utilDateType, readerType, clobType, connectionType, jdbcMapperType, byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, collectionType, calendarType, cleanerType, enumType; //IFJAVA8_START - static TypeMirror instantType, localDateTimeType, localDateType, localTimeType, zonedDateTimeType, offsetDateTimeType, offsetTimeType; + static TypeMirror streamType, instantType, localDateTimeType, localDateType, localTimeType, zonedDateTimeType, offsetDateTimeType, offsetTimeType; //IFJAVA8_END private TypeElement cleanerElement; private JdbcMapper.DatabaseType defaultDatabaseType; @@ -105,6 +106,7 @@ public class JdbcMapperProcessor extends AbstractProcessor { blobType = elements.getTypeElement(Blob.class.getCanonicalName()).asType(); calendarType = elements.getTypeElement(Calendar.class.getCanonicalName()).asType(); //IFJAVA8_START + streamType = types.getDeclaredType(elements.getTypeElement(Stream.class.getCanonicalName()), types.getWildcardType(null, null)); instantType = elements.getTypeElement(Instant.class.getCanonicalName()).asType(); localDateTimeType = elements.getTypeElement(LocalDateTime.class.getCanonicalName()).asType(); localDateType = elements.getTypeElement(LocalDate.class.getCanonicalName()).asType(); @@ -308,6 +310,7 @@ public class JdbcMapperProcessor extends AbstractProcessor { // build query and bind param order final List bindParams = new ArrayList(); + final Map inListBindParams = new LinkedHashMap<>(); final String sqlStatement; String calendarName = null, cleanerName = null; CompileTimeResultSetMapper.MaxRows maxRows = CompileTimeResultSetMapper.MaxRows.getMaxRows(sql.maxRows()); @@ -349,6 +352,7 @@ public class JdbcMapperProcessor extends AbstractProcessor { final Matcher bindParamMatcher = paramPattern.matcher(sql.value()); final StringBuffer sb = new StringBuffer(); + int inListBindParamsIdx = -1; while (bindParamMatcher.find()) { final String paramName = bindParamMatcher.group(7); final VariableElement bindParam = paramMap.get(paramName); @@ -380,7 +384,12 @@ public class JdbcMapperProcessor extends AbstractProcessor { } else { if(clobBlob != null) processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "cannot combine in/not in and clob/blob", bindParam); - bindParams.add(new SpecialVariableElement(bindParam, SpecialVariableElement.SpecialType.IN_LIST)); + SpecialVariableElement inListBindParam = inListBindParams.get(paramName); + if(inListBindParam == null) { + inListBindParam = new SpecialVariableElement(bindParam, SpecialVariableElement.SpecialType.IN_LIST, ++inListBindParamsIdx); + inListBindParams.put(paramName, inListBindParam); + } + bindParams.add(inListBindParam); final boolean not = bindParamMatcher.group(4) != null; final String replacement; switch (databaseType) { @@ -433,7 +442,12 @@ public class JdbcMapperProcessor extends AbstractProcessor { w.write("\t\tPreparedStatement ps = null;\n"); if (parsedSQl.isSelect()) w.write("\t\tResultSet rs = null;\n"); - w.write("\t\ttry {\n\t\t\tps = "); + if(!inListBindParams.isEmpty()) + w.append("\t\tfinal Array[] _bindArrays = new Array[").append(Integer.toString(inListBindParams.size())).append("];\n"); + w.write("\t\ttry {\n"); + for (final SpecialVariableElement param : inListBindParams.values()) + setArray(w, databaseType, arrayNumberTypeName, arrayStringTypeName, param); + w.write("\t\t\tps = "); final boolean cachePreparedStatements = sql.cachePreparedStatement().combine(defaultCachePreparedStatements); if (cachePreparedStatements) { w.write("this.prepareStatement("); @@ -450,7 +464,7 @@ public class JdbcMapperProcessor extends AbstractProcessor { // now bind parameters int count = 0; for (final VariableElement param : bindParams) - setObject(w, ++count, databaseType, arrayNumberTypeName, arrayStringTypeName, param); + setObject(w, ++count, param); boolean closeRs = true; if (!parsedSQl.isSelect()) { @@ -484,6 +498,8 @@ public class JdbcMapperProcessor extends AbstractProcessor { if (!sqlExceptionThrown) w.write("\t\t} catch(SQLException e) {\n\t\t\tthrow new RuntimeException(e);\n"); w.write("\t\t} finally {\n"); + if(!inListBindParams.isEmpty()) + w.append("\t\t\tfor(final Array _bindArray : _bindArrays)\n\t\t\t\ttryClose(_bindArray);\n"); if (parsedSQl.isSelect()) w.write("\t\t\ttryClose(rs);\n"); if (!cachePreparedStatements) @@ -499,6 +515,10 @@ public class JdbcMapperProcessor extends AbstractProcessor { w.write("\t\t\tif(e instanceof SQLException)\n\t\t\t\tthrow (SQLException)e;\n"); w.write("\t\t\tif(e instanceof RuntimeException)\n\t\t\t\tthrow (RuntimeException)e;\n"); w.write("\t\t\tthrow new RuntimeException(e);\n"); + if(!inListBindParams.isEmpty()) { + w.write("\t\t} finally {\n"); + w.append("\t\t\tfor(final Array _bindArray : _bindArrays)\n\t\t\t\ttryClose(_bindArray);\n"); + } } w.write("\t\t}\n"); @@ -658,44 +678,57 @@ public class JdbcMapperProcessor extends AbstractProcessor { w.write("\t}\n"); } - private void setObject(final Writer w, final int index, final JdbcMapper.DatabaseType databaseType, final String arrayNumberTypeName, final String arrayStringTypeName, final VariableElement param) throws SQLException, IOException { - String variableName = param.getSimpleName().toString(); - final TypeMirror o = param.asType(); - w.write("\t\t\t"); - String method = null; + private enum InListArgType { + ARRAY, + COLLECTION, + STREAM, + } - // special behavior - if (param instanceof SpecialVariableElement) { - final SpecialVariableElement specialParam = (SpecialVariableElement) param; - switch (specialParam.specialType) { - case IN_LIST: { - final boolean collection; - final TypeMirror componentType; - if (o.getKind() == TypeKind.ARRAY) { - collection = false; - componentType = ((ArrayType) o).getComponentType(); - } else if (o.getKind() == TypeKind.DECLARED && types.isAssignable(o, collectionType)) { - collection = true; - final DeclaredType dt = (DeclaredType) o; - if (dt.getTypeArguments().isEmpty()) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax requires a Collection with a generic type parameter" + o.toString(), specialParam.delegate); - return; - } - componentType = dt.getTypeArguments().get(0); - } else { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax only valid on Collections or arrays" + o.toString(), specialParam.delegate); - return; - } - w.write("ps.setArray("); - w.write(Integer.toString(index)); - w.write(", "); - final String type = types.isAssignable(componentType, numberType) ? arrayNumberTypeName : arrayStringTypeName; - switch (databaseType) { - case ORACLE: - w.write("conn.unwrap(oracle.jdbc.OracleConnection.class).createOracleArray(\""); + private void setArray(final Writer w, final JdbcMapper.DatabaseType databaseType, final String arrayNumberTypeName, final String arrayStringTypeName, final SpecialVariableElement specialParam) throws IOException { + final String variableName = specialParam.getSimpleName().toString(); + final TypeMirror o = specialParam.asType(); - // todo: if oracle driver is not on compile-time classpath, would need to do: - // we could also create a fake-oracle module that just had a oracle.jdbc.OracleConnection class implementing createOracleArray()... + final InListArgType inListArgType; + final TypeMirror componentType; + if (o.getKind() == TypeKind.ARRAY) { + inListArgType = InListArgType.ARRAY; + componentType = ((ArrayType) o).getComponentType(); + } else if (o.getKind() == TypeKind.DECLARED && types.isAssignable(o, collectionType)) { + inListArgType = InListArgType.COLLECTION; + final DeclaredType dt = (DeclaredType) o; + if (dt.getTypeArguments().isEmpty()) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax requires a Collection with a generic type parameter" + o.toString(), specialParam.delegate); + return; + } + componentType = dt.getTypeArguments().get(0); + //IFJAVA8_START + } else if (o.getKind() == TypeKind.DECLARED && types.isAssignable(o, streamType)) { + inListArgType = InListArgType.STREAM; + final DeclaredType dt = (DeclaredType) o; + if (dt.getTypeArguments().isEmpty()) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax requires a Stream with a generic type parameter" + o.toString(), specialParam.delegate); + return; + } + componentType = dt.getTypeArguments().get(0); + } else { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax only valid on Collections or arrays" + o.toString(), specialParam.delegate); + return; + } + //IFJAVA8_END + /*IFJAVA6_START + } else { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax only valid on Collections or arrays" + o.toString(), specialParam.delegate); + return; + } + IFJAVA6_END*/ + w.append("\t\t\t_bindArrays[").append(Integer.toString(specialParam.index)).append("] = "); + final String type = types.isAssignable(componentType, numberType) ? arrayNumberTypeName : arrayStringTypeName; + switch (databaseType) { + case ORACLE: + w.write("conn.unwrap(oracle.jdbc.OracleConnection.class).createOracleArray(\""); + + // todo: if oracle driver is not on compile-time classpath, would need to do: + // we could also create a fake-oracle module that just had a oracle.jdbc.OracleConnection class implementing createOracleArray()... /* private static final Class oracleConnection; private static final Method createArray; @@ -713,20 +746,42 @@ public class JdbcMapperProcessor extends AbstractProcessor { createArray = ca; } */ - //w.write("(Array) createArray.invoke(conn.unwrap(oracleConnection), \""); - break; - case STANDARD: - w.write("conn.createArrayOf(\""); - break; - default: - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "default DatabaseType? should never happen!!", specialParam.delegate); - } - w.write(type); - w.write("\", "); - w.write(variableName); - if (collection) - w.write(".toArray()"); - w.write("));\n"); + //w.write("(Array) createArray.invoke(conn.unwrap(oracleConnection), \""); + break; + case STANDARD: + w.write("conn.createArrayOf(\""); + break; + default: + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "default DatabaseType? should never happen!!", specialParam.delegate); + } + w.write(type); + w.write("\", "); + w.write(variableName); + switch (inListArgType) { + case COLLECTION: + w.append(".toArray(new ").append(componentType.toString()).append("[").append(variableName).append(".size()])"); + break; + case STREAM: + w.append(".toArray(").append(componentType.toString()).append("[]::new)"); + break; + } + w.write(");\n"); + } + + private void setObject(final Writer w, final int index, final VariableElement param) throws IOException { + String variableName = param.getSimpleName().toString(); + final TypeMirror o = param.asType(); + w.write("\t\t\t"); + String method = null; + + // special behavior + if (param instanceof SpecialVariableElement) { + final SpecialVariableElement specialParam = (SpecialVariableElement) param; + switch (specialParam.specialType) { + case IN_LIST: { + w.write("ps.setArray("); + w.write(Integer.toString(index)); + w.append(", _bindArrays[").append(Integer.toString(specialParam.index)).append("]);\n"); return; } case BLOB: { diff --git a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SpecialVariableElement.java b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SpecialVariableElement.java index bc249bc..cbb906d 100644 --- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SpecialVariableElement.java +++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SpecialVariableElement.java @@ -20,15 +20,25 @@ class SpecialVariableElement implements VariableElement { final VariableElement delegate; final SpecialType specialType; final String blobStringCharset; + final int index; SpecialVariableElement(final VariableElement delegate, final SpecialType specialType) { - this(delegate, specialType, null); + this(delegate, specialType, null, 0); + } + + SpecialVariableElement(final VariableElement delegate, final SpecialType specialType, final int index) { + this(delegate, specialType, null, index); } SpecialVariableElement(final VariableElement delegate, final SpecialType specialType, final String blobStringCharset) { + this(delegate, specialType, blobStringCharset, 0); + } + + SpecialVariableElement(final VariableElement delegate, final SpecialType specialType, final String blobStringCharset, final int index) { this.delegate = delegate; this.specialType = specialType; this.blobStringCharset = blobStringCharset; + this.index = index; } @Override diff --git a/readme.md b/readme.md index 1cbb498..1b6f335 100644 --- a/readme.md +++ b/readme.md @@ -120,4 +120,5 @@ TODO * DOCUMENTATION!!!!! * sql other than select return boolean, int > 0 ? - * @RunInTransaction void support \ No newline at end of file + * @RunInTransaction void support + * QueryMapper mapping errors should be clearer, especially if a .finish(ResultSet) throws an error \ No newline at end of file diff --git a/test/src/main/java/com/moparisthebest/jdbc/codegen/PersonDAO.java b/test/src/main/java/com/moparisthebest/jdbc/codegen/PersonDAO.java index c2d7363..32748a7 100644 --- a/test/src/main/java/com/moparisthebest/jdbc/codegen/PersonDAO.java +++ b/test/src/main/java/com/moparisthebest/jdbc/codegen/PersonDAO.java @@ -226,8 +226,17 @@ public interface PersonDAO extends JdbcMapper { @JdbcMapper.SQL("SELECT str_val FROM val WHERE val_no = 4") SameClassPathEnumPerson.FirstName getSameClassPathEnumNull() throws SQLException; + @SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos}") + List getPeopleInListArray(Long[] personNos); + + @SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos}") + List getPeopleInListCollection(Collection personNos); + //IFJAVA8_START + @SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos} OR {first_name IN names} OR {last_name IN names}") + Stream getPeopleInListStream(Stream personNos, Stream names); + @JdbcMapper.SQL("SELECT birth_date FROM person WHERE person_no = {personNo}") Instant getBirthDateInstant(long personNo); diff --git a/test/src/main/java/com/moparisthebest/jdbc/codegen/PrestoPersonDAO.java b/test/src/main/java/com/moparisthebest/jdbc/codegen/PrestoPersonDAO.java index ff9c83e..6a7fe4b 100644 --- a/test/src/main/java/com/moparisthebest/jdbc/codegen/PrestoPersonDAO.java +++ b/test/src/main/java/com/moparisthebest/jdbc/codegen/PrestoPersonDAO.java @@ -226,8 +226,17 @@ public interface PrestoPersonDAO extends PersonDAO { @JdbcMapper.SQL("SELECT str_val FROM val WHERE val_no = 4") SameClassPathEnumPerson.FirstName getSameClassPathEnumNull() throws SQLException; + @SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos}") + List getPeopleInListArray(Long[] personNos); + + @SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos}") + List getPeopleInListCollection(Collection personNos); + //IFJAVA8_START + @SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos} OR {first_name IN names} OR {last_name IN names}") + Stream getPeopleInListStream(Stream personNos, Stream names); + @JdbcMapper.SQL("SELECT birth_date FROM person WHERE person_no = {personNo}") Instant getBirthDateInstant(long personNo);