fix(agent): cache JDBC column metadata
This commit is contained in:
1 parent
c1d3f9423d
commit
fb4bce90d6
3 files changed
+154
-13
No files matched your search
@@ -113,24 +113,28 @@ public final class JdbcExecutor {
|
|||||||
return unchecked(() -> {
|
return unchecked(() -> {
|
||||||
ResultSetMetaData meta = rs.getMetaData();
|
ResultSetMetaData meta = rs.getMetaData();
|
||||||
int colCount = meta.getColumnCount();
|
int colCount = meta.getColumnCount();
|
||||||
List<String> columns = new ArrayList<>();
|
List<String> columns = new ArrayList<>(colCount);
|
||||||
List<String> columnTypes = new ArrayList<>();
|
List<String> columnTypes = new ArrayList<>(colCount);
|
||||||
|
// Cache JDBC column metadata once; some drivers resolve it lazily,
|
||||||
|
// and row reading is the hot path.
|
||||||
|
int[] sqlTypeByIndex = new int[colCount];
|
||||||
String[] typeNameByIndex = new String[colCount];
|
String[] typeNameByIndex = new String[colCount];
|
||||||
for (int i = 1; i <= colCount; i++) {
|
for (int i = 1; i <= colCount; i++) {
|
||||||
columns.add(meta.getColumnLabel(i));
|
columns.add(meta.getColumnLabel(i));
|
||||||
|
sqlTypeByIndex[i - 1] = safeColumnSqlType(meta, i);
|
||||||
String typeName = safeColumnTypeName(meta, i);
|
String typeName = safeColumnTypeName(meta, i);
|
||||||
columnTypes.add(typeName);
|
columnTypes.add(typeName);
|
||||||
typeNameByIndex[i - 1] = typeName;
|
typeNameByIndex[i - 1] = typeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<List<Object>> rows = new ArrayList<>();
|
List<List<Object>> rows = new ArrayList<>(initialRowCapacity(maxRows));
|
||||||
boolean truncated = false;
|
boolean truncated = false;
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
if (rows.size() >= maxRows) {
|
if (rows.size() >= maxRows) {
|
||||||
truncated = true;
|
truncated = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
rows.add(rowValues(rs, valueReader, typeNameByIndex));
|
rows.add(rowValues(rs, valueReader, sqlTypeByIndex, typeNameByIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new QueryResult(columns, columnTypes, rows, 0L, executionTimeMs, truncated);
|
return new QueryResult(columns, columnTypes, rows, 0L, executionTimeMs, truncated);
|
||||||
@@ -221,9 +225,12 @@ public final class JdbcExecutor {
|
|||||||
int colCount = meta.getColumnCount();
|
int colCount = meta.getColumnCount();
|
||||||
List<String> columns = new ArrayList<>(colCount);
|
List<String> columns = new ArrayList<>(colCount);
|
||||||
List<String> columnTypes = new ArrayList<>(colCount);
|
List<String> columnTypes = new ArrayList<>(colCount);
|
||||||
|
// Cache JDBC column metadata once; fetching a page should only read row values.
|
||||||
|
int[] sqlTypeByIndex = new int[colCount];
|
||||||
String[] typeNameByIndex = new String[colCount];
|
String[] typeNameByIndex = new String[colCount];
|
||||||
for (int i = 1; i <= colCount; i++) {
|
for (int i = 1; i <= colCount; i++) {
|
||||||
columns.add(meta.getColumnLabel(i));
|
columns.add(meta.getColumnLabel(i));
|
||||||
|
sqlTypeByIndex[i - 1] = safeColumnSqlType(meta, i);
|
||||||
String typeName = safeColumnTypeName(meta, i);
|
String typeName = safeColumnTypeName(meta, i);
|
||||||
columnTypes.add(typeName);
|
columnTypes.add(typeName);
|
||||||
typeNameByIndex[i - 1] = typeName;
|
typeNameByIndex[i - 1] = typeName;
|
||||||
@@ -234,6 +241,7 @@ public final class JdbcExecutor {
|
|||||||
rs,
|
rs,
|
||||||
columns,
|
columns,
|
||||||
columnTypes,
|
columnTypes,
|
||||||
|
sqlTypeByIndex,
|
||||||
typeNameByIndex,
|
typeNameByIndex,
|
||||||
Math.max(options.getMaxRows(), 1),
|
Math.max(options.getMaxRows(), 1),
|
||||||
valueReader
|
valueReader
|
||||||
@@ -449,7 +457,7 @@ public final class JdbcExecutor {
|
|||||||
return unchecked(() -> {
|
return unchecked(() -> {
|
||||||
session.lastAccessedAtMillis = System.currentTimeMillis();
|
session.lastAccessedAtMillis = System.currentTimeMillis();
|
||||||
int effectivePageSize = Math.max(pageSize, 1);
|
int effectivePageSize = Math.max(pageSize, 1);
|
||||||
List<List<Object>> rows = new ArrayList<>();
|
List<List<Object>> rows = new ArrayList<>(initialRowCapacity(effectivePageSize));
|
||||||
|
|
||||||
if (session.pendingRow != null) {
|
if (session.pendingRow != null) {
|
||||||
rows.add(session.pendingRow);
|
rows.add(session.pendingRow);
|
||||||
@@ -461,7 +469,7 @@ public final class JdbcExecutor {
|
|||||||
closeSession(targetSessions, session.id);
|
closeSession(targetSessions, session.id);
|
||||||
return new QueryPageResult(session.columns, session.columnTypes, rows, 0L, executionTimeMs, false, null, false);
|
return new QueryPageResult(session.columns, session.columnTypes, rows, 0L, executionTimeMs, false, null, false);
|
||||||
}
|
}
|
||||||
rows.add(rowValues(session.resultSet, session.valueReader, session.typeNameByIndex));
|
rows.add(rowValues(session.resultSet, session.valueReader, session.sqlTypeByIndex, session.typeNameByIndex));
|
||||||
session.rowsRead += 1;
|
session.rowsRead += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,7 +485,7 @@ public final class JdbcExecutor {
|
|||||||
return new QueryPageResult(session.columns, session.columnTypes, rows, 0L, executionTimeMs, false, null, false);
|
return new QueryPageResult(session.columns, session.columnTypes, rows, 0L, executionTimeMs, false, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.pendingRow = rowValues(session.resultSet, session.valueReader, session.typeNameByIndex);
|
session.pendingRow = rowValues(session.resultSet, session.valueReader, session.sqlTypeByIndex, session.typeNameByIndex);
|
||||||
session.rowsRead += 1;
|
session.rowsRead += 1;
|
||||||
return new QueryPageResult(session.columns, session.columnTypes, rows, 0L, executionTimeMs, false, session.id, true);
|
return new QueryPageResult(session.columns, session.columnTypes, rows, 0L, executionTimeMs, false, session.id, true);
|
||||||
});
|
});
|
||||||
@@ -517,17 +525,29 @@ public final class JdbcExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Object> rowValues(ResultSet rs, ResultValueReader valueReader, String[] typeNameByIndex) throws SQLException {
|
static int safeColumnSqlType(ResultSetMetaData meta, int columnIndex) {
|
||||||
ResultSetMetaData meta = rs.getMetaData();
|
try {
|
||||||
int colCount = meta.getColumnCount();
|
return meta.getColumnType(columnIndex);
|
||||||
|
} catch (SQLException ignored) {
|
||||||
|
return Types.OTHER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Object> rowValues(
|
||||||
|
ResultSet rs,
|
||||||
|
ResultValueReader valueReader,
|
||||||
|
int[] sqlTypeByIndex,
|
||||||
|
String[] typeNameByIndex
|
||||||
|
) throws SQLException {
|
||||||
|
int colCount = sqlTypeByIndex.length;
|
||||||
List<Object> row = new ArrayList<>(colCount);
|
List<Object> row = new ArrayList<>(colCount);
|
||||||
for (int i = 1; i <= colCount; i++) {
|
for (int i = 1; i <= colCount; i++) {
|
||||||
int sqlType = meta.getColumnType(i);
|
int sqlType = sqlTypeByIndex[i - 1];
|
||||||
Object value;
|
Object value;
|
||||||
if (valueReader instanceof ColumnAwareResultValueReader) {
|
if (valueReader instanceof ColumnAwareResultValueReader) {
|
||||||
String typeName = typeNameByIndex != null && i - 1 < typeNameByIndex.length
|
String typeName = typeNameByIndex != null && i - 1 < typeNameByIndex.length
|
||||||
? typeNameByIndex[i - 1]
|
? typeNameByIndex[i - 1]
|
||||||
: safeColumnTypeName(meta, i);
|
: "";
|
||||||
value = ((ColumnAwareResultValueReader) valueReader).read(rs, i, sqlType, typeName);
|
value = ((ColumnAwareResultValueReader) valueReader).read(rs, i, sqlType, typeName);
|
||||||
} else {
|
} else {
|
||||||
value = valueReader.read(rs, i, sqlType);
|
value = valueReader.read(rs, i, sqlType);
|
||||||
@@ -537,6 +557,13 @@ public final class JdbcExecutor {
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int initialRowCapacity(int requestedRows) {
|
||||||
|
if (requestedRows <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.min(requestedRows, 1024);
|
||||||
|
}
|
||||||
|
|
||||||
private void applySchema(Connection conn, String schema, Function<String, String> setSchemaSql) throws SQLException {
|
private void applySchema(Connection conn, String schema, Function<String, String> setSchemaSql) throws SQLException {
|
||||||
if (schema == null || schema.trim().isEmpty()) {
|
if (schema == null || schema.trim().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
@@ -664,6 +691,7 @@ public final class JdbcExecutor {
|
|||||||
private final ResultSet resultSet;
|
private final ResultSet resultSet;
|
||||||
private final List<String> columns;
|
private final List<String> columns;
|
||||||
private final List<String> columnTypes;
|
private final List<String> columnTypes;
|
||||||
|
private final int[] sqlTypeByIndex;
|
||||||
private final String[] typeNameByIndex;
|
private final String[] typeNameByIndex;
|
||||||
private final int maxRows;
|
private final int maxRows;
|
||||||
private final ResultValueReader valueReader;
|
private final ResultValueReader valueReader;
|
||||||
@@ -677,6 +705,7 @@ public final class JdbcExecutor {
|
|||||||
ResultSet resultSet,
|
ResultSet resultSet,
|
||||||
List<String> columns,
|
List<String> columns,
|
||||||
List<String> columnTypes,
|
List<String> columnTypes,
|
||||||
|
int[] sqlTypeByIndex,
|
||||||
String[] typeNameByIndex,
|
String[] typeNameByIndex,
|
||||||
int maxRows,
|
int maxRows,
|
||||||
ResultValueReader valueReader
|
ResultValueReader valueReader
|
||||||
@@ -686,6 +715,7 @@ public final class JdbcExecutor {
|
|||||||
this.resultSet = resultSet;
|
this.resultSet = resultSet;
|
||||||
this.columns = columns;
|
this.columns = columns;
|
||||||
this.columnTypes = columnTypes;
|
this.columnTypes = columnTypes;
|
||||||
|
this.sqlTypeByIndex = sqlTypeByIndex;
|
||||||
this.typeNameByIndex = typeNameByIndex;
|
this.typeNameByIndex = typeNameByIndex;
|
||||||
this.maxRows = maxRows;
|
this.maxRows = maxRows;
|
||||||
this.valueReader = valueReader;
|
this.valueReader = valueReader;
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ public final class QueryResult {
|
|||||||
if (input == null) {
|
if (input == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
List<List<Object>> normalized = new ArrayList<>();
|
List<List<Object>> normalized = new ArrayList<>(input.size());
|
||||||
for (List<?> row : input) {
|
for (List<?> row : input) {
|
||||||
normalized.add(row == null ? Collections.emptyList() : new ArrayList<Object>(row));
|
normalized.add(row == null ? Collections.emptyList() : new ArrayList<Object>(row));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import java.lang.reflect.InvocationHandler;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.ResultSetMetaData;
|
||||||
import java.sql.Types;
|
import java.sql.Types;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import javax.sql.rowset.serial.SerialBlob;
|
import javax.sql.rowset.serial.SerialBlob;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
@@ -38,10 +41,90 @@ class JdbcExecutorTest {
|
|||||||
assertEquals("0x0a0b", JdbcExecutor.INSTANCE.defaultResultValue(rs, 1, Types.OTHER));
|
assertEquals("0x0a0b", JdbcExecutor.INSTANCE.defaultResultValue(rs, 1, Types.OTHER));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readResultSetCachesColumnTypeMetadataAcrossRows() {
|
||||||
|
CountingResultSetFixture fixture = countingResultSet(new Object[][]{
|
||||||
|
{1, "Ada"},
|
||||||
|
{2, "Grace"}
|
||||||
|
});
|
||||||
|
|
||||||
|
QueryResult result = JdbcExecutor.INSTANCE.readResultSet(
|
||||||
|
fixture.resultSet(),
|
||||||
|
12L,
|
||||||
|
10,
|
||||||
|
JdbcExecutor.INSTANCE::defaultResultValue
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(Arrays.asList("id", "name"), result.getColumns());
|
||||||
|
assertEquals(Arrays.asList("INTEGER", "VARCHAR"), result.getColumn_types());
|
||||||
|
assertEquals(Arrays.asList(Arrays.asList(1, "Ada"), Arrays.asList(2, "Grace")), result.getRows());
|
||||||
|
assertEquals(1, fixture.getMetaDataCalls());
|
||||||
|
assertEquals(2, fixture.getColumnTypeCalls());
|
||||||
|
}
|
||||||
|
|
||||||
private static ResultSet resultSet(byte[] bytes, StringSupplier stringSupplier) {
|
private static ResultSet resultSet(byte[] bytes, StringSupplier stringSupplier) {
|
||||||
return resultSet(bytes, stringSupplier, false);
|
return resultSet(bytes, stringSupplier, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CountingResultSetFixture countingResultSet(Object[][] rows) {
|
||||||
|
String[] labels = {"id", "name"};
|
||||||
|
int[] sqlTypes = {Types.INTEGER, Types.VARCHAR};
|
||||||
|
String[] typeNames = {"INTEGER", "VARCHAR"};
|
||||||
|
AtomicInteger cursor = new AtomicInteger(-1);
|
||||||
|
AtomicInteger getMetaDataCalls = new AtomicInteger();
|
||||||
|
AtomicInteger getColumnTypeCalls = new AtomicInteger();
|
||||||
|
|
||||||
|
InvocationHandler metaHandler = (Object unused, Method method, Object[] args) -> {
|
||||||
|
switch (method.getName()) {
|
||||||
|
case "getColumnCount":
|
||||||
|
return labels.length;
|
||||||
|
case "getColumnLabel":
|
||||||
|
return labels[(Integer) args[0] - 1];
|
||||||
|
case "getColumnType":
|
||||||
|
getColumnTypeCalls.incrementAndGet();
|
||||||
|
return sqlTypes[(Integer) args[0] - 1];
|
||||||
|
case "getColumnTypeName":
|
||||||
|
return typeNames[(Integer) args[0] - 1];
|
||||||
|
default:
|
||||||
|
return defaultValue(method.getReturnType());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ResultSetMetaData metadata = (ResultSetMetaData) Proxy.newProxyInstance(
|
||||||
|
ResultSetMetaData.class.getClassLoader(),
|
||||||
|
new Class<?>[]{ResultSetMetaData.class},
|
||||||
|
metaHandler
|
||||||
|
);
|
||||||
|
|
||||||
|
InvocationHandler resultSetHandler = (Object unused, Method method, Object[] args) -> {
|
||||||
|
switch (method.getName()) {
|
||||||
|
case "getMetaData":
|
||||||
|
getMetaDataCalls.incrementAndGet();
|
||||||
|
return metadata;
|
||||||
|
case "next":
|
||||||
|
return cursor.incrementAndGet() < rows.length;
|
||||||
|
case "getInt":
|
||||||
|
return ((Number) currentCell(rows, cursor.get(), (Integer) args[0])).intValue();
|
||||||
|
case "getString":
|
||||||
|
Object value = currentCell(rows, cursor.get(), (Integer) args[0]);
|
||||||
|
return value == null ? null : value.toString();
|
||||||
|
case "wasNull":
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return defaultValue(method.getReturnType());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ResultSet resultSet = (ResultSet) Proxy.newProxyInstance(
|
||||||
|
ResultSet.class.getClassLoader(),
|
||||||
|
new Class<?>[]{ResultSet.class},
|
||||||
|
resultSetHandler
|
||||||
|
);
|
||||||
|
return new CountingResultSetFixture(resultSet, getMetaDataCalls, getColumnTypeCalls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object currentCell(Object[][] rows, int rowIndex, int columnIndex) {
|
||||||
|
return rows[rowIndex][columnIndex - 1];
|
||||||
|
}
|
||||||
|
|
||||||
private static ResultSet resultSet(Object objectValue, StringSupplier stringSupplier, boolean wasNull) {
|
private static ResultSet resultSet(Object objectValue, StringSupplier stringSupplier, boolean wasNull) {
|
||||||
InvocationHandler handler = (Object unused, Method method, Object[] args) -> {
|
InvocationHandler handler = (Object unused, Method method, Object[] args) -> {
|
||||||
switch (method.getName()) {
|
switch (method.getName()) {
|
||||||
@@ -95,4 +178,32 @@ class JdbcExecutorTest {
|
|||||||
private interface StringSupplier {
|
private interface StringSupplier {
|
||||||
String get() throws Exception;
|
String get() throws Exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class CountingResultSetFixture {
|
||||||
|
private final ResultSet resultSet;
|
||||||
|
private final AtomicInteger getMetaDataCalls;
|
||||||
|
private final AtomicInteger getColumnTypeCalls;
|
||||||
|
|
||||||
|
private CountingResultSetFixture(
|
||||||
|
ResultSet resultSet,
|
||||||
|
AtomicInteger getMetaDataCalls,
|
||||||
|
AtomicInteger getColumnTypeCalls
|
||||||
|
) {
|
||||||
|
this.resultSet = resultSet;
|
||||||
|
this.getMetaDataCalls = getMetaDataCalls;
|
||||||
|
this.getColumnTypeCalls = getColumnTypeCalls;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResultSet resultSet() {
|
||||||
|
return resultSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getMetaDataCalls() {
|
||||||
|
return getMetaDataCalls.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getColumnTypeCalls() {
|
||||||
|
return getColumnTypeCalls.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user