Skip to content

MSSQL EntityManager.query(EXEC <SP>, [param]) doesn't return all recordsets, only the first one #11566

Open
@lboonekamp

Description

@lboonekamp

Issue description

Calling manager.query(EXEC MySP, [param1]) only returns the first recordset

Expected Behavior

I expect it to return exactly what sql returns. If I pass in a single statement it should return that result, if I pass in multiple, it should return all recordsets.

Let me know if this is by design.

Actual Behavior

Calling manager.query(EXEC MySP, [param1]) (or anywhere you can call query()) will return whatever your first statemement returns. If you're returning multiple recordsets, they are not all returned.

The EXEC example was the first one I tried as I happened to need to call an SP, however, the same applies for any query with multiple statements inside it.

For example a plain SELECT:

manager.query( SELECT * FROM Table1 SELECT * FROM Table 2 )

You only get whatever is inside Table1.

Steps to reproduce

EntityManager is exactly that from typeorm. Nothing special here:

const result = await EntityManager.query(EXEC api.Test ${params.map((_, index) => @${index}).join(', ')}`, ['param1', 'param2'])

`

Minimal stored procedure returning two recordsets:

`USE SomeDatabase
GO
/****** Object: StoredProcedure [api].[Test] Script Date: 9/07/2025 6:39:13 am ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: <Author,,Name>
-- Create date: <Create Date,,>
-- Description: <Description,,>
-- =============================================
ALTER PROCEDURE [api].[Test]
@param1 nvarchar,
@Param2 nvarchar
AS
BEGIN

SET NOCOUNT ON;

SELECT @param1 as Parameter1

SELECT @param2 as Parameter2

END
`

The results should be an array of arrays or however typeorm wants to deconstruct the response from sql (if it doesn't just pass it back but here's what I get instead:

[ { Parameter1: 'v' } ]

My Environment

Dependency Version
Operating System Windows 11
Node.js version 22.14.0
Typescript version 5.8.2
TypeORM version 0.3.25

Additional Context

The underlying mssql driver returns recordset and recordsets (contains all recordsets returned from a query) and rowsAffected. It needs to check for recordsets the same way it checks for recordset and rowsAffected.

I can see in the SqlServerQueryRunner.ts, in the query() method, it currently checks for recordset and rowsAffected:

`
const raw = await new Promise((ok, fail) => {
request.query(query, (err: any, raw: any) => {
// log slow queries if maxQueryExecution time is set
const maxQueryExecutionTime =
this.driver.options.maxQueryExecutionTime
const queryEndTime = Date.now()
const queryExecutionTime = queryEndTime - queryStartTime

                this.broadcaster.broadcastAfterQueryEvent(
                    broadcasterResult,
                    query,
                    parameters,
                    true,
                    queryExecutionTime,
                    raw,
                    undefined,
                )

                if (
                    maxQueryExecutionTime &&
                    queryExecutionTime > maxQueryExecutionTime
                ) {
                    this.driver.connection.logger.logQuerySlow(
                        queryExecutionTime,
                        query,
                        parameters,
                        this,
                    )
                }

                if (err) {
                    fail(new QueryFailedError(query, parameters, err))
                }

                ok(raw)
            })
        })

        const result = new QueryResult()

        if (raw?.hasOwnProperty("recordset")) {
            result.records = raw.recordset
        }

        if (raw?.hasOwnProperty("rowsAffected")) {
            result.affected = raw.rowsAffected[0]
        }

        const queryType = query.slice(0, query.indexOf(" "))
        switch (queryType) {
            case "DELETE":
                // for DELETE query additionally return number of affected rows
                result.raw = [raw.recordset, raw.rowsAffected[0]]
                break
            default:
                result.raw = raw.recordset
        }

        if (useStructuredResult) {
            return result
        } else {
            return result.raw
        }

`

The QueryResult class would need to have the appropriate key added to support this property too.

The return would need to be modified to support this. The useStructuredResult isn't exposed as far as DataSource.query() so one would need to fallback to DataSource.createQueryRunner.query(query, params, true)

So there's some options. It would be nice if useStructuredResult was exposed to DataSource.query() and with the modified QueryResult and the setting of recordSets we'll be able to pull out recordSets and see the full response.

Relevant Database Driver(s)

  • aurora-mysql
  • aurora-postgres
  • better-sqlite3
  • cockroachdb
  • cordova
  • expo
  • mongodb
  • mysql
  • nativescript
  • oracle
  • postgres
  • react-native
  • sap
  • spanner
  • sqlite
  • sqlite-abstract
  • sqljs
  • sqlserver

Are you willing to resolve this issue by submitting a Pull Request?

Yes, I have the time, and I know how to start.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions