Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ad4ec7e
initial commit
naorpeled Oct 3, 2025
3fd2b17
fix build
naorpeled Oct 3, 2025
0ee565b
cleanup
naorpeled Dec 19, 2025
2a73db5
fix: handle ignore behavior in QueryBuilder.where() and add ignore tests
naorpeled Feb 28, 2026
98bfcdb
fix: strip null/undefined from EntityManager criteria to prevent inte…
naorpeled Mar 4, 2026
cda28d4
Merge master: reflect PR #11878 scoping changes with throw defaults
naorpeled Mar 8, 2026
f7dad4e
fix: remove invalidWhereValuesBehavior checks from QueryBuilder.where()
naorpeled Mar 8, 2026
6552cb8
cleanup: simplify normalizeWhereCriteria default parameter
naorpeled Mar 8, 2026
9e5c4e8
cleanup: remove extra blank line in QueryBuilder
naorpeled Mar 8, 2026
601dcdd
cleanup: restore optional param, remove unused stripNullAndUndefined
naorpeled Mar 8, 2026
0913a31
cleanup: remove stale JSDoc from deleted method
naorpeled Mar 8, 2026
9ff1f8a
cleanup: restore early return when no options provided
naorpeled Mar 8, 2026
279e426
docs: add null/undefined default change to v1 migration guide
naorpeled Mar 8, 2026
26dc724
docs: move null/undefined section under Configuration with ### heading
naorpeled Mar 8, 2026
08ce9d5
docs: mention setFindOptions in affected APIs list
naorpeled Mar 8, 2026
2b1f20b
docs: clarify which QueryBuilder methods are affected vs not
naorpeled Mar 8, 2026
cf8ce9a
docs: rephrase affected APIs paragraph for clarity
naorpeled Mar 8, 2026
44a5182
docs: tweak wording for QB methods not affected
naorpeled Mar 8, 2026
08b5d0a
fix: update find-options-where test to expect throw for all-undefined…
naorpeled Mar 9, 2026
615fe8e
Merge branch 'master' into feat/change-default-behavior-of-invalid-wh…
gioboa Mar 9, 2026
d3dee94
Merge branch 'master' into feat/change-default-behavior-of-invalid-wh…
naorpeled Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/docs/data-source/2-data-source-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,14 @@ Different RDBMS-es have their own specific options.

- `invalidWhereValuesBehavior` - Controls how null and undefined values are handled in where conditions for high-level operations (find operations, repository methods, EntityManager methods). Does not affect QueryBuilder's `.where()` directly.
- `null` behavior options:
- `'ignore'` (default) - skips null properties
- `'ignore'` - skips null properties
- `'sql-null'` - transforms null to SQL NULL
- `'throw'` - throws an error
- `'throw'` (default) - throws an error
- `undefined` behavior options:
- `'ignore'` (default) - skips undefined properties
- `'throw'` - throws an error
- `'ignore'` - skips undefined properties
- `'throw'` (default) - throws an error

Example: `invalidWhereValuesBehavior: { null: 'sql-null', undefined: 'throw' }`.
Example: `invalidWhereValuesBehavior: { null: 'sql-null', undefined: 'ignore' }`.

Learn more about [Null and Undefined Handling](./5-null-and-undefined-handling.md).

Expand Down
43 changes: 18 additions & 25 deletions docs/docs/data-source/5-null-and-undefined-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,36 @@

In 'WHERE' conditions the values `null` and `undefined` are not strictly valid values in TypeORM.

