where that a dialect's builder cannot render used to be dropped: thefindAll that means too many rows; for an update or destroy it means everyUnsupportedWhereError before the statement is[!CAUTION] The guard only sees whereobjects
Rawprorm.query()has nowhereobject to check, so it is not guarded.
A hand-writtenDELETE FROM ordersruns as written. See
Raw queries and, for a pattern-based net under raw SQL,
the query firewall.
src/query-builders/expression-render.ts (prepareWhereassertRenderedWhere) and is applied by Prorm to the dialect that everyfindAll, findOne, count, update, destroy,increment, upsert and the aggregates all pass through it. Rawprorm.query() does not: there is no where object to check.WhereSupport profile and makeswhere meets different builders. The SQL and the refusals below wereprepareWhere() and then buildSelectQuery() on eachimport { Op } from 'ts-prorm-orm';
// Orders from one customer that are still pending or have failed, in a price band.
const openOrders = await Order.findAll({
where: {
customerId: 7,
total: { [Op.gte]: 100, [Op.lt]: 500 },
[Op.or]: [{ status: 'pending' }, { status: 'failed' }],
},
});$or next to a plain column key. Each is shown on its own:$or, so the guard rewrites both into $and lists first.IN is where the dialects split into "renders" andUnsupportedWhereError: Cannot render WHERE condition at id for postgres: the array
shorthand for IN is not rendered by its builder; use { id: { [Op.in]: [...] } }.{ id: [1, 2] } as one array value (array-column equality),IN here. With { id: { [Op.in]: [1, 2] } }:UnsupportedWhereError: Cannot render WHERE condition at id for mysql: the array
shorthand for IN is not rendered by its builder; use { id: { [Op.in]: [...] } }.{ id: { [Op.in]: [1, 2] } }:[!TIP] { [Op.in]: [...] }renders on every builder. Use it in shared code rather
than the array shorthand.
prepareWhere with| Condition | Dialect | Refused because |
|---|---|---|
{ id: [1, 2] } | PostgreSQL, MySQL | the array shorthand for IN is not rendered by its builder; use { id: { [Op.in]: [...] } }. |
{ deletedAt: { [Op.eq]: null } } | SQL Server | $eq: null would be sent as '= NULL', which matches nothing; use $isNull: true. |
{ total: { [Op.gt]: literal('"discount" * 2') } } | MySQL, SQL Server | a literal() as an operand would be bound as a JSON string, not compiled as SQL. |
{ [Op.exists]: literal('SELECT 1 FROM refunds …') } | MySQL, SQL Server | EXISTS / NOT EXISTS subquery predicates (exists(), notExists(), Op.exists) are not rendered by its WHERE builder yet. |
{ age: { $foo: 1 } } | any | the operator '$foo' is not implemented by its WHERE builder. |
{ age: { $gt: 1, foo: 2 } } | any | the object mixes operators ($gt) with plain keys (foo), which no builder reads as a condition. |
{ age: { [Op.between]: 5 } } | any | $between needs a [low, high] array. |
{ '$posts.status$': 'x' } outside findAll | any | '$posts.status$' refers to a column of an included association, which only findAll() resolves (via its include list). |
a non-empty where whose built DELETE has no WHERE | any | a non-empty where was given, but the DELETE its builder produced has no condition ("DELETE FROM \"t\""). |
literal() / fn() used as a whole condition{ col: null } on a builder that would send = NULL; $in with a subquery on$and / $or / $not in the same object on a builder that drops them.[!NOTE]
Oracle's builder reads$and/$or/$notat every level, so{ [Op.or]: [{ a: 1 }, { b: 2 }] }renders there as(("a" = :1) OR ("b" = :2)). Earlier releases refused it.
import { UnsupportedWhereError } from 'ts-prorm-orm';
try {
await User.destroy({ where: { id: [1, 2] } });
} catch (err: unknown) {
if (err instanceof UnsupportedWhereError) {
console.error(err.dialect, err.path, err.message); // 'postgres', 'id', 'Cannot render WHERE condition at id for postgres: …'
}
}path is the offending node, spelled like $and[1].age.$foo, and <root>import { Op, UnsupportedWhereError } from 'ts-prorm-orm';
export async function purgeCancelledOrders(before: Date): Promise<number> {
try {
return await Order.destroy({
where: { status: 'cancelled', createdAt: { [Op.lt]: before } },
});
} catch (err: unknown) {
if (err instanceof UnsupportedWhereError) {
// Nothing was deleted: the statement was never sent.
throw new Error(`purge refused on ${err.dialect} at ${err.path}`, { cause: err });
}
throw err;
}
}{ a: { $gte: 1, $lt: 9 } } becomes{ $and: [{ a: { $gte: 1 } }, { a: { $lt: 9 } }] } for a builder that renders{ $or: [...], a: 1 } becomes { $and: [{ $or: [...] }, { a: 1 }] } for aawait User.findAll({ where: { age: { [Op.gte]: 18, [Op.lt]: 65 } } });WhereSupport profile: which logical keys and operators thewhereSupport(); otherwise theBUILTIN_WHERE_SUPPORTbuildWhereClause), so every dialect derived from one of those gets$between without an array, an included-tablefindAll.whereSupportOf() returns| Flag | PostgreSQL | MySQL | SQLite | SQL Server | Oracle |
|---|---|---|---|---|---|
arrayShorthand: { a: [1, 2] } is IN | no | no | yes | yes | yes |
nullShorthand: { a: null } is IS NULL | yes | yes | yes | yes | yes |
nullEqOperator: { $eq: null } is IS NULL | yes | yes | yes | no | yes |
multipleOperatorsPerColumn | yes | no (rewritten) | yes | yes | yes |
logicalWithSiblings | yes | no (rewritten) | yes | yes | yes |
expressionOperands: literal() / fn() compiled | yes | no | yes | no | no |
existsPredicates: $exists / $notExists | yes | no | yes | no | no |
inSubquery: $in with a subquery | yes | no | yes | no | no |
| Column operators rendered | 62 | 26 | 32 | 15 | 34 |
assertSupportedOperator() in= ?, and renderOperand() compiles literal() / fn() / col() / cast()= ? with the object bound as JSON. The statement runs andTRUE. Some builders legitimately render TRUE for{ a: { $notIn: [] } }, so it cannot double as an error marker.UnsupportedWhereError in the error tree