Implement optional compile-time SQL checking with explain plan
parent
1fa08bc74b
commit
a6c5fc7b52
@ -0,0 +1,14 @@
|
||||
package com.moparisthebest.jdbc.codegen;
|
||||
|
||||
import com.moparisthebest.jdbc.ArrayInList;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
public interface SQLChecker {
|
||||
void checkSql(final ProcessingEnvironment processingEnv, final TypeElement classElement, final JdbcMapper.Mapper mapper, final JdbcMapper.DatabaseType databaseType, final ExecutableElement method, final String sqlStatement, final List<VariableElement> bindParams, final ArrayInList arrayInList) throws SQLException;
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
package com.moparisthebest.jdbc.codegen;
|
||||
|
||||
import com.moparisthebest.jdbc.ArrayInList;
|
||||
import com.moparisthebest.jdbc.QueryMapper;
|
||||
|
||||
import javax.annotation.processing.Messager;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.ArrayType;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import javax.tools.Diagnostic;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.StringReader;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static com.moparisthebest.jdbc.TryClose.tryClose;
|
||||
import static com.moparisthebest.jdbc.codegen.JdbcMapperProcessor.*;
|
||||
|
||||
public class SimpleSQLChecker implements SQLChecker {
|
||||
@Override
|
||||
public void checkSql(final ProcessingEnvironment processingEnv, final TypeElement classElement, final JdbcMapper.Mapper mapper, final JdbcMapper.DatabaseType databaseType, final ExecutableElement method, final String sqlStatement, final List<VariableElement> bindParams, final ArrayInList arrayInList) throws SQLException {
|
||||
Connection conn = null;
|
||||
QueryMapper qm = null;
|
||||
try {
|
||||
conn = getConnection(classElement, mapper, databaseType);
|
||||
if (conn == null) {
|
||||
getMessager().printMessage(Diagnostic.Kind.ERROR, "connection must be non-null for testing", classElement);
|
||||
return;
|
||||
}
|
||||
conn.setAutoCommit(false);
|
||||
qm = new QueryMapper(conn);
|
||||
qm.executeUpdate(getSqlToExecute(classElement, conn, databaseType, sqlStatement), getFakeBindParams(bindParams, conn, arrayInList));
|
||||
} catch (Exception e) {
|
||||
handleException(getMessager(), e, classElement, method);
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
try {
|
||||
conn.rollback();
|
||||
conn.setAutoCommit(true);
|
||||
} catch (Throwable e) {
|
||||
// ignore
|
||||
}
|
||||
tryClose(conn);
|
||||
}
|
||||
tryClose(qm);
|
||||
}
|
||||
}
|
||||
|
||||
public Connection getConnection(final TypeElement classElement, final JdbcMapper.Mapper mapper, final JdbcMapper.DatabaseType databaseType) throws SQLException {
|
||||
if (mapper.jndiName().isEmpty()) {
|
||||
throw new RuntimeException("@JdbcMapper.jndiName must be non-null for testing");
|
||||
}
|
||||
return JdbcMapperFactory.connectionFactory(mapper.jndiName()).create();
|
||||
}
|
||||
|
||||
public String getSqlToExecute(final TypeElement classElement, final Connection conn, final JdbcMapper.DatabaseType databaseType, final String sqlStatement) {
|
||||
switch (databaseType) {
|
||||
case ORACLE:
|
||||
// oracle, being terrible, gives this error message for explain plans on MERGES
|
||||
// so we will just execute those directly instead...
|
||||
// ORA-00600: internal error code, arguments: [qctfrc : bfc], [22], [0], [5], [1], [1], [2], [371], [], [], [], []
|
||||
// we are assuming length is at least 5 after being trimmed, if anyone can come up with a valid SQL statement that is shorter let me know...
|
||||
if (sqlStatement.trim().substring(0, 5).toUpperCase().equals("MERGE"))
|
||||
return sqlStatement;
|
||||
return "EXPLAIN PLAN FOR " + sqlStatement;
|
||||
case STANDARD:
|
||||
return "EXPLAIN " + sqlStatement;
|
||||
}
|
||||
return sqlStatement;
|
||||
}
|
||||
|
||||
public void handleException(final Messager messager, final Exception e, final TypeElement classElement, final ExecutableElement method) {
|
||||
if (e.getMessage().startsWith("ORA-02291: integrity constraint"))
|
||||
return; // since we make up values and rollback we can ignore this, it means SQL is correct at least
|
||||
getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage(), method);
|
||||
}
|
||||
|
||||
public Collection<Object> getFakeBindParams(final List<VariableElement> bindParams, final Connection conn, final ArrayInList arrayInList) throws SQLException {
|
||||
final Collection<Object> ret = new ArrayList<Object>(bindParams.size());
|
||||
for (final VariableElement bindParam : bindParams) {
|
||||
ret.add(getFakeBindParam(bindParam, conn, arrayInList));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static final String defaultString = "n"; // oracle considers empty strings to be null...
|
||||
|
||||
public Object getFakeBindParam(final VariableElement param, final Connection conn, final ArrayInList arrayInList) throws SQLException {
|
||||
final TypeMirror o = param.asType();
|
||||
// special behavior
|
||||
if (param instanceof InListVariableElement) {
|
||||
final TypeMirror componentType;
|
||||
if (o.getKind() == TypeKind.ARRAY) {
|
||||
componentType = ((ArrayType) o).getComponentType();
|
||||
} else if (o.getKind() == TypeKind.DECLARED && types.isAssignable(o, collectionType)) {
|
||||
final DeclaredType dt = (DeclaredType) o;
|
||||
componentType = dt.getTypeArguments().get(0);
|
||||
} else {
|
||||
getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "invalid in-list-variable type, returning null", ((InListVariableElement) param).delegate);
|
||||
return null;
|
||||
}
|
||||
if (types.isAssignable(componentType, numberType)) {
|
||||
return arrayInList.toArray(conn, true, new Long[]{0L}); // todo: not quite right, oh well for now
|
||||
} else if (types.isAssignable(componentType, stringType) || types.isAssignable(componentType, enumType)) {
|
||||
return arrayInList.toArray(conn, false, new String[]{defaultString});
|
||||
} else {
|
||||
getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "invalid in-list-variable type, returning null", ((InListVariableElement) param).delegate);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
final JdbcMapper.Blob blob = param.getAnnotation(JdbcMapper.Blob.class);
|
||||
if (blob != null) {
|
||||
return new ByteArrayInputStream(new byte[1]);
|
||||
} else {
|
||||
final JdbcMapper.Clob clob = param.getAnnotation(JdbcMapper.Clob.class);
|
||||
if (clob != null) {
|
||||
return new StringReader(defaultString);
|
||||
}
|
||||
}
|
||||
// end special behavior
|
||||
// we are going to put most common ones up top so it should execute faster normally
|
||||
if (types.isAssignable(o, stringType)) {
|
||||
return defaultString;
|
||||
} else if (o.getKind().isPrimitive() || types.isAssignable(o, numberType)) {
|
||||
switch (o.getKind()) {
|
||||
case BOOLEAN:
|
||||
return true;
|
||||
case BYTE:
|
||||
case SHORT:
|
||||
case INT:
|
||||
return 0;
|
||||
case LONG:
|
||||
return 0L;
|
||||
case CHAR:
|
||||
return ' ';
|
||||
case FLOAT:
|
||||
return 0F;
|
||||
case DOUBLE:
|
||||
return 0D;
|
||||
}
|
||||
return null;//new BigDecimal(0D); // todo: not quite right, maybe good enough
|
||||
// java.util.Date support, put it in a Timestamp
|
||||
} else if (types.isAssignable(o, utilDateType)) {
|
||||
return new java.sql.Timestamp(0);
|
||||
}
|
||||
//IFJAVA8_START
|
||||
else if (types.isAssignable(o, instantType) || types.isAssignable(o, localDateTimeType) || types.isAssignable(o, zonedDateTimeType) || types.isAssignable(o, offsetDateTimeType)) {
|
||||
return new java.sql.Timestamp(0);
|
||||
} else if (types.isAssignable(o, localDateType)) {
|
||||
return new java.sql.Date(0);
|
||||
} else if (types.isAssignable(o, localTimeType) || types.isAssignable(o, offsetTimeType)) {
|
||||
return new java.sql.Time(0);
|
||||
}
|
||||
//IFJAVA8_END
|
||||
// CLOB support
|
||||
else if (types.isAssignable(o, readerType) || types.isAssignable(o, clobType)) {
|
||||
return new StringReader(defaultString);
|
||||
} else if (types.isAssignable(o, inputStreamType) || types.isAssignable(o, blobType) || types.isAssignable(o, fileType)) {
|
||||
return new ByteArrayInputStream(new byte[1]);
|
||||
} else if (types.isAssignable(o, byteArrayType)) {
|
||||
return new byte[1];
|
||||
} else if (types.isAssignable(o, sqlArrayType)) {
|
||||
getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "java.sql.Array not yet supported for getFakeBindParam, returning null", param);
|
||||
return null;
|
||||
} else {
|
||||
// shouldn't get here ever, if we do the types should be more specific
|
||||
getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "unsupported type for getFakeBindParam, returning null", param);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue