From b740507c489dd876f1a330def0a73b9313b70e26 Mon Sep 17 00:00:00 2001 From: kalebaran Date: Mon, 29 Jun 2026 12:17:11 +0200 Subject: [PATCH] Implement extended capability flags and metadata caching --- .../mysql/cj/protocol/ServerCapabilities.java | 4 ++++ .../com/mysql/cj/protocol/ServerSession.java | 12 ++++++++++ .../com/mysql/cj/ServerPreparedQuery.java | 5 ++++ .../cj/protocol/a/BinaryResultsetReader.java | 12 +++++++++- .../a/NativeAuthenticationProvider.java | 8 +++++-- .../cj/protocol/a/NativeCapabilities.java | 15 +++++++++--- .../mysql/cj/protocol/a/NativeProtocol.java | 3 ++- .../cj/protocol/a/NativeServerSession.java | 23 ++++++++++++++++++- .../cj/protocol/a/TextResultsetReader.java | 5 ++++ 9 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/main/core-api/java/com/mysql/cj/protocol/ServerCapabilities.java b/src/main/core-api/java/com/mysql/cj/protocol/ServerCapabilities.java index c6bd44b20..6f860880c 100644 --- a/src/main/core-api/java/com/mysql/cj/protocol/ServerCapabilities.java +++ b/src/main/core-api/java/com/mysql/cj/protocol/ServerCapabilities.java @@ -33,6 +33,10 @@ public interface ServerCapabilities { ServerVersion getServerVersion(); + default int getExtendedCapabilityFlags() { + return 0; + } + long getThreadId(); void setThreadId(long threadId); diff --git a/src/main/core-api/java/com/mysql/cj/protocol/ServerSession.java b/src/main/core-api/java/com/mysql/cj/protocol/ServerSession.java index c70e3f6da..348f091c8 100644 --- a/src/main/core-api/java/com/mysql/cj/protocol/ServerSession.java +++ b/src/main/core-api/java/com/mysql/cj/protocol/ServerSession.java @@ -108,6 +108,18 @@ public interface ServerSession { void setClientParam(long clientParam); + default int getClientParamExtended() { + return 0; + } + + default void setClientParamExtended(int clientParamExtended) { + // no-op by default; protocol implementations that negotiate extended capabilities override this. + } + + default boolean hasCacheMetadataEnabled() { + return false; + } + boolean hasLongColumnInfo(); boolean useMultiResults(); diff --git a/src/main/core-impl/java/com/mysql/cj/ServerPreparedQuery.java b/src/main/core-impl/java/com/mysql/cj/ServerPreparedQuery.java index 36923cfe7..b5c191df8 100644 --- a/src/main/core-impl/java/com/mysql/cj/ServerPreparedQuery.java +++ b/src/main/core-impl/java/com/mysql/cj/ServerPreparedQuery.java @@ -330,6 +330,11 @@ public T readExecuteResult(NativePacketPayload resultPacke T rs = this.session.getProtocol().readAllResults(maxRowsToRetrieve, createStreamingResultSet, resultPacket, true, metadata != null ? metadata : this.resultFields, resultSetFactory); + // Refresh the cached result-set metadata from the first resultset so that future executes, not PREPARE-time copy + if (rs != null && this.resultFields != null) { + this.resultFields = rs.getColumnDefinition(); + } + if (this.session.shouldIntercept()) { T interceptedResults = this.session.invokeQueryInterceptorsPost(this::getOriginalSql, this, rs, true); diff --git a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/BinaryResultsetReader.java b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/BinaryResultsetReader.java index 29b3e3c4a..cda6b7028 100644 --- a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/BinaryResultsetReader.java +++ b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/BinaryResultsetReader.java @@ -56,8 +56,18 @@ public Resultset read(int maxRows, boolean streamResults, NativePacketPayload re if (columnCount > 0) { // Build a result set with rows. + // When CLIENT_CACHE_METADATA is negotiated the server inserts a 1-byte flag after the column count: + // 1 = column definitions follow (regular protocol) + // 0 = column definitions are skipped because metadata is unchanged + boolean metadataFollows = true; + if (this.protocol.getServerSession().hasCacheMetadataEnabled()) { + metadataFollows = resultPacket.readInteger(IntegerDataType.INT1) != 0; + } + // Read in the column information - ColumnDefinition cdef = this.protocol.read(ColumnDefinition.class, new MergingColumnDefinitionFactory(columnCount, metadata)); + ColumnDefinition cdef = metadataFollows // + ? this.protocol.read(ColumnDefinition.class, new MergingColumnDefinitionFactory(columnCount, metadata)) // + : metadata; boolean isCursorPossible = this.protocol.getPropertySet().getBooleanProperty(PropertyKey.useCursorFetch).getValue() && resultSetFactory.getResultSetType() == Type.FORWARD_ONLY && resultSetFactory.getFetchSize() > 0; diff --git a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeAuthenticationProvider.java b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeAuthenticationProvider.java index 5d678afeb..274cf7afb 100644 --- a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeAuthenticationProvider.java +++ b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeAuthenticationProvider.java @@ -159,7 +159,7 @@ public void connect(String user, String pass, String db) { this.useConnectWithDb = this.database != null && this.database.length() > 0 && !this.propertySet.getBooleanProperty(PropertyKey.createDatabaseIfNotExist).getValue(); - long clientParam = capabilityFlags & NativeServerSession.CLIENT_LONG_PASSWORD // + long clientParam = capabilityFlags & NativeServerSession.CLIENT_MYSQL // | (this.propertySet.getBooleanProperty(PropertyKey.useAffectedRows).getValue() ? // 0 : capabilityFlags & NativeServerSession.CLIENT_FOUND_ROWS) // | capabilityFlags & NativeServerSession.CLIENT_LONG_FLAG // @@ -194,6 +194,9 @@ public void connect(String user, String pass, String db) { sessState.setClientParam(clientParam); + int clientParamExtended = capabilities.getExtendedCapabilityFlags() & NativeServerSession.CLIENT_CACHE_METADATA; + sessState.setClientParamExtended(clientParamExtended); + /* First, negotiate SSL connection */ if ((clientParam & NativeServerSession.CLIENT_SSL) != 0) { this.protocol.negotiateSSLConnection(); @@ -614,7 +617,8 @@ private NativePacketPayload createHandshakeResponsePacket(ServerSession serverSe last_sent.writeInteger(IntegerDataType.INT4, clientParam); last_sent.writeInteger(IntegerDataType.INT4, NativeConstants.MAX_PACKET_SIZE); last_sent.writeInteger(IntegerDataType.INT1, collationIndex); - last_sent.writeBytes(StringLengthDataType.STRING_FIXED, new byte[23]); // Set of bytes reserved for future use. + last_sent.writeBytes(StringLengthDataType.STRING_FIXED, new byte[19]); + last_sent.writeInteger(IntegerDataType.INT4, serverSession.getClientParamExtended()); // User/Password data last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(this.username, enc)); diff --git a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeCapabilities.java b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeCapabilities.java index 990f9ad59..a57ab1dfa 100644 --- a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeCapabilities.java +++ b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeCapabilities.java @@ -38,6 +38,7 @@ public class NativeCapabilities implements ServerCapabilities { private long threadId = -1; private String seed; private int capabilityFlags; + private int extendedCapabilityFlags; private int serverDefaultCollationIndex; private int statusFlags = 0; private int authPluginDataLength = 0; @@ -50,7 +51,8 @@ public NativeCapabilities(NativePacketPayload initialHandshakePacket) { this.protocolVersion = (byte) initialHandshakePacket.readInteger(IntegerDataType.INT1); try { - this.serverVersion = ServerVersion.parseVersion(initialHandshakePacket.readString(StringSelfDataType.STRING_TERM, "ASCII")); + String versionString = initialHandshakePacket.readString(StringSelfDataType.STRING_TERM, "ASCII"); + this.serverVersion = ServerVersion.parseVersion(versionString); // read connection id this.threadId = initialHandshakePacket.readInteger(IntegerDataType.INT4); @@ -85,8 +87,10 @@ public NativeCapabilities(NativePacketPayload initialHandshakePacket) { // read filler ([00]) initialHandshakePacket.readInteger(IntegerDataType.INT1); } - // next 10 bytes are reserved (all [00]) - initialHandshakePacket.setPosition(initialHandshakePacket.getPosition() + 10); + // next 6 bytes are reserved (all [00]) + initialHandshakePacket.setPosition(initialHandshakePacket.getPosition() + 6); + // read extended flag (4 bytes) + this.extendedCapabilityFlags = (int) initialHandshakePacket.readInteger(IntegerDataType.INT4); this.serverHasFracSecsSupport = this.serverVersion.meetsMinimum(new ServerVersion(5, 6, 4)); } catch (Throwable t) { @@ -120,6 +124,11 @@ public ServerVersion getServerVersion() { return this.serverVersion; } + @Override + public int getExtendedCapabilityFlags() { + return this.extendedCapabilityFlags; + } + @Override public long getThreadId() { return this.threadId; diff --git a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeProtocol.java b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeProtocol.java index 65a6bea31..021c449c1 100644 --- a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeProtocol.java +++ b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeProtocol.java @@ -348,7 +348,8 @@ public void negotiateSSLConnection() { packet.writeInteger(IntegerDataType.INT4, clientParam); packet.writeInteger(IntegerDataType.INT4, NativeConstants.MAX_PACKET_SIZE); packet.writeInteger(IntegerDataType.INT1, this.serverSession.getCharsetSettings().configurePreHandshake(false)); - packet.writeBytes(StringLengthDataType.STRING_FIXED, new byte[23]); // Set of bytes reserved for future use. + packet.writeBytes(StringLengthDataType.STRING_FIXED, new byte[19]); // Set of bytes reserved for future use. + packet.writeInteger(IntegerDataType.INT4, this.serverSession.getClientParamExtended()); send(packet, packet.getPosition()); diff --git a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeServerSession.java b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeServerSession.java index 5cf817173..f49042c53 100644 --- a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeServerSession.java +++ b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/NativeServerSession.java @@ -48,7 +48,7 @@ public class NativeServerSession implements ServerSession { public static final int SERVER_QUERY_WAS_SLOW = 2048; public static final int SERVER_SESSION_STATE_CHANGED = 1 << 14; // 16384 - public static final int CLIENT_LONG_PASSWORD = 0x00000001; /* new more secure passwords */ + public static final int CLIENT_MYSQL = 0x00000001; /* server is MySQL; cleared by MariaDB */ public static final int CLIENT_FOUND_ROWS = 0x00000002; public static final int CLIENT_LONG_FLAG = 0x00000004; /* Get all column flags */ public static final int CLIENT_CONNECT_WITH_DB = 0x00000008; @@ -72,11 +72,17 @@ public class NativeServerSession implements ServerSession { public static final int CLIENT_QUERY_ATTRIBUTES = 0x08000000; public static final int CLIENT_MULTI_FACTOR_AUTHENTICATION = 0x10000000; + /** + * extended capability bits (sent and received as a separate 4-byte word in the handshake). + */ + public static final int CLIENT_CACHE_METADATA = 0x00000010; // 1 << 4 in extended-cap space + private PropertySet propertySet; private NativeCapabilities capabilities; private int oldStatusFlags = 0; private int statusFlags = 0; private long clientParam = 0; + private int clientParamExtended = 0; private NativeServerSessionStateController serverSessionStateController; /** The map of server variables that we retrieve at connection init. */ @@ -194,6 +200,21 @@ public void setClientParam(long clientParam) { this.clientParam = clientParam; } + @Override + public int getClientParamExtended() { + return this.clientParamExtended; + } + + @Override + public void setClientParamExtended(int clientParamExtended) { + this.clientParamExtended = clientParamExtended; + } + + @Override + public boolean hasCacheMetadataEnabled() { + return (this.clientParamExtended & CLIENT_CACHE_METADATA) != 0; + } + @Override public boolean hasLongColumnInfo() { return (this.clientParam & CLIENT_LONG_FLAG) != 0; diff --git a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/TextResultsetReader.java b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/TextResultsetReader.java index 42163b18e..8f82fb464 100644 --- a/src/main/protocol-impl/java/com/mysql/cj/protocol/a/TextResultsetReader.java +++ b/src/main/protocol-impl/java/com/mysql/cj/protocol/a/TextResultsetReader.java @@ -54,6 +54,11 @@ public Resultset read(int maxRows, boolean streamResults, NativePacketPayload re if (columnCount > 0) { // Build a result set with rows. + if (this.protocol.getServerSession().hasCacheMetadataEnabled()) { + // metadata_follows byte: COM_QUERY has no cached metadata to reuse, so the server always sets it to 1. Consume the byte to keep the stream aligned. + resultPacket.readInteger(IntegerDataType.INT1); + } + // Read in the column information ColumnDefinition cdef = this.protocol.read(ColumnDefinition.class, new ColumnDefinitionFactory(columnCount, metadata));