Fix/11466 mssql find operator (#11468)

* chore: enable driver `mssql` for test of issue #3113

* chore: add test case for #11298 to issue #11285 to prevent functionality of #11285

* fix: unhandled find operator with array value for mssql (#11466)

* fixup! fix: unhandled find operator with array value for mssql (#11466)

* Revert "chore: enable driver `mssql` for test of issue #3113"

This reverts commit a302d63eeac5892e920d97705b4230414ef81e6d.

* fixup! chore: add test case for #11298 to issue #11285 to prevent functionality of #11285

---------

Co-authored-by: Christian Forgács <christian@wunderbit.de>
This commit is contained in:
Christian Forgács 2025-05-13 02:45:06 +02:00 committed by GitHub
parent 23bb1ee271
commit e9eaf79604
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 150 additions and 17 deletions

View File

@ -27,6 +27,7 @@ import { TableForeignKey } from "../../schema-builder/table/TableForeignKey"
import { TypeORMError } from "../../error"
import { InstanceChecker } from "../../util/InstanceChecker"
import { UpsertType } from "../types/UpsertType"
import { FindOperator } from "../../find-options/FindOperator"
/**
* Organizes communication with SQL Server DBMS.
@ -973,6 +974,39 @@ export class SqlServerDriver implements Driver {
return new MssqlParameter(value, normalizedType as any)
}
/**
* Recursively wraps values (including those inside FindOperators) into MssqlParameter instances,
* ensuring correct type metadata is passed to the SQL Server driver.
*
* - If the value is a FindOperator containing an array, all elements are individually parametrized.
* - If the value is a non-raw FindOperator, a transformation is applied to its internal value.
* - Otherwise, the value is passed directly to parametrizeValue for wrapping.
*
* This ensures SQL Server receives properly typed parameters for queries involving operators like
* In, MoreThan, Between, etc.
*/
parametrizeValues(column: ColumnMetadata, value: any) {
if (value instanceof FindOperator) {
if (Array.isArray(value.value)) {
for (let i = 0; i < value.value.length; i++) {
value.value[i] = this.parametrizeValues(
column,
value.value[i],
)
}
} else if (value.type !== "raw") {
value.transformValue({
to: (v) => this.parametrizeValue(column, v),
from: (v) => v,
})
}
} else {
value = this.parametrizeValue(column, value)
}
return value
}
/**
* Sql server's parameters needs to be wrapped into special object with type information about this value.
* This method wraps all values of the given object into MssqlParameter based on their column definitions in the given table.

View File

@ -4280,21 +4280,9 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
// MSSQL requires parameters to carry extra type information
if (this.connection.driver.options.type === "mssql") {
const driver = this.connection.driver as SqlServerDriver
if (parameterValue instanceof FindOperator) {
if (parameterValue.type !== "raw") {
parameterValue.transformValue({
to: (v) =>
driver.parametrizeValue(column, v),
from: (v) => v,
})
}
} else {
parameterValue = driver.parametrizeValue(
column,
parameterValue,
)
}
parameterValue = (
this.connection.driver as SqlServerDriver
).parametrizeValues(column, parameterValue)
}
// if (parameterValue === null) {

View File

@ -1,6 +1,15 @@
import "reflect-metadata"
import "../../../utils/test-setup"
import { DataSource, LessThan, MoreThan } from "../../../../src"
import {
And,
DataSource,
In,
IsNull,
LessThan,
MoreThan,
Not,
Or,
} from "../../../../src"
import {
closeTestingConnections,
createTestingConnections,
@ -468,6 +477,58 @@ describe("find options > where", () => {
}),
))
it("where with or + and find operator", () =>
Promise.all(
connections.map(async (connection) => {
await prepareData(connection.manager)
const posts = await connection
.createQueryBuilder(Post, "post")
.setFindOptions({
where: {
counters: {
likedUsers: {
firstName: And(
In(["Gyro", "Timber"]),
Not(Or(IsNull(), In(["Foo", "Bar"]))),
),
},
},
},
order: {
id: "asc",
},
})
.getMany()
posts.should.be.eql([
{
id: 1,
title: "Post #1",
text: "About post #1",
counters: { likes: 1 },
},
{
id: 2,
title: "Post #2",
text: "About post #2",
counters: { likes: 2 },
},
{
id: 3,
title: "Post #3",
text: "About post #3",
counters: { likes: 1 },
},
{
id: 4,
title: "Post #4",
text: "About post #4",
counters: { likes: 1 },
},
])
}),
))
it("where relations with operators", () =>
Promise.all(
connections.map(async (connection) => {

View File

@ -7,7 +7,14 @@ import {
closeTestingConnections,
reloadTestingDatabases,
} from "../../utils/test-utils"
import { DataSource, MssqlParameter, Not, Raw } from "../../../src/index.js"
import {
And,
DataSource,
In,
MssqlParameter,
Not,
Raw,
} from "../../../src/index.js"
import { SqlServerQueryRunner } from "../../../src/driver/sqlserver/SqlServerQueryRunner"
import { User } from "./entity/user"
import { PostgresQueryRunner } from "../../../src/driver/postgres/PostgresQueryRunner"
@ -143,6 +150,49 @@ describe("github issues > #11285 Missing MSSQL input type", () => {
)
}),
))
it("should convert input parameter with FindOperator with array to MssqlParameter", () =>
Promise.all(
dataSources.map(async (dataSource) => {
const user = new User()
user.memberId = "test-member-id"
const user2 = new User()
user2.memberId = "test-member-id-2"
await dataSource.manager.save([user, user2])
const selectSpy = sinon.spy(
SqlServerQueryRunner.prototype,
"query",
)
const users = await dataSource.getRepository(User).find({
where: {
memberId: And(Not(In([user2.memberId]))),
},
})
expect(users).to.have.length(1)
expect(users[0].memberId).to.be.equal(user.memberId)
expect(selectSpy.calledOnce).to.be.true
sinon.assert.calledWithMatch(
selectSpy,
sinon.match.any,
sinon.match((value) => {
return (
Array.isArray(value) &&
value.length === 1 &&
value[0] instanceof MssqlParameter &&
value[0].value === user2.memberId &&
value[0].type === "varchar"
)
}),
)
}),
))
})
describe("other connections", () => {