microsoft/mssql-jdbc

12.6.x has broken the ability to call procedures with output parameters by their four-part syntax

mjadczak opened this issue · 1 comments

Driver version

12.6.1 (issue introduced in 12.6.0)

SQL Server version

Microsoft SQL Server 2019 (RTM-CU25) (KB5033688) - 15.0.4355.3 (X64) 
	Jan 30 2024 17:02:22 
	Copyright (C) 2019 Microsoft Corporation
	Developer Edition (64-bit) on Windows Server 2019 Standard 10.0 <X64> (Build 17763: )

Client Operating System

Linux

JAVA/JVM version

Java 21.0.1

Table schema

N/A

Problem description

Since 12.6.0, stored procedure calls using the four-part syntax (i.e. including a linked server name) are broken, at least when using output parameters and being executed using the CALL JDBC escape syntax. I have not tested other permutations, such as the EXEC syntax and no return parameters.

Here is a short script to reproduce the issue:

package mssql.repro;

import com.microsoft.sqlserver.jdbc.SQLServerDataSource;

import java.sql.JDBCType;
import java.sql.SQLException;

public class MsSqlRepro {
    private static final String server = ""; // the server to connect to
    private static final String linkedServer = "LOCAL_L"; // the linked server pointing back to the same server
    private static final String procDefinition = """
            create or alter procedure dbo.TestAdd(@Num1 int, @Num2 int, @Result int output) as
            begin
                set @Result = @Num1 + @Num2;
            end;
            """;

    public static void main(String[] args) throws SQLException {
        System.setProperty("sun.security.jgss.native", "true");
        System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
        var ds = new SQLServerDataSource();
        ds.setServerName(server);
        ds.setDatabaseName("tempdb");
        ds.setTrustServerCertificate(true);
        ds.setIntegratedSecurity(true);
        ds.setAuthenticationScheme("JavaKerberos");
        ds.setUseDefaultGSSCredential(true);

        try (var conn = ds.getConnection()) {
            try (var stmt = conn.createStatement()) {
                stmt.execute(procDefinition);
            }
            try (var stmt = conn.prepareCall("{call %s.tempdb.dbo.TestAdd(?, ?, ?)}".formatted(linkedServer))) {
                stmt.setInt(1, 1);
                stmt.setInt(2, 2);
                stmt.registerOutParameter(3, JDBCType.INTEGER);
                stmt.execute();
                System.out.println(stmt.getInt(3));
            }
        }
    }
}

Expected behavior

The above code snippet prints "3", as it does if used with driver version 12.4.x and earlier, or when used without the linked-server call syntax.

Actual behavior

An exception is thrown within the driver when stmt.getInt(3) is called.

Error message/stack trace

Mar 07, 2024 4:50:15 PM com.microsoft.sqlserver.jdbc.SQLServerCallableStatement skipOutParameters
INFO: sp_executesql SQL: EXEC LOCAL_L.tempdb.dbo.TestAdd  @P0 ,  @P1 ,  @P2  OUT Unexpected outParamIndex: 0; adjustment: 0
Exception in thread "main" com.microsoft.sqlserver.jdbc.SQLServerException: The TDS protocol stream is not valid.
	at com.microsoft.sqlserver.jdbc.SQLServerConnection.terminate(SQLServerConnection.java:4266)
	at com.microsoft.sqlserver.jdbc.SQLServerConnection.terminate(SQLServerConnection.java:4255)
	at com.microsoft.sqlserver.jdbc.SQLServerConnection.throwInvalidTDS(SQLServerConnection.java:4238)
	at com.microsoft.sqlserver.jdbc.SQLServerCallableStatement.skipOutParameters(SQLServerCallableStatement.java:431)
	at com.microsoft.sqlserver.jdbc.SQLServerCallableStatement.getOutParameter(SQLServerCallableStatement.java:226)
	at com.microsoft.sqlserver.jdbc.SQLServerCallableStatement.getterGetParam(SQLServerCallableStatement.java:517)
	at com.microsoft.sqlserver.jdbc.SQLServerCallableStatement.getValue(SQLServerCallableStatement.java:522)
	at com.microsoft.sqlserver.jdbc.SQLServerCallableStatement.getInt(SQLServerCallableStatement.java:563)
	at mssql.repro.MsSqlRepro.main(MsSqlRepro.java:37)

Any other details that can be helpful

From what I know of the changes in the 12.6.x series, I believe the issue here may be that the code which tries to detect whether a statement is using exec or call syntax already (and therefore for which the additional sp_executesql wrapping layer can be skipped) does not correctly recognise the four-part syntax being used here.

JDBC trace logs

Mar 07, 2024 5:04:08 PM com.microsoft.sqlserver.jdbc.SQLServerCallableStatement skipOutParameters
INFO: sp_executesql SQL: EXEC LOCAL_L.tempdb.dbo.TestAdd  @P0 ,  @P1 ,  @P2  OUT Unexpected outParamIndex: 0; adjustment: 0
Mar 07, 2024 5:04:08 PM com.microsoft.sqlserver.jdbc.SQLServerCallableStatement skipOutParameters
INFO: sp_executesql SQL: EXEC LOCAL_L.tempdb.dbo.TestAdd  @P0 ,  @P1 ,  @P2  OUT Unexpected outParamIndex: 0; adjustment: 0

Hi @mjadczak,

We'll look into this immediately, thank you.