Passing a known `null` value is disallowed by TypeScript (when you've enabled `strictNullChecks` in tsconfig.json) at compile time. But the default behavior is for `null` values encountered at runtime to be ignored. Similarly, `undefined` values are allowed by TypeScript and ignored at runtime.

The acceptance of `null` and `undefined` values can sometimes cause unexpected results and requires caution. This is especially a concern when values are passed from user input without adequate validation.

For example, calling `Repository.findOneBy({ id: undefined })` returns the first row from the table, and `Repository.findBy({ userId: null })` is unfiltered and returns all rows.
Passing a known `null` value is disallowed by TypeScript (when you've enabled `strictNullChecks` in tsconfig.json) at compile time. The default behavior is for `null` and `undefined` values encountered at runtime to throw an error.

The way in which `null` and `undefined` values are handled can be customised through the `invalidWhereValuesBehavior` configuration option in your data source options. This applies to high-level operations such as find operations, repository methods, and EntityManager methods (update, delete, softDelete, restore).

:::warning
This setting does **not** affect QueryBuilder's `.where()`, `.andWhere()`, or `.orWhere()` methods. QueryBuilder is a low-level API where null/undefined values pass through as-is. Use the `IsNull()` operator or parameterized conditions in QueryBuilder for explicit null handling.
:::

:::note
The current behavior will be changing in future versions of TypeORM,
we recommend setting both `null` and `undefined` behaviors to throw to prepare for these changes
:::

## Default Behavior

By default, TypeORM skips both `null` and `undefined` values in where conditions. This means that if you include a property with a `null` or `undefined` value in your where clause, it will be ignored:
By default, TypeORM throws an error when `null` or `undefined` values are encountered in where conditions. This prevents unexpected results and helps catch potential bugs early:

```typescript
// Both queries will return all posts, ignoring the text property
// Both queries will throw an error
const posts1 = await repository.find({
where: {
text: null,
},
})
// Error: Null value encountered in property 'text' of a where condition.

const posts2 = await repository.find({
where: {
text: undefined,
},
})
// Error: Undefined value encountered in property 'text' of a where condition.
```

The correct way to match null values in where conditions is to use the `IsNull` operator (for details see [Find Options](../working-with-entity-manager/3-find-options.md)):
To match null values in where conditions, use the `IsNull` operator (for details see [Find Options](../working-with-entity-manager/3-find-options.md)):

```typescript
const posts = await repository.find({
Expand Down Expand Up @@ -66,7 +59,7 @@ const dataSource = new DataSource({

The `null` behavior can be set to one of three values:

#### `'ignore'` (default)
#### `'ignore'`

JavaScript `null` values in where conditions are ignored and the property is skipped:

Expand Down Expand Up @@ -106,7 +99,7 @@ const posts = await repository.find({
})
```

#### `'throw'`
#### `'throw'` (default)

JavaScript `null` values cause a TypeORMError to be thrown:

Expand All @@ -133,7 +126,7 @@ const posts = await repository.find({

The `undefined` behavior can be set to one of two values:

#### `'ignore'` (default)
#### `'ignore'`

JavaScript `undefined` values in where conditions are ignored and the property is skipped:

Expand All @@ -153,7 +146,7 @@ const posts = await repository.find({
})
```

#### `'throw'`
#### `'throw'` (default)

JavaScript `undefined` values cause a TypeORMError to be thrown:

Expand Down Expand Up @@ -311,14 +304,14 @@ await dataSource

### Summary table

| Value | High-level API (find/repository/manager) | QueryBuilder `.where()` |
| ------------------------------------- | ---------------------------------------- | --------------------------------- |
| `null` with `"ignore"` (default) | Property skipped — no filter | `WHERE col = NULL` — zero results |
| `null` with `"sql-null"` | `WHERE col IS NULL` | `WHERE col = NULL` — zero results |
| `null` with `"throw"` | Throws error | `WHERE col = NULL` — zero results |
| `undefined` with `"ignore"` (default) | Property skipped — no filter | `WHERE col = NULL` — zero results |
| `undefined` with `"throw"` | Throws error | `WHERE col = NULL` — zero results |
| `IsNull()` | `WHERE col IS NULL` | `WHERE col IS NULL` |
| Value | High-level API (find/repository/manager) | QueryBuilder `.where()` |
| ------------------------------------ | ---------------------------------------- | --------------------------------- |
| `null` with `"ignore"` | Property skipped — no filter | `WHERE col = NULL` — zero results |
| `null` with `"sql-null"` | `WHERE col IS NULL` | `WHERE col = NULL` — zero results |
| `null` with `"throw"` (default) | Throws error | `WHERE col = NULL` — zero results |
| `undefined` with `"ignore"` | Property skipped — no filter | `WHERE col = NULL` — zero results |
| `undefined` with `"throw"` (default) | Throws error | `WHERE col = NULL` — zero results |
| `IsNull()` | `WHERE col IS NULL` | `WHERE col IS NULL` |

:::tip
Always use `IsNull()` when you want to match SQL NULL values, regardless of which API you use. It works correctly in both high-level and QueryBuilder contexts.
Expand Down
38 changes: 38 additions & 0 deletions docs/docs/guides/8-migration-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,44 @@ const migrations = migrationExecutor.getMigrations()

## Configuration

### `invalidWhereValuesBehavior` default changed to `throw`

The default behavior for null and undefined values in where conditions has changed. Previously, null and undefined values were silently ignored (the property was skipped). Now, both **throw an error by default**.

This change prevents subtle bugs where queries like `findBy({ id: undefined })` would silently return the first row instead of failing.

```typescript
// v0.3: silently returns all posts (null is ignored)
// v1.0: throws TypeORMError
await repository.find({ where: { text: null } })

// v0.3: silently returns all posts (undefined is ignored)
// v1.0: throws TypeORMError
await repository.find({ where: { text: undefined } })
```

To match null values, use the `IsNull()` operator:

```typescript
import { IsNull } from "typeorm"

await repository.find({ where: { text: IsNull() } })
```

To restore the previous behavior, set `invalidWhereValuesBehavior` in your data source options:

```typescript
new DataSource({
// ...
invalidWhereValuesBehavior: {
null: "ignore",
undefined: "ignore",
},
})
```

This setting guards all high-level APIs — find operations, repository/manager mutation methods, and `queryBuilder.setFindOptions()` (the only QueryBuilder method that is affected). The rest of the QueryBuilder methods (`.where()`, `.andWhere()`, `.orWhere()`) are **not** affected — null and undefined values pass through as-is. See [Null and undefined handling](../data-source/5-null-and-undefined-handling.md) for full details.

### Drop support for configuration via environment variables

The deprecated `ConnectionOptionsEnvReader` class and the ability to configure connections via `TYPEORM_CONNECTION`, `TYPEORM_URL`, and other `TYPEORM_*` environment variables has been removed. The `ormconfig.env` file format is also no longer supported. TypeORM no longer auto-loads `.env` files or depends on `dotenv`.
Expand Down
8 changes: 4 additions & 4 deletions src/data-source/BaseDataSourceOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,16 +212,16 @@ export interface BaseDataSourceOptions {
readonly invalidWhereValuesBehavior?: {
/**
* How to handle null values in where conditions.
* - 'ignore': Skip null properties (default)
* - 'ignore': Skip null properties
* - 'sql-null': Transform null to SQL NULL
* - 'throw': Throw an error when null is encountered
* - 'throw': Throw an error when null is encountered (default)
*/
readonly null?: "ignore" | "sql-null" | "throw"

/**
* How to handle undefined values in where conditions.
* - 'ignore': Skip undefined properties (default)
* - 'throw': Throw an error when undefined is encountered
* - 'ignore': Skip undefined properties
* - 'throw': Throw an error when undefined is encountered (default)
*/
readonly undefined?: "ignore" | "throw"
}
Expand Down
8 changes: 4 additions & 4 deletions src/query-builder/SelectQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4408,7 +4408,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
if (parameterValue === undefined) {
const undefinedBehavior =
this.connection.options.invalidWhereValuesBehavior
?.undefined || "ignore"
?.undefined || "throw"
if (undefinedBehavior === "throw") {
throw new TypeORMError(
`Undefined value encountered in property '${alias}.${key}' of a where condition. ` +
Expand All @@ -4421,7 +4421,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
if (parameterValue === null) {
const nullBehavior =
this.connection.options.invalidWhereValuesBehavior
?.null || "ignore"
?.null || "throw"
if (nullBehavior === "ignore") {
continue
} else if (nullBehavior === "throw") {
Expand Down Expand Up @@ -4494,7 +4494,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
if (where[key] === null) {
const nullBehavior =
this.connection.options.invalidWhereValuesBehavior
?.null || "ignore"
?.null || "throw"
if (nullBehavior === "sql-null") {
andConditions.push(
`${alias}.${propertyPath} IS NULL`,
Expand Down Expand Up @@ -4526,7 +4526,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
const undefinedBehavior =
this.connection.options
.invalidWhereValuesBehavior?.undefined ||
"ignore"
"throw"
if (undefinedBehavior === "throw") {
throw new TypeORMError(
`Undefined value encountered in nested relation '${alias}.${key}' of a where condition. ` +
Expand Down
4 changes: 2 additions & 2 deletions src/util/OrmUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ export class OrmUtils {
const propertyPath = path ? `${path}.${key}` : key

if (value === undefined) {
const behavior = options.undefined || "ignore"
const behavior = options?.undefined || "throw"
if (behavior === "throw") {
throw new TypeORMError(
`Undefined value encountered in property '${propertyPath}' of a where condition. ` +
Expand All @@ -672,7 +672,7 @@ export class OrmUtils {
}
// "ignore" — skip this key
} else if (value === null) {
const behavior = options.null || "ignore"
const behavior = options?.null || "throw"
if (behavior === "throw") {
throw new TypeORMError(
`Null value encountered in property '${propertyPath}' of a where condition. ` +
Expand Down
79 changes: 30 additions & 49 deletions test/functional/find-options/basic-usage/find-options-where.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import "reflect-metadata"
import "../../../utils/test-setup"
import type { DataSource } from "../../../../src"
import { And, In, IsNull, LessThan, MoreThan, Not, Or } from "../../../../src"
import {
And,
In,
IsNull,
LessThan,
MoreThan,
Not,
Or,
TypeORMError,
} from "../../../../src"
import {
closeTestingConnections,
createTestingConnections,
Expand All @@ -12,6 +21,7 @@ import { Counters } from "./entity/Counters"
import { Post } from "./entity/Post"
import { Tag } from "./entity/Tag"
import { prepareData } from "./find-options-test-utils"
import { expect } from "chai"

describe("find options > where", () => {
let dataSources: DataSource[]
Expand Down Expand Up @@ -636,59 +646,30 @@ describe("find options > where", () => {
}),
))

it("should not apply inner join if all conditions return undefined", () =>
it("should throw when all nested relation conditions are undefined by default", () =>
Promise.all(
dataSources.map(async (dataSource) => {
await prepareData(dataSource.manager)

const post4 = new Post()
post4.id = 4
post4.title = "Post #4"
post4.text = "About post #4"
post4.counters = new Counters()
post4.counters.likes = 1
await dataSource.manager.save(post4)

const posts = await dataSource
.createQueryBuilder(Post, "post")
.setFindOptions({
where: {
author: {
id: undefined,
firstName: undefined,
try {
await dataSource
.createQueryBuilder(Post, "post")
.setFindOptions({
where: {
author: {
id: undefined,
firstName: undefined,
},
},
},
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 },
},
])
})
.getMany()
expect.fail("Expected query to throw an error")
} catch (error) {
expect(error).to.be.instanceOf(TypeORMError)
expect(error.message).to.include(
"Undefined value encountered",
)
}
}),
))

Expand Down
13 changes: 0 additions & 13 deletions test/functional/find-options/empty-properties/entity/Post.ts

This file was deleted.

Loading
Loading